mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Updated: Only auto-import from package.json (#32517)
* Move package.json related utils to utilities * Add failing test * Make first test pass * Don’t filter when there’s no package.json, fix scoped package imports * Use type acquisition as a heuristic for whether a JS project is using node core * Make same fix in getCompletionDetails * Fix re-exporting * Change JS node core module heuristic to same-file utilization * Remove unused method * Remove other unused method * Remove unused triple-slash ref * Update comment * Refactor findAlias to forEachAlias to reduce iterations * Really fix re-exporting * Use getModuleSpecifier instead of custom hack * Fix offering auto imports to paths within node modules * Rename things and make comments better * Add another reexport test * Inline `symbolHasBeenSeen` * Simplify forEachAlias to findAlias * Add note that symbols is mutated * Symbol order doesn’t matter here * Style nits * Add test with nested package.jsons * Fix and add tests for export * re-exports * Don’t fail when alias isn’t found * Make some easy optimizations * Clean up memoization when done * Remove unnecessary semicolon * Make getSymbolsFromOtherSourceFileExports pure * Cache auto imports * Revert "Cache auto imports" This reverts commit8ea4829587. * Handle merged symbols through cache * Be safer with symbol declarations, add logging * Improve cache invalidation for imports and exports * Check symbol presence first * Only run cache invalidation logic if there’s something to clear * Consolidate cache invalidation logic * Fix reuseProgramStructure test * Add more logging * Only clear cache if symbols are different * Refactor ambient module handling * Start caching package.json stuff * Support package.json searching in fourslash * Move import suggestions cache to Project * Start making more module specifier work available without having the importing file * Going to backtrack some from here * Get rid of dumb cache, fix node core modules stuff * Start determining changes to a file have invalidated its own auto imports * Move package.json related utils to utilities * Add failing test * Make first test pass * Don’t filter when there’s no package.json, fix scoped package imports * Use type acquisition as a heuristic for whether a JS project is using node core * Make same fix in getCompletionDetails * Fix re-exporting * Change JS node core module heuristic to same-file utilization * Remove unused method * Remove other unused method * Remove unused triple-slash ref * Update comment * Refactor findAlias to forEachAlias to reduce iterations * Really fix re-exporting * Use getModuleSpecifier instead of custom hack * Fix offering auto imports to paths within node modules * Rename things and make comments better * Add another reexport test * Inline `symbolHasBeenSeen` * Simplify forEachAlias to findAlias * Add note that symbols is mutated * Symbol order doesn’t matter here * Style nits * Add test with nested package.jsons * Fix and add tests for export * re-exports * Don’t fail when alias isn’t found * Make some easy optimizations * Clean up memoization when done * Remove unnecessary semicolon * Make getSymbolsFromOtherSourceFileExports pure * Cache auto imports * Revert "Cache auto imports" This reverts commit8ea4829587. * Handle merged symbols through cache * Be safer with symbol declarations, add logging * Improve cache invalidation for imports and exports * Check symbol presence first * Only run cache invalidation logic if there’s something to clear * Consolidate cache invalidation logic * Fix reuseProgramStructure test * Add more logging * Only clear cache if symbols are different * Refactor ambient module handling * Finish(?) sourceFileHasChangedOwnImportSuggestions * Make package.json info model better * Fix misplaced paren * Use file structure cache for package.json detection when possible * Revert unnecessary changes in moduleSpecifiers * Revert more unnecessary changes * Don’t watch package.jsons inside node_modules, fix tests * Work around declaration emit bug * Sync submodules? * Delete unused type * Add server cache tests * Fix server fourslash editing * Fix packageJsonInfo tests * Add node core modules cache test and fix more fourslash * Clean up symlink caching * Improve logging * Function name doesn’t make any sense anymore * Move symlinks cache to host * Fix markFileAsDirty from ScriptInfo * Mark new Project members internal * Use Path instead of fileName * Rename AutoImportSuggestionsCache * Improve WatchType description * Remove entries() from packageJsonCache * Fix path/fileName bug * Also cache symlinks on Program for benefit of d.ts emit * Let language service use Program’s symlink cache
This commit is contained in:
@@ -3682,7 +3682,8 @@ namespace ts {
|
||||
tracker: tracker && tracker.trackSymbol ? tracker : { trackSymbol: noop, moduleResolverHost: flags! & NodeBuilderFlags.DoNotIncludeSymbolChain ? {
|
||||
getCommonSourceDirectory: (host as Program).getCommonSourceDirectory ? () => (host as Program).getCommonSourceDirectory() : () => "",
|
||||
getSourceFiles: () => host.getSourceFiles(),
|
||||
getCurrentDirectory: host.getCurrentDirectory && (() => host.getCurrentDirectory!())
|
||||
getCurrentDirectory: maybeBind(host, host.getCurrentDirectory),
|
||||
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks),
|
||||
} : undefined },
|
||||
encounteredError: false,
|
||||
visitedTypes: undefined,
|
||||
|
||||
@@ -64,6 +64,20 @@ namespace ts.moduleSpecifiers {
|
||||
return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferences(preferences, compilerOptions, importingSourceFile));
|
||||
}
|
||||
|
||||
export function getNodeModulesPackageName(
|
||||
compilerOptions: CompilerOptions,
|
||||
importingSourceFileName: Path,
|
||||
nodeModulesFileName: string,
|
||||
host: ModuleSpecifierResolutionHost,
|
||||
files: readonly SourceFile[],
|
||||
redirectTargetsMap: RedirectTargetsMap,
|
||||
): string | undefined {
|
||||
const info = getInfo(importingSourceFileName, host);
|
||||
const modulePaths = getAllModulePaths(files, importingSourceFileName, nodeModulesFileName, info.getCanonicalFileName, host, redirectTargetsMap);
|
||||
return firstDefined(modulePaths,
|
||||
moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions, /*packageNameOnly*/ true));
|
||||
}
|
||||
|
||||
function getModuleSpecifierWorker(
|
||||
compilerOptions: CompilerOptions,
|
||||
importingSourceFileName: Path,
|
||||
@@ -79,7 +93,7 @@ namespace ts.moduleSpecifiers {
|
||||
getLocalModuleSpecifier(toFileName, info, compilerOptions, preferences);
|
||||
}
|
||||
|
||||
// Returns an import for each symlink and for the realpath.
|
||||
/** Returns an import for each symlink and for the realpath. */
|
||||
export function getModuleSpecifiers(
|
||||
moduleSymbol: Symbol,
|
||||
compilerOptions: CompilerOptions,
|
||||
@@ -152,40 +166,6 @@ namespace ts.moduleSpecifiers {
|
||||
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSOrJsonFileExtension(text) : undefined) || false;
|
||||
}
|
||||
|
||||
function stringsEqual(a: string, b: string, getCanonicalFileName: GetCanonicalFileName): boolean {
|
||||
return getCanonicalFileName(a) === getCanonicalFileName(b);
|
||||
}
|
||||
|
||||
// KLUDGE: Don't assume one 'node_modules' links to another. More likely a single directory inside the node_modules is the symlink.
|
||||
// ALso, don't assume that an `@foo` directory is linked. More likely the contents of that are linked.
|
||||
function isNodeModulesOrScopedPackageDirectory(s: string, getCanonicalFileName: GetCanonicalFileName): boolean {
|
||||
return getCanonicalFileName(s) === "node_modules" || startsWith(s, "@");
|
||||
}
|
||||
|
||||
function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] {
|
||||
const aParts = getPathComponents(toPath(a, cwd, getCanonicalFileName));
|
||||
const bParts = getPathComponents(toPath(b, cwd, getCanonicalFileName));
|
||||
while (!isNodeModulesOrScopedPackageDirectory(aParts[aParts.length - 2], getCanonicalFileName) &&
|
||||
!isNodeModulesOrScopedPackageDirectory(bParts[bParts.length - 2], getCanonicalFileName) &&
|
||||
stringsEqual(aParts[aParts.length - 1], bParts[bParts.length - 1], getCanonicalFileName)) {
|
||||
aParts.pop();
|
||||
bParts.pop();
|
||||
}
|
||||
return [getPathFromPathComponents(aParts), getPathFromPathComponents(bParts)];
|
||||
}
|
||||
|
||||
function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): ReadonlyMap<string> {
|
||||
const result = createMap<string>();
|
||||
const symlinks = flatten<readonly [string, string]>(mapDefined(files, sf =>
|
||||
sf.resolvedModules && compact(arrayFrom(mapIterator(sf.resolvedModules.values(), res =>
|
||||
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] as const : undefined)))));
|
||||
for (const [resolvedPath, originalPath] of symlinks) {
|
||||
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName);
|
||||
result.set(commonOriginal, commonResolved);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function numberOfDirectorySeparators(str: string) {
|
||||
const match = str.match(/\//g);
|
||||
return match ? match.length : 0;
|
||||
@@ -207,7 +187,9 @@ namespace ts.moduleSpecifiers {
|
||||
const importedFileNames = redirects ? [...redirects, importedFileName] : [importedFileName];
|
||||
const cwd = host.getCurrentDirectory ? host.getCurrentDirectory() : "";
|
||||
const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
|
||||
const links = discoverProbableSymlinks(files, getCanonicalFileName, cwd);
|
||||
const links = host.getProbableSymlinks
|
||||
? host.getProbableSymlinks(files)
|
||||
: discoverProbableSymlinks(files, getCanonicalFileName, cwd);
|
||||
|
||||
const result: string[] = [];
|
||||
const compareStrings = (!host.useCaseSensitiveFileNames || host.useCaseSensitiveFileNames()) ? compareStringsCaseSensitive : compareStringsCaseInsensitive;
|
||||
@@ -299,7 +281,7 @@ namespace ts.moduleSpecifiers {
|
||||
: removeFileExtension(relativePath);
|
||||
}
|
||||
|
||||
function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions): string | undefined {
|
||||
function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined {
|
||||
if (!host.fileExists || !host.readFile) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -308,30 +290,33 @@ namespace ts.moduleSpecifiers {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let packageJsonContent: any | undefined;
|
||||
const packageRootPath = moduleFileName.substring(0, parts.packageRootIndex);
|
||||
const packageJsonPath = combinePaths(packageRootPath, "package.json");
|
||||
const packageJsonContent = host.fileExists(packageJsonPath)
|
||||
? JSON.parse(host.readFile(packageJsonPath)!)
|
||||
: undefined;
|
||||
const versionPaths = packageJsonContent && packageJsonContent.typesVersions
|
||||
? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)
|
||||
: undefined;
|
||||
if (versionPaths) {
|
||||
const subModuleName = moduleFileName.slice(parts.packageRootIndex + 1);
|
||||
const fromPaths = tryGetModuleNameFromPaths(
|
||||
removeFileExtension(subModuleName),
|
||||
removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options),
|
||||
versionPaths.paths
|
||||
);
|
||||
if (fromPaths !== undefined) {
|
||||
moduleFileName = combinePaths(moduleFileName.slice(0, parts.packageRootIndex), fromPaths);
|
||||
if (!packageNameOnly) {
|
||||
const packageJsonPath = combinePaths(packageRootPath, "package.json");
|
||||
packageJsonContent = host.fileExists(packageJsonPath)
|
||||
? JSON.parse(host.readFile(packageJsonPath)!)
|
||||
: undefined;
|
||||
const versionPaths = packageJsonContent && packageJsonContent.typesVersions
|
||||
? getPackageJsonTypesVersionsPaths(packageJsonContent.typesVersions)
|
||||
: undefined;
|
||||
if (versionPaths) {
|
||||
const subModuleName = moduleFileName.slice(parts.packageRootIndex + 1);
|
||||
const fromPaths = tryGetModuleNameFromPaths(
|
||||
removeFileExtension(subModuleName),
|
||||
removeExtensionAndIndexPostFix(subModuleName, Ending.Minimal, options),
|
||||
versionPaths.paths
|
||||
);
|
||||
if (fromPaths !== undefined) {
|
||||
moduleFileName = combinePaths(moduleFileName.slice(0, parts.packageRootIndex), fromPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simplify the full file path to something that can be resolved by Node.
|
||||
|
||||
// If the module could be imported by a directory name, use that directory's name
|
||||
const moduleSpecifier = getDirectoryOrExtensionlessFileName(moduleFileName);
|
||||
const moduleSpecifier = packageNameOnly ? moduleFileName : getDirectoryOrExtensionlessFileName(moduleFileName);
|
||||
// Get a path that's relative to node_modules or the importing file's path
|
||||
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
|
||||
if (!startsWith(sourceDirectory, getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)))) return undefined;
|
||||
|
||||
+14
-1
@@ -715,6 +715,7 @@ namespace ts {
|
||||
let processingDefaultLibFiles: SourceFile[] | undefined;
|
||||
let processingOtherFiles: SourceFile[] | undefined;
|
||||
let files: SourceFile[];
|
||||
let symlinks: ReadonlyMap<string> | undefined;
|
||||
let commonSourceDirectory: string;
|
||||
let diagnosticsProducingTypeChecker: TypeChecker;
|
||||
let noDiagnosticsTypeChecker: TypeChecker;
|
||||
@@ -973,7 +974,8 @@ namespace ts {
|
||||
getResolvedProjectReferenceByPath,
|
||||
forEachResolvedProjectReference,
|
||||
isSourceOfProjectReferenceRedirect,
|
||||
emitBuildInfo
|
||||
emitBuildInfo,
|
||||
getProbableSymlinks
|
||||
};
|
||||
|
||||
verifyCompilerOptions();
|
||||
@@ -1472,6 +1474,7 @@ namespace ts {
|
||||
getLibFileFromReference: program.getLibFileFromReference,
|
||||
isSourceFileFromExternalLibrary,
|
||||
getResolvedProjectReferenceToRedirect,
|
||||
getProbableSymlinks,
|
||||
writeFile: writeFileCallback || (
|
||||
(fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
|
||||
isEmitBlocked,
|
||||
@@ -3358,6 +3361,16 @@ namespace ts {
|
||||
function isSameFile(file1: string, file2: string) {
|
||||
return comparePaths(file1, file2, currentDirectory, !host.useCaseSensitiveFileNames()) === Comparison.EqualTo;
|
||||
}
|
||||
|
||||
function getProbableSymlinks(): ReadonlyMap<string> {
|
||||
if (host.getSymlinks) {
|
||||
return host.getSymlinks();
|
||||
}
|
||||
return symlinks || (symlinks = discoverProbableSymlinks(
|
||||
files,
|
||||
getCanonicalFileName,
|
||||
host.getCurrentDirectory()));
|
||||
}
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
|
||||
@@ -3068,6 +3068,7 @@ namespace ts {
|
||||
/*@internal*/ isSourceOfProjectReferenceRedirect(fileName: string): boolean;
|
||||
/*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined;
|
||||
/*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
|
||||
/*@internal*/ getProbableSymlinks(): ReadonlyMap<string>;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
@@ -4885,7 +4886,8 @@ namespace ts {
|
||||
}
|
||||
|
||||
export interface TypeAcquisition {
|
||||
/* @deprecated typingOptions.enableAutoDiscovery
|
||||
/**
|
||||
* @deprecated typingOptions.enableAutoDiscovery
|
||||
* Use typeAcquisition.enable instead.
|
||||
*/
|
||||
enableAutoDiscovery?: boolean;
|
||||
@@ -5335,6 +5337,7 @@ namespace ts {
|
||||
|
||||
// TODO: later handle this in better way in builder host instead once the api for tsbuild finalizes and doesn't use compilerHost as base
|
||||
/*@internal*/createDirectory?(directory: string): void;
|
||||
/*@internal*/getSymlinks?(): ReadonlyMap<string>;
|
||||
}
|
||||
|
||||
/** true if --out otherwise source file name */
|
||||
@@ -6020,11 +6023,13 @@ namespace ts {
|
||||
directoryExists?(directoryName: string): boolean;
|
||||
getCurrentDirectory?(): string;
|
||||
}
|
||||
/** @internal */
|
||||
|
||||
export interface ModuleSpecifierResolutionHost extends GetEffectiveTypeRootsHost {
|
||||
useCaseSensitiveFileNames?(): boolean;
|
||||
fileExists?(path: string): boolean;
|
||||
readFile?(path: string): string | undefined;
|
||||
/* @internal */
|
||||
getProbableSymlinks?(files: readonly SourceFile[]): ReadonlyMap<string>;
|
||||
}
|
||||
|
||||
// Note: this used to be deprecated in our public API, but is still used internally
|
||||
|
||||
@@ -4624,7 +4624,9 @@ namespace ts {
|
||||
}
|
||||
|
||||
/** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */
|
||||
export function forEachAncestorDirectory<T>(directory: string, callback: (directory: string) => T | undefined): T | undefined {
|
||||
export function forEachAncestorDirectory<T>(directory: Path, callback: (directory: Path) => T | undefined): T | undefined;
|
||||
export function forEachAncestorDirectory<T>(directory: string, callback: (directory: string) => T | undefined): T | undefined;
|
||||
export function forEachAncestorDirectory<T>(directory: Path, callback: (directory: Path) => T | undefined): T | undefined {
|
||||
while (true) {
|
||||
const result = callback(directory);
|
||||
if (result !== undefined) {
|
||||
@@ -7627,7 +7629,7 @@ namespace ts {
|
||||
|
||||
/**
|
||||
* Returns the path except for its basename. Semantics align with NodeJS's `path.dirname`
|
||||
* except that we support URL's as well.
|
||||
* except that we support URLs as well.
|
||||
*
|
||||
* ```ts
|
||||
* getDirectoryPath("/path/to/file.ext") === "/path/to"
|
||||
@@ -7638,7 +7640,7 @@ namespace ts {
|
||||
export function getDirectoryPath(path: Path): Path;
|
||||
/**
|
||||
* Returns the path except for its basename. Semantics align with NodeJS's `path.dirname`
|
||||
* except that we support URL's as well.
|
||||
* except that we support URLs as well.
|
||||
*
|
||||
* ```ts
|
||||
* getDirectoryPath("/path/to/file.ext") === "/path/to"
|
||||
@@ -7773,6 +7775,36 @@ namespace ts {
|
||||
if (pathComponents.length === 0) return "";
|
||||
return pathComponents.slice(1).join(directorySeparator);
|
||||
}
|
||||
|
||||
export function discoverProbableSymlinks(files: readonly SourceFile[], getCanonicalFileName: GetCanonicalFileName, cwd: string): ReadonlyMap<string> {
|
||||
const result = createMap<string>();
|
||||
const symlinks = flatten<readonly [string, string]>(mapDefined(files, sf =>
|
||||
sf.resolvedModules && compact(arrayFrom(mapIterator(sf.resolvedModules.values(), res =>
|
||||
res && res.originalPath && res.resolvedFileName !== res.originalPath ? [res.resolvedFileName, res.originalPath] as const : undefined)))));
|
||||
for (const [resolvedPath, originalPath] of symlinks) {
|
||||
const [commonResolved, commonOriginal] = guessDirectorySymlink(resolvedPath, originalPath, cwd, getCanonicalFileName);
|
||||
result.set(commonOriginal, commonResolved);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function guessDirectorySymlink(a: string, b: string, cwd: string, getCanonicalFileName: GetCanonicalFileName): [string, string] {
|
||||
const aParts = getPathComponents(toPath(a, cwd, getCanonicalFileName));
|
||||
const bParts = getPathComponents(toPath(b, cwd, getCanonicalFileName));
|
||||
while (!isNodeModulesOrScopedPackageDirectory(aParts[aParts.length - 2], getCanonicalFileName) &&
|
||||
!isNodeModulesOrScopedPackageDirectory(bParts[bParts.length - 2], getCanonicalFileName) &&
|
||||
getCanonicalFileName(aParts[aParts.length - 1]) === getCanonicalFileName(bParts[bParts.length - 1])) {
|
||||
aParts.pop();
|
||||
bParts.pop();
|
||||
}
|
||||
return [getPathFromPathComponents(aParts), getPathFromPathComponents(bParts)];
|
||||
}
|
||||
|
||||
// KLUDGE: Don't assume one 'node_modules' links to another. More likely a single directory inside the node_modules is the symlink.
|
||||
// ALso, don't assume that an `@foo` directory is linked. More likely the contents of that are linked.
|
||||
function isNodeModulesOrScopedPackageDirectory(s: string, getCanonicalFileName: GetCanonicalFileName): boolean {
|
||||
return getCanonicalFileName(s) === "node_modules" || startsWith(s, "@");
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
|
||||
@@ -124,6 +124,13 @@ namespace ts.server {
|
||||
return response;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
configure(preferences: UserPreferences) {
|
||||
const args: protocol.ConfigureRequestArguments = { preferences };
|
||||
const request = this.processRequest(CommandNames.Configure, args);
|
||||
this.processResponse(request, /*expectEmptyBody*/ true);
|
||||
}
|
||||
|
||||
openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void {
|
||||
const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName };
|
||||
this.processRequest(CommandNames.Open, args);
|
||||
|
||||
@@ -421,7 +421,10 @@ namespace FourSlash {
|
||||
})!;
|
||||
}
|
||||
|
||||
public goToPosition(pos: number) {
|
||||
public goToPosition(positionOrLineAndCharacter: number | ts.LineAndCharacter) {
|
||||
const pos = typeof positionOrLineAndCharacter === "number"
|
||||
? positionOrLineAndCharacter
|
||||
: this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, positionOrLineAndCharacter);
|
||||
this.currentCaretPosition = pos;
|
||||
this.selectionEnd = -1;
|
||||
}
|
||||
@@ -447,6 +450,12 @@ namespace FourSlash {
|
||||
this.selectionEnd = range.end;
|
||||
}
|
||||
|
||||
public selectLine(index: number) {
|
||||
const lineStart = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: index, character: 0 });
|
||||
const lineEnd = lineStart + this.getLineContent(index).length;
|
||||
this.selectRange({ fileName: this.activeFile.fileName, pos: lineStart, end: lineEnd });
|
||||
}
|
||||
|
||||
public moveCaretRight(count = 1) {
|
||||
this.currentCaretPosition += count;
|
||||
this.currentCaretPosition = Math.min(this.currentCaretPosition, this.getFileContent(this.activeFile.fileName).length);
|
||||
@@ -803,7 +812,7 @@ namespace FourSlash {
|
||||
const name = typeof include === "string" ? include : include.name;
|
||||
const found = nameToEntries.get(name);
|
||||
if (!found) throw this.raiseError(`Includes: completion '${name}' not found.`);
|
||||
assert(found.length === 1); // Must use 'exact' for multiple completions with same name
|
||||
assert(found.length === 1, `Must use 'exact' for multiple completions with same name: '${name}'`);
|
||||
this.verifyCompletionEntry(ts.first(found), include);
|
||||
}
|
||||
}
|
||||
@@ -1081,11 +1090,23 @@ namespace FourSlash {
|
||||
TestState.getDisplayPartsJson(expected), this.messageAtLastKnownMarker("referenced symbol definition display parts"));
|
||||
}
|
||||
|
||||
private configure(preferences: ts.UserPreferences) {
|
||||
if (this.testType === FourSlashTestType.Server) {
|
||||
(this.languageService as ts.server.SessionClient).configure(preferences);
|
||||
}
|
||||
}
|
||||
|
||||
private getCompletionListAtCaret(options?: ts.GetCompletionsAtPositionOptions): ts.CompletionInfo | undefined {
|
||||
if (options) {
|
||||
this.configure(options);
|
||||
}
|
||||
return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options);
|
||||
}
|
||||
|
||||
private getCompletionEntryDetails(entryName: string, source?: string, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined {
|
||||
if (preferences) {
|
||||
this.configure(preferences);
|
||||
}
|
||||
return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences);
|
||||
}
|
||||
|
||||
@@ -1696,6 +1717,12 @@ namespace FourSlash {
|
||||
this.checkPostEditInvariants();
|
||||
}
|
||||
|
||||
public deleteLineRange(startIndex: number, endIndexInclusive: number) {
|
||||
const startPos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: startIndex, character: 0 });
|
||||
const endPos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: endIndexInclusive + 1, character: 0 });
|
||||
this.replace(startPos, endPos - startPos, "");
|
||||
}
|
||||
|
||||
public deleteCharBehindMarker(count = 1) {
|
||||
let offset = this.currentCaretPosition;
|
||||
const ch = "";
|
||||
@@ -1721,6 +1748,8 @@ namespace FourSlash {
|
||||
let offset = this.currentCaretPosition;
|
||||
const prevChar = " ";
|
||||
const checkCadence = (text.length >> 2) + 1;
|
||||
const selection = this.getSelection();
|
||||
this.replace(selection.pos, selection.end - selection.pos, "");
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text.charAt(i);
|
||||
@@ -3049,11 +3078,9 @@ namespace FourSlash {
|
||||
Harness.IO.log(stringify(codeFixes));
|
||||
}
|
||||
|
||||
// Get the text of the entire line the caret is currently at
|
||||
private getCurrentLineContent() {
|
||||
private getLineContent(index: number) {
|
||||
const text = this.getFileContent(this.activeFile.fileName);
|
||||
|
||||
const pos = this.currentCaretPosition;
|
||||
const pos = this.languageServiceAdapterHost.lineAndCharacterToPosition(this.activeFile.fileName, { line: index, character: 0 });
|
||||
let startPos = pos, endPos = pos;
|
||||
|
||||
while (startPos > 0) {
|
||||
@@ -3078,6 +3105,14 @@ namespace FourSlash {
|
||||
return text.substring(startPos, endPos);
|
||||
}
|
||||
|
||||
// Get the text of the entire line the caret is currently at
|
||||
private getCurrentLineContent() {
|
||||
return this.getLineContent(this.languageServiceAdapterHost.positionToLineAndCharacter(
|
||||
this.activeFile.fileName,
|
||||
this.currentCaretPosition,
|
||||
).line);
|
||||
}
|
||||
|
||||
private findFile(indexOrName: string | number): FourSlashFile {
|
||||
if (typeof indexOrName === "number") {
|
||||
const index = indexOrName;
|
||||
@@ -3812,11 +3847,11 @@ namespace FourSlashInterface {
|
||||
this.state.goToImplementation();
|
||||
}
|
||||
|
||||
public position(position: number, fileNameOrIndex?: string | number): void {
|
||||
public position(positionOrLineAndCharacter: number | ts.LineAndCharacter, fileNameOrIndex?: string | number): void {
|
||||
if (fileNameOrIndex !== undefined) {
|
||||
this.file(fileNameOrIndex);
|
||||
}
|
||||
this.state.goToPosition(position);
|
||||
this.state.goToPosition(positionOrLineAndCharacter);
|
||||
}
|
||||
|
||||
// Opens a file, given either its index as it
|
||||
@@ -4289,6 +4324,19 @@ namespace FourSlashInterface {
|
||||
this.state.type(lines.join("\n"));
|
||||
}
|
||||
|
||||
public deleteLine(index: number) {
|
||||
this.deleteLineRange(index, index);
|
||||
}
|
||||
|
||||
public deleteLineRange(startIndex: number, endIndexInclusive: number) {
|
||||
this.state.deleteLineRange(startIndex, endIndexInclusive);
|
||||
}
|
||||
|
||||
public replaceLine(index: number, text: string) {
|
||||
this.state.selectLine(index);
|
||||
this.state.type(text);
|
||||
}
|
||||
|
||||
public moveRight(count?: number) {
|
||||
this.state.moveCaretRight(count);
|
||||
}
|
||||
|
||||
@@ -203,6 +203,12 @@ namespace Harness.LanguageService {
|
||||
return ts.computeLineAndCharacterOfPosition(script.getLineMap(), position);
|
||||
}
|
||||
|
||||
public lineAndCharacterToPosition(fileName: string, lineAndCharacter: ts.LineAndCharacter): number {
|
||||
const script: ScriptInfo = this.getScriptInfo(fileName)!;
|
||||
assert.isOk(script);
|
||||
return ts.computePositionOfLineAndCharacter(script.getLineMap(), lineAndCharacter.line, lineAndCharacter.character);
|
||||
}
|
||||
|
||||
useCaseSensitiveFileNames() {
|
||||
return !this.vfs.ignoreCase;
|
||||
}
|
||||
|
||||
@@ -1005,7 +1005,7 @@ namespace ts.server {
|
||||
directory,
|
||||
fileOrDirectory => {
|
||||
const fileOrDirectoryPath = this.toPath(fileOrDirectory);
|
||||
project.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
|
||||
const fsResult = project.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
|
||||
|
||||
// don't trigger callback on open, existing files
|
||||
if (project.fileIsOpen(fileOrDirectoryPath)) {
|
||||
@@ -1015,6 +1015,13 @@ namespace ts.server {
|
||||
if (isPathIgnored(fileOrDirectoryPath)) return;
|
||||
const configFilename = project.getConfigFilePath();
|
||||
|
||||
if (getBaseFileName(fileOrDirectoryPath) === "package.json" && !isInsideNodeModules(fileOrDirectoryPath) &&
|
||||
(fsResult && fsResult.fileExists || !fsResult && this.host.fileExists(fileOrDirectoryPath))
|
||||
) {
|
||||
this.logger.info(`Project: ${configFilename} Detected new package.json: ${fileOrDirectory}`);
|
||||
project.onAddPackageJson(fileOrDirectoryPath);
|
||||
}
|
||||
|
||||
// If the the added or created file or directory is not supported file name, ignore the file
|
||||
// But when watched directory is added/removed, we need to reload the file list
|
||||
if (fileOrDirectoryPath !== directory && hasExtension(fileOrDirectoryPath) && !isSupportedSourceFileName(fileOrDirectory, project.getCompilationSettings(), this.hostConfiguration.extraFileExtensions)) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*@internal*/
|
||||
namespace ts.server {
|
||||
export interface PackageJsonCache {
|
||||
addOrUpdate(fileName: Path): void;
|
||||
delete(fileName: Path): void;
|
||||
getInDirectory(directory: Path): PackageJsonInfo | undefined;
|
||||
directoryHasPackageJson(directory: Path): Ternary;
|
||||
searchDirectoryAndAncestors(directory: Path): void;
|
||||
}
|
||||
|
||||
export function createPackageJsonCache(project: Project): PackageJsonCache {
|
||||
const packageJsons = createMap<PackageJsonInfo>();
|
||||
const directoriesWithoutPackageJson = createMap<true>();
|
||||
return {
|
||||
addOrUpdate,
|
||||
delete: fileName => {
|
||||
packageJsons.delete(fileName);
|
||||
directoriesWithoutPackageJson.set(getDirectoryPath(fileName), true);
|
||||
},
|
||||
getInDirectory: directory => {
|
||||
return packageJsons.get(combinePaths(directory, "package.json"));
|
||||
},
|
||||
directoryHasPackageJson,
|
||||
searchDirectoryAndAncestors: directory => {
|
||||
forEachAncestorDirectory(directory, ancestor => {
|
||||
if (directoryHasPackageJson(ancestor) !== Ternary.Maybe) {
|
||||
return true;
|
||||
}
|
||||
const packageJsonFileName = project.toPath(combinePaths(ancestor, "package.json"));
|
||||
if (tryFileExists(project, packageJsonFileName)) {
|
||||
addOrUpdate(packageJsonFileName);
|
||||
}
|
||||
else {
|
||||
directoriesWithoutPackageJson.set(ancestor, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function addOrUpdate(fileName: Path) {
|
||||
const packageJsonInfo = createPackageJsonInfo(fileName, project);
|
||||
if (packageJsonInfo) {
|
||||
packageJsons.set(fileName, packageJsonInfo);
|
||||
directoriesWithoutPackageJson.delete(getDirectoryPath(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
function directoryHasPackageJson(directory: Path) {
|
||||
return packageJsons.has(combinePaths(directory, "package.json")) ? Ternary.True :
|
||||
directoriesWithoutPackageJson.has(directory) ? Ternary.False :
|
||||
Ternary.Maybe;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,9 @@ namespace ts.server {
|
||||
private generatedFilesMap: GeneratedFileWatcherMap | undefined;
|
||||
private plugins: PluginModuleWithName[] = [];
|
||||
|
||||
/*@internal*/
|
||||
private packageJsonFilesMap: Map<FileWatcher> | undefined;
|
||||
|
||||
/*@internal*/
|
||||
/**
|
||||
* This is map from files to unresolved imports in it
|
||||
@@ -234,6 +237,16 @@ namespace ts.server {
|
||||
/*@internal*/
|
||||
public readonly getCanonicalFileName: GetCanonicalFileName;
|
||||
|
||||
/*@internal*/
|
||||
readonly packageJsonCache: PackageJsonCache;
|
||||
|
||||
/*@internal*/
|
||||
private importSuggestionsCache = Completions.createImportSuggestionsForFileCache();
|
||||
/*@internal*/
|
||||
private dirtyFilesForSuggestions: Map<true> | undefined;
|
||||
/*@internal*/
|
||||
private symlinks: ReadonlyMap<string> | undefined;
|
||||
|
||||
/*@internal*/
|
||||
constructor(
|
||||
/*@internal*/ readonly projectName: string,
|
||||
@@ -284,6 +297,7 @@ namespace ts.server {
|
||||
}
|
||||
this.markAsDirty();
|
||||
this.projectService.pendingEnsureProjectForOpenFiles = true;
|
||||
this.packageJsonCache = createPackageJsonCache(this);
|
||||
}
|
||||
|
||||
isKnownTypesPackageName(name: string): boolean {
|
||||
@@ -297,6 +311,14 @@ namespace ts.server {
|
||||
return this.projectService.typingsCache;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getProbableSymlinks(files: readonly SourceFile[]): ReadonlyMap<string> {
|
||||
return this.symlinks || (this.symlinks = discoverProbableSymlinks(
|
||||
files,
|
||||
this.getCanonicalFileName,
|
||||
this.getCurrentDirectory()));
|
||||
}
|
||||
|
||||
// Method of LanguageServiceHost
|
||||
getCompilationSettings() {
|
||||
return this.compilerOptions;
|
||||
@@ -673,6 +695,10 @@ namespace ts.server {
|
||||
clearMap(this.missingFilesMap, closeFileWatcher);
|
||||
this.missingFilesMap = undefined!;
|
||||
}
|
||||
if (this.packageJsonFilesMap) {
|
||||
clearMap(this.packageJsonFilesMap, closeFileWatcher);
|
||||
this.packageJsonFilesMap = undefined;
|
||||
}
|
||||
this.clearGeneratedFileWatch();
|
||||
|
||||
// signal language service to release source files acquired from document registry
|
||||
@@ -847,6 +873,14 @@ namespace ts.server {
|
||||
(this.updatedFileNames || (this.updatedFileNames = createMap<true>())).set(fileName, true);
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
markFileAsDirty(changedFile: Path) {
|
||||
this.markAsDirty();
|
||||
if (!this.importSuggestionsCache.isEmpty()) {
|
||||
(this.dirtyFilesForSuggestions || (this.dirtyFilesForSuggestions = createMap())).set(changedFile, true);
|
||||
}
|
||||
}
|
||||
|
||||
markAsDirty() {
|
||||
if (!this.dirty) {
|
||||
this.projectStateVersion++;
|
||||
@@ -1008,6 +1042,29 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.importSuggestionsCache.isEmpty()) {
|
||||
if (this.hasAddedorRemovedFiles || oldProgram && !oldProgram.structureIsReused) {
|
||||
this.importSuggestionsCache.clear();
|
||||
}
|
||||
else if (this.dirtyFilesForSuggestions && oldProgram && this.program) {
|
||||
forEachKey(this.dirtyFilesForSuggestions, fileName => {
|
||||
const oldSourceFile = oldProgram.getSourceFile(fileName);
|
||||
const sourceFile = this.program!.getSourceFile(fileName);
|
||||
if (this.sourceFileHasChangedOwnImportSuggestions(oldSourceFile, sourceFile)) {
|
||||
this.importSuggestionsCache.clear();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.dirtyFilesForSuggestions) {
|
||||
this.dirtyFilesForSuggestions.clear();
|
||||
}
|
||||
|
||||
if (this.hasAddedorRemovedFiles) {
|
||||
this.symlinks = undefined;
|
||||
}
|
||||
|
||||
const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray<string>;
|
||||
this.externalFiles = this.getExternalFiles();
|
||||
enumerateInsertsAndDeletes<string, string>(this.externalFiles, oldExternalFiles, getStringComparer(!this.useCaseSensitiveFileNames()),
|
||||
@@ -1031,6 +1088,54 @@ namespace ts.server {
|
||||
return hasNewProgram;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
private sourceFileHasChangedOwnImportSuggestions(oldSourceFile: SourceFile | undefined, newSourceFile: SourceFile | undefined) {
|
||||
if (!oldSourceFile && !newSourceFile) {
|
||||
return false;
|
||||
}
|
||||
// Probably shouldn’t get this far, but on the off chance the file was added or removed,
|
||||
// we can’t reliably tell anything about it.
|
||||
if (!oldSourceFile || !newSourceFile) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Debug.assertEqual(oldSourceFile.fileName, newSourceFile.fileName);
|
||||
// If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node.
|
||||
// Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list.
|
||||
if (this.getTypeAcquisition().enable && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Module agumentation and ambient module changes can add or remove exports available to be auto-imported.
|
||||
// Changes elsewhere in the file can change the *type* of an export in a module augmentation,
|
||||
// but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache.
|
||||
if (
|
||||
!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) ||
|
||||
!this.ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
private ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) {
|
||||
if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) {
|
||||
return false;
|
||||
}
|
||||
let oldFileStatementIndex = -1;
|
||||
let newFileStatementIndex = -1;
|
||||
for (const ambientModuleName of newSourceFile.ambientModuleNames) {
|
||||
const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName;
|
||||
oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1);
|
||||
newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1);
|
||||
if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean) {
|
||||
const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName);
|
||||
if (scriptInfoToDetach) {
|
||||
@@ -1341,6 +1446,71 @@ namespace ts.server {
|
||||
refreshDiagnostics() {
|
||||
this.projectService.sendProjectsUpdatedInBackgroundEvent();
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getPackageJsonsVisibleToFile(fileName: string, rootDir?: string): readonly PackageJsonInfo[] {
|
||||
const packageJsonCache = this.packageJsonCache;
|
||||
const watchPackageJsonFile = this.watchPackageJsonFile.bind(this);
|
||||
const toPath = this.toPath.bind(this);
|
||||
const rootPath = rootDir && toPath(rootDir);
|
||||
const filePath = toPath(fileName);
|
||||
const result: PackageJsonInfo[] = [];
|
||||
forEachAncestorDirectory(getDirectoryPath(filePath), function processDirectory(directory): boolean | undefined {
|
||||
switch (packageJsonCache.directoryHasPackageJson(directory)) {
|
||||
// Sync and check same directory again
|
||||
case Ternary.Maybe:
|
||||
packageJsonCache.searchDirectoryAndAncestors(directory);
|
||||
return processDirectory(directory);
|
||||
// Check package.json
|
||||
case Ternary.True:
|
||||
const packageJsonFileName = combinePaths(directory, "package.json");
|
||||
watchPackageJsonFile(packageJsonFileName);
|
||||
result.push(Debug.assertDefined(packageJsonCache.getInDirectory(directory)));
|
||||
}
|
||||
if (rootPath && rootPath === toPath(directory)) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
onAddPackageJson(path: Path) {
|
||||
this.packageJsonCache.addOrUpdate(path);
|
||||
this.watchPackageJsonFile(path);
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getImportSuggestionsCache() {
|
||||
return this.importSuggestionsCache;
|
||||
}
|
||||
|
||||
private watchPackageJsonFile(path: Path) {
|
||||
const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = createMap());
|
||||
if (!watchers.has(path)) {
|
||||
watchers.set(path, this.projectService.watchFactory.watchFile(
|
||||
this.projectService.host,
|
||||
path,
|
||||
(fileName, eventKind) => {
|
||||
const path = this.toPath(fileName);
|
||||
switch (eventKind) {
|
||||
case FileWatcherEventKind.Created:
|
||||
return Debug.fail();
|
||||
case FileWatcherEventKind.Changed:
|
||||
this.packageJsonCache.addOrUpdate(path);
|
||||
break;
|
||||
case FileWatcherEventKind.Deleted:
|
||||
this.packageJsonCache.delete(path);
|
||||
watchers.get(path)!.close();
|
||||
watchers.delete(path);
|
||||
}
|
||||
},
|
||||
PollingInterval.Low,
|
||||
WatchType.PackageJsonFile,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: Map<readonly string[]>): SortedReadonlyArray<string> {
|
||||
|
||||
@@ -576,7 +576,7 @@ namespace ts.server {
|
||||
|
||||
markContainingProjectsAsDirty() {
|
||||
for (const p of this.containingProjects) {
|
||||
p.markAsDirty();
|
||||
p.markFileAsDirty(this.path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-27
@@ -1,27 +1,28 @@
|
||||
{
|
||||
"extends": "../tsconfig-base",
|
||||
"compilerOptions": {
|
||||
"removeComments": false,
|
||||
"outFile": "../../built/local/server.js",
|
||||
"preserveConstEnums": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../compiler" },
|
||||
{ "path": "../jsTyping" },
|
||||
{ "path": "../services" }
|
||||
],
|
||||
"files": [
|
||||
"types.ts",
|
||||
"utilities.ts",
|
||||
"protocol.ts",
|
||||
"scriptInfo.ts",
|
||||
"typingsCache.ts",
|
||||
"project.ts",
|
||||
"editorServices.ts",
|
||||
"session.ts",
|
||||
"scriptVersionCache.ts"
|
||||
]
|
||||
}
|
||||
{
|
||||
"extends": "../tsconfig-base",
|
||||
"compilerOptions": {
|
||||
"removeComments": false,
|
||||
"outFile": "../../built/local/server.js",
|
||||
"preserveConstEnums": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../compiler" },
|
||||
{ "path": "../jsTyping" },
|
||||
{ "path": "../services" }
|
||||
],
|
||||
"files": [
|
||||
"types.ts",
|
||||
"utilities.ts",
|
||||
"protocol.ts",
|
||||
"scriptInfo.ts",
|
||||
"typingsCache.ts",
|
||||
"project.ts",
|
||||
"editorServices.ts",
|
||||
"packageJsonCache.ts",
|
||||
"session.ts",
|
||||
"scriptVersionCache.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@ namespace ts {
|
||||
NodeModulesForClosedScriptInfo = "node_modules for closed script infos in them",
|
||||
MissingSourceMapFile = "Missing source map file",
|
||||
NoopConfigFileForInferredRoot = "Noop Config file for the inferred project root",
|
||||
MissingGeneratedFile = "Missing generated file"
|
||||
MissingGeneratedFile = "Missing generated file",
|
||||
PackageJsonFile = "package.json file for import suggestions"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,13 +284,27 @@ namespace ts.codefix {
|
||||
preferences: UserPreferences,
|
||||
): readonly (FixAddNewImport | FixUseImportType)[] {
|
||||
const isJs = isSourceFileJS(sourceFile);
|
||||
const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host);
|
||||
const choicesForEachExportingModule = flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) =>
|
||||
moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getCompilerOptions(), sourceFile, host, program.getSourceFiles(), preferences, program.redirectTargetsMap)
|
||||
.map((moduleSpecifier): FixAddNewImport | FixUseImportType =>
|
||||
// `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
|
||||
exportedSymbolIsTypeOnly && isJs ? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.assertDefined(position, "position should be defined") } : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind }));
|
||||
// Sort to keep the shortest paths first
|
||||
return sort(choicesForEachExportingModule, (a, b) => a.moduleSpecifier.length - b.moduleSpecifier.length);
|
||||
exportedSymbolIsTypeOnly && isJs
|
||||
? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.assertDefined(position, "position should be defined") }
|
||||
: { kind: ImportFixKind.AddNew, moduleSpecifier, importKind }));
|
||||
|
||||
// Sort by presence in package.json, then shortest paths first
|
||||
return sort(choicesForEachExportingModule, (a, b) => {
|
||||
const allowsImportingA = allowsImportingSpecifier(a.moduleSpecifier);
|
||||
const allowsImportingB = allowsImportingSpecifier(b.moduleSpecifier);
|
||||
if (allowsImportingA && !allowsImportingB) {
|
||||
return -1;
|
||||
}
|
||||
if (allowsImportingB && !allowsImportingA) {
|
||||
return 1;
|
||||
}
|
||||
return a.moduleSpecifier.length - b.moduleSpecifier.length;
|
||||
});
|
||||
}
|
||||
|
||||
function getFixesForAddImport(
|
||||
@@ -384,7 +398,8 @@ namespace ts.codefix {
|
||||
// "default" is a keyword and not a legal identifier for the import, so we don't expect it here
|
||||
Debug.assert(symbolName !== InternalSymbolName.Default, "'default' isn't a legal identifier and couldn't occur here");
|
||||
|
||||
const fixes = arrayFrom(flatMapIterator(getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program).entries(), ([_, exportInfos]) =>
|
||||
const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program, host);
|
||||
const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) =>
|
||||
getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), program, sourceFile, host, preferences)));
|
||||
return { fixes, symbolName };
|
||||
}
|
||||
@@ -397,6 +412,7 @@ namespace ts.codefix {
|
||||
sourceFile: SourceFile,
|
||||
checker: TypeChecker,
|
||||
program: Program,
|
||||
host: LanguageServiceHost
|
||||
): ReadonlyMap<readonly SymbolExportInfo[]> {
|
||||
// For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once.
|
||||
// Maps symbol id to info for modules providing that symbol (original export + re-exports).
|
||||
@@ -404,7 +420,7 @@ namespace ts.codefix {
|
||||
function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind): void {
|
||||
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker) });
|
||||
}
|
||||
forEachExternalModuleToImportFrom(checker, sourceFile, program.getSourceFiles(), moduleSymbol => {
|
||||
forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, moduleSymbol => {
|
||||
cancellationToken.throwIfCancellationRequested();
|
||||
|
||||
const defaultInfo = getDefaultLikeExportInfo(sourceFile, moduleSymbol, checker, program.getCompilerOptions());
|
||||
@@ -605,12 +621,37 @@ namespace ts.codefix {
|
||||
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
|
||||
}
|
||||
|
||||
export function forEachExternalModuleToImportFrom(checker: TypeChecker, from: SourceFile, allSourceFiles: readonly SourceFile[], cb: (module: Symbol) => void) {
|
||||
forEachExternalModule(checker, allSourceFiles, (module, sourceFile) => {
|
||||
if (sourceFile === undefined || sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName)) {
|
||||
cb(module);
|
||||
export function forEachExternalModuleToImportFrom(
|
||||
program: Program,
|
||||
host: LanguageServiceHost,
|
||||
from: SourceFile,
|
||||
filterByPackageJson: boolean,
|
||||
cb: (module: Symbol) => void,
|
||||
) {
|
||||
let filteredCount = 0;
|
||||
const packageJson = filterByPackageJson && createAutoImportFilter(from, program, host);
|
||||
const allSourceFiles = program.getSourceFiles();
|
||||
forEachExternalModule(program.getTypeChecker(), allSourceFiles, (module, sourceFile) => {
|
||||
if (sourceFile === undefined) {
|
||||
if (!packageJson || packageJson.allowsImportingAmbientModule(module, allSourceFiles)) {
|
||||
cb(module);
|
||||
}
|
||||
else if (packageJson) {
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
else if (sourceFile && sourceFile !== from && isImportablePath(from.fileName, sourceFile.fileName)) {
|
||||
if (!packageJson || packageJson.allowsImportingSourceFile(sourceFile, allSourceFiles)) {
|
||||
cb(module);
|
||||
}
|
||||
else if (packageJson) {
|
||||
filteredCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (host.log) {
|
||||
host.log(`forEachExternalModuleToImportFrom: filtered out ${filteredCount} modules by package.json contents`);
|
||||
}
|
||||
}
|
||||
|
||||
function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
|
||||
@@ -664,4 +705,127 @@ namespace ts.codefix {
|
||||
// Need `|| "_"` to ensure result isn't empty.
|
||||
return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`;
|
||||
}
|
||||
|
||||
function createAutoImportFilter(fromFile: SourceFile, program: Program, host: LanguageServiceHost) {
|
||||
const packageJsons = host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName) || getPackageJsonsVisibleToFile(fromFile.fileName, host);
|
||||
const dependencyGroups = PackageJsonDependencyGroup.Dependencies | PackageJsonDependencyGroup.DevDependencies | PackageJsonDependencyGroup.OptionalDependencies;
|
||||
// Mix in `getProbablySymlinks` from Program when host doesn't have it
|
||||
// in order for non-Project hosts to have a symlinks cache.
|
||||
const moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost = {
|
||||
directoryExists: maybeBind(host, host.directoryExists),
|
||||
fileExists: maybeBind(host, host.fileExists),
|
||||
getCurrentDirectory: maybeBind(host, host.getCurrentDirectory),
|
||||
readFile: maybeBind(host, host.readFile),
|
||||
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
|
||||
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks) || program.getProbableSymlinks,
|
||||
};
|
||||
|
||||
let usesNodeCoreModules: boolean | undefined;
|
||||
return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier };
|
||||
|
||||
function moduleSpecifierIsCoveredByPackageJson(specifier: string) {
|
||||
const packageName = getNodeModuleRootSpecifier(specifier);
|
||||
for (const packageJson of packageJsons) {
|
||||
if (packageJson.has(packageName, dependencyGroups) || packageJson.has(getTypesPackageName(packageName), dependencyGroups)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function allowsImportingAmbientModule(moduleSymbol: Symbol, allSourceFiles: readonly SourceFile[]): boolean {
|
||||
if (!packageJsons.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile();
|
||||
const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName, allSourceFiles);
|
||||
if (typeof declaringNodeModuleName === "undefined") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const declaredModuleSpecifier = stripQuotes(moduleSymbol.getName());
|
||||
if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName)
|
||||
|| moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier);
|
||||
}
|
||||
|
||||
function allowsImportingSourceFile(sourceFile: SourceFile, allSourceFiles: readonly SourceFile[]): boolean {
|
||||
if (!packageJsons.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName, allSourceFiles);
|
||||
if (!moduleSpecifier) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use for a specific module specifier that has already been resolved.
|
||||
* Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve
|
||||
* the best module specifier for a given module _and_ determine if it’s importable.
|
||||
*/
|
||||
function allowsImportingSpecifier(moduleSpecifier: string) {
|
||||
if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) {
|
||||
return true;
|
||||
}
|
||||
if (pathIsRelative(moduleSpecifier) || isRootedDiskPath(moduleSpecifier)) {
|
||||
return true;
|
||||
}
|
||||
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
|
||||
}
|
||||
|
||||
function isAllowedCoreNodeModulesImport(moduleSpecifier: string) {
|
||||
// If we’re in JavaScript, it can be difficult to tell whether the user wants to import
|
||||
// from Node core modules or not. We can start by seeing if the user is actually using
|
||||
// any node core modules, as opposed to simply having @types/node accidentally as a
|
||||
// dependency of a dependency.
|
||||
if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) {
|
||||
if (usesNodeCoreModules === undefined) {
|
||||
usesNodeCoreModules = consumesNodeCoreModules(fromFile);
|
||||
}
|
||||
if (usesNodeCoreModules) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNodeModulesPackageNameFromFileName(importedFileName: string, allSourceFiles: readonly SourceFile[]): string | undefined {
|
||||
if (!stringContains(importedFileName, "node_modules")) {
|
||||
return undefined;
|
||||
}
|
||||
const specifier = moduleSpecifiers.getNodeModulesPackageName(
|
||||
host.getCompilationSettings(),
|
||||
fromFile.path,
|
||||
importedFileName,
|
||||
moduleSpecifierResolutionHost,
|
||||
allSourceFiles,
|
||||
program.redirectTargetsMap);
|
||||
|
||||
if (!specifier) {
|
||||
return undefined;
|
||||
}
|
||||
// Paths here are not node_modules, so we don’t care about them;
|
||||
// returning anything will trigger a lookup in package.json.
|
||||
if (!pathIsRelative(specifier) && !isRootedDiskPath(specifier)) {
|
||||
return getNodeModuleRootSpecifier(specifier);
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeModuleRootSpecifier(fullSpecifier: string): string {
|
||||
const components = getPathComponents(getPackageNameFromTypesPackageName(fullSpecifier)).slice(1);
|
||||
// Scoped packages
|
||||
if (startsWith(components[0], "@")) {
|
||||
return `${components[0]}/${components[1]}`;
|
||||
}
|
||||
return components[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+242
-38
@@ -50,7 +50,72 @@ namespace ts.Completions {
|
||||
|
||||
const enum GlobalsSearch { Continue, Success, Fail }
|
||||
|
||||
export function getCompletionsAtPosition(host: LanguageServiceHost, program: Program, log: Log, sourceFile: SourceFile, position: number, preferences: UserPreferences, triggerCharacter: CompletionsTriggerCharacter | undefined): CompletionInfo | undefined {
|
||||
export interface AutoImportSuggestion {
|
||||
symbol: Symbol;
|
||||
symbolName: string;
|
||||
skipFilter: boolean;
|
||||
origin: SymbolOriginInfoExport;
|
||||
}
|
||||
export interface ImportSuggestionsForFileCache {
|
||||
clear(): void;
|
||||
get(fileName: string, checker: TypeChecker, projectVersion?: string): readonly AutoImportSuggestion[] | undefined;
|
||||
set(fileName: string, suggestions: readonly AutoImportSuggestion[], projectVersion?: string): void;
|
||||
isEmpty(): boolean;
|
||||
}
|
||||
export function createImportSuggestionsForFileCache(): ImportSuggestionsForFileCache {
|
||||
let cache: readonly AutoImportSuggestion[] | undefined;
|
||||
let projectVersion: string | undefined;
|
||||
let fileName: string | undefined;
|
||||
return {
|
||||
isEmpty() {
|
||||
return !cache;
|
||||
},
|
||||
clear: () => {
|
||||
cache = undefined;
|
||||
fileName = undefined;
|
||||
projectVersion = undefined;
|
||||
},
|
||||
set: (file, suggestions, version) => {
|
||||
cache = suggestions;
|
||||
fileName = file;
|
||||
if (version) {
|
||||
projectVersion = version;
|
||||
}
|
||||
},
|
||||
get: (file, checker, version) => {
|
||||
if (file !== fileName) {
|
||||
return undefined;
|
||||
}
|
||||
if (version) {
|
||||
return projectVersion === version ? cache : undefined;
|
||||
}
|
||||
forEach(cache, suggestion => {
|
||||
// If the symbol/moduleSymbol was a merged symbol, it will have a new identity
|
||||
// in the checker, even though the symbols to merge are the same (guaranteed by
|
||||
// cache invalidation in synchronizeHostData).
|
||||
if (suggestion.symbol.declarations) {
|
||||
suggestion.symbol = checker.getMergedSymbol(suggestion.origin.isDefaultExport
|
||||
? suggestion.symbol.declarations[0].localSymbol || suggestion.symbol.declarations[0].symbol
|
||||
: suggestion.symbol.declarations[0].symbol);
|
||||
}
|
||||
if (suggestion.origin.moduleSymbol.declarations) {
|
||||
suggestion.origin.moduleSymbol = checker.getMergedSymbol(suggestion.origin.moduleSymbol.declarations[0].symbol);
|
||||
}
|
||||
});
|
||||
return cache;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getCompletionsAtPosition(
|
||||
host: LanguageServiceHost,
|
||||
program: Program,
|
||||
log: Log,
|
||||
sourceFile: SourceFile,
|
||||
position: number,
|
||||
preferences: UserPreferences,
|
||||
triggerCharacter: CompletionsTriggerCharacter | undefined,
|
||||
): CompletionInfo | undefined {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
|
||||
@@ -69,7 +134,7 @@ namespace ts.Completions {
|
||||
return getLabelCompletionAtPosition(contextToken.parent);
|
||||
}
|
||||
|
||||
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, preferences, /*detailsEntryId*/ undefined);
|
||||
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, preferences, /*detailsEntryId*/ undefined, host);
|
||||
if (!completionData) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -418,10 +483,16 @@ namespace ts.Completions {
|
||||
previousToken: Node | undefined;
|
||||
readonly isJsxInitializer: IsJsxInitializer;
|
||||
}
|
||||
function getSymbolCompletionFromEntryId(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier,
|
||||
function getSymbolCompletionFromEntryId(
|
||||
program: Program,
|
||||
log: Log,
|
||||
sourceFile: SourceFile,
|
||||
position: number,
|
||||
entryId: CompletionEntryIdentifier,
|
||||
host: LanguageServiceHost,
|
||||
): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "none" } {
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId);
|
||||
const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host);
|
||||
if (!completionData) {
|
||||
return { type: "none" };
|
||||
}
|
||||
@@ -442,7 +513,7 @@ namespace ts.Completions {
|
||||
const origin = symbolToOriginInfoMap[getSymbolId(symbol)];
|
||||
const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind);
|
||||
return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source
|
||||
? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer }
|
||||
? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer }
|
||||
: undefined;
|
||||
}) || { type: "none" };
|
||||
}
|
||||
@@ -483,7 +554,7 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
// Compute all the completion symbols again.
|
||||
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
|
||||
const symbolCompletion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host);
|
||||
switch (symbolCompletion.type) {
|
||||
case "request": {
|
||||
const { request } = symbolCompletion;
|
||||
@@ -568,8 +639,15 @@ namespace ts.Completions {
|
||||
return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] };
|
||||
}
|
||||
|
||||
export function getCompletionEntrySymbol(program: Program, log: Log, sourceFile: SourceFile, position: number, entryId: CompletionEntryIdentifier): Symbol | undefined {
|
||||
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId);
|
||||
export function getCompletionEntrySymbol(
|
||||
program: Program,
|
||||
log: Log,
|
||||
sourceFile: SourceFile,
|
||||
position: number,
|
||||
entryId: CompletionEntryIdentifier,
|
||||
host: LanguageServiceHost,
|
||||
): Symbol | undefined {
|
||||
const completion = getSymbolCompletionFromEntryId(program, log, sourceFile, position, entryId, host);
|
||||
return completion.type === "symbol" ? completion.symbol : undefined;
|
||||
}
|
||||
|
||||
@@ -668,6 +746,7 @@ namespace ts.Completions {
|
||||
position: number,
|
||||
preferences: Pick<UserPreferences, "includeCompletionsForModuleExports" | "includeCompletionsWithInsertText">,
|
||||
detailsEntryId: CompletionEntryIdentifier | undefined,
|
||||
host: LanguageServiceHost,
|
||||
): CompletionData | Request | undefined {
|
||||
const typeChecker = program.getTypeChecker();
|
||||
|
||||
@@ -886,6 +965,7 @@ namespace ts.Completions {
|
||||
let symbols: Symbol[] = [];
|
||||
const symbolToOriginInfoMap: SymbolOriginInfoMap = [];
|
||||
const symbolToSortTextMap: SymbolSortTextMap = [];
|
||||
const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache();
|
||||
|
||||
if (isRightOfDot) {
|
||||
getTypeScriptMemberSymbols();
|
||||
@@ -916,7 +996,6 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
|
||||
|
||||
const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker);
|
||||
const literals = mapDefined(contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]), t => t.isLiteral() ? t.value : undefined);
|
||||
|
||||
@@ -1183,7 +1262,26 @@ namespace ts.Completions {
|
||||
}
|
||||
|
||||
if (shouldOfferImportCompletions()) {
|
||||
getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : "", program.getCompilerOptions().target!);
|
||||
const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : "";
|
||||
const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host);
|
||||
if (!detailsEntryId && importSuggestionsCache) {
|
||||
importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion());
|
||||
}
|
||||
autoImportSuggestions.forEach(({ symbol, symbolName, skipFilter, origin }) => {
|
||||
if (detailsEntryId) {
|
||||
if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!skipFilter && !stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const symbolId = getSymbolId(symbol);
|
||||
symbols.push(symbol);
|
||||
symbolToOriginInfoMap[symbolId] = origin;
|
||||
symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions;
|
||||
});
|
||||
}
|
||||
filterGlobalCompletion(symbols);
|
||||
}
|
||||
@@ -1301,12 +1399,77 @@ namespace ts.Completions {
|
||||
typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules));
|
||||
}
|
||||
|
||||
function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string, target: ScriptTarget): void {
|
||||
const tokenTextLowerCase = tokenText.toLowerCase();
|
||||
/**
|
||||
* Gathers symbols that can be imported from other files, deduplicating along the way. Symbols can be “duplicates”
|
||||
* if re-exported from another module, e.g. `export { foo } from "./a"`. That syntax creates a fresh symbol, but
|
||||
* it’s just an alias to the first, and both have the same name, so we generally want to filter those aliases out,
|
||||
* if and only if the the first can be imported (it may be excluded due to package.json filtering in
|
||||
* `codefix.forEachExternalModuleToImportFrom`).
|
||||
*
|
||||
* Example. Imagine a chain of node_modules re-exporting one original symbol:
|
||||
*
|
||||
* ```js
|
||||
* node_modules/x/index.js node_modules/y/index.js node_modules/z/index.js
|
||||
* +-----------------------+ +--------------------------+ +--------------------------+
|
||||
* | | | | | |
|
||||
* | export const foo = 0; | <--- | export { foo } from 'x'; | <--- | export { foo } from 'y'; |
|
||||
* | | | | | |
|
||||
* +-----------------------+ +--------------------------+ +--------------------------+
|
||||
* ```
|
||||
*
|
||||
* Also imagine three buckets, which we’ll reference soon:
|
||||
*
|
||||
* ```md
|
||||
* | | | | | |
|
||||
* | **Bucket A** | | **Bucket B** | | **Bucket C** |
|
||||
* | Symbols to | | Aliases to symbols | | Symbols to return |
|
||||
* | definitely | | in Buckets A or C | | if nothing better |
|
||||
* | return | | (don’t return these) | | comes along |
|
||||
* |__________________| |______________________| |___________________|
|
||||
* ```
|
||||
*
|
||||
* We _probably_ want to show `foo` from 'x', but not from 'y' or 'z'. However, if 'x' is not in a package.json, it
|
||||
* will not appear in a `forEachExternalModuleToImportFrom` iteration. Furthermore, the order of iterations is not
|
||||
* guaranteed, as it is host-dependent. Therefore, when presented with the symbol `foo` from module 'y' alone, we
|
||||
* may not be sure whether or not it should go in the list. So, we’ll take the following steps:
|
||||
*
|
||||
* 1. Resolve alias `foo` from 'y' to the export declaration in 'x', get the symbol there, and see if that symbol is
|
||||
* already in Bucket A (symbols we already know will be returned). If it is, put `foo` from 'y' in Bucket B
|
||||
* (symbols that are aliases to symbols in Bucket A). If it’s not, put it in Bucket C.
|
||||
* 2. Next, imagine we see `foo` from module 'z'. Again, we resolve the alias to the nearest export, which is in 'y'.
|
||||
* At this point, if that nearest export from 'y' is in _any_ of the three buckets, we know the symbol in 'z'
|
||||
* should never be returned in the final list, so put it in Bucket B.
|
||||
* 3. Next, imagine we see `foo` from module 'x', the original. Syntactically, it doesn’t look like a re-export, so
|
||||
* we can just check Bucket C to see if we put any aliases to the original in there. If they exist, throw them out.
|
||||
* Put this symbol in Bucket A.
|
||||
* 4. After we’ve iterated through every symbol of every module, any symbol left in Bucket C means that step 3 didn’t
|
||||
* occur for that symbol---that is, the original symbol is not in Bucket A, so we should include the alias. Move
|
||||
* everything from Bucket C to Bucket A.
|
||||
*/
|
||||
function getSymbolsFromOtherSourceFileExports(target: ScriptTarget, host: LanguageServiceHost): readonly AutoImportSuggestion[] {
|
||||
const cached = importSuggestionsCache && importSuggestionsCache.get(
|
||||
sourceFile.fileName,
|
||||
typeChecker,
|
||||
detailsEntryId && host.getProjectVersion ? host.getProjectVersion() : undefined);
|
||||
|
||||
if (cached) {
|
||||
log("getSymbolsFromOtherSourceFileExports: Using cached list");
|
||||
return cached;
|
||||
}
|
||||
|
||||
const startTime = timestamp();
|
||||
log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`);
|
||||
const seenResolvedModules = createMap<true>();
|
||||
/** Bucket B */
|
||||
const aliasesToAlreadyIncludedSymbols = createMap<true>();
|
||||
/** Bucket C */
|
||||
const aliasesToReturnIfOriginalsAreMissing = createMap<{ alias: Symbol, moduleSymbol: Symbol }>();
|
||||
/** Bucket A */
|
||||
const results: AutoImportSuggestion[] = [];
|
||||
/** Ids present in `results` for faster lookup */
|
||||
const resultSymbolIds = createMap<true>();
|
||||
|
||||
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, program.getSourceFiles(), moduleSymbol => {
|
||||
codefix.forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, moduleSymbol => {
|
||||
// Perf -- ignore other modules if this is a request for details
|
||||
if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) {
|
||||
return;
|
||||
@@ -1318,42 +1481,74 @@ namespace ts.Completions {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't add another completion for `export =` of a symbol that's already global.
|
||||
// So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`.
|
||||
if (resolvedModuleSymbol !== moduleSymbol &&
|
||||
// Don't add another completion for `export =` of a symbol that's already global.
|
||||
// So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`.
|
||||
every(resolvedModuleSymbol.declarations, d => !!d.getSourceFile().externalModuleIndicator)) {
|
||||
symbols.push(resolvedModuleSymbol);
|
||||
symbolToSortTextMap[getSymbolId(resolvedModuleSymbol)] = SortText.AutoImportSuggestions;
|
||||
symbolToOriginInfoMap[getSymbolId(resolvedModuleSymbol)] = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport: false };
|
||||
pushSymbol(resolvedModuleSymbol, moduleSymbol, /*skipFilter*/ true);
|
||||
}
|
||||
|
||||
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
|
||||
// Don't add a completion for a re-export, only for the original.
|
||||
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
|
||||
// This is just to avoid adding duplicate completion entries.
|
||||
//
|
||||
// If `symbol.parent !== ...`, this is an `export * from "foo"` re-export. Those don't create new symbols.
|
||||
if (typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol
|
||||
|| some(symbol.declarations, d =>
|
||||
// If `!!d.name.originalKeywordKind`, this is `export { _break as break };` -- skip this and prefer the keyword completion.
|
||||
// If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
|
||||
isExportSpecifier(d) && (d.propertyName ? isIdentifierANonContextualKeyword(d.name) : !!d.parent.parent.moduleSpecifier))) {
|
||||
for (const symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
|
||||
// If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion.
|
||||
if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isDefaultExport = symbol.escapedName === InternalSymbolName.Default;
|
||||
if (isDefaultExport) {
|
||||
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
|
||||
const symbolId = getSymbolId(symbol).toString();
|
||||
// If `symbol.parent !== moduleSymbol`, this is an `export * from "foo"` re-export. Those don't create new symbols.
|
||||
const isExportStarFromReExport = typeChecker.getMergedSymbol(symbol.parent!) !== resolvedModuleSymbol;
|
||||
// If `!!d.parent.parent.moduleSpecifier`, this is `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
|
||||
if (isExportStarFromReExport || some(symbol.declarations, d => isExportSpecifier(d) && !d.propertyName && !!d.parent.parent.moduleSpecifier)) {
|
||||
// Walk the export chain back one module (step 1 or 2 in diagrammed example).
|
||||
// Or, in the case of `export * from "foo"`, `symbol` already points to the original export, so just use that.
|
||||
const nearestExportSymbol = isExportStarFromReExport ? symbol : getNearestExportSymbol(symbol);
|
||||
if (!nearestExportSymbol) continue;
|
||||
const nearestExportSymbolId = getSymbolId(nearestExportSymbol).toString();
|
||||
const symbolHasBeenSeen = resultSymbolIds.has(nearestExportSymbolId) || aliasesToAlreadyIncludedSymbols.has(nearestExportSymbolId);
|
||||
if (!symbolHasBeenSeen) {
|
||||
aliasesToReturnIfOriginalsAreMissing.set(nearestExportSymbolId, { alias: symbol, moduleSymbol });
|
||||
aliasesToAlreadyIncludedSymbols.set(symbolId, true);
|
||||
}
|
||||
else {
|
||||
// Perf - we know this symbol is an alias to one that’s already covered in `symbols`, so store it here
|
||||
// in case another symbol re-exports this one; that way we can short-circuit as soon as we see this symbol id.
|
||||
addToSeen(aliasesToAlreadyIncludedSymbols, symbolId);
|
||||
}
|
||||
}
|
||||
|
||||
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport };
|
||||
if (detailsEntryId || stringContainsCharactersInOrder(getSymbolName(symbol, origin, target).toLowerCase(), tokenTextLowerCase)) {
|
||||
symbols.push(symbol);
|
||||
symbolToSortTextMap[getSymbolId(symbol)] = SortText.AutoImportSuggestions;
|
||||
symbolToOriginInfoMap[getSymbolId(symbol)] = origin;
|
||||
else {
|
||||
// This is not a re-export, so see if we have any aliases pending and remove them (step 3 in diagrammed example)
|
||||
aliasesToReturnIfOriginalsAreMissing.delete(symbolId);
|
||||
pushSymbol(symbol, moduleSymbol);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// By this point, any potential duplicates that were actually duplicates have been
|
||||
// removed, so the rest need to be added. (Step 4 in diagrammed example)
|
||||
aliasesToReturnIfOriginalsAreMissing.forEach(({ alias, moduleSymbol }) => pushSymbol(alias, moduleSymbol));
|
||||
log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`);
|
||||
return results;
|
||||
|
||||
function pushSymbol(symbol: Symbol, moduleSymbol: Symbol, skipFilter = false) {
|
||||
const isDefaultExport = symbol.escapedName === InternalSymbolName.Default;
|
||||
if (isDefaultExport) {
|
||||
symbol = getLocalSymbolForExportDefault(symbol) || symbol;
|
||||
}
|
||||
addToSeen(resultSymbolIds, getSymbolId(symbol));
|
||||
const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport };
|
||||
results.push({
|
||||
symbol,
|
||||
symbolName: getSymbolName(symbol, origin, target),
|
||||
origin,
|
||||
skipFilter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getNearestExportSymbol(fromSymbol: Symbol) {
|
||||
return findAlias(typeChecker, fromSymbol, alias => {
|
||||
return some(alias.declarations, d => isExportSpecifier(d) || !!d.localSymbol);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2331,4 +2526,13 @@ namespace ts.Completions {
|
||||
function binaryExpressionMayBeOpenTag({ left }: BinaryExpression): boolean {
|
||||
return nodeIsMissing(left);
|
||||
}
|
||||
|
||||
function findAlias(typeChecker: TypeChecker, symbol: Symbol, predicate: (symbol: Symbol) => boolean): Symbol | undefined {
|
||||
let currentAlias: Symbol | undefined = symbol;
|
||||
while (currentAlias.flags & SymbolFlags.Alias && (currentAlias = typeChecker.getImmediateAliasedSymbol(currentAlias))) {
|
||||
if (predicate(currentAlias)) {
|
||||
return currentAlias;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1462,7 +1462,7 @@ namespace ts {
|
||||
|
||||
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol | undefined {
|
||||
synchronizeHostData();
|
||||
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source });
|
||||
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host);
|
||||
}
|
||||
|
||||
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
|
||||
|
||||
@@ -624,30 +624,6 @@ namespace ts.Completions.StringCompletions {
|
||||
}
|
||||
}
|
||||
|
||||
function findPackageJsons(directory: string, host: LanguageServiceHost): string[] {
|
||||
const paths: string[] = [];
|
||||
forEachAncestorDirectory(directory, ancestor => {
|
||||
const currentConfigPath = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
|
||||
if (!currentConfigPath) {
|
||||
return true; // break out
|
||||
}
|
||||
paths.push(currentConfigPath);
|
||||
});
|
||||
return paths;
|
||||
}
|
||||
|
||||
function findPackageJson(directory: string, host: LanguageServiceHost): string | undefined {
|
||||
let packageJson: string | undefined;
|
||||
forEachAncestorDirectory(directory, ancestor => {
|
||||
if (ancestor === "node_modules") return true;
|
||||
packageJson = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
|
||||
if (packageJson) {
|
||||
return true; // break out
|
||||
}
|
||||
});
|
||||
return packageJson;
|
||||
}
|
||||
|
||||
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): readonly string[] {
|
||||
if (!host.readFile || !host.fileExists) return emptyArray;
|
||||
|
||||
@@ -703,31 +679,6 @@ namespace ts.Completions.StringCompletions {
|
||||
|
||||
const nodeModulesDependencyKeys: readonly string[] = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
||||
|
||||
function tryGetDirectories(host: LanguageServiceHost, directoryName: string): string[] {
|
||||
return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || [];
|
||||
}
|
||||
|
||||
function tryReadDirectory(host: LanguageServiceHost, path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[]): readonly string[] {
|
||||
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray;
|
||||
}
|
||||
|
||||
function tryFileExists(host: LanguageServiceHost, path: string): boolean {
|
||||
return tryIOAndConsumeErrors(host, host.fileExists, path);
|
||||
}
|
||||
|
||||
function tryDirectoryExists(host: LanguageServiceHost, path: string): boolean {
|
||||
return tryAndIgnoreErrors(() => directoryProbablyExists(path, host)) || false;
|
||||
}
|
||||
|
||||
function tryIOAndConsumeErrors<T>(host: LanguageServiceHost, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) {
|
||||
return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args));
|
||||
}
|
||||
|
||||
function tryAndIgnoreErrors<T>(cb: () => T): T | undefined {
|
||||
try { return cb(); }
|
||||
catch { return undefined; }
|
||||
}
|
||||
|
||||
function containsSlash(fragment: string) {
|
||||
return stringContains(fragment, directorySeparator);
|
||||
}
|
||||
|
||||
+25
-2
@@ -171,10 +171,30 @@ namespace ts {
|
||||
packageName: string;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export const enum PackageJsonDependencyGroup {
|
||||
Dependencies = 1 << 0,
|
||||
DevDependencies = 1 << 1,
|
||||
PeerDependencies = 1 << 2,
|
||||
OptionalDependencies = 1 << 3,
|
||||
All = Dependencies | DevDependencies | PeerDependencies | OptionalDependencies,
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export interface PackageJsonInfo {
|
||||
fileName: string;
|
||||
dependencies?: Map<string>;
|
||||
devDependencies?: Map<string>;
|
||||
peerDependencies?: Map<string>;
|
||||
optionalDependencies?: Map<string>;
|
||||
get(dependencyName: string, inGroups?: PackageJsonDependencyGroup): string | undefined;
|
||||
has(dependencyName: string, inGroups?: PackageJsonDependencyGroup): boolean;
|
||||
}
|
||||
|
||||
//
|
||||
// Public interface of the host of a language service instance.
|
||||
//
|
||||
export interface LanguageServiceHost extends GetEffectiveTypeRootsHost {
|
||||
export interface LanguageServiceHost extends ModuleSpecifierResolutionHost {
|
||||
getCompilationSettings(): CompilerOptions;
|
||||
getNewLine?(): string;
|
||||
getProjectVersion?(): string;
|
||||
@@ -190,7 +210,6 @@ namespace ts {
|
||||
log?(s: string): void;
|
||||
trace?(s: string): void;
|
||||
error?(s: string): void;
|
||||
useCaseSensitiveFileNames?(): boolean;
|
||||
|
||||
/*
|
||||
* LS host can optionally implement these methods to support completions for module specifiers.
|
||||
@@ -239,6 +258,10 @@ namespace ts {
|
||||
/* @internal */
|
||||
getSourceFileLike?(fileName: string): SourceFileLike | undefined;
|
||||
/* @internal */
|
||||
getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[];
|
||||
/* @internal */
|
||||
getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache;
|
||||
/* @internal */
|
||||
setResolvedProjectReferenceCallbacks?(callbacks: ResolvedProjectReferenceCallbacks): void;
|
||||
/* @internal */
|
||||
useSourceOfProjectReferenceRedirect?(): boolean;
|
||||
|
||||
@@ -2105,4 +2105,145 @@ namespace ts {
|
||||
// If even 2/5 places have a semicolon, the user probably wants semicolons
|
||||
return withSemicolon / withoutSemicolon > 1 / nStatementsToObserve;
|
||||
}
|
||||
|
||||
export function tryGetDirectories(host: Pick<LanguageServiceHost, "getDirectories">, directoryName: string): string[] {
|
||||
return tryIOAndConsumeErrors(host, host.getDirectories, directoryName) || [];
|
||||
}
|
||||
|
||||
export function tryReadDirectory(host: Pick<LanguageServiceHost, "readDirectory">, path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[]): readonly string[] {
|
||||
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray;
|
||||
}
|
||||
|
||||
export function tryFileExists(host: Pick<LanguageServiceHost, "fileExists">, path: string): boolean {
|
||||
return tryIOAndConsumeErrors(host, host.fileExists, path);
|
||||
}
|
||||
|
||||
export function tryDirectoryExists(host: LanguageServiceHost, path: string): boolean {
|
||||
return tryAndIgnoreErrors(() => directoryProbablyExists(path, host)) || false;
|
||||
}
|
||||
|
||||
export function tryAndIgnoreErrors<T>(cb: () => T): T | undefined {
|
||||
try { return cb(); }
|
||||
catch { return undefined; }
|
||||
}
|
||||
|
||||
export function tryIOAndConsumeErrors<T>(host: unknown, toApply: ((...a: any[]) => T) | undefined, ...args: any[]) {
|
||||
return tryAndIgnoreErrors(() => toApply && toApply.apply(host, args));
|
||||
}
|
||||
|
||||
export function findPackageJsons(startDirectory: string, host: Pick<LanguageServiceHost, "fileExists">, stopDirectory?: string): string[] {
|
||||
const paths: string[] = [];
|
||||
forEachAncestorDirectory(startDirectory, ancestor => {
|
||||
if (ancestor === stopDirectory) {
|
||||
return true;
|
||||
}
|
||||
const currentConfigPath = combinePaths(ancestor, "package.json");
|
||||
if (tryFileExists(host, currentConfigPath)) {
|
||||
paths.push(currentConfigPath);
|
||||
}
|
||||
});
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function findPackageJson(directory: string, host: LanguageServiceHost): string | undefined {
|
||||
let packageJson: string | undefined;
|
||||
forEachAncestorDirectory(directory, ancestor => {
|
||||
if (ancestor === "node_modules") return true;
|
||||
packageJson = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
|
||||
if (packageJson) {
|
||||
return true; // break out
|
||||
}
|
||||
});
|
||||
return packageJson;
|
||||
}
|
||||
|
||||
export function getPackageJsonsVisibleToFile(fileName: string, host: LanguageServiceHost): readonly PackageJsonInfo[] {
|
||||
if (!host.fileExists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const packageJsons: PackageJsonInfo[] = [];
|
||||
forEachAncestorDirectory(getDirectoryPath(fileName), ancestor => {
|
||||
const packageJsonFileName = combinePaths(ancestor, "package.json");
|
||||
if (host.fileExists!(packageJsonFileName)) {
|
||||
const info = createPackageJsonInfo(packageJsonFileName, host);
|
||||
if (info) {
|
||||
packageJsons.push(info);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return packageJsons;
|
||||
}
|
||||
|
||||
export function createPackageJsonInfo(fileName: string, host: LanguageServiceHost): PackageJsonInfo | undefined {
|
||||
if (!host.readFile) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type PackageJsonRaw = Record<typeof dependencyKeys[number], Record<string, string> | undefined>;
|
||||
const dependencyKeys = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"] as const;
|
||||
const stringContent = host.readFile(fileName);
|
||||
const content = stringContent && tryParseJson(stringContent) as PackageJsonRaw;
|
||||
if (!content) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const info: Pick<PackageJsonInfo, typeof dependencyKeys[number]> = {};
|
||||
for (const key of dependencyKeys) {
|
||||
const dependencies = content[key];
|
||||
if (!dependencies) {
|
||||
continue;
|
||||
}
|
||||
const dependencyMap = createMap<string>();
|
||||
for (const packageName in dependencies) {
|
||||
dependencyMap.set(packageName, dependencies[packageName]);
|
||||
}
|
||||
info[key] = dependencyMap;
|
||||
}
|
||||
|
||||
const dependencyGroups = [
|
||||
[PackageJsonDependencyGroup.Dependencies, info.dependencies],
|
||||
[PackageJsonDependencyGroup.DevDependencies, info.devDependencies],
|
||||
[PackageJsonDependencyGroup.OptionalDependencies, info.optionalDependencies],
|
||||
[PackageJsonDependencyGroup.PeerDependencies, info.peerDependencies],
|
||||
] as const;
|
||||
|
||||
return {
|
||||
...info,
|
||||
fileName,
|
||||
get,
|
||||
has(dependencyName, inGroups) {
|
||||
return !!get(dependencyName, inGroups);
|
||||
},
|
||||
};
|
||||
|
||||
function get(dependencyName: string, inGroups = PackageJsonDependencyGroup.All) {
|
||||
for (const [group, deps] of dependencyGroups) {
|
||||
if (deps && (inGroups & group)) {
|
||||
const dep = deps.get(dependencyName);
|
||||
if (dep !== undefined) {
|
||||
return dep;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseJson(text: string) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function consumesNodeCoreModules(sourceFile: SourceFile): boolean {
|
||||
return some(sourceFile.imports, ({ text }) => JsTyping.nodeCoreModules.has(text));
|
||||
}
|
||||
|
||||
export function isInsideNodeModules(fileOrDirectory: string): boolean {
|
||||
return contains(getPathComponents(fileOrDirectory), "node_modules");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
"unittests/tsserver/getEditsForFileRename.ts",
|
||||
"unittests/tsserver/getExportReferences.ts",
|
||||
"unittests/tsserver/importHelpers.ts",
|
||||
"unittests/tsserver/importSuggestionsCache.ts",
|
||||
"unittests/tsserver/inferredProjects.ts",
|
||||
"unittests/tsserver/languageService.ts",
|
||||
"unittests/tsserver/maxNodeModuleJsDepth.ts",
|
||||
@@ -147,6 +148,7 @@
|
||||
"unittests/tsserver/navTo.ts",
|
||||
"unittests/tsserver/occurences.ts",
|
||||
"unittests/tsserver/openFile.ts",
|
||||
"unittests/tsserver/packageJsonInfo.ts",
|
||||
"unittests/tsserver/projectErrors.ts",
|
||||
"unittests/tsserver/projectReferenceCompileOnSave.ts",
|
||||
"unittests/tsserver/projectReferenceErrors.ts",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace ts.projectSystem {
|
||||
const aTs: File = {
|
||||
path: "/a.ts",
|
||||
content: "export const foo = 0;",
|
||||
};
|
||||
const bTs: File = {
|
||||
path: "/b.ts",
|
||||
content: "foo",
|
||||
};
|
||||
const tsconfig: File = {
|
||||
path: "/tsconfig.json",
|
||||
content: "{}",
|
||||
};
|
||||
const ambientDeclaration: File = {
|
||||
path: "/ambient.d.ts",
|
||||
content: "declare module 'ambient' {}"
|
||||
};
|
||||
|
||||
describe("unittests:: tsserver:: importSuggestionsCache", () => {
|
||||
it("caches auto-imports in the same file", () => {
|
||||
const { importSuggestionsCache, checker } = setup();
|
||||
assert.ok(importSuggestionsCache.get(bTs.path, checker));
|
||||
});
|
||||
|
||||
it("invalidates the cache when new files are added", () => {
|
||||
const { host, importSuggestionsCache, checker } = setup();
|
||||
host.reloadFS([aTs, bTs, ambientDeclaration, tsconfig, { ...aTs, path: "/src/a2.ts" }]);
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
assert.isUndefined(importSuggestionsCache.get(bTs.path, checker));
|
||||
});
|
||||
|
||||
it("invalidates the cache when files are deleted", () => {
|
||||
const { host, projectService, importSuggestionsCache, checker } = setup();
|
||||
projectService.closeClientFile(aTs.path);
|
||||
host.reloadFS([bTs, ambientDeclaration, tsconfig]);
|
||||
host.runQueuedTimeoutCallbacks();
|
||||
assert.isUndefined(importSuggestionsCache.get(bTs.path, checker));
|
||||
});
|
||||
});
|
||||
|
||||
function setup() {
|
||||
const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig]);
|
||||
const session = createSession(host);
|
||||
openFilesForSession([aTs, bTs], session);
|
||||
const projectService = session.getProjectService();
|
||||
const project = configuredProjectAt(projectService, 0);
|
||||
const requestLocation: protocol.FileLocationRequestArgs = {
|
||||
file: bTs.path,
|
||||
line: 1,
|
||||
offset: 3,
|
||||
};
|
||||
executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
|
||||
...requestLocation,
|
||||
includeExternalModuleExports: true,
|
||||
prefix: "foo",
|
||||
});
|
||||
const checker = project.getLanguageService().getProgram()!.getTypeChecker();
|
||||
return { host, project, projectService, importSuggestionsCache: project.getImportSuggestionsCache(), checker };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
namespace ts.projectSystem {
|
||||
const tsConfig: File = {
|
||||
path: "/tsconfig.json",
|
||||
content: "{}"
|
||||
};
|
||||
const packageJsonContent = {
|
||||
dependencies: {
|
||||
redux: "*"
|
||||
},
|
||||
peerDependencies: {
|
||||
react: "*"
|
||||
},
|
||||
optionalDependencies: {
|
||||
typescript: "*"
|
||||
},
|
||||
devDependencies: {
|
||||
webpack: "*"
|
||||
}
|
||||
};
|
||||
const packageJson: File = {
|
||||
path: "/package.json",
|
||||
content: JSON.stringify(packageJsonContent, undefined, 2)
|
||||
};
|
||||
|
||||
describe("unittests:: tsserver:: packageJsonInfo", () => {
|
||||
it("detects new package.json files that are added, caches them, and watches them", () => {
|
||||
// Initialize project without package.json
|
||||
const { project, host } = setup([tsConfig]);
|
||||
assert.isUndefined(project.packageJsonCache.getInDirectory("/" as Path));
|
||||
|
||||
// Add package.json
|
||||
host.reloadFS([tsConfig, packageJson]);
|
||||
let packageJsonInfo = project.packageJsonCache.getInDirectory("/" as Path)!;
|
||||
assert.ok(packageJsonInfo);
|
||||
assert.ok(packageJsonInfo.dependencies);
|
||||
assert.ok(packageJsonInfo.devDependencies);
|
||||
assert.ok(packageJsonInfo.peerDependencies);
|
||||
assert.ok(packageJsonInfo.optionalDependencies);
|
||||
|
||||
// Edit package.json
|
||||
host.reloadFS([
|
||||
tsConfig,
|
||||
{
|
||||
...packageJson,
|
||||
content: JSON.stringify({
|
||||
...packageJsonContent,
|
||||
dependencies: undefined
|
||||
})
|
||||
}
|
||||
]);
|
||||
packageJsonInfo = project.packageJsonCache.getInDirectory("/" as Path)!;
|
||||
assert.isUndefined(packageJsonInfo.dependencies);
|
||||
});
|
||||
|
||||
it("finds package.json on demand, watches for deletion, and removes them from cache", () => {
|
||||
// Initialize project with package.json
|
||||
const { project, host } = setup();
|
||||
project.getPackageJsonsVisibleToFile("/src/whatever/blah.ts" as Path);
|
||||
assert.ok(project.packageJsonCache.getInDirectory("/" as Path));
|
||||
|
||||
// Delete package.json
|
||||
host.reloadFS([tsConfig]);
|
||||
assert.isUndefined(project.packageJsonCache.getInDirectory("/" as Path));
|
||||
});
|
||||
|
||||
it("finds multiple package.json files when present", () => {
|
||||
// Initialize project with package.json at root
|
||||
const { project, host } = setup();
|
||||
// Add package.json in /src
|
||||
host.reloadFS([tsConfig, packageJson, { ...packageJson, path: "/src/package.json" }]);
|
||||
assert.lengthOf(project.getPackageJsonsVisibleToFile("/a.ts" as Path), 1);
|
||||
assert.lengthOf(project.getPackageJsonsVisibleToFile("/src/b.ts" as Path), 2);
|
||||
});
|
||||
});
|
||||
|
||||
function setup(files: readonly File[] = [tsConfig, packageJson]) {
|
||||
const host = createServerHost(files);
|
||||
const session = createSession(host);
|
||||
const projectService = session.getProjectService();
|
||||
projectService.openClientFile(files[0].path);
|
||||
const project = configuredProjectAt(projectService, 0);
|
||||
return { host, session, project, projectService };
|
||||
}
|
||||
}
|
||||
@@ -840,6 +840,7 @@ namespace ts.projectSystem {
|
||||
const watchedFilesExpected = createMap<number>();
|
||||
watchedFilesExpected.set(jsconfig.path, 1); // project files
|
||||
watchedFilesExpected.set(libFile.path, 1); // project files
|
||||
watchedFilesExpected.set(combinePaths(installer.globalTypingsCacheLocation, "package.json"), 1);
|
||||
checkWatchedFilesDetailed(host, watchedFilesExpected);
|
||||
|
||||
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
|
||||
|
||||
+11
-2
@@ -2641,6 +2641,10 @@ declare namespace ts {
|
||||
[option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined;
|
||||
}
|
||||
export interface TypeAcquisition {
|
||||
/**
|
||||
* @deprecated typingOptions.enableAutoDiscovery
|
||||
* Use typeAcquisition.enable instead.
|
||||
*/
|
||||
enableAutoDiscovery?: boolean;
|
||||
enable?: boolean;
|
||||
include?: string[];
|
||||
@@ -3059,6 +3063,11 @@ declare namespace ts {
|
||||
directoryExists?(directoryName: string): boolean;
|
||||
getCurrentDirectory?(): string;
|
||||
}
|
||||
export interface ModuleSpecifierResolutionHost extends GetEffectiveTypeRootsHost {
|
||||
useCaseSensitiveFileNames?(): boolean;
|
||||
fileExists?(path: string): boolean;
|
||||
readFile?(path: string): string | undefined;
|
||||
}
|
||||
export interface TextSpan {
|
||||
start: number;
|
||||
length: number;
|
||||
@@ -4922,7 +4931,7 @@ declare namespace ts {
|
||||
fileName: Path;
|
||||
packageName: string;
|
||||
}
|
||||
interface LanguageServiceHost extends GetEffectiveTypeRootsHost {
|
||||
interface LanguageServiceHost extends ModuleSpecifierResolutionHost {
|
||||
getCompilationSettings(): CompilerOptions;
|
||||
getNewLine?(): string;
|
||||
getProjectVersion?(): string;
|
||||
@@ -4938,7 +4947,6 @@ declare namespace ts {
|
||||
log?(s: string): void;
|
||||
trace?(s: string): void;
|
||||
error?(s: string): void;
|
||||
useCaseSensitiveFileNames?(): boolean;
|
||||
readDirectory?(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[];
|
||||
readFile?(path: string, encoding?: string): string | undefined;
|
||||
realpath?(path: string): string;
|
||||
@@ -8600,6 +8608,7 @@ declare namespace ts.server {
|
||||
private enableProxy;
|
||||
/** Starts a new check for diagnostics. Call this if some file has updated that would cause diagnostics to be changed. */
|
||||
refreshDiagnostics(): void;
|
||||
private watchPackageJsonFile;
|
||||
}
|
||||
/**
|
||||
* If a file is opened and no tsconfig (or jsconfig) is found,
|
||||
|
||||
+10
-2
@@ -2641,6 +2641,10 @@ declare namespace ts {
|
||||
[option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined;
|
||||
}
|
||||
export interface TypeAcquisition {
|
||||
/**
|
||||
* @deprecated typingOptions.enableAutoDiscovery
|
||||
* Use typeAcquisition.enable instead.
|
||||
*/
|
||||
enableAutoDiscovery?: boolean;
|
||||
enable?: boolean;
|
||||
include?: string[];
|
||||
@@ -3059,6 +3063,11 @@ declare namespace ts {
|
||||
directoryExists?(directoryName: string): boolean;
|
||||
getCurrentDirectory?(): string;
|
||||
}
|
||||
export interface ModuleSpecifierResolutionHost extends GetEffectiveTypeRootsHost {
|
||||
useCaseSensitiveFileNames?(): boolean;
|
||||
fileExists?(path: string): boolean;
|
||||
readFile?(path: string): string | undefined;
|
||||
}
|
||||
export interface TextSpan {
|
||||
start: number;
|
||||
length: number;
|
||||
@@ -4922,7 +4931,7 @@ declare namespace ts {
|
||||
fileName: Path;
|
||||
packageName: string;
|
||||
}
|
||||
interface LanguageServiceHost extends GetEffectiveTypeRootsHost {
|
||||
interface LanguageServiceHost extends ModuleSpecifierResolutionHost {
|
||||
getCompilationSettings(): CompilerOptions;
|
||||
getNewLine?(): string;
|
||||
getProjectVersion?(): string;
|
||||
@@ -4938,7 +4947,6 @@ declare namespace ts {
|
||||
log?(s: string): void;
|
||||
trace?(s: string): void;
|
||||
error?(s: string): void;
|
||||
useCaseSensitiveFileNames?(): boolean;
|
||||
readDirectory?(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[];
|
||||
readFile?(path: string, encoding?: string): string | undefined;
|
||||
realpath?(path: string): string;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "react": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/react/index.d.ts
|
||||
////export declare var React: any;
|
||||
|
||||
//@Filename: /node_modules/@types/react/package.json
|
||||
////{
|
||||
//// "name": "@types/react"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/fake-react/index.d.ts
|
||||
////export declare var ReactFake: any;
|
||||
|
||||
//@Filename: /node_modules/@types/fake-react/package.json
|
||||
////{
|
||||
//// "name": "@types/fake-react"
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////const x = Re/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
isNewIdentifierLocation: true,
|
||||
includes: {
|
||||
name: "React",
|
||||
hasAction: true,
|
||||
source: "/node_modules/@types/react/index",
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
excludes: "ReactFake",
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "devDependencies": {
|
||||
//// "@types/react": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/react/index.d.ts
|
||||
////export declare var React: any;
|
||||
|
||||
//@Filename: /node_modules/@types/react/package.json
|
||||
////{
|
||||
//// "name": "@types/react"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/fake-react/index.d.ts
|
||||
////export declare var ReactFake: any;
|
||||
|
||||
//@Filename: /node_modules/@types/fake-react/package.json
|
||||
////{
|
||||
//// "name": "@types/fake-react"
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////const x = Re/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
isNewIdentifierLocation: true,
|
||||
includes: {
|
||||
name: "React",
|
||||
hasAction: true,
|
||||
source: "/node_modules/@types/react/index",
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
excludes: "ReactFake",
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "react-syntax-highlighter": "*",
|
||||
//// "declared-by-foo": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/foo/index.d.ts
|
||||
////declare module "foo" {
|
||||
//// export const foo: any;
|
||||
////}
|
||||
////declare module "declared-by-foo" {
|
||||
//// export const declaredBySomethingNotInPackageJson: any;
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/foo/package.json
|
||||
////{
|
||||
//// "name": "@types/node"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/react-syntax-highlighter/index.d.ts
|
||||
////declare module "react-syntax-highlighter/sub" {
|
||||
//// const agate: any;
|
||||
//// export default agate;
|
||||
////}
|
||||
////declare module "something-else" {
|
||||
//// export const somethingElse: any;
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@types/react-syntax-highlighter/package.json
|
||||
////{
|
||||
//// "name": "@types/react-syntax-highlighter"
|
||||
////}
|
||||
|
||||
//@Filename: /src/ambient.ts
|
||||
////declare module "local" {
|
||||
//// export const local: any';
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////fo/*1*/
|
||||
////aga/*2*/
|
||||
////somethi/*3*/
|
||||
////declaredBy/*4*/
|
||||
////loca/*5*/
|
||||
|
||||
// 1. Ambient modules declared in node_modules should be included if
|
||||
// a) the declaring package is in package.json, or
|
||||
// b) the ambient module name is in package.json
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker("1"),
|
||||
exact: completion.globals,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
|
||||
// sub-modules count
|
||||
verify.completions({
|
||||
marker: test.marker("2"),
|
||||
includes: {
|
||||
name: "agate",
|
||||
source: "react-syntax-highlighter/sub",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
|
||||
// not in package.json but declared by something in package.json
|
||||
verify.completions({
|
||||
marker: test.marker("3"),
|
||||
includes: {
|
||||
name: "somethingElse",
|
||||
source: "something-else",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
|
||||
// in package.json but declared by something not in package.json
|
||||
verify.completions({
|
||||
marker: test.marker("4"),
|
||||
includes: {
|
||||
name: "declaredBySomethingNotInPackageJson",
|
||||
source: "declared-by-foo",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Ambient modules declared outside of node_modules should always be included
|
||||
verify.completions({
|
||||
marker: test.marker("5"),
|
||||
includes: {
|
||||
name: "local",
|
||||
source: "local",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "react": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/react/index.d.ts
|
||||
////export declare var React: any;
|
||||
|
||||
//@Filename: /node_modules/react/package.json
|
||||
////{
|
||||
//// "name": "react",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/fake-react/index.d.ts
|
||||
////export declare var ReactFake: any;
|
||||
|
||||
//@Filename: /node_modules/fake-react/package.json
|
||||
////{
|
||||
//// "name": "fake-react",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////const x = Re/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
isNewIdentifierLocation: true,
|
||||
includes: {
|
||||
name: "React",
|
||||
hasAction: true,
|
||||
source: "/node_modules/react/index",
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
excludes: "ReactFake",
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "react": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/react/index.d.ts
|
||||
////export declare var React: any;
|
||||
|
||||
//@Filename: /node_modules/react/package.json
|
||||
////{
|
||||
//// "name": "react",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /dir/package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "redux": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /dir/node_modules/redux/package.json
|
||||
////{
|
||||
//// "name": "redux",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /dir/node_modules/redux/index.d.ts
|
||||
////export declare var Redux: any;
|
||||
|
||||
//@Filename: /dir/index.ts
|
||||
////const x = Re/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
isNewIdentifierLocation: true,
|
||||
includes: {
|
||||
name: "React",
|
||||
hasAction: true,
|
||||
source: "/node_modules/react/index",
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
isNewIdentifierLocation: true,
|
||||
includes: {
|
||||
name: "Redux",
|
||||
hasAction: true,
|
||||
source: "/dir/node_modules/redux/index",
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "@emotion/core": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@emotion/css/index.d.ts
|
||||
////export declare const css: any;
|
||||
////const css2: any;
|
||||
////export { css2 };
|
||||
|
||||
//@Filename: /node_modules/@emotion/css/package.json
|
||||
////{
|
||||
//// "name": "@emotion/css",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/@emotion/core/index.d.ts
|
||||
////import { css2 } from "@emotion/css";
|
||||
////export { css } from "@emotion/css";
|
||||
////export { css2 };
|
||||
|
||||
//@Filename: /node_modules/@emotion/core/package.json
|
||||
////{
|
||||
//// "name": "@emotion/core",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////cs/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
includes: [
|
||||
completion.undefinedVarEntry,
|
||||
{
|
||||
name: "css",
|
||||
source: "/node_modules/@emotion/core/index",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
{
|
||||
name: "css2",
|
||||
source: "/node_modules/@emotion/core/index",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
...completion.statementKeywordsWithTypes
|
||||
],
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "b_": "*",
|
||||
//// "_c": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/a/index.d.ts
|
||||
////export const foo = 0;
|
||||
|
||||
//@Filename: /node_modules/a/package.json
|
||||
////{
|
||||
//// "name": "a",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/b_/index.d.ts
|
||||
////export { foo } from "a";
|
||||
|
||||
//@Filename: /node_modules/b_/package.json
|
||||
////{
|
||||
//// "name": "b_",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/_c/index.d.ts
|
||||
////export { foo } from "b_";
|
||||
|
||||
//@Filename: /node_modules/_c/package.json
|
||||
////{
|
||||
//// "name": "_c",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////fo/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
includes: [
|
||||
completion.undefinedVarEntry,
|
||||
{
|
||||
name: "foo",
|
||||
source: "/node_modules/b_/index",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
...completion.statementKeywordsWithTypes
|
||||
],
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "b": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/a/index.d.ts
|
||||
////export const foo = 0;
|
||||
|
||||
//@Filename: /node_modules/a/package.json
|
||||
////{
|
||||
//// "name": "a",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/b/index.d.ts
|
||||
////export * from "a";
|
||||
|
||||
//@Filename: /node_modules/b/package.json
|
||||
////{
|
||||
//// "name": "b",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////fo/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
includes: [
|
||||
completion.undefinedVarEntry,
|
||||
{
|
||||
name: "foo",
|
||||
source: "/node_modules/b/index",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
...completion.statementKeywordsWithTypes
|
||||
],
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
//@noEmit: true
|
||||
|
||||
//@Filename: /package.json
|
||||
////{
|
||||
//// "dependencies": {
|
||||
//// "c": "*"
|
||||
//// }
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/a/index.d.ts
|
||||
////export const foo = 0;
|
||||
|
||||
//@Filename: /node_modules/a/package.json
|
||||
////{
|
||||
//// "name": "a",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/b/index.d.ts
|
||||
////export * from "a";
|
||||
|
||||
//@Filename: /node_modules/b/package.json
|
||||
////{
|
||||
//// "name": "b",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /node_modules/c/index.d.ts
|
||||
////export * from "a";
|
||||
|
||||
//@Filename: /node_modules/c/package.json
|
||||
////{
|
||||
//// "name": "c",
|
||||
//// "types": "./index.d.ts"
|
||||
////}
|
||||
|
||||
//@Filename: /src/index.ts
|
||||
////fo/**/
|
||||
|
||||
verify.completions({
|
||||
marker: test.marker(""),
|
||||
includes: [
|
||||
completion.undefinedVarEntry,
|
||||
{
|
||||
name: "foo",
|
||||
source: "/node_modules/c/index",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
...completion.statementKeywordsWithTypes
|
||||
],
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @module: esnext
|
||||
|
||||
// @Filename: /a.ts
|
||||
////declare module "m" {
|
||||
//// export class M {}
|
||||
////}
|
||||
|
||||
// @Filename: /b.ts
|
||||
////declare module "m" {
|
||||
//// export interface M {}
|
||||
////}
|
||||
|
||||
// @Filename: /c.ts
|
||||
/////**/
|
||||
|
||||
verify.completions({
|
||||
marker: "",
|
||||
includes: {
|
||||
name: "M",
|
||||
source: "m",
|
||||
sourceDisplay: "m",
|
||||
text: "class M\ninterface M",
|
||||
kind: "class",
|
||||
kindModifiers: "export,declare",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
preferences: { includeCompletionsForModuleExports: true },
|
||||
});
|
||||
verify.applyCodeActionFromCompletion("", {
|
||||
name: "M",
|
||||
source: "m",
|
||||
description: `Import 'M' from module "m"`,
|
||||
newFileContent: `import { M } from "m";
|
||||
|
||||
`,
|
||||
});
|
||||
@@ -16,6 +16,9 @@
|
||||
// @Filename: /a_reexport_2.ts
|
||||
////export * from "./a";
|
||||
|
||||
// @Filename: /a_reexport_3.ts
|
||||
////export { foo } from "./a_reexport";
|
||||
|
||||
// @Filename: /b.ts
|
||||
////fo/**/
|
||||
|
||||
@@ -24,13 +27,13 @@ verify.completions({
|
||||
includes: [
|
||||
completion.undefinedVarEntry,
|
||||
{
|
||||
name: "foo",
|
||||
source: "/a",
|
||||
sourceDisplay: "./a",
|
||||
text: "(alias) const foo: 0\nexport foo",
|
||||
kind: "alias",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
name: "foo",
|
||||
source: "/a",
|
||||
sourceDisplay: "./a",
|
||||
text: "(alias) const foo: 0\nexport foo",
|
||||
kind: "alias",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions
|
||||
},
|
||||
...completion.statementKeywordsWithTypes,
|
||||
],
|
||||
|
||||
@@ -93,6 +93,11 @@ declare module ts {
|
||||
reportsUnnecessary?: {};
|
||||
}
|
||||
|
||||
interface LineAndCharacter {
|
||||
line: number;
|
||||
character: number;
|
||||
}
|
||||
|
||||
function flatMap<T, U>(array: ReadonlyArray<T>, mapfn: (x: T, i: number) => U | ReadonlyArray<U> | undefined): U[];
|
||||
}
|
||||
|
||||
@@ -194,10 +199,16 @@ declare namespace FourSlashInterface {
|
||||
implementation(): void;
|
||||
position(position: number, fileIndex?: number): any;
|
||||
position(position: number, fileName?: string): any;
|
||||
position(lineAndCharacter: ts.LineAndCharacter, fileName?: string): void;
|
||||
file(index: number, content?: string, scriptKindName?: string): any;
|
||||
file(name: string, content?: string, scriptKindName?: string): any;
|
||||
select(startMarker: string, endMarker: string): void;
|
||||
selectRange(range: Range): void;
|
||||
/**
|
||||
* Selects a line at a given index, not including any newline characters.
|
||||
* @param index 0-based
|
||||
*/
|
||||
selectLine(index: number): void;
|
||||
}
|
||||
class verifyNegatable {
|
||||
private negative;
|
||||
@@ -385,6 +396,15 @@ declare namespace FourSlashInterface {
|
||||
insert(text: string): void;
|
||||
insertLine(text: string): void;
|
||||
insertLines(...lines: string[]): void;
|
||||
/** @param index 0-based */
|
||||
deleteLine(index: number): void;
|
||||
/**
|
||||
* @param startIndex 0-based
|
||||
* @param endIndexInclusive 0-based
|
||||
*/
|
||||
deleteLineRange(startIndex: number, endIndexInclusive: number): void;
|
||||
/** @param index 0-based */
|
||||
replaceLine(index: number, text: string): void;
|
||||
moveRight(count?: number): void;
|
||||
moveLeft(count?: number): void;
|
||||
enableFormatting(): void;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//// [|f1/*0*/('');|]
|
||||
|
||||
// @Filename: package.json
|
||||
//// { "dependencies": { "package-name": "latest" } }
|
||||
//// { "dependencies": { "@scope/package-name": "latest" } }
|
||||
|
||||
// @Filename: node_modules/@scope/package-name/bin/lib/index.d.ts
|
||||
//// export function f1(text: string): string;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/// <reference path="../fourslash.ts" />
|
||||
|
||||
// @Filename: /tsconfig.json
|
||||
////{ "compilerOptions": { "module": "esnext" } }
|
||||
|
||||
// @Filename: /ambient.d.ts
|
||||
////declare module 'ambient' {
|
||||
//// export const ambient = 0;
|
||||
////}
|
||||
////a/**/
|
||||
|
||||
edit.disableFormatting();
|
||||
|
||||
// Ensure 'ambient' shows up
|
||||
verifyIncludes("ambient");
|
||||
|
||||
// Delete it, ensure it doesn’t show up
|
||||
edit.deleteLineRange(0, 2);
|
||||
verifyExcludes("ambient");
|
||||
|
||||
// Add it back with changes, ensure it shows up
|
||||
goTo.marker("");
|
||||
edit.insertLines(`
|
||||
declare module 'ambient' {
|
||||
export const ambient2 = 0;
|
||||
}`);
|
||||
verifyIncludes("ambient2");
|
||||
|
||||
// Replace 'ambient2' with 'ambient3'
|
||||
edit.replaceLine(2, " export const ambient3 = 0");
|
||||
verifyExcludes("ambient2");
|
||||
verifyIncludes("ambient3");
|
||||
|
||||
function verifyIncludes(name: string) {
|
||||
goTo.marker("");
|
||||
verify.completions({
|
||||
includes: {
|
||||
name,
|
||||
source: "ambient",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions,
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function verifyExcludes(name: string) {
|
||||
goTo.marker("");
|
||||
verify.completions({
|
||||
excludes: name,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/// <reference path="../fourslash.ts" />
|
||||
|
||||
// @Filename: /tsconfig.json
|
||||
////{
|
||||
//// "compilerOptions": {
|
||||
//// "module": "esnext",
|
||||
//// "allowJs": true,
|
||||
//// "checkJs": true,
|
||||
//// "typeRoots": [
|
||||
//// "node_modules/@types"
|
||||
//// ]
|
||||
//// },
|
||||
//// "include": ["**/*"],
|
||||
//// "typeAcquisition": {
|
||||
//// "enable": true
|
||||
//// }
|
||||
////}
|
||||
|
||||
// @Filename: /node_modules/@types/node/index.d.ts
|
||||
////declare module 'fs' {
|
||||
//// export function readFile(): void;
|
||||
////}
|
||||
////declare module 'util' {
|
||||
//// export function promisify(): void;
|
||||
////}
|
||||
|
||||
// @Filename: /package.json
|
||||
////{}
|
||||
|
||||
// @Filename: /a.js
|
||||
////
|
||||
////readF/**/
|
||||
|
||||
verifyExcludes("readFile");
|
||||
edit.replaceLine(0, "import { promisify } from 'util';");
|
||||
verifyIncludes("readFile");
|
||||
edit.deleteLine(0);
|
||||
verifyExcludes("readFile");
|
||||
|
||||
function verifyIncludes(name: string) {
|
||||
goTo.marker("");
|
||||
verify.completions({
|
||||
includes: {
|
||||
name,
|
||||
source: "fs",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions,
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function verifyExcludes(name: string) {
|
||||
goTo.marker("");
|
||||
verify.completions({
|
||||
excludes: name,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/// <reference path="../fourslash.ts" />
|
||||
|
||||
// @Filename: /tsconfig.json
|
||||
////{ "compilerOptions": { "module": "esnext" } }
|
||||
|
||||
// @Filename: /node_modules/@types/react/index.d.ts
|
||||
////export function useState(): void;
|
||||
|
||||
// @Filename: /a.ts
|
||||
////import 'react';
|
||||
////declare module 'react' {
|
||||
//// export function useBlah(): void;
|
||||
////}
|
||||
////0;
|
||||
////use/**/
|
||||
|
||||
verifyIncludes("useState");
|
||||
verifyIncludes("useBlah");
|
||||
|
||||
edit.replaceLine(2, " export function useYes(): true");
|
||||
verifyExcludes("useBlah");
|
||||
verifyIncludes("useYes");
|
||||
|
||||
function verifyIncludes(name: string) {
|
||||
goTo.marker("");
|
||||
verify.completions({
|
||||
includes: {
|
||||
name,
|
||||
source: "/node_modules/@types/react/index",
|
||||
hasAction: true,
|
||||
sortText: completion.SortText.AutoImportSuggestions,
|
||||
},
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function verifyExcludes(name: string) {
|
||||
goTo.marker("");
|
||||
verify.completions({
|
||||
excludes: name,
|
||||
preferences: {
|
||||
includeCompletionsForModuleExports: true,
|
||||
includeInsertTextCompletions: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user