mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-15 11:35:42 -06:00
1782 lines
87 KiB
TypeScript
1782 lines
87 KiB
TypeScript
/* @internal */
|
|
namespace ts.Completions {
|
|
export type Log = (message: string) => void;
|
|
|
|
export function getCompletionsAtPosition(host: LanguageServiceHost, typeChecker: TypeChecker, log: Log, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number): CompletionInfo | undefined {
|
|
if (isInReferenceComment(sourceFile, position)) {
|
|
return getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host);
|
|
}
|
|
|
|
if (isInString(sourceFile, position)) {
|
|
return getStringLiteralCompletionEntries(sourceFile, position, typeChecker, compilerOptions, host, log);
|
|
}
|
|
|
|
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
|
|
if (!completionData) {
|
|
return undefined;
|
|
}
|
|
|
|
const { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isJsDocTagName } = completionData;
|
|
|
|
if (isJsDocTagName) {
|
|
// If the current position is a jsDoc tag name, only tag names should be provided for completion
|
|
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries: JsDoc.getAllJsDocCompletionEntries() };
|
|
}
|
|
|
|
const entries: CompletionEntry[] = [];
|
|
|
|
if (isSourceFileJavaScript(sourceFile)) {
|
|
const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
|
|
addRange(entries, getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target));
|
|
}
|
|
else {
|
|
if (!symbols || symbols.length === 0) {
|
|
if (sourceFile.languageVariant === LanguageVariant.JSX &&
|
|
location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) {
|
|
// In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag,
|
|
// instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element.
|
|
// For example:
|
|
// var x = <div> </ /*1*/> completion list at "1" will contain "div" with type any
|
|
const tagName = (<JsxElement>location.parent.parent).openingElement.tagName;
|
|
entries.push({
|
|
name: (<Identifier>tagName).text,
|
|
kind: undefined,
|
|
kindModifiers: undefined,
|
|
sortText: "0",
|
|
});
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log);
|
|
}
|
|
|
|
// Add keywords if this is not a member completion list
|
|
if (!isMemberCompletion && !isJsDocTagName) {
|
|
addRange(entries, keywordCompletions);
|
|
}
|
|
|
|
return { isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation: isNewIdentifierLocation, entries };
|
|
}
|
|
|
|
function getJavaScriptCompletionEntries(sourceFile: SourceFile, position: number, uniqueNames: Map<string>, target: ScriptTarget): CompletionEntry[] {
|
|
const entries: CompletionEntry[] = [];
|
|
|
|
const nameTable = getNameTable(sourceFile);
|
|
nameTable.forEach((pos, name) => {
|
|
// Skip identifiers produced only from the current location
|
|
if (pos === position) {
|
|
return;
|
|
}
|
|
|
|
if (!uniqueNames.get(name)) {
|
|
uniqueNames.set(name, name);
|
|
const displayName = getCompletionEntryDisplayName(unescapeIdentifier(name), target, /*performCharacterChecks*/ true);
|
|
if (displayName) {
|
|
const entry = {
|
|
name: displayName,
|
|
kind: ScriptElementKind.warning,
|
|
kindModifiers: "",
|
|
sortText: "1"
|
|
};
|
|
entries.push(entry);
|
|
}
|
|
}
|
|
});
|
|
|
|
return entries;
|
|
}
|
|
|
|
function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget): CompletionEntry {
|
|
// Try to get a valid display name for this symbol, if we could not find one, then ignore it.
|
|
// We would like to only show things that can be added after a dot, so for instance numeric properties can
|
|
// not be accessed with a dot (a.1 <- invalid)
|
|
const displayName = getCompletionEntryDisplayNameForSymbol(typeChecker, symbol, target, performCharacterChecks, location);
|
|
if (!displayName) {
|
|
return undefined;
|
|
}
|
|
|
|
// TODO(drosen): Right now we just permit *all* semantic meanings when calling
|
|
// 'getSymbolKind' which is permissible given that it is backwards compatible; but
|
|
// really we should consider passing the meaning for the node so that we don't report
|
|
// that a suggestion for a value is an interface. We COULD also just do what
|
|
// 'getSymbolModifiers' does, which is to use the first declaration.
|
|
|
|
// Use a 'sortText' of 0' so that all symbol completion entries come before any other
|
|
// entries (like JavaScript identifier entries).
|
|
return {
|
|
name: displayName,
|
|
kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location),
|
|
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
|
|
sortText: "0",
|
|
};
|
|
}
|
|
|
|
function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: Push<CompletionEntry>, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, log: Log): Map<string> {
|
|
const start = timestamp();
|
|
const uniqueNames = createMap<string>();
|
|
if (symbols) {
|
|
for (const symbol of symbols) {
|
|
const entry = createCompletionEntry(symbol, location, performCharacterChecks, typeChecker, target);
|
|
if (entry) {
|
|
const id = escapeIdentifier(entry.name);
|
|
if (!uniqueNames.get(id)) {
|
|
entries.push(entry);
|
|
uniqueNames.set(id, id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start));
|
|
return uniqueNames;
|
|
}
|
|
|
|
function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined {
|
|
const node = findPrecedingToken(position, sourceFile);
|
|
if (!node || node.kind !== SyntaxKind.StringLiteral) {
|
|
return undefined;
|
|
}
|
|
|
|
if (node.parent.kind === SyntaxKind.PropertyAssignment &&
|
|
node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression &&
|
|
(<PropertyAssignment>node.parent).name === node) {
|
|
// Get quoted name of properties of the object literal expression
|
|
// i.e. interface ConfigFiles {
|
|
// 'jspm:dev': string
|
|
// }
|
|
// let files: ConfigFiles = {
|
|
// '/*completion position*/'
|
|
// }
|
|
//
|
|
// function foo(c: ConfigFiles) {}
|
|
// foo({
|
|
// '/*completion position*/'
|
|
// });
|
|
return getStringLiteralCompletionEntriesFromPropertyAssignment(<ObjectLiteralElement>node.parent, typeChecker, compilerOptions.target, log);
|
|
}
|
|
else if (isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) {
|
|
// Get all names of properties on the expression
|
|
// i.e. interface A {
|
|
// 'prop1': string
|
|
// }
|
|
// let a: A;
|
|
// a['/*completion position*/']
|
|
return getStringLiteralCompletionEntriesFromElementAccess(node.parent, typeChecker, compilerOptions.target, log);
|
|
}
|
|
else if (node.parent.kind === SyntaxKind.ImportDeclaration || isExpressionOfExternalModuleImportEqualsDeclaration(node) || isRequireCall(node.parent, false)) {
|
|
// Get all known external module names or complete a path to a module
|
|
// i.e. import * as ns from "/*completion position*/";
|
|
// import x = require("/*completion position*/");
|
|
// var y = require("/*completion position*/");
|
|
return getStringLiteralCompletionEntriesFromModuleNames(<StringLiteral>node, compilerOptions, host, typeChecker);
|
|
}
|
|
else if (isEqualityExpression(node.parent)) {
|
|
// Get completions from the type of the other operand
|
|
// i.e. switch (a) {
|
|
// case '/*completion position*/'
|
|
// }
|
|
return getStringLiteralCompletionEntriesFromType(typeChecker.getTypeAtLocation(node.parent.left === node ? node.parent.right : node.parent.left), typeChecker);
|
|
}
|
|
else if (isCaseOrDefaultClause(node.parent)) {
|
|
// Get completions from the type of the switch expression
|
|
// i.e. x === '/*completion position'
|
|
return getStringLiteralCompletionEntriesFromType(typeChecker.getTypeAtLocation((<SwitchStatement>node.parent.parent.parent).expression), typeChecker);
|
|
}
|
|
else {
|
|
const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile);
|
|
if (argumentInfo) {
|
|
// Get string literal completions from specialized signatures of the target
|
|
// i.e. declare function f(a: 'A');
|
|
// f("/*completion position*/")
|
|
return getStringLiteralCompletionEntriesFromCallExpression(argumentInfo, typeChecker);
|
|
}
|
|
|
|
// Get completion for string literal from string literal type
|
|
// i.e. var x: "hi" | "hello" = "/*completion position*/"
|
|
return getStringLiteralCompletionEntriesFromType(typeChecker.getContextualType(<StringLiteral>node), typeChecker);
|
|
}
|
|
}
|
|
|
|
function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
|
|
const type = typeChecker.getContextualType((<ObjectLiteralExpression>element.parent));
|
|
const entries: CompletionEntry[] = [];
|
|
if (type) {
|
|
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, /*performCharacterChecks*/false, typeChecker, target, log);
|
|
if (entries.length) {
|
|
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
|
|
}
|
|
}
|
|
}
|
|
|
|
function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo, typeChecker: TypeChecker): CompletionInfo | undefined {
|
|
const candidates: Signature[] = [];
|
|
const entries: CompletionEntry[] = [];
|
|
|
|
typeChecker.getResolvedSignature(argumentInfo.invocation, candidates);
|
|
|
|
for (const candidate of candidates) {
|
|
addStringLiteralCompletionsFromType(typeChecker.getParameterType(candidate, argumentInfo.argumentIndex), entries, typeChecker);
|
|
}
|
|
|
|
if (entries.length) {
|
|
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: true, entries };
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function getStringLiteralCompletionEntriesFromElementAccess(node: ElementAccessExpression, typeChecker: TypeChecker, target: ScriptTarget, log: Log): CompletionInfo | undefined {
|
|
const type = typeChecker.getTypeAtLocation(node.expression);
|
|
const entries: CompletionEntry[] = [];
|
|
if (type) {
|
|
getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, node, /*performCharacterChecks*/false, typeChecker, target, log);
|
|
if (entries.length) {
|
|
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: true, entries };
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getStringLiteralCompletionEntriesFromType(type: Type, typeChecker: TypeChecker): CompletionInfo | undefined {
|
|
if (type) {
|
|
const entries: CompletionEntry[] = [];
|
|
addStringLiteralCompletionsFromType(type, entries, typeChecker);
|
|
if (entries.length) {
|
|
return { isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, entries };
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function addStringLiteralCompletionsFromType(type: Type, result: Push<CompletionEntry>, typeChecker: TypeChecker): void {
|
|
if (type && type.flags & TypeFlags.TypeParameter) {
|
|
type = typeChecker.getApparentType(type);
|
|
}
|
|
if (!type) {
|
|
return;
|
|
}
|
|
if (type.flags & TypeFlags.Union) {
|
|
for (const t of (<UnionType>type).types) {
|
|
addStringLiteralCompletionsFromType(t, result, typeChecker);
|
|
}
|
|
}
|
|
else if (type.flags & TypeFlags.StringLiteral) {
|
|
result.push({
|
|
name: (<LiteralType>type).text,
|
|
kindModifiers: ScriptElementKindModifier.none,
|
|
kind: ScriptElementKind.variableElement,
|
|
sortText: "0"
|
|
});
|
|
}
|
|
}
|
|
|
|
function getStringLiteralCompletionEntriesFromModuleNames(node: StringLiteral, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionInfo {
|
|
const literalValue = normalizeSlashes(node.text);
|
|
|
|
const scriptPath = node.getSourceFile().path;
|
|
const scriptDirectory = getDirectoryPath(scriptPath);
|
|
|
|
const span = getDirectoryFragmentTextSpan((<StringLiteral>node).text, node.getStart() + 1);
|
|
let entries: CompletionEntry[];
|
|
if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) {
|
|
const extensions = getSupportedExtensions(compilerOptions);
|
|
if (compilerOptions.rootDirs) {
|
|
entries = getCompletionEntriesForDirectoryFragmentWithRootDirs(
|
|
compilerOptions.rootDirs, literalValue, scriptDirectory, extensions, /*includeExtensions*/false, span, compilerOptions, host, scriptPath);
|
|
}
|
|
else {
|
|
entries = getCompletionEntriesForDirectoryFragment(
|
|
literalValue, scriptDirectory, extensions, /*includeExtensions*/false, span, host, scriptPath);
|
|
}
|
|
}
|
|
else {
|
|
// Check for node modules
|
|
entries = getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, span, compilerOptions, host, typeChecker);
|
|
}
|
|
return {
|
|
isGlobalCompletion: false,
|
|
isMemberCompletion: false,
|
|
isNewIdentifierLocation: true,
|
|
entries
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Takes a script path and returns paths for all potential folders that could be merged with its
|
|
* containing folder via the "rootDirs" compiler option
|
|
*/
|
|
function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptPath: string, ignoreCase: boolean): string[] {
|
|
// Make all paths absolute/normalized if they are not already
|
|
rootDirs = map(rootDirs, rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory)));
|
|
|
|
// Determine the path to the directory containing the script relative to the root directory it is contained within
|
|
let relativeDirectory: string;
|
|
for (const rootDirectory of rootDirs) {
|
|
if (containsPath(rootDirectory, scriptPath, basePath, ignoreCase)) {
|
|
relativeDirectory = scriptPath.substr(rootDirectory.length);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Now find a path for each potential directory that is to be merged with the one containing the script
|
|
return deduplicate(map(rootDirs, rootDirectory => combinePaths(rootDirectory, relativeDirectory)));
|
|
}
|
|
|
|
function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, exclude?: string): CompletionEntry[] {
|
|
const basePath = compilerOptions.project || host.getCurrentDirectory();
|
|
const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
|
|
const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptPath, ignoreCase);
|
|
|
|
const result: CompletionEntry[] = [];
|
|
|
|
for (const baseDirectory of baseDirectories) {
|
|
getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensions, includeExtensions, span, host, exclude, result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename.
|
|
*/
|
|
function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean, span: TextSpan, host: LanguageServiceHost, exclude?: string, result: CompletionEntry[] = []): CompletionEntry[] {
|
|
if (fragment === undefined) {
|
|
fragment = "";
|
|
}
|
|
|
|
fragment = normalizeSlashes(fragment);
|
|
|
|
/**
|
|
* Remove the basename from the path. Note that we don't use the basename to filter completions;
|
|
* the client is responsible for refining completions.
|
|
*/
|
|
fragment = getDirectoryPath(fragment);
|
|
|
|
if (fragment === "") {
|
|
fragment = "." + directorySeparator;
|
|
}
|
|
|
|
fragment = ensureTrailingDirectorySeparator(fragment);
|
|
|
|
const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment));
|
|
const baseDirectory = getDirectoryPath(absolutePath);
|
|
const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
|
|
|
|
if (tryDirectoryExists(host, baseDirectory)) {
|
|
// Enumerate the available files if possible
|
|
const files = tryReadDirectory(host, baseDirectory, extensions, /*exclude*/undefined, /*include*/["./*"]);
|
|
|
|
if (files) {
|
|
/**
|
|
* Multiple file entries might map to the same truncated name once we remove extensions
|
|
* (happens iff includeExtensions === false)so we use a set-like data structure. Eg:
|
|
*
|
|
* both foo.ts and foo.tsx become foo
|
|
*/
|
|
const foundFiles = createMap<boolean>();
|
|
for (let filePath of files) {
|
|
filePath = normalizePath(filePath);
|
|
if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) {
|
|
continue;
|
|
}
|
|
|
|
const foundFileName = includeExtensions ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath));
|
|
|
|
if (!foundFiles.get(foundFileName)) {
|
|
foundFiles.set(foundFileName, true);
|
|
}
|
|
}
|
|
|
|
forEachKey(foundFiles, foundFile => {
|
|
result.push(createCompletionEntryForModule(foundFile, ScriptElementKind.scriptElement, span));
|
|
});
|
|
}
|
|
|
|
// If possible, get folder completion as well
|
|
const directories = tryGetDirectories(host, baseDirectory);
|
|
|
|
if (directories) {
|
|
for (const directory of directories) {
|
|
const directoryName = getBaseFileName(normalizePath(directory));
|
|
|
|
result.push(createCompletionEntryForModule(directoryName, ScriptElementKind.directory, span));
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Check all of the declared modules and those in node modules. Possible sources of modules:
|
|
* Modules that are found by the type checker
|
|
* Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option)
|
|
* 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, span: TextSpan, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
|
|
const { baseUrl, paths } = compilerOptions;
|
|
|
|
let result: CompletionEntry[];
|
|
|
|
if (baseUrl) {
|
|
const fileExtensions = getSupportedExtensions(compilerOptions);
|
|
const projectDir = compilerOptions.project || host.getCurrentDirectory();
|
|
const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(projectDir, baseUrl);
|
|
result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false, span, host);
|
|
|
|
if (paths) {
|
|
for (const path in paths) {
|
|
if (paths.hasOwnProperty(path)) {
|
|
if (path === "*") {
|
|
if (paths[path]) {
|
|
for (const pattern of paths[path]) {
|
|
for (const match of getModulesForPathsPattern(fragment, baseUrl, pattern, fileExtensions, host)) {
|
|
result.push(createCompletionEntryForModule(match, ScriptElementKind.externalModuleName, span));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (startsWith(path, fragment)) {
|
|
const entry = paths[path] && paths[path].length === 1 && paths[path][0];
|
|
if (entry) {
|
|
result.push(createCompletionEntryForModule(path, ScriptElementKind.externalModuleName, span));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
result = [];
|
|
}
|
|
|
|
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span, result);
|
|
|
|
for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions, typeChecker, host)) {
|
|
result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: string[], host: LanguageServiceHost): string[] {
|
|
if (host.readDirectory) {
|
|
const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined;
|
|
if (parsed) {
|
|
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
|
|
// full directory component. For example: directory/path/of/prefix/base*
|
|
const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix);
|
|
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
|
|
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
|
|
|
|
const fragmentHasPath = fragment.indexOf(directorySeparator) !== -1;
|
|
|
|
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
|
|
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
|
|
|
|
const normalizedSuffix = normalizePath(parsed.suffix);
|
|
const baseDirectory = combinePaths(baseUrl, expandedPrefixDirectory);
|
|
const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
|
|
|
|
// If we have a suffix, then we need to read the directory all the way down. We could create a glob
|
|
// that encodes the suffix, but we would have to escape the character "?" which readDirectory
|
|
// doesn't support. For now, this is safer but slower
|
|
const includeGlob = normalizedSuffix ? "**/*" : "./*";
|
|
|
|
const matches = tryReadDirectory(host, baseDirectory, fileExtensions, undefined, [includeGlob]);
|
|
if (matches) {
|
|
const result: string[] = [];
|
|
|
|
// Trim away prefix and suffix
|
|
for (const match of matches) {
|
|
const normalizedMatch = normalizePath(match);
|
|
if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) {
|
|
continue;
|
|
}
|
|
|
|
const start = completePrefix.length;
|
|
const length = normalizedMatch.length - start - normalizedSuffix.length;
|
|
|
|
result.push(removeFileExtension(normalizedMatch.substr(start, length)));
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions, typeChecker: TypeChecker, host: LanguageServiceHost): string[] {
|
|
// Check If this is a nested module
|
|
const isNestedModule = fragment.indexOf(directorySeparator) !== -1;
|
|
const moduleNameFragment = isNestedModule ? fragment.substr(0, fragment.lastIndexOf(directorySeparator)) : undefined;
|
|
|
|
// Get modules that the type checker picked up
|
|
const ambientModules = map(typeChecker.getAmbientModules(), sym => stripQuotes(sym.name));
|
|
let nonRelativeModules = filter(ambientModules, moduleName => startsWith(moduleName, fragment));
|
|
|
|
// Nested modules of the form "module-name/sub" need to be adjusted to only return the string
|
|
// after the last '/' that appears in the fragment because that's where the replacement span
|
|
// starts
|
|
if (isNestedModule) {
|
|
const moduleNameWithSeperator = ensureTrailingDirectorySeparator(moduleNameFragment);
|
|
nonRelativeModules = map(nonRelativeModules, moduleName => {
|
|
if (startsWith(fragment, moduleNameWithSeperator)) {
|
|
return moduleName.substr(moduleNameWithSeperator.length);
|
|
}
|
|
return moduleName;
|
|
});
|
|
}
|
|
|
|
|
|
if (!options.moduleResolution || options.moduleResolution === ModuleResolutionKind.NodeJs) {
|
|
for (const visibleModule of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
|
|
if (!isNestedModule) {
|
|
nonRelativeModules.push(visibleModule.moduleName);
|
|
}
|
|
else if (startsWith(visibleModule.moduleName, moduleNameFragment)) {
|
|
const nestedFiles = tryReadDirectory(host, visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/undefined, /*include*/["./*"]);
|
|
if (nestedFiles) {
|
|
for (let f of nestedFiles) {
|
|
f = normalizePath(f);
|
|
const nestedModule = removeFileExtension(getBaseFileName(f));
|
|
nonRelativeModules.push(nestedModule);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return deduplicate(nonRelativeModules);
|
|
}
|
|
|
|
function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): CompletionInfo {
|
|
const token = getTokenAtPosition(sourceFile, position);
|
|
if (!token) {
|
|
return undefined;
|
|
}
|
|
const commentRanges: CommentRange[] = getLeadingCommentRanges(sourceFile.text, token.pos);
|
|
|
|
if (!commentRanges || !commentRanges.length) {
|
|
return undefined;
|
|
}
|
|
|
|
const range = forEach(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end && commentRange);
|
|
|
|
if (!range) {
|
|
return undefined;
|
|
}
|
|
|
|
const completionInfo: CompletionInfo = {
|
|
/**
|
|
* We don't want the editor to offer any other completions, such as snippets, inside a comment.
|
|
*/
|
|
isGlobalCompletion: false,
|
|
isMemberCompletion: false,
|
|
/**
|
|
* 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.
|
|
*/
|
|
isNewIdentifierLocation: true,
|
|
|
|
entries: []
|
|
};
|
|
|
|
const text = sourceFile.text.substr(range.pos, position - range.pos);
|
|
|
|
const match = tripleSlashDirectiveFragmentRegex.exec(text);
|
|
|
|
if (match) {
|
|
const prefix = match[1];
|
|
const kind = match[2];
|
|
const toComplete = match[3];
|
|
|
|
const scriptPath = getDirectoryPath(sourceFile.path);
|
|
if (kind === "path") {
|
|
// Give completions for a relative path
|
|
const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length);
|
|
completionInfo.entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/true, span, host, sourceFile.path);
|
|
}
|
|
else {
|
|
// Give completions based on the typings available
|
|
const span: TextSpan = { start: range.pos + prefix.length, length: match[0].length - prefix.length };
|
|
completionInfo.entries = getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span);
|
|
}
|
|
}
|
|
|
|
return completionInfo;
|
|
}
|
|
|
|
function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: CompletionEntry[] = []): CompletionEntry[] {
|
|
// Check for typings specified in compiler options
|
|
if (options.types) {
|
|
for (const moduleName of options.types) {
|
|
result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span));
|
|
}
|
|
}
|
|
else if (host.getDirectories) {
|
|
let typeRoots: string[];
|
|
try {
|
|
// Wrap in try catch because getEffectiveTypeRoots touches the filesystem
|
|
typeRoots = getEffectiveTypeRoots(options, host);
|
|
}
|
|
catch (e) {}
|
|
|
|
if (typeRoots) {
|
|
for (const root of typeRoots) {
|
|
getCompletionEntriesFromDirectories(host, root, span, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (host.getDirectories) {
|
|
// Also get all @types typings installed in visible node_modules directories
|
|
for (const packageJson of findPackageJsons(scriptPath, host)) {
|
|
const typesDir = combinePaths(getDirectoryPath(packageJson), "node_modules/@types");
|
|
getCompletionEntriesFromDirectories(host, typesDir, span, result);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getCompletionEntriesFromDirectories(host: LanguageServiceHost, directory: string, span: TextSpan, result: Push<CompletionEntry>) {
|
|
if (host.getDirectories && tryDirectoryExists(host, directory)) {
|
|
const directories = tryGetDirectories(host, directory);
|
|
if (directories) {
|
|
for (let typeDirectory of directories) {
|
|
typeDirectory = normalizePath(typeDirectory);
|
|
result.push(createCompletionEntryForModule(getBaseFileName(typeDirectory), ScriptElementKind.externalModuleName, span));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function findPackageJsons(currentDir: string, host: LanguageServiceHost): string[] {
|
|
const paths: string[] = [];
|
|
let currentConfigPath: string;
|
|
while (true) {
|
|
currentConfigPath = findConfigFile(currentDir, (f) => tryFileExists(host, f), "package.json");
|
|
if (currentConfigPath) {
|
|
paths.push(currentConfigPath);
|
|
|
|
currentDir = getDirectoryPath(currentConfigPath);
|
|
const parent = getDirectoryPath(currentDir);
|
|
if (currentDir === parent) {
|
|
break;
|
|
}
|
|
currentDir = parent;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
|
|
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string) {
|
|
const result: VisibleModuleInfo[] = [];
|
|
|
|
if (host.readFile && host.fileExists) {
|
|
for (const packageJson of findPackageJsons(scriptPath, host)) {
|
|
const contents = tryReadingPackageJson(packageJson);
|
|
if (!contents) {
|
|
return;
|
|
}
|
|
|
|
const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules");
|
|
const foundModuleNames: string[] = [];
|
|
|
|
// Provide completions for all non @types dependencies
|
|
for (const key of nodeModulesDependencyKeys) {
|
|
addPotentialPackageNames(contents[key], foundModuleNames);
|
|
}
|
|
|
|
for (const moduleName of foundModuleNames) {
|
|
const moduleDir = combinePaths(nodeModulesDir, moduleName);
|
|
result.push({
|
|
moduleName,
|
|
moduleDir
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
|
|
function tryReadingPackageJson(filePath: string) {
|
|
try {
|
|
const fileText = tryReadFile(host, filePath);
|
|
return fileText ? JSON.parse(fileText) : undefined;
|
|
}
|
|
catch (e) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function addPotentialPackageNames(dependencies: any, result: string[]) {
|
|
if (dependencies) {
|
|
for (const dep in dependencies) {
|
|
if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) {
|
|
result.push(dep);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function createCompletionEntryForModule(name: string, kind: string, replacementSpan: TextSpan): CompletionEntry {
|
|
return { name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: name, replacementSpan };
|
|
}
|
|
|
|
// Replace everything after the last directory seperator that appears
|
|
function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan {
|
|
const index = text.lastIndexOf(directorySeparator);
|
|
const offset = index !== -1 ? index + 1 : 0;
|
|
return { start: textStart + offset, length: text.length - offset };
|
|
}
|
|
|
|
// Returns true if the path is explicitly relative to the script (i.e. relative to . or ..)
|
|
function isPathRelativeToScript(path: string) {
|
|
if (path && path.length >= 2 && path.charCodeAt(0) === CharacterCodes.dot) {
|
|
const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1;
|
|
const slashCharCode = path.charCodeAt(slashIndex);
|
|
return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function normalizeAndPreserveTrailingSlash(path: string) {
|
|
return hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalizePath(path)) : normalizePath(path);
|
|
}
|
|
|
|
export function getCompletionEntryDetails(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): CompletionEntryDetails {
|
|
// Compute all the completion symbols again.
|
|
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
|
|
if (completionData) {
|
|
const { symbols, location } = completionData;
|
|
|
|
// Find the symbol with the matching entry name.
|
|
// We don't need to perform character checks here because we're only comparing the
|
|
// name against 'entryName' (which is known to be good), not building a new
|
|
// completion entry.
|
|
const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);
|
|
|
|
if (symbol) {
|
|
const { displayParts, documentation, symbolKind } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All);
|
|
return {
|
|
name: entryName,
|
|
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
|
|
kind: symbolKind,
|
|
displayParts,
|
|
documentation
|
|
};
|
|
}
|
|
}
|
|
|
|
// Didn't find a symbol with this name. See if we can find a keyword instead.
|
|
const keywordCompletion = forEach(keywordCompletions, c => c.name === entryName);
|
|
if (keywordCompletion) {
|
|
return {
|
|
name: entryName,
|
|
kind: ScriptElementKind.keyword,
|
|
kindModifiers: ScriptElementKindModifier.none,
|
|
displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)],
|
|
documentation: undefined
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function getCompletionEntrySymbol(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): Symbol {
|
|
// Compute all the completion symbols again.
|
|
const completionData = getCompletionData(typeChecker, log, sourceFile, position);
|
|
if (completionData) {
|
|
const { symbols, location } = completionData;
|
|
|
|
// Find the symbol with the matching entry name.
|
|
// We don't need to perform character checks here because we're only comparing the
|
|
// name against 'entryName' (which is known to be good), not building a new
|
|
// completion entry.
|
|
return forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number) {
|
|
const isJavaScriptFile = isSourceFileJavaScript(sourceFile);
|
|
|
|
let isJsDocTagName = false;
|
|
|
|
let start = timestamp();
|
|
const currentToken = getTokenAtPosition(sourceFile, position);
|
|
log("getCompletionData: Get current token: " + (timestamp() - start));
|
|
|
|
start = timestamp();
|
|
// Completion not allowed inside comments, bail out if this is the case
|
|
const insideComment = isInsideComment(sourceFile, currentToken, position);
|
|
log("getCompletionData: Is inside comment: " + (timestamp() - start));
|
|
|
|
if (insideComment) {
|
|
// The current position is next to the '@' sign, when no tag name being provided yet.
|
|
// Provide a full list of tag names
|
|
if (hasDocComment(sourceFile, position) && sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) {
|
|
isJsDocTagName = true;
|
|
}
|
|
|
|
// Completion should work inside certain JsDoc tags. For example:
|
|
// /** @type {number | string} */
|
|
// Completion should work in the brackets
|
|
let insideJsDocTagExpression = false;
|
|
const tag = getJsDocTagAtPosition(sourceFile, position);
|
|
if (tag) {
|
|
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
|
|
isJsDocTagName = true;
|
|
}
|
|
|
|
switch (tag.kind) {
|
|
case SyntaxKind.JSDocTypeTag:
|
|
case SyntaxKind.JSDocParameterTag:
|
|
case SyntaxKind.JSDocReturnTag:
|
|
const tagWithExpression = <JSDocTypeTag | JSDocParameterTag | JSDocReturnTag>tag;
|
|
if (tagWithExpression.typeExpression) {
|
|
insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isJsDocTagName) {
|
|
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, isJsDocTagName };
|
|
}
|
|
|
|
if (!insideJsDocTagExpression) {
|
|
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
|
|
// comment or the plain text part of a jsDoc comment, so no completion should be available
|
|
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
start = timestamp();
|
|
const previousToken = findPrecedingToken(position, sourceFile);
|
|
log("getCompletionData: Get previous token 1: " + (timestamp() - start));
|
|
|
|
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
|
|
// Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file
|
|
let contextToken = previousToken;
|
|
|
|
// Check if the caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS|
|
|
// Skip this partial identifier and adjust the contextToken to the token that precedes it.
|
|
if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) {
|
|
const start = timestamp();
|
|
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile);
|
|
log("getCompletionData: Get previous token 2: " + (timestamp() - start));
|
|
}
|
|
|
|
// Find the node where completion is requested on.
|
|
// Also determine whether we are trying to complete with members of that node
|
|
// or attributes of a JSX tag.
|
|
let node = currentToken;
|
|
let isRightOfDot = false;
|
|
let isRightOfOpenTag = false;
|
|
let isStartingCloseTag = false;
|
|
|
|
let location = getTouchingPropertyName(sourceFile, position);
|
|
if (contextToken) {
|
|
// Bail out if this is a known invalid completion location
|
|
if (isCompletionListBlocker(contextToken)) {
|
|
log("Returning an empty list because completion was requested in an invalid position.");
|
|
return undefined;
|
|
}
|
|
|
|
const { parent, kind } = contextToken;
|
|
if (kind === SyntaxKind.DotToken) {
|
|
if (parent.kind === SyntaxKind.PropertyAccessExpression) {
|
|
node = (<PropertyAccessExpression>contextToken.parent).expression;
|
|
isRightOfDot = true;
|
|
}
|
|
else if (parent.kind === SyntaxKind.QualifiedName) {
|
|
node = (<QualifiedName>contextToken.parent).left;
|
|
isRightOfDot = true;
|
|
}
|
|
else {
|
|
// There is nothing that precedes the dot, so this likely just a stray character
|
|
// or leading into a '...' token. Just bail out instead.
|
|
return undefined;
|
|
}
|
|
}
|
|
else if (sourceFile.languageVariant === LanguageVariant.JSX) {
|
|
if (kind === SyntaxKind.LessThanToken) {
|
|
isRightOfOpenTag = true;
|
|
location = contextToken;
|
|
}
|
|
else if (kind === SyntaxKind.SlashToken && contextToken.parent.kind === SyntaxKind.JsxClosingElement) {
|
|
isStartingCloseTag = true;
|
|
location = contextToken;
|
|
}
|
|
}
|
|
}
|
|
|
|
const semanticStart = timestamp();
|
|
let isGlobalCompletion = false;
|
|
let isMemberCompletion: boolean;
|
|
let isNewIdentifierLocation: boolean;
|
|
let symbols: Symbol[] = [];
|
|
|
|
if (isRightOfDot) {
|
|
getTypeScriptMemberSymbols();
|
|
}
|
|
else if (isRightOfOpenTag) {
|
|
const tagSymbols = typeChecker.getJsxIntrinsicTagNames();
|
|
if (tryGetGlobalSymbols()) {
|
|
symbols = tagSymbols.concat(symbols.filter(s => !!(s.flags & (SymbolFlags.Value | SymbolFlags.Alias))));
|
|
}
|
|
else {
|
|
symbols = tagSymbols;
|
|
}
|
|
isMemberCompletion = true;
|
|
isNewIdentifierLocation = false;
|
|
}
|
|
else if (isStartingCloseTag) {
|
|
const tagName = (<JsxElement>contextToken.parent.parent).openingElement.tagName;
|
|
const tagSymbol = typeChecker.getSymbolAtLocation(tagName);
|
|
|
|
if (!typeChecker.isUnknownSymbol(tagSymbol)) {
|
|
symbols = [tagSymbol];
|
|
}
|
|
isMemberCompletion = true;
|
|
isNewIdentifierLocation = false;
|
|
}
|
|
else {
|
|
// For JavaScript or TypeScript, if we're not after a dot, then just try to get the
|
|
// global symbols in scope. These results should be valid for either language as
|
|
// the set of symbols that can be referenced from this location.
|
|
if (!tryGetGlobalSymbols()) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
|
|
|
|
return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), isJsDocTagName };
|
|
|
|
function getTypeScriptMemberSymbols(): void {
|
|
// Right of dot member completion list
|
|
isGlobalCompletion = false;
|
|
isMemberCompletion = true;
|
|
isNewIdentifierLocation = false;
|
|
|
|
if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName || node.kind === SyntaxKind.PropertyAccessExpression) {
|
|
let symbol = typeChecker.getSymbolAtLocation(node);
|
|
|
|
// This is an alias, follow what it aliases
|
|
if (symbol && symbol.flags & SymbolFlags.Alias) {
|
|
symbol = typeChecker.getAliasedSymbol(symbol);
|
|
}
|
|
|
|
if (symbol && symbol.flags & SymbolFlags.HasExports) {
|
|
// Extract module or enum members
|
|
const exportedSymbols = typeChecker.getExportsOfModule(symbol);
|
|
forEach(exportedSymbols, symbol => {
|
|
if (typeChecker.isValidPropertyAccess(<PropertyAccessExpression>(node.parent), symbol.name)) {
|
|
symbols.push(symbol);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const type = typeChecker.getTypeAtLocation(node);
|
|
addTypeProperties(type);
|
|
}
|
|
|
|
function addTypeProperties(type: Type) {
|
|
if (type) {
|
|
// Filter private properties
|
|
for (const symbol of type.getApparentProperties()) {
|
|
if (typeChecker.isValidPropertyAccess(<PropertyAccessExpression>(node.parent), symbol.name)) {
|
|
symbols.push(symbol);
|
|
}
|
|
}
|
|
|
|
if (isJavaScriptFile && type.flags & TypeFlags.Union) {
|
|
// In javascript files, for union types, we don't just get the members that
|
|
// the individual types have in common, we also include all the members that
|
|
// each individual type has. This is because we're going to add all identifiers
|
|
// anyways. So we might as well elevate the members that were at least part
|
|
// of the individual types to a higher status since we know what they are.
|
|
const unionType = <UnionType>type;
|
|
for (const elementType of unionType.types) {
|
|
addTypeProperties(elementType);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function tryGetGlobalSymbols(): boolean {
|
|
let objectLikeContainer: ObjectLiteralExpression | BindingPattern;
|
|
let namedImportsOrExports: NamedImportsOrExports;
|
|
let jsxContainer: JsxOpeningLikeElement;
|
|
|
|
if (objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken)) {
|
|
return tryGetObjectLikeCompletionSymbols(objectLikeContainer);
|
|
}
|
|
|
|
if (namedImportsOrExports = tryGetNamedImportsOrExportsForCompletion(contextToken)) {
|
|
// cursor is in an import clause
|
|
// try to show exported member for imported module
|
|
return tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports);
|
|
}
|
|
|
|
if (jsxContainer = tryGetContainingJsxElement(contextToken)) {
|
|
let attrsType: Type;
|
|
if ((jsxContainer.kind === SyntaxKind.JsxSelfClosingElement) || (jsxContainer.kind === SyntaxKind.JsxOpeningElement)) {
|
|
// Cursor is inside a JSX self-closing element or opening element
|
|
attrsType = typeChecker.getJsxElementAttributesType(<JsxOpeningLikeElement>jsxContainer);
|
|
|
|
if (attrsType) {
|
|
symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (<JsxOpeningLikeElement>jsxContainer).attributes);
|
|
isMemberCompletion = true;
|
|
isNewIdentifierLocation = false;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get all entities in the current scope.
|
|
isMemberCompletion = false;
|
|
isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken);
|
|
|
|
if (previousToken !== contextToken) {
|
|
Debug.assert(!!previousToken, "Expected 'contextToken' to be defined when different from 'previousToken'.");
|
|
}
|
|
// We need to find the node that will give us an appropriate scope to begin
|
|
// aggregating completion candidates. This is achieved in 'getScopeNode'
|
|
// by finding the first node that encompasses a position, accounting for whether a node
|
|
// is "complete" to decide whether a position belongs to the node.
|
|
//
|
|
// However, at the end of an identifier, we are interested in the scope of the identifier
|
|
// itself, but fall outside of the identifier. For instance:
|
|
//
|
|
// xyz => x$
|
|
//
|
|
// the cursor is outside of both the 'x' and the arrow function 'xyz => x',
|
|
// so 'xyz' is not returned in our results.
|
|
//
|
|
// We define 'adjustedPosition' so that we may appropriately account for
|
|
// being at the end of an identifier. The intention is that if requesting completion
|
|
// at the end of an identifier, it should be effectively equivalent to requesting completion
|
|
// anywhere inside/at the beginning of the identifier. So in the previous case, the
|
|
// 'adjustedPosition' will work as if requesting completion in the following:
|
|
//
|
|
// xyz => $x
|
|
//
|
|
// If previousToken !== contextToken, then
|
|
// - 'contextToken' was adjusted to the token prior to 'previousToken'
|
|
// because we were at the end of an identifier.
|
|
// - 'previousToken' is defined.
|
|
const adjustedPosition = previousToken !== contextToken ?
|
|
previousToken.getStart() :
|
|
position;
|
|
|
|
const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile;
|
|
if (scopeNode) {
|
|
isGlobalCompletion =
|
|
scopeNode.kind === SyntaxKind.SourceFile ||
|
|
scopeNode.kind === SyntaxKind.TemplateExpression ||
|
|
scopeNode.kind === SyntaxKind.JsxExpression ||
|
|
isStatement(scopeNode);
|
|
}
|
|
|
|
/// TODO filter meaning based on the current context
|
|
const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias;
|
|
symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Finds the first node that "embraces" the position, so that one may
|
|
* accurately aggregate locals from the closest containing scope.
|
|
*/
|
|
function getScopeNode(initialToken: Node, position: number, sourceFile: SourceFile) {
|
|
let scope = initialToken;
|
|
while (scope && !positionBelongsToNode(scope, position, sourceFile)) {
|
|
scope = scope.parent;
|
|
}
|
|
return scope;
|
|
}
|
|
|
|
function isCompletionListBlocker(contextToken: Node): boolean {
|
|
const start = timestamp();
|
|
const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) ||
|
|
isSolelyIdentifierDefinitionLocation(contextToken) ||
|
|
isDotOfNumericLiteral(contextToken) ||
|
|
isInJsxText(contextToken);
|
|
log("getCompletionsAtPosition: isCompletionListBlocker: " + (timestamp() - start));
|
|
return result;
|
|
}
|
|
|
|
function isInJsxText(contextToken: Node): boolean {
|
|
if (contextToken.kind === SyntaxKind.JsxText) {
|
|
return true;
|
|
}
|
|
|
|
if (contextToken.kind === SyntaxKind.GreaterThanToken && contextToken.parent) {
|
|
if (contextToken.parent.kind === SyntaxKind.JsxOpeningElement) {
|
|
return true;
|
|
}
|
|
|
|
if (contextToken.parent.kind === SyntaxKind.JsxClosingElement || contextToken.parent.kind === SyntaxKind.JsxSelfClosingElement) {
|
|
return contextToken.parent.parent && contextToken.parent.parent.kind === SyntaxKind.JsxElement;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isNewIdentifierDefinitionLocation(previousToken: Node): boolean {
|
|
if (previousToken) {
|
|
const containingNodeKind = previousToken.parent.kind;
|
|
switch (previousToken.kind) {
|
|
case SyntaxKind.CommaToken:
|
|
return containingNodeKind === SyntaxKind.CallExpression // func( a, |
|
|
|| containingNodeKind === SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */
|
|
|| containingNodeKind === SyntaxKind.NewExpression // new C(a, |
|
|
|| containingNodeKind === SyntaxKind.ArrayLiteralExpression // [a, |
|
|
|| containingNodeKind === SyntaxKind.BinaryExpression // const x = (a, |
|
|
|| containingNodeKind === SyntaxKind.FunctionType; // var x: (s: string, list|
|
|
|
|
case SyntaxKind.OpenParenToken:
|
|
return containingNodeKind === SyntaxKind.CallExpression // func( |
|
|
|| containingNodeKind === SyntaxKind.Constructor // constructor( |
|
|
|| containingNodeKind === SyntaxKind.NewExpression // new C(a|
|
|
|| containingNodeKind === SyntaxKind.ParenthesizedExpression // const x = (a|
|
|
|| containingNodeKind === SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */
|
|
|
|
case SyntaxKind.OpenBracketToken:
|
|
return containingNodeKind === SyntaxKind.ArrayLiteralExpression // [ |
|
|
|| containingNodeKind === SyntaxKind.IndexSignature // [ | : string ]
|
|
|| containingNodeKind === SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */
|
|
|
|
case SyntaxKind.ModuleKeyword: // module |
|
|
case SyntaxKind.NamespaceKeyword: // namespace |
|
|
return true;
|
|
|
|
case SyntaxKind.DotToken:
|
|
return containingNodeKind === SyntaxKind.ModuleDeclaration; // module A.|
|
|
|
|
case SyntaxKind.OpenBraceToken:
|
|
return containingNodeKind === SyntaxKind.ClassDeclaration; // class A{ |
|
|
|
|
case SyntaxKind.EqualsToken:
|
|
return containingNodeKind === SyntaxKind.VariableDeclaration // const x = a|
|
|
|| containingNodeKind === SyntaxKind.BinaryExpression; // x = a|
|
|
|
|
case SyntaxKind.TemplateHead:
|
|
return containingNodeKind === SyntaxKind.TemplateExpression; // `aa ${|
|
|
|
|
case SyntaxKind.TemplateMiddle:
|
|
return containingNodeKind === SyntaxKind.TemplateSpan; // `aa ${10} dd ${|
|
|
|
|
case SyntaxKind.PublicKeyword:
|
|
case SyntaxKind.PrivateKeyword:
|
|
case SyntaxKind.ProtectedKeyword:
|
|
return containingNodeKind === SyntaxKind.PropertyDeclaration; // class A{ public |
|
|
}
|
|
|
|
// Previous token may have been a keyword that was converted to an identifier.
|
|
switch (previousToken.getText()) {
|
|
case "public":
|
|
case "protected":
|
|
case "private":
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: Node): boolean {
|
|
if (contextToken.kind === SyntaxKind.StringLiteral
|
|
|| contextToken.kind === SyntaxKind.RegularExpressionLiteral
|
|
|| isTemplateLiteralKind(contextToken.kind)) {
|
|
const start = contextToken.getStart();
|
|
const end = contextToken.getEnd();
|
|
|
|
// To be "in" one of these literals, the position has to be:
|
|
// 1. entirely within the token text.
|
|
// 2. at the end position of an unterminated token.
|
|
// 3. at the end of a regular expression (due to trailing flags like '/foo/g').
|
|
if (start < position && position < end) {
|
|
return true;
|
|
}
|
|
|
|
if (position === end) {
|
|
return !!(<LiteralExpression>contextToken).isUnterminated
|
|
|| contextToken.kind === SyntaxKind.RegularExpressionLiteral;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Aggregates relevant symbols for completion in object literals and object binding patterns.
|
|
* Relevant symbols are stored in the captured 'symbols' variable.
|
|
*
|
|
* @returns true if 'symbols' was successfully populated; false otherwise.
|
|
*/
|
|
function tryGetObjectLikeCompletionSymbols(objectLikeContainer: ObjectLiteralExpression | BindingPattern): boolean {
|
|
// We're looking up possible property names from contextual/inferred/declared type.
|
|
isMemberCompletion = true;
|
|
|
|
let typeForObject: Type;
|
|
let existingMembers: Declaration[];
|
|
|
|
if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) {
|
|
// We are completing on contextual types, but may also include properties
|
|
// other than those within the declared type.
|
|
isNewIdentifierLocation = true;
|
|
|
|
// If the object literal is being assigned to something of type 'null | { hello: string }',
|
|
// it clearly isn't trying to satisfy the 'null' type. So we grab the non-nullable type if possible.
|
|
typeForObject = typeChecker.getContextualType(<ObjectLiteralExpression>objectLikeContainer);
|
|
typeForObject = typeForObject && typeForObject.getNonNullableType();
|
|
|
|
existingMembers = (<ObjectLiteralExpression>objectLikeContainer).properties;
|
|
}
|
|
else if (objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern) {
|
|
// We are *only* completing on properties from the type being destructured.
|
|
isNewIdentifierLocation = false;
|
|
|
|
const rootDeclaration = getRootDeclaration(objectLikeContainer.parent);
|
|
if (isVariableLike(rootDeclaration)) {
|
|
// We don't want to complete using the type acquired by the shape
|
|
// of the binding pattern; we are only interested in types acquired
|
|
// through type declaration or inference.
|
|
// Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed -
|
|
// type of parameter will flow in from the contextual type of the function
|
|
let canGetType = !!(rootDeclaration.initializer || rootDeclaration.type);
|
|
if (!canGetType && rootDeclaration.kind === SyntaxKind.Parameter) {
|
|
if (isExpression(rootDeclaration.parent)) {
|
|
canGetType = !!typeChecker.getContextualType(<Expression>rootDeclaration.parent);
|
|
}
|
|
else if (rootDeclaration.parent.kind === SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === SyntaxKind.SetAccessor) {
|
|
canGetType = isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType(<Expression>rootDeclaration.parent.parent);
|
|
}
|
|
}
|
|
if (canGetType) {
|
|
typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer);
|
|
existingMembers = (<ObjectBindingPattern>objectLikeContainer).elements;
|
|
}
|
|
}
|
|
else {
|
|
Debug.fail("Root declaration is not variable-like.");
|
|
}
|
|
}
|
|
else {
|
|
Debug.fail("Expected object literal or binding pattern, got " + objectLikeContainer.kind);
|
|
}
|
|
|
|
if (!typeForObject) {
|
|
return false;
|
|
}
|
|
|
|
const typeMembers = typeChecker.getPropertiesOfType(typeForObject);
|
|
if (typeMembers && typeMembers.length > 0) {
|
|
// Add filtered items to the completion list
|
|
symbols = filterObjectMembersList(typeMembers, existingMembers);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Aggregates relevant symbols for completion in import clauses and export clauses
|
|
* whose declarations have a module specifier; for instance, symbols will be aggregated for
|
|
*
|
|
* import { | } from "moduleName";
|
|
* export { a as foo, | } from "moduleName";
|
|
*
|
|
* but not for
|
|
*
|
|
* export { | };
|
|
*
|
|
* Relevant symbols are stored in the captured 'symbols' variable.
|
|
*
|
|
* @returns true if 'symbols' was successfully populated; false otherwise.
|
|
*/
|
|
function tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports: NamedImportsOrExports): boolean {
|
|
const declarationKind = namedImportsOrExports.kind === SyntaxKind.NamedImports ?
|
|
SyntaxKind.ImportDeclaration :
|
|
SyntaxKind.ExportDeclaration;
|
|
const importOrExportDeclaration = <ImportDeclaration | ExportDeclaration>getAncestor(namedImportsOrExports, declarationKind);
|
|
const moduleSpecifier = importOrExportDeclaration.moduleSpecifier;
|
|
|
|
if (!moduleSpecifier) {
|
|
return false;
|
|
}
|
|
|
|
isMemberCompletion = true;
|
|
isNewIdentifierLocation = false;
|
|
|
|
const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(moduleSpecifier);
|
|
if (!moduleSpecifierSymbol) {
|
|
symbols = emptyArray;
|
|
return true;
|
|
}
|
|
|
|
const exports = typeChecker.getExportsAndPropertiesOfModule(moduleSpecifierSymbol);
|
|
symbols = filterNamedImportOrExportCompletionItems(exports, namedImportsOrExports.elements);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the immediate owning object literal or binding pattern of a context token,
|
|
* on the condition that one exists and that the context implies completion should be given.
|
|
*/
|
|
function tryGetObjectLikeCompletionContainer(contextToken: Node): ObjectLiteralExpression | BindingPattern {
|
|
if (contextToken) {
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.OpenBraceToken: // const x = { |
|
|
case SyntaxKind.CommaToken: // const x = { a: 0, |
|
|
const parent = contextToken.parent;
|
|
if (parent && (parent.kind === SyntaxKind.ObjectLiteralExpression || parent.kind === SyntaxKind.ObjectBindingPattern)) {
|
|
return <ObjectLiteralExpression | BindingPattern>parent;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Returns the containing list of named imports or exports of a context token,
|
|
* on the condition that one exists and that the context implies completion should be given.
|
|
*/
|
|
function tryGetNamedImportsOrExportsForCompletion(contextToken: Node): NamedImportsOrExports {
|
|
if (contextToken) {
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.OpenBraceToken: // import { |
|
|
case SyntaxKind.CommaToken: // import { a as 0, |
|
|
switch (contextToken.parent.kind) {
|
|
case SyntaxKind.NamedImports:
|
|
case SyntaxKind.NamedExports:
|
|
return <NamedImportsOrExports>contextToken.parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement {
|
|
if (contextToken) {
|
|
const parent = contextToken.parent;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.LessThanSlashToken:
|
|
case SyntaxKind.SlashToken:
|
|
case SyntaxKind.Identifier:
|
|
case SyntaxKind.JsxAttribute:
|
|
case SyntaxKind.JsxSpreadAttribute:
|
|
if (parent && (parent.kind === SyntaxKind.JsxSelfClosingElement || parent.kind === SyntaxKind.JsxOpeningElement)) {
|
|
return <JsxOpeningLikeElement>parent;
|
|
}
|
|
else if (parent.kind === SyntaxKind.JsxAttribute) {
|
|
return <JsxOpeningLikeElement>parent.parent;
|
|
}
|
|
break;
|
|
|
|
// The context token is the closing } or " of an attribute, which means
|
|
// its parent is a JsxExpression, whose parent is a JsxAttribute,
|
|
// whose parent is a JsxOpeningLikeElement
|
|
case SyntaxKind.StringLiteral:
|
|
if (parent && ((parent.kind === SyntaxKind.JsxAttribute) || (parent.kind === SyntaxKind.JsxSpreadAttribute))) {
|
|
return <JsxOpeningLikeElement>parent.parent;
|
|
}
|
|
|
|
break;
|
|
|
|
case SyntaxKind.CloseBraceToken:
|
|
if (parent &&
|
|
parent.kind === SyntaxKind.JsxExpression &&
|
|
parent.parent &&
|
|
(parent.parent.kind === SyntaxKind.JsxAttribute)) {
|
|
return <JsxOpeningLikeElement>parent.parent.parent;
|
|
}
|
|
|
|
if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) {
|
|
return <JsxOpeningLikeElement>parent.parent;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function isFunction(kind: SyntaxKind): boolean {
|
|
switch (kind) {
|
|
case SyntaxKind.FunctionExpression:
|
|
case SyntaxKind.ArrowFunction:
|
|
case SyntaxKind.FunctionDeclaration:
|
|
case SyntaxKind.MethodDeclaration:
|
|
case SyntaxKind.MethodSignature:
|
|
case SyntaxKind.GetAccessor:
|
|
case SyntaxKind.SetAccessor:
|
|
case SyntaxKind.CallSignature:
|
|
case SyntaxKind.ConstructSignature:
|
|
case SyntaxKind.IndexSignature:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @returns true if we are certain that the currently edited location must define a new location; false otherwise.
|
|
*/
|
|
function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean {
|
|
const containingNodeKind = contextToken.parent.kind;
|
|
switch (contextToken.kind) {
|
|
case SyntaxKind.CommaToken:
|
|
return containingNodeKind === SyntaxKind.VariableDeclaration ||
|
|
containingNodeKind === SyntaxKind.VariableDeclarationList ||
|
|
containingNodeKind === SyntaxKind.VariableStatement ||
|
|
containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { foo, |
|
|
isFunction(containingNodeKind) ||
|
|
containingNodeKind === SyntaxKind.ClassDeclaration || // class A<T, |
|
|
containingNodeKind === SyntaxKind.ClassExpression || // var C = class D<T, |
|
|
containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A<T, |
|
|
containingNodeKind === SyntaxKind.ArrayBindingPattern || // var [x, y|
|
|
containingNodeKind === SyntaxKind.TypeAliasDeclaration; // type Map, K, |
|
|
|
|
case SyntaxKind.DotToken:
|
|
return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [.|
|
|
|
|
case SyntaxKind.ColonToken:
|
|
return containingNodeKind === SyntaxKind.BindingElement; // var {x :html|
|
|
|
|
case SyntaxKind.OpenBracketToken:
|
|
return containingNodeKind === SyntaxKind.ArrayBindingPattern; // var [x|
|
|
|
|
case SyntaxKind.OpenParenToken:
|
|
return containingNodeKind === SyntaxKind.CatchClause ||
|
|
isFunction(containingNodeKind);
|
|
|
|
case SyntaxKind.OpenBraceToken:
|
|
return containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { |
|
|
containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface a { |
|
|
containingNodeKind === SyntaxKind.TypeLiteral; // const x : { |
|
|
|
|
case SyntaxKind.SemicolonToken:
|
|
return containingNodeKind === SyntaxKind.PropertySignature &&
|
|
contextToken.parent && contextToken.parent.parent &&
|
|
(contextToken.parent.parent.kind === SyntaxKind.InterfaceDeclaration || // interface a { f; |
|
|
contextToken.parent.parent.kind === SyntaxKind.TypeLiteral); // const x : { a; |
|
|
|
|
case SyntaxKind.LessThanToken:
|
|
return containingNodeKind === SyntaxKind.ClassDeclaration || // class A< |
|
|
containingNodeKind === SyntaxKind.ClassExpression || // var C = class D< |
|
|
containingNodeKind === SyntaxKind.InterfaceDeclaration || // interface A< |
|
|
containingNodeKind === SyntaxKind.TypeAliasDeclaration || // type List< |
|
|
isFunction(containingNodeKind);
|
|
|
|
case SyntaxKind.StaticKeyword:
|
|
return containingNodeKind === SyntaxKind.PropertyDeclaration;
|
|
|
|
case SyntaxKind.DotDotDotToken:
|
|
return containingNodeKind === SyntaxKind.Parameter ||
|
|
(contextToken.parent && contextToken.parent.parent &&
|
|
contextToken.parent.parent.kind === SyntaxKind.ArrayBindingPattern); // var [...z|
|
|
|
|
case SyntaxKind.PublicKeyword:
|
|
case SyntaxKind.PrivateKeyword:
|
|
case SyntaxKind.ProtectedKeyword:
|
|
return containingNodeKind === SyntaxKind.Parameter;
|
|
|
|
case SyntaxKind.AsKeyword:
|
|
return containingNodeKind === SyntaxKind.ImportSpecifier ||
|
|
containingNodeKind === SyntaxKind.ExportSpecifier ||
|
|
containingNodeKind === SyntaxKind.NamespaceImport;
|
|
|
|
case SyntaxKind.ClassKeyword:
|
|
case SyntaxKind.EnumKeyword:
|
|
case SyntaxKind.InterfaceKeyword:
|
|
case SyntaxKind.FunctionKeyword:
|
|
case SyntaxKind.VarKeyword:
|
|
case SyntaxKind.GetKeyword:
|
|
case SyntaxKind.SetKeyword:
|
|
case SyntaxKind.ImportKeyword:
|
|
case SyntaxKind.LetKeyword:
|
|
case SyntaxKind.ConstKeyword:
|
|
case SyntaxKind.YieldKeyword:
|
|
case SyntaxKind.TypeKeyword: // type htm|
|
|
return true;
|
|
}
|
|
|
|
// Previous token may have been a keyword that was converted to an identifier.
|
|
switch (contextToken.getText()) {
|
|
case "abstract":
|
|
case "async":
|
|
case "class":
|
|
case "const":
|
|
case "declare":
|
|
case "enum":
|
|
case "function":
|
|
case "interface":
|
|
case "let":
|
|
case "private":
|
|
case "protected":
|
|
case "public":
|
|
case "static":
|
|
case "var":
|
|
case "yield":
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function isDotOfNumericLiteral(contextToken: Node): boolean {
|
|
if (contextToken.kind === SyntaxKind.NumericLiteral) {
|
|
const text = contextToken.getFullText();
|
|
return text.charAt(text.length - 1) === ".";
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Filters out completion suggestions for named imports or exports.
|
|
*
|
|
* @param exportsOfModule The list of symbols which a module exposes.
|
|
* @param namedImportsOrExports The list of existing import/export specifiers in the import/export clause.
|
|
*
|
|
* @returns Symbols to be suggested at an import/export clause, barring those whose named imports/exports
|
|
* do not occur at the current position and have not otherwise been typed.
|
|
*/
|
|
function filterNamedImportOrExportCompletionItems(exportsOfModule: Symbol[], namedImportsOrExports: ImportOrExportSpecifier[]): Symbol[] {
|
|
const existingImportsOrExports = createMap<boolean>();
|
|
|
|
for (const element of namedImportsOrExports) {
|
|
// If this is the current item we are editing right now, do not filter it out
|
|
if (element.getStart() <= position && position <= element.getEnd()) {
|
|
continue;
|
|
}
|
|
|
|
const name = element.propertyName || element.name;
|
|
existingImportsOrExports.set(name.text, true);
|
|
}
|
|
|
|
if (existingImportsOrExports.size === 0) {
|
|
return filter(exportsOfModule, e => e.name !== "default");
|
|
}
|
|
|
|
return filter(exportsOfModule, e => e.name !== "default" && !existingImportsOrExports.get(e.name));
|
|
}
|
|
|
|
/**
|
|
* Filters out completion suggestions for named imports or exports.
|
|
*
|
|
* @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations
|
|
* do not occur at the current position and have not otherwise been typed.
|
|
*/
|
|
function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: Declaration[]): Symbol[] {
|
|
if (!existingMembers || existingMembers.length === 0) {
|
|
return contextualMemberSymbols;
|
|
}
|
|
|
|
const existingMemberNames = createMap<boolean>();
|
|
for (const m of existingMembers) {
|
|
// Ignore omitted expressions for missing members
|
|
if (m.kind !== SyntaxKind.PropertyAssignment &&
|
|
m.kind !== SyntaxKind.ShorthandPropertyAssignment &&
|
|
m.kind !== SyntaxKind.BindingElement &&
|
|
m.kind !== SyntaxKind.MethodDeclaration &&
|
|
m.kind !== SyntaxKind.GetAccessor &&
|
|
m.kind !== SyntaxKind.SetAccessor) {
|
|
continue;
|
|
}
|
|
|
|
// If this is the current item we are editing right now, do not filter it out
|
|
if (m.getStart() <= position && position <= m.getEnd()) {
|
|
continue;
|
|
}
|
|
|
|
let existingName: string;
|
|
|
|
if (m.kind === SyntaxKind.BindingElement && (<BindingElement>m).propertyName) {
|
|
// include only identifiers in completion list
|
|
if ((<BindingElement>m).propertyName.kind === SyntaxKind.Identifier) {
|
|
existingName = (<Identifier>(<BindingElement>m).propertyName).text;
|
|
}
|
|
}
|
|
else {
|
|
// TODO(jfreeman): Account for computed property name
|
|
// NOTE: if one only performs this step when m.name is an identifier,
|
|
// things like '__proto__' are not filtered out.
|
|
existingName = (<Identifier>m.name).text;
|
|
}
|
|
|
|
existingMemberNames.set(existingName, true);
|
|
}
|
|
|
|
return filter(contextualMemberSymbols, m => !existingMemberNames.get(m.name));
|
|
}
|
|
|
|
/**
|
|
* Filters out completion suggestions from 'symbols' according to existing JSX attributes.
|
|
*
|
|
* @returns Symbols to be suggested in a JSX element, barring those whose attributes
|
|
* do not occur at the current position and have not otherwise been typed.
|
|
*/
|
|
function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray<JsxAttribute | JsxSpreadAttribute>): Symbol[] {
|
|
const seenNames = createMap<boolean>();
|
|
for (const attr of attributes) {
|
|
// If this is the current item we are editing right now, do not filter it out
|
|
if (attr.getStart() <= position && position <= attr.getEnd()) {
|
|
continue;
|
|
}
|
|
|
|
if (attr.kind === SyntaxKind.JsxAttribute) {
|
|
seenNames.set((<JsxAttribute>attr).name.text, true);
|
|
}
|
|
}
|
|
|
|
return filter(symbols, a => !seenNames.get(a.name));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the name to be display in completion from a given symbol.
|
|
*
|
|
* @return undefined if the name is of external module otherwise a name with striped of any quote
|
|
*/
|
|
function getCompletionEntryDisplayNameForSymbol(typeChecker: TypeChecker, symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, location: Node): string {
|
|
const displayName: string = getDeclaredName(typeChecker, symbol, location);
|
|
|
|
if (displayName) {
|
|
const firstCharCode = displayName.charCodeAt(0);
|
|
// First check of the displayName is not external module; if it is an external module, it is not valid entry
|
|
if ((symbol.flags & SymbolFlags.Namespace) && (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote)) {
|
|
// If the symbol is external module, don't show it in the completion list
|
|
// (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there)
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return getCompletionEntryDisplayName(displayName, target, performCharacterChecks);
|
|
}
|
|
|
|
/**
|
|
* Get a displayName from a given for completion list, performing any necessary quotes stripping
|
|
* and checking whether the name is valid identifier name.
|
|
*/
|
|
function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean): string {
|
|
if (!name) {
|
|
return undefined;
|
|
}
|
|
|
|
name = stripQuotes(name);
|
|
|
|
if (!name) {
|
|
return undefined;
|
|
}
|
|
|
|
// If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an
|
|
// invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name.
|
|
// e.g "b a" is valid quoted name but when we strip off the quotes, it is invalid.
|
|
// We, thus, need to check if whatever was inside the quotes is actually a valid identifier name.
|
|
if (performCharacterChecks) {
|
|
if (!isIdentifierText(name, target)) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
// A cache of completion entries for keywords, these do not change between sessions
|
|
const keywordCompletions: CompletionEntry[] = [];
|
|
for (let i = SyntaxKind.FirstKeyword; i <= SyntaxKind.LastKeyword; i++) {
|
|
keywordCompletions.push({
|
|
name: tokenToString(i),
|
|
kind: ScriptElementKind.keyword,
|
|
kindModifiers: ScriptElementKindModifier.none,
|
|
sortText: "0"
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Matches a triple slash reference directive with an incomplete string literal for its path. Used
|
|
* to determine if the caret is currently within the string literal and capture the literal fragment
|
|
* for completions.
|
|
* For example, this matches
|
|
*
|
|
* /// <reference path="fragment
|
|
*
|
|
* but not
|
|
*
|
|
* /// <reference path="fragment"
|
|
*/
|
|
const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\3"]*)$/;
|
|
|
|
interface VisibleModuleInfo {
|
|
moduleName: string;
|
|
moduleDir: string;
|
|
}
|
|
|
|
const nodeModulesDependencyKeys = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
|
|
function tryGetDirectories(host: LanguageServiceHost, directoryName: string): string[] {
|
|
return tryIOAndConsumeErrors(host, host.getDirectories, directoryName);
|
|
}
|
|
|
|
function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: string[], exclude?: string[], include?: string[]): string[] {
|
|
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include);
|
|
}
|
|
|
|
function tryReadFile(host: LanguageServiceHost, path: string): string {
|
|
return tryIOAndConsumeErrors(host, host.readFile, path);
|
|
}
|
|
|
|
function tryFileExists(host: LanguageServiceHost, path: string): boolean {
|
|
return tryIOAndConsumeErrors(host, host.fileExists, path);
|
|
}
|
|
|
|
function tryDirectoryExists(host: LanguageServiceHost, path: string): boolean {
|
|
try {
|
|
return directoryProbablyExists(path, host);
|
|
}
|
|
catch (e) {}
|
|
return undefined;
|
|
}
|
|
|
|
function tryIOAndConsumeErrors<T>(host: LanguageServiceHost, toApply: (...a: any[]) => T, ...args: any[]) {
|
|
try {
|
|
return toApply && toApply.apply(host, args);
|
|
}
|
|
catch (e) {}
|
|
return undefined;
|
|
}
|
|
|
|
function isEqualityExpression(node: Node): node is BinaryExpression {
|
|
return isBinaryExpression(node) && isEqualityOperatorKind(node.operatorToken.kind);
|
|
}
|
|
|
|
function isEqualityOperatorKind(kind: SyntaxKind) {
|
|
return kind == SyntaxKind.EqualsEqualsToken ||
|
|
kind === SyntaxKind.ExclamationEqualsToken ||
|
|
kind === SyntaxKind.EqualsEqualsEqualsToken ||
|
|
kind === SyntaxKind.ExclamationEqualsEqualsToken;
|
|
}
|
|
}
|