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"
}
}
}