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 commit 8ea4829587.

* 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 commit 8ea4829587.

* 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:
Andrew Branch
2019-09-27 13:38:31 -07:00
committed by GitHub
parent 558ece72cb
commit 304fcee09b
42 changed files with 1963 additions and 210 deletions
+2 -1
View File
@@ -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,
+39 -54
View File
@@ -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
View File
@@ -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*/
+7 -2
View File
@@ -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
+35 -3
View File
@@ -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 */
+7
View File
@@ -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);
+56 -8
View File
@@ -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);
}
+6
View File
@@ -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;
}
+8 -1
View File
@@ -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)) {
+54
View File
@@ -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;
}
}
}
+170
View File
@@ -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 shouldnt get this far, but on the off chance the file was added or removed,
// we cant 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 doesnt 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> {
+1 -1
View File
@@ -576,7 +576,7 @@ namespace ts.server {
markContainingProjectsAsDirty() {
for (const p of this.containingProjects) {
p.markAsDirty();
p.markFileAsDirty(this.path);
}
}
+28 -27
View File
@@ -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"
]
}
+2 -1
View File
@@ -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"
}
}
+173 -9
View File
@@ -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 its 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 were 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 dont 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
View File
@@ -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
* its 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 well 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 | | (dont 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, well 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 its 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 doesnt 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 weve iterated through every symbol of every module, any symbol left in Bucket C means that step 3 didnt
* 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 thats 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;
}
}
}
}
+1 -1
View File
@@ -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 {
-49
View File
@@ -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
View File
@@ -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;
+141
View File
@@ -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");
}
}
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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,
],
+20
View File
@@ -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 doesnt 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,
},
});
}