diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 14575e7f1733d..342cfc59be965 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -9936,6 +9936,7 @@ export interface UserPreferences { readonly organizeImportsNumericCollation?: boolean; readonly organizeImportsAccentCollation?: boolean; readonly organizeImportsCaseFirst?: "upper" | "lower" | false; + readonly excludeLibrarySymbolsInNavTo?: boolean; } /** Represents a bigint literal value without requiring bigint support */ diff --git a/src/harness/client.ts b/src/harness/client.ts index 6f3a3cf9bbac5..619fece8cae62 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -314,15 +314,25 @@ export class SessionClient implements LanguageService { return notImplemented(); } - getNavigateToItems(searchValue: string): NavigateToItem[] { + getNavigateToItems(searchValue: string, maxResultCount: number, file: string | undefined, _excludeDtsFiles: boolean | undefined, excludeLibFiles: boolean | undefined): NavigateToItem[] { const args: protocol.NavtoRequestArgs = { searchValue, - file: this.host.getScriptFileNames()[0], + file, + currentFileOnly: !!file, + maxResultCount, }; + const oldPreferences = this.preferences; + if (excludeLibFiles) { + this.configure({ excludeLibrarySymbolsInNavTo: true }); + } const request = this.processRequest(protocol.CommandTypes.Navto, args); const response = this.processResponse(request); + if (excludeLibFiles) { + this.configure(oldPreferences || {}); + } + return response.body!.map(entry => ({ // TODO: GH#18217 name: entry.name, containerName: entry.containerName || "", @@ -579,14 +589,13 @@ export class SessionClient implements LanguageService { const providePrefixAndSuffixTextForRename = typeof preferences === "boolean" ? preferences : preferences?.providePrefixAndSuffixTextForRename; const quotePreference = typeof preferences === "boolean" ? undefined : preferences?.quotePreference; if (providePrefixAndSuffixTextForRename !== undefined || quotePreference !== undefined) { + const oldPreferences = this.preferences; // User preferences have to be set through the `Configure` command this.configure({ providePrefixAndSuffixTextForRename, quotePreference }); // Options argument is not used, so don't pass in options this.getRenameInfo(fileName, position, /*preferences*/ {}, findInStrings, findInComments); // Restore previous user preferences - if (this.preferences) { - this.configure(this.preferences); - } + this.configure(oldPreferences || {}); } else { this.getRenameInfo(fileName, position, /*preferences*/ {}, findInStrings, findInComments); @@ -812,6 +821,7 @@ export class SessionClient implements LanguageService { kind?: string, includeInteractiveActions?: boolean, ): ApplicableRefactorInfo[] { + const oldPreferences = this.preferences; if (preferences) { // Temporarily set preferences this.configure(preferences); } @@ -822,7 +832,7 @@ export class SessionClient implements LanguageService { const request = this.processRequest(protocol.CommandTypes.GetApplicableRefactors, args); const response = this.processResponse(request); if (preferences) { // Restore preferences - this.configure(this.preferences || {}); + this.configure(oldPreferences || {}); } return response.body!; // TODO: GH#18217 } @@ -844,6 +854,7 @@ export class SessionClient implements LanguageService { preferences: UserPreferences | undefined, interactiveRefactorArguments?: InteractiveRefactorArguments, ): RefactorEditInfo { + const oldPreferences = this.preferences; if (preferences) { // Temporarily set preferences this.configure(preferences); } @@ -868,7 +879,7 @@ export class SessionClient implements LanguageService { } if (preferences) { // Restore preferences - this.configure(this.preferences || {}); + this.configure(oldPreferences || {}); } return { diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index aff225aec806b..68b097d185cb5 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3765,9 +3765,9 @@ export class TestState { } public verifyNavigateTo(options: readonly FourSlashInterface.VerifyNavigateToOptions[]): void { - for (const { pattern, expected, fileName } of options) { + for (const { pattern, expected, fileName, excludeLibFiles } of options) { const file = fileName && this.findFile(fileName).fileName; - const items = this.languageService.getNavigateToItems(pattern, /*maxResultCount*/ undefined, file); + const items = this.languageService.getNavigateToItems(pattern, /*maxResultCount*/ undefined, file, /*excludeDtsFiles*/ undefined, excludeLibFiles); this.assertObjectsEqual( items, expected.map((e): ts.NavigateToItem => ({ diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 6a15ca0a3ff09..d115c7b42dc90 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -1809,6 +1809,7 @@ export interface VerifyNavigateToOptions { readonly pattern: string; readonly fileName?: string; readonly expected: readonly ExpectedNavigateToItem[]; + readonly excludeLibFiles?: boolean; } export interface ExpectedNavigateToItem { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 1b0624497ef6c..6d06376d196eb 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3599,6 +3599,11 @@ export interface UserPreferences { * Indicates whether {@link ReferencesResponseItem.lineText} is supported. */ readonly disableLineTextInReferences?: boolean; + + /** + * Indicates whether to exclude standard library and node_modules file symbols from navTo results. + */ + readonly excludeLibrarySymbolsInNavTo?: boolean; } export interface CompilerOptions { diff --git a/src/server/session.ts b/src/server/session.ts index 39a57df5ebfcf..1da59c00e763a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -2613,6 +2613,7 @@ export class Session implements EventSender { const { file, project } = this.getFileAndProject(args as protocol.FileRequestArgs); return [{ project, navigateToItems: project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, file) }]; } + const preferences = this.getHostPreferences(); const outputs: ProjectNavigateToItems[] = []; @@ -2646,7 +2647,13 @@ export class Session implements EventSender { // Mutates `outputs` function addItemsForProject(project: Project) { - const projectItems = project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, /*fileName*/ undefined, /*excludeDts*/ project.isNonTsProject()); + const projectItems = project.getLanguageService().getNavigateToItems( + searchValue, + maxResultCount, + /*fileName*/ undefined, + /*excludeDts*/ project.isNonTsProject(), + /*excludeLibFiles*/ preferences.excludeLibrarySymbolsInNavTo, + ); const unseenItems = filter(projectItems, item => tryAddSeenItem(item) && !getMappedLocationForProject(documentSpanLocation(item), project)); if (unseenItems.length) { outputs.push({ project, navigateToItems: unseenItems }); diff --git a/src/services/navigateTo.ts b/src/services/navigateTo.ts index 873bba76e9278..5beb279698d2e 100644 --- a/src/services/navigateTo.ts +++ b/src/services/navigateTo.ts @@ -16,6 +16,7 @@ import { ImportClause, ImportEqualsDeclaration, ImportSpecifier, + isInsideNodeModules, isPropertyAccessExpression, isPropertyNameLiteral, NavigateToItem, @@ -37,11 +38,11 @@ interface RawNavigateToItem { } /** @internal */ -export function getNavigateToItems(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken, searchValue: string, maxResultCount: number | undefined, excludeDtsFiles: boolean): NavigateToItem[] { +export function getNavigateToItems(sourceFiles: readonly SourceFile[], checker: TypeChecker, cancellationToken: CancellationToken, searchValue: string, maxResultCount: number | undefined, excludeDtsFiles: boolean, excludeLibFiles?: boolean): NavigateToItem[] { const patternMatcher = createPatternMatcher(searchValue); if (!patternMatcher) return emptyArray; const rawItems: RawNavigateToItem[] = []; - + const singleCurrentFile = sourceFiles.length === 1 ? sourceFiles[0] : undefined; // Search the declarations in all files and output matched NavigateToItem into array of NavigateToItem[] for (const sourceFile of sourceFiles) { cancellationToken.throwIfCancellationRequested(); @@ -50,8 +51,12 @@ export function getNavigateToItems(sourceFiles: readonly SourceFile[], checker: continue; } + if (shouldExcludeFile(sourceFile, !!excludeLibFiles, singleCurrentFile)) { + continue; + } + sourceFile.getNamedDeclarations().forEach((declarations, name) => { - getItemsFromNamedDeclaration(patternMatcher, name, declarations, checker, sourceFile.fileName, rawItems); + getItemsFromNamedDeclaration(patternMatcher, name, declarations, checker, sourceFile.fileName, !!excludeLibFiles, singleCurrentFile, rawItems); }); } @@ -59,7 +64,24 @@ export function getNavigateToItems(sourceFiles: readonly SourceFile[], checker: return (maxResultCount === undefined ? rawItems : rawItems.slice(0, maxResultCount)).map(createNavigateToItem); } -function getItemsFromNamedDeclaration(patternMatcher: PatternMatcher, name: string, declarations: readonly Declaration[], checker: TypeChecker, fileName: string, rawItems: RawNavigateToItem[]): void { +/** + * Exclude 'node_modules/' files and standard library files if 'excludeLibFiles' is true. + * If we're in current file only mode, we don't exclude the current file, even if it is a library file. + */ +function shouldExcludeFile(file: SourceFile, excludeLibFiles: boolean, singleCurrentFile: SourceFile | undefined): boolean { + return file !== singleCurrentFile && excludeLibFiles && (isInsideNodeModules(file.path) || file.hasNoDefaultLib); +} + +function getItemsFromNamedDeclaration( + patternMatcher: PatternMatcher, + name: string, + declarations: readonly Declaration[], + checker: TypeChecker, + fileName: string, + excludeLibFiles: boolean, + singleCurrentFile: SourceFile | undefined, + rawItems: RawNavigateToItem[], +): void { // First do a quick check to see if the name of the declaration matches the // last portion of the (possibly) dotted name they're searching for. const match = patternMatcher.getMatchForLastSegmentOfPattern(name); @@ -68,7 +90,7 @@ function getItemsFromNamedDeclaration(patternMatcher: PatternMatcher, name: stri } for (const declaration of declarations) { - if (!shouldKeepItem(declaration, checker)) continue; + if (!shouldKeepItem(declaration, checker, excludeLibFiles, singleCurrentFile)) continue; if (patternMatcher.patternContainsDots) { // If the pattern has dots in it, then also see if the declaration container matches as well. @@ -83,14 +105,20 @@ function getItemsFromNamedDeclaration(patternMatcher: PatternMatcher, name: stri } } -function shouldKeepItem(declaration: Declaration, checker: TypeChecker): boolean { +function shouldKeepItem( + declaration: Declaration, + checker: TypeChecker, + excludeLibFiles: boolean, + singleCurrentFile: SourceFile | undefined, +): boolean { switch (declaration.kind) { case SyntaxKind.ImportClause: case SyntaxKind.ImportSpecifier: case SyntaxKind.ImportEqualsDeclaration: const importer = checker.getSymbolAtLocation((declaration as ImportClause | ImportSpecifier | ImportEqualsDeclaration).name!)!; // TODO: GH#18217 const imported = checker.getAliasedSymbol(importer); - return importer.escapedName !== imported.escapedName; + return importer.escapedName !== imported.escapedName + && !imported.declarations?.every(d => shouldExcludeFile(d.getSourceFile(), excludeLibFiles, singleCurrentFile)); default: return true; } diff --git a/src/services/services.ts b/src/services/services.ts index 44d93e544d0ba..7da1e0bff9143 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2192,10 +2192,10 @@ export function createLanguageService( return FindAllReferences.Core.getReferencesForFileName(fileName, program, program.getSourceFiles()).map(FindAllReferences.toReferenceEntry); } - function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): NavigateToItem[] { + function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false, excludeLibFiles = false): NavigateToItem[] { synchronizeHostData(); const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles(); - return NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); + return NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles, excludeLibFiles); } function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean) { diff --git a/src/services/types.ts b/src/services/types.ts index 9c1fe3a73eb73..e18b15264b54b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -594,7 +594,7 @@ export interface LanguageService { getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; getFileReferences(fileName: string): ReferenceEntry[]; - getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; + getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean, excludeLibFiles?: boolean): NavigateToItem[]; getNavigationBarItems(fileName: string): NavigationBarItem[]; getNavigationTree(fileName: string): NavigationTree; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index dcea4be64289a..fd798b0487be1 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2897,6 +2897,10 @@ declare namespace ts { * Indicates whether {@link ReferencesResponseItem.lineText} is supported. */ readonly disableLineTextInReferences?: boolean; + /** + * Indicates whether to exclude standard library and node_modules file symbols from navTo results. + */ + readonly excludeLibrarySymbolsInNavTo?: boolean; } interface CompilerOptions { allowJs?: boolean; @@ -8640,6 +8644,7 @@ declare namespace ts { readonly organizeImportsNumericCollation?: boolean; readonly organizeImportsAccentCollation?: boolean; readonly organizeImportsCaseFirst?: "upper" | "lower" | false; + readonly excludeLibrarySymbolsInNavTo?: boolean; } /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { @@ -10396,7 +10401,7 @@ declare namespace ts { findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined; getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; getFileReferences(fileName: string): ReferenceEntry[]; - getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; + getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean, excludeLibFiles?: boolean): NavigateToItem[]; getNavigationBarItems(fileName: string): NavigationBarItem[]; getNavigationTree(fileName: string): NavigationTree; prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 87f0019695c37..858f545e416a0 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -811,6 +811,7 @@ declare namespace FourSlashInterface { readonly pattern: string; readonly fileName?: string; readonly expected: ReadonlyArray; + readonly excludeLibFiles?: boolean; } interface ExpectedNavigateToItem { readonly name: string; diff --git a/tests/cases/fourslash/navto_excludeLib1.ts b/tests/cases/fourslash/navto_excludeLib1.ts new file mode 100644 index 0000000000000..aafd416100954 --- /dev/null +++ b/tests/cases/fourslash/navto_excludeLib1.ts @@ -0,0 +1,46 @@ +/// + +// @filename: /index.ts +//// import { weirdName as otherName } from "bar"; +//// const [|weirdName: number = 1|]; + +// @filename: /tsconfig.json +//// {} + +// @filename: /node_modules/bar/index.d.ts +//// export const [|weirdName: number|]; + +// @filename: /node_modules/bar/package.json +//// {} + + +const [weird, otherWeird] = test.ranges(); + +verify.navigateTo({ + pattern: "weirdName", + expected: [{ + name: "weirdName", + kind: "const", + kindModifiers: "export,declare", + range: otherWeird, + matchKind: "exact", + }, + { + name: "weirdName", + kind: "const", + range: weird, + matchKind: "exact", + }], +}); + +verify.navigateTo({ + pattern: "weirdName", + excludeLibFiles: true, + expected: [{ + name: "weirdName", + kind: "const", + range: weird, + matchKind: "exact", + }], + +}); \ No newline at end of file diff --git a/tests/cases/fourslash/navto_excludeLib2.ts b/tests/cases/fourslash/navto_excludeLib2.ts new file mode 100644 index 0000000000000..918a5f3398244 --- /dev/null +++ b/tests/cases/fourslash/navto_excludeLib2.ts @@ -0,0 +1,32 @@ +/// + +// @filename: /index.ts +//// import { [|someName as weirdName|] } from "bar"; + +// @filename: /tsconfig.json +//// {} + +// @filename: /node_modules/bar/index.d.ts +//// export const someName: number; + +// @filename: /node_modules/bar/package.json +//// {} + + +const [weird] = test.ranges(); + +verify.navigateTo({ + pattern: "weirdName", + expected: [{ + name: "weirdName", + kind: "alias", + range: weird, + matchKind: "exact", + }], +}); + +verify.navigateTo({ + pattern: "weirdName", + excludeLibFiles: true, + expected: [], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/navto_excludeLib3.ts b/tests/cases/fourslash/navto_excludeLib3.ts new file mode 100644 index 0000000000000..4cd2d7b99b736 --- /dev/null +++ b/tests/cases/fourslash/navto_excludeLib3.ts @@ -0,0 +1,19 @@ +/// + +// @filename: /index.ts +//// [|function parseInt(s: string): number {}|] + +const [local] = test.ranges(); + +verify.navigateTo({ + pattern: "parseInt", + excludeLibFiles: true, + expected: [ + { + name: "parseInt", + kind: "function", + range: local, + matchKind: "exact", + }, + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/navto_excludeLib4.ts b/tests/cases/fourslash/navto_excludeLib4.ts new file mode 100644 index 0000000000000..3bf90de0b1bf2 --- /dev/null +++ b/tests/cases/fourslash/navto_excludeLib4.ts @@ -0,0 +1,24 @@ +/// + +// @filename: /node_modules/bar/index.d.ts +//// import { someOtherName } from "./baz"; +//// export const [|someName: number|]; +// @filename: /node_modules/bar/baz.d.ts +//// export const someOtherName: string; + +const [some] = test.ranges(); + +verify.navigateTo({ + pattern: "some", + excludeLibFiles: true, + fileName: "/node_modules/bar/index.d.ts", + expected: [ + { + name: "someName", + kind: "const", + kindModifiers: "export,declare", + range: some, + matchKind: "prefix", + }, + ], +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/navto_serverExcludeLib.ts b/tests/cases/fourslash/server/navto_serverExcludeLib.ts new file mode 100644 index 0000000000000..cbe0d5fc7e2a9 --- /dev/null +++ b/tests/cases/fourslash/server/navto_serverExcludeLib.ts @@ -0,0 +1,45 @@ +/// + +// @filename: /index.ts +//// import { weirdName as otherName } from "bar"; +//// const [|weirdName: number = 1|]; + +// @filename: /tsconfig.json +//// {} + +// @filename: /node_modules/bar/index.d.ts +//// export const [|weirdName: number|]; + +// @filename: /node_modules/bar/package.json +//// {} + + +const [weird, otherWeird] = test.ranges(); + +verify.navigateTo({ + pattern: "weirdName", + expected: [{ + name: "weirdName", + kind: "const", + kindModifiers: "export,declare", + range: otherWeird, + matchKind: "exact", + }, + { + name: "weirdName", + kind: "const", + range: weird, + matchKind: "exact", + }], +}); + +verify.navigateTo({ + pattern: "weirdName", + excludeLibFiles: true, + expected: [{ + name: "weirdName", + kind: "const", + range: weird, + matchKind: "exact", + }], +}); \ No newline at end of file