Skip to content

Commit f6f2d36

Browse files
authored
Add optionalReplacementSpan to completions response (#40347)
* Add optionalReplacementRange to completions response * Get the name right * Fix unit tests * Fix comment typo * Fix comment typo * Baseline
1 parent 8384018 commit f6f2d36

File tree

13 files changed

+76
-5
lines changed

13 files changed

+76
-5
lines changed

src/harness/fourslashImpl.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,13 @@ namespace FourSlash {
830830
this.raiseError(`Expected 'isGlobalCompletion to be ${options.isGlobalCompletion}, got ${actualCompletions.isGlobalCompletion}`);
831831
}
832832

833+
if (ts.hasProperty(options, "optionalReplacementSpan")) {
834+
assert.deepEqual(
835+
actualCompletions.optionalReplacementSpan && actualCompletions.optionalReplacementSpan,
836+
options.optionalReplacementSpan && ts.createTextSpanFromRange(options.optionalReplacementSpan),
837+
"Expected 'optionalReplacementSpan' properties to match");
838+
}
839+
833840
const nameToEntries = new ts.Map<string, ts.CompletionEntry[]>();
834841
for (const entry of actualCompletions.entries) {
835842
const entries = nameToEntries.get(entry.name);

src/harness/fourslashInterfaceImpl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,7 @@ namespace FourSlashInterface {
15291529
readonly marker?: ArrayOrSingle<string | FourSlash.Marker>;
15301530
readonly isNewIdentifierLocation?: boolean; // Always tested
15311531
readonly isGlobalCompletion?: boolean; // Only tested if set
1532+
readonly optionalReplacementSpan?: FourSlash.Range; // Only tested if set
15321533
readonly exact?: ArrayOrSingle<ExpectedCompletionEntry>;
15331534
readonly includes?: ArrayOrSingle<ExpectedCompletionEntry>;
15341535
readonly excludes?: ArrayOrSingle<string>;

src/server/protocol.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2256,6 +2256,12 @@ namespace ts.server.protocol {
22562256
readonly isGlobalCompletion: boolean;
22572257
readonly isMemberCompletion: boolean;
22582258
readonly isNewIdentifierLocation: boolean;
2259+
/**
2260+
* In the absence of `CompletionEntry["replacementSpan"]`, the editor may choose whether to use
2261+
* this span or its default one. If `CompletionEntry["replacementSpan"]` is defined, that span
2262+
* must be used to commit that completion entry.
2263+
*/
2264+
readonly optionalReplacementSpan?: TextSpan;
22592265
readonly entries: readonly CompletionEntry[];
22602266
}
22612267

src/server/session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,6 +1783,7 @@ namespace ts.server {
17831783

17841784
const res: protocol.CompletionInfo = {
17851785
...completions,
1786+
optionalReplacementSpan: completions.optionalReplacementSpan && toProtocolTextSpan(completions.optionalReplacementSpan, scriptInfo),
17861787
entries,
17871788
};
17881789
return res;

src/services/completions.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ namespace ts.Completions {
209209
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
210210
}
211211

212+
function getOptionalReplacementSpan(location: Node | undefined) {
213+
// StringLiteralLike locations are handled separately in stringCompletions.ts
214+
return location?.kind === SyntaxKind.Identifier ? createTextSpanFromNode(location) : undefined;
215+
}
216+
212217
function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, preferences: UserPreferences): CompletionInfo | undefined {
213218
const {
214219
symbols,
@@ -241,7 +246,7 @@ namespace ts.Completions {
241246
kindModifiers: undefined,
242247
sortText: SortText.LocationPriority,
243248
};
244-
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, entries: [entry] };
249+
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: false, optionalReplacementSpan: getOptionalReplacementSpan(location), entries: [entry] };
245250
}
246251

247252
const entries: CompletionEntry[] = [];
@@ -305,7 +310,13 @@ namespace ts.Completions {
305310
entries.push(createCompletionEntryForLiteral(literal, preferences));
306311
}
307312

308-
return { isGlobalCompletion: isInSnippetScope, isMemberCompletion: isMemberCompletionKind(completionKind), isNewIdentifierLocation, entries };
313+
return {
314+
isGlobalCompletion: isInSnippetScope,
315+
isMemberCompletion: isMemberCompletionKind(completionKind),
316+
isNewIdentifierLocation,
317+
optionalReplacementSpan: getOptionalReplacementSpan(location),
318+
entries
319+
};
309320
}
310321

311322
function isUncheckedFile(sourceFile: SourceFile, compilerOptions: CompilerOptions): boolean {

src/services/stringCompletions.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ namespace ts.Completions.StringCompletions {
1212
}
1313
}
1414

15-
function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: Node, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined {
15+
function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined {
1616
if (completion === undefined) {
1717
return undefined;
1818
}
19+
20+
const optionalReplacementSpan = createTextSpanFromStringLiteralLikeContent(contextToken);
1921
switch (completion.kind) {
2022
case StringLiteralCompletionKind.Paths:
2123
return convertPathCompletions(completion.paths);
@@ -33,7 +35,7 @@ namespace ts.Completions.StringCompletions {
3335
CompletionKind.String,
3436
preferences
3537
); // Target will not be used, so arbitrary
36-
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, entries };
38+
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries };
3739
}
3840
case StringLiteralCompletionKind.Types: {
3941
const entries = completion.types.map(type => ({
@@ -43,7 +45,7 @@ namespace ts.Completions.StringCompletions {
4345
sortText: "0",
4446
replacementSpan: getReplacementSpanForContextToken(contextToken)
4547
}));
46-
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, entries };
48+
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: completion.isNewIdentifier, optionalReplacementSpan, entries };
4749
}
4850
default:
4951
return Debug.assertNever(completion);

src/services/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,12 @@ namespace ts {
10871087
/** Not true for all global completions. This will be true if the enclosing scope matches a few syntax kinds. See `isSnippetScope`. */
10881088
isGlobalCompletion: boolean;
10891089
isMemberCompletion: boolean;
1090+
/**
1091+
* In the absence of `CompletionEntry["replacementSpan"], the editor may choose whether to use
1092+
* this span or its default one. If `CompletionEntry["replacementSpan"]` is defined, that span
1093+
* must be used to commit that completion entry.
1094+
*/
1095+
optionalReplacementSpan?: TextSpan;
10901096

10911097
/**
10921098
* true when the current location also allows for a new identifier

src/testRunner/unittests/tsserver/completions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ namespace ts.projectSystem {
4444
isGlobalCompletion: true,
4545
isMemberCompletion: false,
4646
isNewIdentifierLocation: false,
47+
optionalReplacementSpan: { start: { line: 1, offset: 1 }, end: { line: 1, offset: 4 } },
4748
entries: [entry],
4849
});
4950

src/testRunner/unittests/tsserver/metadataInResponse.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ namespace ts.projectSystem {
8080
isGlobalCompletion: false,
8181
isMemberCompletion: true,
8282
isNewIdentifierLocation: false,
83+
optionalReplacementSpan: {
84+
start: { line: 1, offset: aTs.content.indexOf("prop;") + 1 },
85+
end: { line: 1, offset: aTs.content.indexOf("prop;") + 1 + "prop".length }
86+
},
8387
entries: expectedCompletionEntries
8488
});
8589
});

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5964,6 +5964,12 @@ declare namespace ts {
59645964
/** Not true for all global completions. This will be true if the enclosing scope matches a few syntax kinds. See `isSnippetScope`. */
59655965
isGlobalCompletion: boolean;
59665966
isMemberCompletion: boolean;
5967+
/**
5968+
* In the absence of `CompletionEntry["replacementSpan"], the editor may choose whether to use
5969+
* this span or its default one. If `CompletionEntry["replacementSpan"]` is defined, that span
5970+
* must be used to commit that completion entry.
5971+
*/
5972+
optionalReplacementSpan?: TextSpan;
59675973
/**
59685974
* true when the current location also allows for a new identifier
59695975
*/
@@ -8102,6 +8108,12 @@ declare namespace ts.server.protocol {
81028108
readonly isGlobalCompletion: boolean;
81038109
readonly isMemberCompletion: boolean;
81048110
readonly isNewIdentifierLocation: boolean;
8111+
/**
8112+
* In the absence of `CompletionEntry["replacementSpan"]`, the editor may choose whether to use
8113+
* this span or its default one. If `CompletionEntry["replacementSpan"]` is defined, that span
8114+
* must be used to commit that completion entry.
8115+
*/
8116+
readonly optionalReplacementSpan?: TextSpan;
81058117
readonly entries: readonly CompletionEntry[];
81068118
}
81078119
interface CompletionDetailsResponse extends Response {

0 commit comments

Comments
 (0)