mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-06-20 20:21:36 -05:00
For path completions, include extension as a kindModifier (#28148)
This commit is contained in:
@@ -796,8 +796,8 @@ namespace FourSlash {
|
||||
}
|
||||
|
||||
private verifyCompletionEntry(actual: ts.CompletionEntry, expected: FourSlashInterface.ExpectedCompletionEntry) {
|
||||
const { insertText, replacementSpan, hasAction, isRecommended, kind, text, documentation, tags, source, sourceDisplay } = typeof expected === "string"
|
||||
? { insertText: undefined, replacementSpan: undefined, hasAction: undefined, isRecommended: undefined, kind: undefined, text: undefined, documentation: undefined, tags: undefined, source: undefined, sourceDisplay: undefined }
|
||||
const { insertText, replacementSpan, hasAction, isRecommended, kind, kindModifiers, text, documentation, tags, source, sourceDisplay } = typeof expected === "string"
|
||||
? { insertText: undefined, replacementSpan: undefined, hasAction: undefined, isRecommended: undefined, kind: undefined, kindModifiers: undefined, text: undefined, documentation: undefined, tags: undefined, source: undefined, sourceDisplay: undefined }
|
||||
: expected;
|
||||
|
||||
if (actual.insertText !== insertText) {
|
||||
@@ -811,8 +811,12 @@ namespace FourSlash {
|
||||
this.raiseError(`Expected completion replacementSpan to be ${stringify(convertedReplacementSpan)}, got ${stringify(actual.replacementSpan)}`);
|
||||
}
|
||||
|
||||
if (kind !== undefined) assert.equal(actual.kind, kind);
|
||||
if (typeof expected !== "string" && "kindModifiers" in expected) assert.equal(actual.kindModifiers, expected.kindModifiers);
|
||||
if (kind !== undefined || kindModifiers !== undefined) {
|
||||
assert.equal(actual.kind, kind);
|
||||
if (actual.kindModifiers !== (kindModifiers || "")) {
|
||||
this.raiseError(`Bad kind modifiers for ${actual.name}: Expected ${kindModifiers || ""}, actual ${actual.kindModifiers}`);
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(actual.hasAction, hasAction);
|
||||
assert.equal(actual.isRecommended, isRecommended);
|
||||
@@ -4916,7 +4920,7 @@ namespace FourSlashInterface {
|
||||
readonly hasAction?: boolean, // If not specified, will assert that this is false.
|
||||
readonly isRecommended?: boolean; // If not specified, will assert that this is false.
|
||||
readonly kind?: string, // If not specified, won't assert about this
|
||||
readonly kindModifiers?: string;
|
||||
readonly kindModifiers?: string, // Must be paired with 'kind'
|
||||
readonly text?: string;
|
||||
readonly documentation?: string;
|
||||
readonly sourceDisplay?: string;
|
||||
|
||||
@@ -101,9 +101,23 @@ namespace ts.Completions {
|
||||
function convertPathCompletions(pathCompletions: ReadonlyArray<PathCompletions.PathCompletion>): CompletionInfo {
|
||||
const isGlobalCompletion = false; // We don't want the editor to offer any other completions, such as snippets, inside a comment.
|
||||
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.
|
||||
const entries = pathCompletions.map(({ name, kind, span }) => ({ name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: "0", replacementSpan: span }));
|
||||
const entries = pathCompletions.map(({ name, kind, span, extension }): CompletionEntry =>
|
||||
({ name, kind, kindModifiers: kindModifiersFromExtension(extension), sortText: "0", replacementSpan: span }));
|
||||
return { isGlobalCompletion, isMemberCompletion: false, isNewIdentifierLocation, entries };
|
||||
}
|
||||
function kindModifiersFromExtension(extension: Extension | undefined): ScriptElementKindModifier {
|
||||
switch (extension) {
|
||||
case Extension.Dts: return ScriptElementKindModifier.dtsModifier;
|
||||
case Extension.Js: return ScriptElementKindModifier.jsModifier;
|
||||
case Extension.Json: return ScriptElementKindModifier.jsonModifier;
|
||||
case Extension.Jsx: return ScriptElementKindModifier.jsxModifier;
|
||||
case Extension.Ts: return ScriptElementKindModifier.tsModifier;
|
||||
case Extension.Tsx: return ScriptElementKindModifier.tsxModifier;
|
||||
case undefined: return ScriptElementKindModifier.none;
|
||||
default:
|
||||
return Debug.assertNever(extension);
|
||||
}
|
||||
}
|
||||
|
||||
function jsdocCompletionInfo(entries: CompletionEntry[]): CompletionInfo {
|
||||
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
|
||||
@@ -638,7 +652,7 @@ namespace ts.Completions {
|
||||
switch (completion.kind) {
|
||||
case StringLiteralCompletionKind.Paths: {
|
||||
const match = find(completion.paths, p => p.name === name);
|
||||
return match && createCompletionDetails(name, ScriptElementKindModifier.none, match.kind, [textPart(name)]);
|
||||
return match && createCompletionDetails(name, kindModifiersFromExtension(match.extension), match.kind, [textPart(name)]);
|
||||
}
|
||||
case StringLiteralCompletionKind.Properties: {
|
||||
const match = find(completion.symbols, s => s.name === name);
|
||||
|
||||
@@ -3,17 +3,22 @@ namespace ts.Completions.PathCompletions {
|
||||
export interface NameAndKind {
|
||||
readonly name: string;
|
||||
readonly kind: ScriptElementKind.scriptElement | ScriptElementKind.directory | ScriptElementKind.externalModuleName;
|
||||
readonly extension: Extension | undefined;
|
||||
}
|
||||
export interface PathCompletion extends NameAndKind {
|
||||
readonly span: TextSpan | undefined;
|
||||
}
|
||||
|
||||
function nameAndKind(name: string, kind: NameAndKind["kind"]): NameAndKind {
|
||||
return { name, kind };
|
||||
function nameAndKind(name: string, kind: NameAndKind["kind"], extension: Extension | undefined): NameAndKind {
|
||||
return { name, kind, extension };
|
||||
}
|
||||
function directoryResult(name: string): NameAndKind {
|
||||
return nameAndKind(name, ScriptElementKind.directory, /*extension*/ undefined);
|
||||
}
|
||||
|
||||
function addReplacementSpans(text: string, textStart: number, names: ReadonlyArray<NameAndKind>): ReadonlyArray<PathCompletion> {
|
||||
const span = getDirectoryFragmentTextSpan(text, textStart);
|
||||
return names.map(({ name, kind }): PathCompletion => ({ name, kind, span }));
|
||||
return names.map(({ name, kind, extension }): PathCompletion => ({ name, kind, extension, span }));
|
||||
}
|
||||
|
||||
export function getStringLiteralCompletionsFromModuleNames(sourceFile: SourceFile, node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): ReadonlyArray<PathCompletion> {
|
||||
@@ -129,7 +134,7 @@ namespace ts.Completions.PathCompletions {
|
||||
*
|
||||
* both foo.ts and foo.tsx become foo
|
||||
*/
|
||||
const foundFiles = createMap<true>();
|
||||
const foundFiles = createMap<Extension | undefined>(); // maps file to its extension
|
||||
for (let filePath of files) {
|
||||
filePath = normalizePath(filePath);
|
||||
if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) {
|
||||
@@ -137,14 +142,11 @@ namespace ts.Completions.PathCompletions {
|
||||
}
|
||||
|
||||
const foundFileName = includeExtensions || fileExtensionIs(filePath, Extension.Json) ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath));
|
||||
|
||||
if (!foundFiles.has(foundFileName)) {
|
||||
foundFiles.set(foundFileName, true);
|
||||
}
|
||||
foundFiles.set(foundFileName, tryGetExtensionFromPath(filePath));
|
||||
}
|
||||
|
||||
forEachKey(foundFiles, foundFile => {
|
||||
result.push(nameAndKind(foundFile, ScriptElementKind.scriptElement));
|
||||
foundFiles.forEach((ext, foundFile) => {
|
||||
result.push(nameAndKind(foundFile, ScriptElementKind.scriptElement, ext));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,7 +157,7 @@ namespace ts.Completions.PathCompletions {
|
||||
for (const directory of directories) {
|
||||
const directoryName = getBaseFileName(normalizePath(directory));
|
||||
if (directoryName !== "@types") {
|
||||
result.push(nameAndKind(directoryName, ScriptElementKind.directory));
|
||||
result.push(directoryResult(directoryName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,10 +185,10 @@ namespace ts.Completions.PathCompletions {
|
||||
if (!hasProperty(paths, path)) continue;
|
||||
const patterns = paths[path];
|
||||
if (patterns) {
|
||||
for (const { name, kind } of getCompletionsForPathMapping(path, patterns, fragment, baseDirectory, fileExtensions, host)) {
|
||||
for (const { name, kind, extension } of getCompletionsForPathMapping(path, patterns, fragment, baseDirectory, fileExtensions, host)) {
|
||||
// Path mappings may provide a duplicate way to get to something we've already added, so don't add again.
|
||||
if (!result.some(entry => entry.name === name)) {
|
||||
result.push(nameAndKind(name, kind));
|
||||
result.push(nameAndKind(name, kind, extension));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +202,7 @@ namespace ts.Completions.PathCompletions {
|
||||
* Modules from node_modules (i.e. those listed in package.json)
|
||||
* This includes all files that are found in node_modules/moduleName/ with acceptable file extensions
|
||||
*/
|
||||
function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): NameAndKind[] {
|
||||
function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): ReadonlyArray<NameAndKind> {
|
||||
const { baseUrl, paths } = compilerOptions;
|
||||
|
||||
const result: NameAndKind[] = [];
|
||||
@@ -217,7 +219,7 @@ namespace ts.Completions.PathCompletions {
|
||||
|
||||
const fragmentDirectory = getFragmentDirectory(fragment);
|
||||
for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
|
||||
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName));
|
||||
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
|
||||
}
|
||||
|
||||
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result);
|
||||
@@ -230,7 +232,7 @@ namespace ts.Completions.PathCompletions {
|
||||
for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
|
||||
if (!result.some(entry => entry.name === moduleName)) {
|
||||
foundGlobal = true;
|
||||
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
|
||||
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,7 +267,7 @@ namespace ts.Completions.PathCompletions {
|
||||
getModulesForPathsPattern(remainingFragment, baseUrl, pattern, fileExtensions, host));
|
||||
|
||||
function justPathMappingName(name: string): ReadonlyArray<NameAndKind> {
|
||||
return startsWith(name, fragment) ? [{ name, kind: ScriptElementKind.directory }] : emptyArray;
|
||||
return startsWith(name, fragment) ? [directoryResult(name)] : emptyArray;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,15 +303,21 @@ namespace ts.Completions.PathCompletions {
|
||||
// doesn't support. For now, this is safer but slower
|
||||
const includeGlob = normalizedSuffix ? "**/*" : "./*";
|
||||
|
||||
const matches = tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]).map<NameAndKind>(name => ({ name, kind: ScriptElementKind.scriptElement }));
|
||||
const directories = tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)).map<NameAndKind>(name => ({ name, kind: ScriptElementKind.directory }));
|
||||
|
||||
// Trim away prefix and suffix
|
||||
return mapDefined<NameAndKind, NameAndKind>(concatenate(matches, directories), ({ name, kind }) => {
|
||||
const normalizedMatch = normalizePath(name);
|
||||
const inner = withoutStartAndEnd(normalizedMatch, completePrefix, normalizedSuffix);
|
||||
return inner !== undefined ? { name: removeLeadingDirectorySeparator(removeFileExtension(inner)), kind } : undefined;
|
||||
const matches = mapDefined(tryReadDirectory(host, baseDirectory, fileExtensions, /*exclude*/ undefined, [includeGlob]), match => {
|
||||
const extension = tryGetExtensionFromPath(match);
|
||||
const name = trimPrefixAndSuffix(match);
|
||||
return name === undefined ? undefined : nameAndKind(removeFileExtension(name), ScriptElementKind.scriptElement, extension);
|
||||
});
|
||||
const directories = mapDefined(tryGetDirectories(host, baseDirectory).map(d => combinePaths(baseDirectory, d)), dir => {
|
||||
const name = trimPrefixAndSuffix(dir);
|
||||
return name === undefined ? undefined : directoryResult(name);
|
||||
});
|
||||
return [...matches, ...directories];
|
||||
|
||||
function trimPrefixAndSuffix(path: string): string | undefined {
|
||||
const inner = withoutStartAndEnd(normalizePath(path), completePrefix, normalizedSuffix);
|
||||
return inner === undefined ? undefined : removeLeadingDirectorySeparator(inner);
|
||||
}
|
||||
}
|
||||
|
||||
function withoutStartAndEnd(s: string, start: string, end: string): string | undefined {
|
||||
@@ -382,7 +390,10 @@ namespace ts.Completions.PathCompletions {
|
||||
if (options.types && !contains(options.types, packageName)) continue;
|
||||
|
||||
if (fragmentDirectory === undefined) {
|
||||
pushResult(packageName);
|
||||
if (!seen.has(packageName)) {
|
||||
result.push(nameAndKind(packageName, ScriptElementKind.externalModuleName, /*extension*/ undefined));
|
||||
seen.set(packageName, true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const baseDirectory = combinePaths(directory, typeDirectoryName);
|
||||
@@ -393,13 +404,6 @@ namespace ts.Completions.PathCompletions {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pushResult(moduleName: string) {
|
||||
if (!seen.has(moduleName)) {
|
||||
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
|
||||
seen.set(moduleName, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findPackageJsons(directory: string, host: LanguageServiceHost): string[] {
|
||||
|
||||
@@ -1126,7 +1126,14 @@ namespace ts {
|
||||
ambientModifier = "declare",
|
||||
staticModifier = "static",
|
||||
abstractModifier = "abstract",
|
||||
optionalModifier = "optional"
|
||||
optionalModifier = "optional",
|
||||
|
||||
dtsModifier = ".d.ts",
|
||||
tsModifier = ".ts",
|
||||
tsxModifier = ".tsx",
|
||||
jsModifier = ".js",
|
||||
jsxModifier = ".jsx",
|
||||
jsonModifier = ".json",
|
||||
}
|
||||
|
||||
export const enum ClassificationTypeNames {
|
||||
|
||||
Reference in New Issue
Block a user