diff --git a/_extension/src/client.ts b/_extension/src/client.ts index 896750eb440..50a995a3450 100644 --- a/_extension/src/client.ts +++ b/_extension/src/client.ts @@ -36,6 +36,7 @@ import { readNativePreviewConfig, } from "./util"; import { getLanguageForUri } from "./util"; +import { workspaceSymbolSendRequestMiddleware } from "./workspaceSymbolMiddleware"; export class Client implements vscode.Disposable { private outputChannel: vscode.LogOutputChannel; @@ -97,6 +98,7 @@ export class Client implements vscode.Disposable { }, }, sendNotification: sendNotificationMiddleware, + sendRequest: workspaceSymbolSendRequestMiddleware, provideHover: () => undefined, }, diagnosticCollectionName: "typescript-push", diff --git a/_extension/src/workspaceSymbolMiddleware.ts b/_extension/src/workspaceSymbolMiddleware.ts new file mode 100644 index 00000000000..af7da47a8b8 --- /dev/null +++ b/_extension/src/workspaceSymbolMiddleware.ts @@ -0,0 +1,41 @@ +import * as vscode from "vscode"; +import type { CancellationToken } from "vscode"; +import type { MessageSignature } from "vscode-languageserver-protocol"; +import { isSupportedLanguageMode } from "./util"; + +const supportedSchemes = new Set(["file", "untitled"]); + +function isSupportedDocument(document: vscode.TextDocument): boolean { + return isSupportedLanguageMode(document) && supportedSchemes.has(document.uri.scheme); +} + +function getDocument(): vscode.TextDocument | undefined { + const activeDocument = vscode.window.activeTextEditor?.document; + if (activeDocument && isSupportedDocument(activeDocument)) { + return activeDocument; + } + + return vscode.workspace.textDocuments.find(isSupportedDocument); +} + +export function workspaceSymbolSendRequestMiddleware( + type: string | MessageSignature, + params: P | undefined, + token: CancellationToken | undefined, + next: (type: string | MessageSignature, params?: P, token?: CancellationToken) => Promise, +): Promise { + const method = typeof type === "string" ? type : type.method; + if (method !== "workspace/symbol") { + return next(type, params, token); + } + + const document = getDocument(); + if (!document) { + return next(type, params, token); + } + + return next(type, { + ...params, + textDocument: { uri: document.uri.toString() }, + } as P, token); +} diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index ea185a3e4b1..50021d0716d 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -5591,7 +5591,12 @@ func (f *FourslashTest) VerifyWorkspaceSymbol(t *testing.T, cases []*VerifyWorks preferences = new(lsutil.NewDefaultUserPreferences()) } f.Configure(t, *preferences) - result := sendRequest(t, f, lsproto.WorkspaceSymbolInfo, &lsproto.WorkspaceSymbolParams{Query: testCase.Pattern}) + result := sendRequest(t, f, lsproto.WorkspaceSymbolInfo, &lsproto.WorkspaceSymbolParams{ + Query: testCase.Pattern, + TextDocument: &lsproto.TextDocumentIdentifier{ + Uri: lsconv.FileNameToDocumentURI(f.activeFilename), + }, + }) if result.SymbolInformations == nil { t.Fatalf("Expected non-nil symbol information array from workspace symbol request") } diff --git a/internal/fourslash/tests/workspaceSymbolCurrentProject_test.go b/internal/fourslash/tests/workspaceSymbolCurrentProject_test.go new file mode 100644 index 00000000000..33712e6c12e --- /dev/null +++ b/internal/fourslash/tests/workspaceSymbolCurrentProject_test.go @@ -0,0 +1,65 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/typescript-go/internal/fourslash" + "github.com/microsoft/typescript-go/internal/ls/lsutil" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil" +) + +func TestWorkspaceSymbolCurrentProject(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = ` +// @Filename: /home/projects/a/tsconfig.json +{} + +// @Filename: /home/projects/a/index.ts +export function [|fromA|]() {} + +// @Filename: /home/projects/b/tsconfig.json +{} + +// @Filename: /home/projects/b/index.ts +export function [|fromB|]() {} +` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/home/projects/a/index.ts") + + allOpenProjects := lsutil.NewDefaultUserPreferences() + currentProject := lsutil.NewDefaultUserPreferences() + currentProject.WorkspaceSymbolsScope = lsutil.WorkspaceSymbolsScopeCurrentProject + + f.VerifyWorkspaceSymbol(t, []*fourslash.VerifyWorkspaceSymbolCase{ + { + Pattern: "from", + Preferences: &allOpenProjects, + Exact: new([]*lsproto.SymbolInformation{ + { + Name: "fromA", + Kind: lsproto.SymbolKindFunction, + Location: f.Ranges()[0].LSLocation(), + }, + { + Name: "fromB", + Kind: lsproto.SymbolKindFunction, + Location: f.Ranges()[1].LSLocation(), + }, + }), + }, + { + Pattern: "from", + Preferences: ¤tProject, + Exact: new([]*lsproto.SymbolInformation{ + { + Name: "fromA", + Kind: lsproto.SymbolKindFunction, + Location: f.Ranges()[0].LSLocation(), + }, + }), + }, + }) +} diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 83ef0cfbc55..0f683667903 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -32,6 +32,7 @@ func NewDefaultUserPreferences() UserPreferences { ReportStyleChecksAsWarnings: core.TSTrue, ExcludeLibrarySymbolsInNavTo: core.TSTrue, + WorkspaceSymbolsScope: WorkspaceSymbolsScopeAllOpenProjects, } } @@ -167,7 +168,8 @@ type UserPreferences struct { // ------- Symbols ------- - ExcludeLibrarySymbolsInNavTo core.Tristate `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"` + ExcludeLibrarySymbolsInNavTo core.Tristate `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"` + WorkspaceSymbolsScope WorkspaceSymbolsScope `config:"workspaceSymbols.scope"` // ------- Misc ------- @@ -227,6 +229,13 @@ type CodeLensUserPreferences struct { type QuotePreference string +type WorkspaceSymbolsScope string + +const ( + WorkspaceSymbolsScopeAllOpenProjects WorkspaceSymbolsScope = "allOpenProjects" + WorkspaceSymbolsScopeCurrentProject WorkspaceSymbolsScope = "currentProject" +) + const ( QuotePreferenceUnknown QuotePreference = "" QuotePreferenceAuto QuotePreference = "auto" diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go index 941ff84ef8b..580a36d6517 100644 --- a/internal/ls/lsutil/userpreferences_test.go +++ b/internal/ls/lsutil/userpreferences_test.go @@ -239,13 +239,15 @@ func TestUserPreferencesParseUnstable(t *testing.T) { "importModuleSpecifier": "relative" }, "workspaceSymbols": { - "excludeLibrarySymbols": true + "excludeLibrarySymbols": true, + "scope": "currentProject" } }`, expected: UserPreferences{ DisplayPartsForJSDoc: core.TSTrue, ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative, ExcludeLibrarySymbolsInNavTo: core.TSTrue, + WorkspaceSymbolsScope: WorkspaceSymbolsScopeCurrentProject, }, }, { diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index e50350414a5..a230e80e323 100755 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -1051,6 +1051,17 @@ function patchAndPreprocessModel() { }); } + // Patch WorkspaceSymbolParams to optionally scope the search to projects + // containing a document, matching Strada's currentProject mode. + if (structure.name === "WorkspaceSymbolParams") { + structure.properties.push({ + name: "textDocument", + type: { kind: "reference", name: "TextDocumentIdentifier" }, + optional: true, + documentation: "Scopes the workspace symbol search to projects containing this document.", + }); + } + // Patch Hover to add canIncreaseVerbosity if (structure.name === "Hover") { structure.properties.push( diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index c8f34a4f055..5962f28f150 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -2993,6 +2993,9 @@ type WorkspaceSymbolParams struct { // characters of *query* appear in their order in a candidate symbol. // Servers shouldn't use prefix, substring, or similar strict matching. Query string `json:"query" lsp:"required"` + + // Scopes the workspace symbol search to projects containing this document. + TextDocument *TextDocumentIdentifier `json:"textDocument,omitzero"` } var _ json.UnmarshalerFrom = (*WorkspaceSymbolParams)(nil) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 5ba37e85880..f04c2c4f9b4 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -1704,9 +1704,8 @@ func (s *Server) handleDocumentOnTypeFormat(ctx context.Context, ls *ls.Language func (s *Server) handleWorkspaceSymbol(ctx context.Context, params *lsproto.WorkspaceSymbolParams, reqMsg *lsproto.RequestMessage) (lsproto.WorkspaceSymbolResponse, error) { var resp lsproto.WorkspaceSymbolResponse var lsErr error - s.session.WithSnapshotLoadingProjectTree(ctx, nil, func(snapshot *project.Snapshot) { + provideSymbols := func(snapshot *project.Snapshot, programs []*compiler.Program) { defer s.recover(reqMsg) - programs := core.Map(snapshot.ProjectCollection.Projects(), (*project.Project).GetProgram) resp, lsErr = ls.ProvideWorkspaceSymbols( ctx, programs, @@ -1714,7 +1713,19 @@ func (s *Server) handleWorkspaceSymbol(ctx context.Context, params *lsproto.Work snapshot.UserPreferences(), params.Query, ) - }) + } + if params.TextDocument != nil && s.session.Config().WorkspaceSymbolsScope == lsutil.WorkspaceSymbolsScopeCurrentProject { + uri := params.TextDocument.Uri + s.session.WithSnapshotForDocument(ctx, uri, func(snapshot *project.Snapshot) { + programs := core.Map(snapshot.GetProjectsContainingFile(uri), ls.Project.GetProgram) + provideSymbols(snapshot, programs) + }) + } else { + s.session.WithSnapshotLoadingProjectTree(ctx, nil, func(snapshot *project.Snapshot) { + programs := core.Map(snapshot.ProjectCollection.Projects(), (*project.Project).GetProgram) + provideSymbols(snapshot, programs) + }) + } return resp, lsErr } diff --git a/internal/project/session.go b/internal/project/session.go index a29a8e471e6..a22eaa7b039 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -1132,6 +1132,20 @@ func (s *Session) WithSnapshotLoadingProjectTree( fn(snapshot) } +func (s *Session) WithSnapshotForDocument( + ctx context.Context, + uri lsproto.DocumentUri, + fn func(*Snapshot), +) { + snapshot := s.getSnapshot( + ctx, + ResourceRequest{Documents: []lsproto.DocumentUri{uri}}, + true, /*callerRef*/ + ) + defer snapshot.Deref(s) + fn(snapshot) +} + // GetCurrentLanguageServiceWithAutoImports flushes pending file changes, clones the // current snapshot with auto-import preparation for the given URI, then returns a // LanguageService for the default project. Use this only outside of request handling diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline index 4dc4403bec8..660f022b21b 100644 --- a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -637,7 +637,8 @@ Config:: "enabled": true }, "workspaceSymbols": { - "excludeLibrarySymbols": true + "excludeLibrarySymbols": true, + "scope": "allOpenProjects" } } }