Skip to content

Commit 423be97

Browse files
author
Andy Hanson
committed
For path completions, include extension as a kindModifier
1 parent 1c45903 commit 423be97

File tree

81 files changed

+405
-147
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+405
-147
lines changed

src/harness/fourslash.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -796,8 +796,8 @@ namespace FourSlash {
796796
}
797797

798798
private verifyCompletionEntry(actual: ts.CompletionEntry, expected: FourSlashInterface.ExpectedCompletionEntry) {
799-
const { insertText, replacementSpan, hasAction, isRecommended, kind, text, documentation, tags, source, sourceDisplay } = typeof expected === "string"
800-
? { insertText: undefined, replacementSpan: undefined, hasAction: undefined, isRecommended: undefined, kind: undefined, text: undefined, documentation: undefined, tags: undefined, source: undefined, sourceDisplay: undefined }
799+
const { insertText, replacementSpan, hasAction, isRecommended, kind, kindModifiers, text, documentation, tags, source, sourceDisplay } = typeof expected === "string"
800+
? { insertText: undefined, replacementSpan: undefined, hasAction: undefined, isRecommended: undefined, kind: undefined, kindModifiers: undefined, text: undefined, documentation: undefined, tags: undefined, source: undefined, sourceDisplay: undefined }
801801
: expected;
802802

803803
if (actual.insertText !== insertText) {
@@ -811,8 +811,12 @@ namespace FourSlash {
811811
this.raiseError(`Expected completion replacementSpan to be ${stringify(convertedReplacementSpan)}, got ${stringify(actual.replacementSpan)}`);
812812
}
813813

814-
if (kind !== undefined) assert.equal(actual.kind, kind);
815-
if (typeof expected !== "string" && "kindModifiers" in expected) assert.equal(actual.kindModifiers, expected.kindModifiers);
814+
if (kind !== undefined || kindModifiers !== undefined) {
815+
assert.equal(actual.kind, kind);
816+
if (actual.kindModifiers !== (kindModifiers || "")) {
817+
this.raiseError(`Bad kind modifiers for ${actual.name}: Expected ${kindModifiers || ""}, actual ${actual.kindModifiers}`);
818+
}
819+
}
816820

817821
assert.equal(actual.hasAction, hasAction);
818822
assert.equal(actual.isRecommended, isRecommended);
@@ -4916,7 +4920,7 @@ namespace FourSlashInterface {
49164920
readonly hasAction?: boolean, // If not specified, will assert that this is false.
49174921
readonly isRecommended?: boolean; // If not specified, will assert that this is false.
49184922
readonly kind?: string, // If not specified, won't assert about this
4919-
readonly kindModifiers?: string;
4923+
readonly kindModifiers?: string, // Must be paired with 'kind'
49204924
readonly text?: string;
49214925
readonly documentation?: string;
49224926
readonly sourceDisplay?: string;

src/services/completions.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,23 @@ namespace ts.Completions {
101101
function convertPathCompletions(pathCompletions: ReadonlyArray<PathCompletions.PathCompletion>): CompletionInfo {
102102
const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment.
103103
const isNewIdentifierLocation = true; // The user may type in a path that doesn't yet exist, creating a "new identifier" with respect to the collection of identifiers the server is aware of.
104-
const entries = pathCompletions.map(({ name, kind, span }) => ({ name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: "0", replacementSpan: span }));
104+
const entries = pathCompletions.map(({ name, kind, span, extension }): CompletionEntry =>
105+
({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: "0", replacementSpan: span }));
105106
return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries };
106107
}
108+
function kindModifiersFromExtension(extension: Extension | undefined): ScriptElementKindModifier {
109+
switch (extension) {
110+
case Extension.Dts: return ScriptElementKindModifier.dtsModifier;
111+
case Extension.Js: return ScriptElementKindModifier.jsModifier;
112+
case Extension.Json: return ScriptElementKindModifier.jsonModifier;
113+
case Extension.Jsx: return ScriptElementKindModifier.jsxModifier;
114+
case Extension.Ts: return ScriptElementKindModifier.tsModifier;
115+
case Extension.Tsx: return ScriptElementKindModifier.tsxModifier;
116+
case undefined: return ScriptElementKindModifier.none;
117+
default:
118+
return Debug.assertNever(extension);
119+
}
120+
}
107121

108122
function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo {
109123
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
@@ -638,7 +652,7 @@ namespace ts.Completions {
638652
switch (completion.kind) {
639653
case StringLiteralCompletionKind.Paths: {
640654
const match = find(completion.paths, p => p.name === name);
641-
return match && createCompletionDetails(name, ScriptElementKindModifier.none, match.kind, [textPart(name)]);
655+
return match && createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [textPart(name)]);
642656
}
643657
case StringLiteralCompletionKind.Properties: {
644658
const match = find(completion.symbols, s => s.name === name);

src/services/pathCompletions.ts

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@ namespace ts.Completions.PathCompletions {
33
export interface NameAndKind {
44
readonly name: string;
55
readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName;
6+
readonly extension: Extension | undefined;
67
}
78
export interface PathCompletion extends NameAndKind {
89
readonly span: TextSpan | undefined;
910
}
1011

11-
function nameAndKind(name: string, kind: NameAndKind["kind"]): NameAndKind {
12-
return { name, kind };
12+
function nameAndKind(name: string, kind: NameAndKind["kind"], extension: Extension | undefined): NameAndKind {
13+
return { name, kind, extension };
1314
}
15+
function directoryResult(name: string): NameAndKind {
16+
return nameAndKind(name, ScriptElementKind.directory, /*extension*/ undefined);
17+
}
18+
1419
function addReplacementSpans(text: string, textStart: number, names: ReadonlyArray<NameAndKind>): ReadonlyArray<PathCompletion> {
1520
const span = getDirectoryFragmentTextSpan(text, textStart);
16-
return names.map(({ name, kind }): PathCompletion => ({ name, kind, span }));
21+
return names.map(({ name, kind, extension }): PathCompletion => ({ name, kind, extension, span }));
1722
}
1823

1924
export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): ReadonlyArray<PathCompletion> {
@@ -129,22 +134,19 @@ namespace ts.Completions.PathCompletions {
129134
*
130135
* both foo.ts and foo.tsx become foo
131136
*/
132-
const foundFiles = createMap<true>();
137+
const foundFiles = createMap<Extension | undefined>(); // maps file to its extension
133138
for (let filePath of files) {
134139
filePath = normalizePath(filePath);
135140
if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) {
136141
continue;
137142
}
138143

139144
const foundFileName = includeExtensions || fileExtensionIs(filePath, Extension.Json) ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath));
140-
141-
if (!foundFiles.has(foundFileName)) {
142-
foundFiles.set(foundFileName, true);
143-
}
145+
foundFiles.set(foundFileName, tryGetExtensionFromPath(filePath));
144146
}
145147

146-
forEachKey(foundFiles, foundFile => {
147-
result.push(nameAndKind(foundFile, ScriptElementKind.scriptElement));
148+
foundFiles.forEach((ext, foundFile) => {
149+
result.push(nameAndKind(foundFile, ScriptElementKind.scriptElement, ext));
148150
});
149151
}
150152

@@ -155,7 +157,7 @@ namespace ts.Completions.PathCompletions {
155157
for (const directory of directories) {
156158
const directoryName = getBaseFileName(normalizePath(directory));
157159
if (directoryName !== "@types") {
158-
result.push(nameAndKind(directoryName, ScriptElementKind.directory));
160+
result.push(directoryResult(directoryName));
159161
}
160162
}
161163
}
@@ -183,10 +185,10 @@ namespace ts.Completions.PathCompletions {
183185
if (!hasProperty(paths, path)) continue;
184186
const patterns = paths[path];
185187
if (patterns) {
186-
for (const { name, kind } of getCompletionsForPathMapping(path, patterns, fragment, baseDirectory, fileExtensions, host)) {
188+
for (const { name, kind, extension } of getCompletionsForPathMapping(path, patterns, fragment, baseDirectory, fileExtensions, host)) {
187189
// Path mappings may provide a duplicate way to get to something we've already added, so don't add again.
188190
if (!result.some(entry => entry.name === name)) {
189-
result.push(nameAndKind(name, kind));
191+
result.push(nameAndKind(name, kind, extension));
190192
}
191193
}
192194
}
@@ -200,7 +202,7 @@ namespace ts.Completions.PathCompletions {
200202
* Modules from node_modules (i.e. those listed in package.json)
201203
* This includes all files that are found in node_modules/moduleName/ with acceptable file extensions
202204
*/
203-
function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): NameAndKind[] {
205+
function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): ReadonlyArray<NameAndKind> {
204206
const { baseUrl, paths } = compilerOptions;
205207

206208
const result: NameAndKind[] = [];
@@ -217,7 +219,7 @@ namespace ts.Completions.PathCompletions {
217219

218220
const fragmentDirectory = getFragmentDirectory(fragment);
219221
for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
220-
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName));
222+
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
221223
}
222224

223225
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result);
@@ -230,7 +232,7 @@ namespace ts.Completions.PathCompletions {
230232
for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
231233
if (!result.some(entry => entry.name === moduleName)) {
232234
foundGlobal = true;
233-
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
235+
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
234236
}
235237
}
236238
}
@@ -265,7 +267,7 @@ namespace ts.Completions.PathCompletions {
265267
getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host));
266268

267269
function justPathMappingName(name: string): ReadonlyArray<NameAndKind> {
268-
return startsWith(name, fragment) ? [{ name, kind: ScriptElementKind.directory }] : emptyArray;
270+
return startsWith(name, fragment) ? [directoryResult(name)] : emptyArray;
269271
}
270272
}
271273

@@ -301,15 +303,21 @@ namespace ts.Completions.PathCompletions {
301303
// doesn't support. For now, this is safer but slower
302304
const includeGlob = normalizedSuffix ? "**/*" : "./*";
303305

304-
const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]).map<NameAndKind>(name => ({ name, kind: ScriptElementKind.scriptElement }));
305-
const directories = tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)).map<NameAndKind>(name => ({ name, kind: ScriptElementKind.directory }));
306-
307-
// Trim away prefix and suffix
308-
return mapDefined<NameAndKind, NameAndKind>(concatenate(matches, directories), ({ name, kind }) => {
309-
const normalizedMatch = normalizePath(name);
310-
const inner = withoutStartAndEnd(normalizedMatch, completePrefix, normalizedSuffix);
311-
return inner !== undefined ? { name: removeLeadingDirectorySeparator(removeFileExtension(inner)), kind } : undefined;
306+
const matches = mapDefined(tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]), match => {
307+
const extension = tryGetExtensionFromPath(match);
308+
const name = trimPrefixAndSuffix(match);
309+
return name === undefined ? undefined : nameAndKind(removeFileExtension(name), ScriptElementKind.scriptElement, extension);
310+
});
311+
const directories = mapDefined(tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)), dir => {
312+
const name = trimPrefixAndSuffix(dir);
313+
return name === undefined ? undefined : directoryResult(name);
312314
});
315+
return [...matches, ...directories];
316+
317+
function trimPrefixAndSuffix(path: string): string | undefined {
318+
const inner = withoutStartAndEnd(normalizePath(path), completePrefix, normalizedSuffix);
319+
return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner);
320+
}
313321
}
314322

315323
function withoutStartAndEnd(s: string, start: string, end: string): string | undefined {
@@ -382,7 +390,10 @@ namespace ts.Completions.PathCompletions {
382390
if (options.types && !contains(options.types, packageName)) continue;
383391

384392
if (fragmentDirectory === undefined) {
385-
pushResult(packageName);
393+
if (!seen.has(packageName)) {
394+
result.push(nameAndKind(packageName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
395+
seen.set(packageName, true);
396+
}
386397
}
387398
else {
388399
const baseDirectory = combinePaths(directory, typeDirectoryName);
@@ -393,13 +404,6 @@ namespace ts.Completions.PathCompletions {
393404
}
394405
}
395406
}
396-
397-
function pushResult(moduleName: string) {
398-
if (!seen.has(moduleName)) {
399-
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
400-
seen.set(moduleName, true);
401-
}
402-
}
403407
}
404408

405409
function findPackageJsons(directory: string, host: LanguageServiceHost): string[] {

src/services/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,14 @@ namespace ts {
11261126
ambientModifier = "declare",
11271127
staticModifier = "static",
11281128
abstractModifier = "abstract",
1129-
optionalModifier = "optional"
1129+
optionalModifier = "optional",
1130+
1131+
dtsModifier = ".d.ts",
1132+
tsModifier = ".ts",
1133+
tsxModifier = ".tsx",
1134+
jsModifier = ".js",
1135+
jsxModifier = ".jsx",
1136+
jsonModifier = ".json",
11301137
}
11311138

11321139
export const enum ClassificationTypeNames {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5369,7 +5369,13 @@ declare namespace ts {
53695369
ambientModifier = "declare",
53705370
staticModifier = "static",
53715371
abstractModifier = "abstract",
5372-
optionalModifier = "optional"
5372+
optionalModifier = "optional",
5373+
dtsModifier = ".d.ts",
5374+
tsModifier = ".ts",
5375+
tsxModifier = ".tsx",
5376+
jsModifier = ".js",
5377+
jsxModifier = ".jsx",
5378+
jsonModifier = ".json"
53735379
}
53745380
enum ClassificationTypeNames {
53755381
comment = "comment",

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5369,7 +5369,13 @@ declare namespace ts {
53695369
ambientModifier = "declare",
53705370
staticModifier = "static",
53715371
abstractModifier = "abstract",
5372-
optionalModifier = "optional"
5372+
optionalModifier = "optional",
5373+
dtsModifier = ".d.ts",
5374+
tsModifier = ".ts",
5375+
tsxModifier = ".tsx",
5376+
jsModifier = ".js",
5377+
jsxModifier = ".jsx",
5378+
jsonModifier = ".json"
53735379
}
53745380
enum ClassificationTypeNames {
53755381
comment = "comment",

tests/cases/fourslash/completionEntryForClassMembers2.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,18 @@
189189
////}
190190

191191
const validInstanceMembersOfBaseClassB: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
192-
{ name: "protectedMethod", text: "(method) B.protectedMethod(): void" },
192+
{ name: "protectedMethod", text: "(method) B.protectedMethod(): void", kindModifiers: "protected" },
193193
{ name: "getValue", text: "(method) B.getValue(): string | boolean" },
194194
];
195195
const validStaticMembersOfBaseClassB: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
196-
{ name: "staticMethod", text: "(method) B.staticMethod(): void" },
196+
{ name: "staticMethod", text: "(method) B.staticMethod(): void", kindModifiers: "static" },
197197
];
198198
const privateMembersOfBaseClassB: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
199199
{ name: "privateMethod", text: "(method) B.privateMethod(): void" },
200200
];
201201
const protectedPropertiesOfBaseClassB0: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
202-
{ name: "protectedMethod", text: "(method) B0.protectedMethod(): void" },
203-
{ name: "protectedMethod1", text: "(method) B0.protectedMethod1(): void" },
202+
{ name: "protectedMethod", text: "(method) B0.protectedMethod(): void", kindModifiers: "protected" },
203+
{ name: "protectedMethod1", text: "(method) B0.protectedMethod1(): void", kindModifiers: "protected" },
204204
];
205205
const publicPropertiesOfBaseClassB0: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
206206
{ name: "getValue", text: "(method) B0.getValue(): string | boolean" },
@@ -214,12 +214,12 @@ const validInstanceMembersOfBaseClassB0_2 : ReadonlyArray<FourSlashInterface.Exp
214214
publicPropertiesOfBaseClassB0[1],
215215
];
216216
const validStaticMembersOfBaseClassB0: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
217-
{ name: "staticMethod", text: "(method) B0.staticMethod(): void" },
218-
{ name: "staticMethod1", text: "(method) B0.staticMethod1(): void" },
217+
{ name: "staticMethod", text: "(method) B0.staticMethod(): void", kindModifiers: "static" },
218+
{ name: "staticMethod1", text: "(method) B0.staticMethod1(): void", kindModifiers: "static" },
219219
];
220220
const privateMembersOfBaseClassB0: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
221-
{ name: "privateMethod", text: "(method) B0.privateMethod(): void" },
222-
{ name: "privateMethod1", text: "(method) B0.privateMethod1(): void" },
221+
{ name: "privateMethod", text: "(method) B0.privateMethod(): void", kindModifiers: "private" },
222+
{ name: "privateMethod1", text: "(method) B0.privateMethod1(): void", kindModifiers: "private" },
223223
];
224224
const membersOfI: ReadonlyArray<FourSlashInterface.ExpectedCompletionEntryObject> = [
225225
{ name: "methodOfInterface", text: "(method) I.methodOfInterface(): number" },

tests/cases/fourslash/completionForStringLiteral_details.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
////o["/*prop*/"];
1919

2020
verify.completions(
21-
{ marker: "path", includes: { name: "other", text: "other", kind: "script" }, isNewIdentifierLocation: true },
21+
{ marker: "path", includes: { name: "other", text: "other", kind: "script", kindModifiers: ".ts" }, isNewIdentifierLocation: true },
2222
{ marker: "type", exact: { name: "a", text: "a", kind: "string" } },
2323
{
2424
marker: "prop",

tests/cases/fourslash/completionInJsDocQualifiedNames.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,13 @@
1111
/////** @type {Foo./**/} */
1212
////const x = 0;
1313

14-
verify.completions({ marker: "", includes: { name: "T", text: "type T = number", documentation: "tee", kind: "type" } });
14+
verify.completions({
15+
marker: "",
16+
includes: {
17+
name: "T",
18+
text: "type T = number",
19+
documentation: "tee",
20+
kind: "type",
21+
kindModifiers: "export,declare",
22+
},
23+
});

0 commit comments

Comments
 (0)