Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1940,7 +1940,7 @@ namespace ts {
emitPlaceholder(hint, node, snippet);
break;
case SnippetKind.TabStop:
emitTabStop(snippet);
emitTabStop(hint, node, snippet);
break;
}
}
Expand All @@ -1952,7 +1952,12 @@ namespace ts {
// `${2:...}`
}

function emitTabStop(snippet: TabStop) {
function emitTabStop(hint: EmitHint, node: Node, snippet: TabStop) {
// A tab stop should only be attached to an empty node, i.e. a node that doesn't emit any text.
Debug.assert(node.kind === SyntaxKind.EmptyStatement,
`A tab stop cannot be attached to a node of kind ${Debug.formatSyntaxKind(node.kind)}.`);
Debug.assert(hint !== EmitHint.EmbeddedStatement,
`A tab stop cannot be attached to an embedded statement.`);
nonEscapingWrite(`\$${snippet.order}`);
}

Expand Down Expand Up @@ -4156,9 +4161,13 @@ namespace ts {
}

function emitModifiers(node: Node, modifiers: NodeArray<Modifier> | undefined) {
if (modifiers && modifiers.length) {
emitList(node, modifiers, ListFormat.Modifiers);
writeSpace();
if (modifiers) {
onBeforeEmitNodeArray?.(modifiers);
if (modifiers.length) {
emitList(node, modifiers, ListFormat.Modifiers);
writeSpace();
}
onAfterEmitNodeArray?.(modifiers);
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1317,7 +1317,11 @@ namespace FourSlash {
if (options) {
this.configure(options);
}
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options);
return this.languageService.getCompletionsAtPosition(
this.activeFile.fileName,
this.currentCaretPosition,
options,
this.formatCodeSettings);
}

private getCompletionEntryDetails(entryName: string, source: string | undefined, data: ts.CompletionEntryData | undefined, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined {
Expand Down
4 changes: 2 additions & 2 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,8 @@ namespace Harness.LanguageService {
const responseFormat = format || ts.SemanticClassificationFormat.Original;
return unwrapJSONCallResult(this.shim.getEncodedSemanticClassifications(fileName, span.start, span.length, responseFormat));
}
getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo {
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences));
getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined, formattingSettings: ts.FormatCodeSettings | undefined): ts.CompletionInfo {
return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences, formattingSettings));
}
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined): ts.CompletionEntryDetails {
return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences, data));
Expand Down
19 changes: 12 additions & 7 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1842,13 +1842,18 @@ namespace ts.server {
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
const position = this.getPosition(args, scriptInfo);

const completions = project.getLanguageService().getCompletionsAtPosition(file, position, {
...convertUserPreferences(this.getPreferences(file)),
triggerCharacter: args.triggerCharacter,
triggerKind: args.triggerKind as CompletionTriggerKind | undefined,
includeExternalModuleExports: args.includeExternalModuleExports,
includeInsertTextCompletions: args.includeInsertTextCompletions
});
const completions = project.getLanguageService().getCompletionsAtPosition(
file,
position,
{
...convertUserPreferences(this.getPreferences(file)),
triggerCharacter: args.triggerCharacter,
triggerKind: args.triggerKind as CompletionTriggerKind | undefined,
includeExternalModuleExports: args.includeExternalModuleExports,
includeInsertTextCompletions: args.includeInsertTextCompletions,
},
project.projectService.getFormatCodeOptions(file),
);
if (completions === undefined) return undefined;

if (kind === protocol.CommandTypes.CompletionsFull) return completions;
Expand Down
63 changes: 50 additions & 13 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ namespace ts.Completions {
triggerCharacter: CompletionsTriggerCharacter | undefined,
completionKind: CompletionTriggerKind | undefined,
cancellationToken: CancellationToken,
formatContext?: formatting.FormatContext,
): CompletionInfo | undefined {
const { previousToken } = getRelevantTokens(position, sourceFile);
if (triggerCharacter && !isInString(sourceFile, position, previousToken) && !isValidTrigger(sourceFile, triggerCharacter, previousToken, position)) {
Expand Down Expand Up @@ -275,7 +276,7 @@ namespace ts.Completions {

switch (completionData.kind) {
case CompletionDataKind.Data:
const response = completionInfoFromData(sourceFile, host, program, compilerOptions, log, completionData, preferences);
const response = completionInfoFromData(sourceFile, host, program, compilerOptions, log, completionData, preferences, formatContext);
if (response?.isIncomplete) {
incompleteCompletionsCache?.set(response);
}
Expand Down Expand Up @@ -412,6 +413,7 @@ namespace ts.Completions {
log: Log,
completionData: CompletionData,
preferences: UserPreferences,
formatContext: formatting.FormatContext | undefined,
): CompletionInfo | undefined {
const {
symbols,
Expand Down Expand Up @@ -459,6 +461,7 @@ namespace ts.Completions {
completionKind,
preferences,
compilerOptions,
formatContext,
isTypeOnlyLocation,
propertyAccessToConvert,
isJsxIdentifierExpected,
Expand Down Expand Up @@ -489,6 +492,7 @@ namespace ts.Completions {
completionKind,
preferences,
compilerOptions,
formatContext,
isTypeOnlyLocation,
propertyAccessToConvert,
isJsxIdentifierExpected,
Expand Down Expand Up @@ -638,6 +642,7 @@ namespace ts.Completions {
options: CompilerOptions,
preferences: UserPreferences,
completionKind: CompletionKind,
formatContext: formatting.FormatContext | undefined,
): CompletionEntry | undefined {
let insertText: string | undefined;
let replacementSpan = getReplacementSpanForContextToken(replacementToken);
Expand Down Expand Up @@ -706,7 +711,7 @@ namespace ts.Completions {
completionKind === CompletionKind.MemberLike &&
isClassLikeMemberCompletion(symbol, location)) {
let importAdder;
({ insertText, isSnippet, importAdder } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken));
({ insertText, isSnippet, importAdder } = getEntryForMemberCompletion(host, program, options, preferences, name, symbol, location, contextToken, formatContext));
if (importAdder?.hasFixes()) {
hasAction = true;
source = CompletionSource.ClassMemberSnippet;
Expand Down Expand Up @@ -832,6 +837,7 @@ namespace ts.Completions {
symbol: Symbol,
location: Node,
contextToken: Node | undefined,
formatContext: formatting.FormatContext | undefined,
): { insertText: string, isSnippet?: true, importAdder?: codefix.ImportAdder } {
const classLikeDeclaration = findAncestor(location, isClassLike);
if (!classLikeDeclaration) {
Expand All @@ -852,15 +858,16 @@ namespace ts.Completions {
});
const importAdder = codefix.createImportAdder(sourceFile, program, preferences, host);

// Create empty body for possible method implementation.
let body;
if (preferences.includeCompletionsWithSnippetText) {
isSnippet = true;
// We are adding a tabstop (i.e. `$0`) in the body of the suggested member,
// if it has one, so that the cursor ends up in the body once the completion is inserted.
// Note: this assumes we won't have more than one body in the completion nodes, which should be the case.
const emptyStatement = factory.createExpressionStatement(factory.createIdentifier(""));
setSnippetElement(emptyStatement, { kind: SnippetKind.TabStop, order: 0 });
body = factory.createBlock([emptyStatement], /* multiline */ true);
const emptyStmt = factory.createEmptyStatement();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conceptually, it only makes sense for a tab stop snippet to be attached to a node that doesn't cause anything to be emitted. using an empty identifier, as I was before, was causing some troubles when setting positions for synthetic nodes, because we can't assume the identifier will be empty. an empty statement seemed more appropriate then.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can’t tell if you already got this, but an empty statement is only parsable when terminated by a semicolon, so in the reverse, it’s impossible to emit an empty statement without emitting a semicolon. Is the formatter removing this semicolon or did you somehow get this to emit without one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hadn't thought of that, I think. The semicolon doesn't exist in this case because the empty statement is never emitted -- when we call emitTabStop, it doesn't emit the node (empty statement) the tab stop is attached to. Do you think that could be a problem?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I don’t think it’s a problem, it’s just a little weird to think about.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's definitely weird. I tried to go for the least weird option :/

body = factory.createBlock([emptyStmt], /* multiline */ true);
setSnippetElement(emptyStmt, { kind: SnippetKind.TabStop, order: 0 });
}
else {
body = factory.createBlock([], /* multiline */ true);
Expand Down Expand Up @@ -911,18 +918,45 @@ namespace ts.Completions {
modifiers = node.modifierFlagsCache | requiredModifiers | presentModifiers;
}
node = factory.updateModifiers(node, modifiers & (~presentModifiers));

completionNodes.push(node);
},
body,
codefix.PreserveOptionalFlags.Property,
isAbstract);

if (completionNodes.length) {
insertText = printer.printSnippetList(
ListFormat.MultiLine | ListFormat.NoTrailingNewLine,
factory.createNodeArray(completionNodes),
sourceFile);
// If we have access to formatting settings, we print the nodes using the emitter,
// and then format the printed text.
if (formatContext) {
const syntheticFile = {
text: printer.printSnippetList(
ListFormat.MultiLine | ListFormat.NoTrailingNewLine,
factory.createNodeArray(completionNodes),
sourceFile),
getLineAndCharacterOfPosition(pos: number) {
return getLineAndCharacterOfPosition(this, pos);
},
};

const formatOptions = getFormatCodeSettingsForWriting(formatContext, sourceFile);
const changes = flatMap(completionNodes, node => {
const nodeWithPos = textChanges.assignPositionsToNode(node);
return formatting.formatNodeGivenIndentation(
nodeWithPos,
syntheticFile,
sourceFile.languageVariant,
/* indentation */ 0,
/* delta */ 0,
{ ...formatContext, options: formatOptions });
});
insertText = textChanges.applyChanges(syntheticFile.text, changes);
}
else { // Otherwise, just use emitter to print the new nodes.
insertText = printer.printSnippetList(
ListFormat.MultiLine | ListFormat.NoTrailingNewLine,
factory.createNodeArray(completionNodes),
sourceFile);
}
}

return { insertText, isSnippet, importAdder };
Expand Down Expand Up @@ -972,8 +1006,8 @@ namespace ts.Completions {
function createSnippetPrinter(
printerOptions: PrinterOptions,
) {
const printer = createPrinter(printerOptions);
const baseWriter = createTextWriter(getNewLineCharacter(printerOptions));
const baseWriter = textChanges.createWriter(getNewLineCharacter(printerOptions));
const printer = createPrinter(printerOptions, baseWriter);
const writer: EmitTextWriter = {
...baseWriter,
write: s => baseWriter.write(escapeSnippetText(s)),
Expand Down Expand Up @@ -1117,6 +1151,7 @@ namespace ts.Completions {
kind: CompletionKind,
preferences: UserPreferences,
compilerOptions: CompilerOptions,
formatContext: formatting.FormatContext | undefined,
isTypeOnlyLocation?: boolean,
propertyAccessToConvert?: PropertyAccessExpression,
jsxIdentifierExpected?: boolean,
Expand Down Expand Up @@ -1166,6 +1201,7 @@ namespace ts.Completions {
compilerOptions,
preferences,
kind,
formatContext,
);
if (!entry) {
continue;
Expand Down Expand Up @@ -1444,7 +1480,8 @@ namespace ts.Completions {
name,
symbol,
location,
contextToken);
contextToken,
formatContext);
if (importAdder) {
const changes = textChanges.ChangeTracker.with(
{ host, formatContext, preferences },
Expand Down
5 changes: 3 additions & 2 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1593,7 +1593,7 @@ namespace ts {
return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)];
}

function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = emptyOptions): CompletionInfo | undefined {
function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = emptyOptions, formattingSettings?: FormatCodeSettings): CompletionInfo | undefined {
// Convert from deprecated options names to new names
const fullPreferences: UserPreferences = {
...identity<UserPreferences>(options), // avoid excess property check
Expand All @@ -1610,7 +1610,8 @@ namespace ts {
fullPreferences,
options.triggerCharacter,
options.triggerKind,
cancellationToken);
cancellationToken,
formattingSettings && formatting.getFormatContext(formattingSettings, host));
}

function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions, data?: CompletionEntryData): CompletionEntryDetails | undefined {
Expand Down
8 changes: 4 additions & 4 deletions src/services/shims.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ namespace ts {
getEncodedSyntacticClassifications(fileName: string, start: number, length: number): string;
getEncodedSemanticClassifications(fileName: string, start: number, length: number, format?: SemanticClassificationFormat): string;

getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined): string;
getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined, formattingSettings: FormatCodeSettings | undefined): string;
getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): string;

getQuickInfoAtPosition(fileName: string, position: number): string;
Expand Down Expand Up @@ -956,10 +956,10 @@ namespace ts {
* to provide at the given source position and providing a member completion
* list if requested.
*/
public getCompletionsAtPosition(fileName: string, position: number, preferences: GetCompletionsAtPositionOptions | undefined) {
public getCompletionsAtPosition(fileName: string, position: number, preferences: GetCompletionsAtPositionOptions | undefined, formattingSettings: FormatCodeSettings | undefined) {
return this.forwardJSONCall(
`getCompletionsAtPosition('${fileName}', ${position}, ${preferences})`,
() => this.languageService.getCompletionsAtPosition(fileName, position, preferences)
`getCompletionsAtPosition('${fileName}', ${position}, ${preferences}, ${formattingSettings})`,
() => this.languageService.getCompletionsAtPosition(fileName, position, preferences, formattingSettings)
);
}

Expand Down
1 change: 1 addition & 0 deletions src/services/stringCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ namespace ts.Completions.StringCompletions {
CompletionKind.String,
preferences,
options,
/*formatContext*/ undefined,
); // Target will not be used, so arbitrary
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries };
}
Expand Down
13 changes: 2 additions & 11 deletions src/services/textChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,15 +1052,6 @@ namespace ts.textChanges {
? "" : options.suffix);
}

function getFormatCodeSettingsForWriting({ options }: formatting.FormatContext, sourceFile: SourceFile): FormatCodeSettings {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to utilities

const shouldAutoDetectSemicolonPreference = !options.semicolons || options.semicolons === SemicolonPreference.Ignore;
const shouldRemoveSemicolons = options.semicolons === SemicolonPreference.Remove || shouldAutoDetectSemicolonPreference && !probablyUsesSemicolons(sourceFile);
return {
...options,
semicolons: shouldRemoveSemicolons ? SemicolonPreference.Remove : SemicolonPreference.Ignore,
};
}

/** Note: this may mutate `nodeIn`. */
function getFormattedTextOfNode(nodeIn: Node, sourceFile: SourceFile, pos: number, { indentation, prefix, delta }: InsertNodeOptions, newLineCharacter: string, formatContext: formatting.FormatContext, validate: ValidateNonFormattedText | undefined): string {
const { node, text } = getNonformattedText(nodeIn, sourceFile, newLineCharacter);
Expand Down Expand Up @@ -1110,7 +1101,7 @@ namespace ts.textChanges {
return skipTrivia(s, 0) === s.length;
}

function assignPositionsToNode(node: Node): Node {
export function assignPositionsToNode(node: Node): Node {
const visited = visitEachChild(node, assignPositionsToNode, nullTransformationContext, assignPositionsToNodeArray, assignPositionsToNode);
// create proxy node for non synthesized nodes
const newNode = nodeIsSynthesized(visited) ? visited : Object.create(visited) as Node;
Expand All @@ -1131,7 +1122,7 @@ namespace ts.textChanges {

interface TextChangesWriter extends EmitTextWriter, PrintHandlers {}

function createWriter(newLine: string): TextChangesWriter {
export function createWriter(newLine: string): TextChangesWriter {
let lastNonTriviaPosition = 0;

const writer = createTextWriter(newLine);
Expand Down
Loading