Add support for additional_workspace_folders, currently only for TS

The TypeScript language server was initialized with a single workspace folder,
so tsserver could not discover references across sibling packages in a monorepo.

This adds an `additional_workspace_folders` setting in
`ls_specific_settings` that accepts a list of directory paths.
Each folder is registered as an LSP workspace folder during initialization,
and a representative .ts file is opened from each to trigger tsserver project
loading. This enables `find_referencing_symbols` and `request_references` to
return results from across package boundaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
meir lixenbrgandClaude Opus 4.6 authored and Michael Panchenko committed 2026-05-04 14:22:20 +02:00
1 parent 7a8b5fb212
commit 34575f9898
14 files changed
+396 -30

No files matched your search

+5
View File
@@ -3,6 +3,11 @@
Status of the `main` branch. Changes prior to the next official version change will appear here.
* General:
- Add cross-package reference support via `additional_workspace_folders` project setting (currently implemented for TypeScript).
Configure additional workspace folder paths in `project.yml` to enable `find_referencing_symbols` and `request_references`
to discover symbols across package boundaries in monorepos. The base `SolidLanguageServer` class provides the workspace
folder resolution and activation framework; other language servers can enable support by overriding
`_find_representative_source_file()`. #750
- Support `serena --version` CLI command for displaying the current version #1347
- Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350
- `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`)
+3
View File
@@ -259,6 +259,7 @@ class ProjectConfig(SharedConfig):
project_name: str
languages: list[Language]
ignored_paths: list[str] = field(default_factory=list)
additional_workspace_folders: list[str] = field(default_factory=list)
read_only: bool = False
ignore_all_files_in_gitignore: bool = True
initial_prompt: str = ""
@@ -470,11 +471,13 @@ class ProjectConfig(SharedConfig):
fixed_tools = data["fixed_tools"] or []
excluded_tools = data["excluded_tools"] or []
included_optional_tools = data["included_optional_tools"] or []
additional_workspace_folders = data.get("additional_workspace_folders") or []
return cls(
project_name=data["project_name"],
languages=languages,
ignored_paths=ignored_paths,
additional_workspace_folders=additional_workspace_folders,
excluded_tools=excluded_tools,
fixed_tools=fixed_tools,
included_optional_tools=included_optional_tools,
+3
View File
@@ -27,6 +27,7 @@ class LanguageServerFactory:
ignored_patterns: list[str],
ls_timeout: float | None = None,
ls_specific_settings: dict | None = None,
additional_workspace_folders: list[str] | None = None,
trace_lsp_communication: bool = False,
):
self.project_root = project_root
@@ -35,6 +36,7 @@ class LanguageServerFactory:
self.ignored_patterns = ignored_patterns
self.ls_timeout = ls_timeout
self.ls_specific_settings = ls_specific_settings
self.additional_workspace_folders = additional_workspace_folders or []
self.trace_lsp_communication = trace_lsp_communication
def create_language_server(self, language: Language) -> SolidLanguageServer:
@@ -54,6 +56,7 @@ class LanguageServerFactory:
solidlsp_dir=SerenaPaths().serena_user_home_dir,
project_data_path=self.project_data_path,
ls_specific_settings=self.ls_specific_settings or {},
additional_workspace_folders=self.additional_workspace_folders,
),
)
+1
View File
@@ -660,6 +660,7 @@ class Project(ToStringMixin):
ignored_patterns=self._ignored_patterns,
ls_timeout=ls_timeout,
ls_specific_settings=ls_specific_settings,
additional_workspace_folders=self.project_config.additional_workspace_folders,
trace_lsp_communication=self.serena_config.trace_lsp_communication,
)
self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory)
+11
View File
@@ -53,6 +53,17 @@ ignore_all_files_in_gitignore: true
# No documentation on options means no options are available.
ls_specific_settings: {}
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
additional_workspace_folders: []
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
@@ -252,12 +252,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"workspaceFolders": self._build_workspace_folders_param(repository_absolute_path),
}
return cast(InitializeParams, initialize_params)
@@ -393,6 +388,45 @@ class TypeScriptLanguageServer(SolidLanguageServer):
self.INDEXING_PROGRESS_TIMEOUT,
)
self._activate_additional_workspaces()
@override
def _find_representative_source_file(self, directory: str) -> str | None:
"""Find a TypeScript file suitable for triggering project loading.
Prefers a file adjacent to tsconfig.json (indicating the project root),
then falls back to the first .ts/.tsx file found.
"""
for root, dirs, files in os.walk(directory):
dirs[:] = [d for d in dirs if not self.is_ignored_dirname(d)]
if "tsconfig.json" in files:
for f in files:
if f.endswith((".ts", ".tsx")) and not f.endswith(".d.ts"):
return os.path.join(root, f)
src_dir = os.path.join(root, "src")
if os.path.isdir(src_dir):
for f in os.listdir(src_dir):
if f.endswith((".ts", ".tsx")) and not f.endswith(".d.ts"):
return os.path.join(src_dir, f)
for root, dirs, files in os.walk(directory):
dirs[:] = [d for d in dirs if not self.is_ignored_dirname(d)]
for f in files:
if f.endswith((".ts", ".tsx")) and not f.endswith(".d.ts"):
return os.path.join(root, f)
return None
@override
def _signal_expect_indexing(self) -> None:
self.expect_indexing()
@override
def _wait_for_additional_workspace_indexing(self) -> None:
if self.wait_for_indexing(timeout=self.INDEXING_PROGRESS_TIMEOUT):
log.info("Additional workspace indexing complete")
else:
log.warning("Additional workspace indexing did not complete within timeout; proceeding anyway")
@override
def _get_wait_time_for_cross_file_referencing(self) -> float:
return 2
+172 -23
View File
@@ -592,6 +592,154 @@ class SolidLanguageServer(ABC):
"""
return 2
# --- Cross-workspace / additional workspace folder support ---
@staticmethod
def _is_cross_workspace_path(relative_file_path: str) -> bool:
"""Check if a relative path traverses outside the workspace root via '..' components."""
return ".." in PurePath(relative_file_path).parts
def _resolve_file_uri(self, relative_file_path: str) -> str:
"""Construct a canonical file URI from a relative path.
For cross-workspace paths containing '..', the path is resolved to
produce a clean URI without '..' segments.
"""
p = pathlib.Path(os.path.join(self.repository_root_path, relative_file_path))
if self._is_cross_workspace_path(relative_file_path):
p = p.resolve()
return p.as_uri()
def _resolve_additional_workspace_folders(self) -> list[str]:
"""Resolve additional_workspace_folders from settings.
Returns a deduplicated list of absolute, resolved directory paths.
Non-existent paths are skipped with a warning, and paths that overlap
with the repository root are ignored. Results are cached after first call.
"""
if not hasattr(self, "_cached_additional_workspace_folders"):
raw_folders = self._solidlsp_settings.additional_workspace_folders
if not raw_folders:
self._cached_additional_workspace_folders: list[str] = []
return self._cached_additional_workspace_folders
seen: set[str] = set()
resolved: list[str] = []
repo_real = os.path.realpath(self.repository_root_path)
for folder in raw_folders:
abs_folder = os.path.realpath(os.path.join(self.repository_root_path, folder))
if abs_folder == repo_real:
log.info("additional_workspace_folders: skipping %s (same as repository root)", folder)
continue
if not os.path.isdir(abs_folder):
log.warning(
"additional_workspace_folders: skipping non-existent directory %s (resolved to %s)",
folder,
abs_folder,
)
continue
if abs_folder in seen:
log.info("additional_workspace_folders: skipping duplicate %s", folder)
continue
seen.add(abs_folder)
resolved.append(abs_folder)
self._cached_additional_workspace_folders = resolved
return self._cached_additional_workspace_folders
def _build_workspace_folders_param(self, repository_absolute_path: str) -> list[dict[str, str]]:
"""Build the ``workspaceFolders`` list for LSP initialization.
Returns a list containing the primary workspace folder followed by any
additional workspace folders configured in settings.
"""
root_uri = pathlib.Path(repository_absolute_path).as_uri()
folders: list[dict[str, str]] = [
{"uri": root_uri, "name": os.path.basename(repository_absolute_path)},
]
for abs_folder in self._resolve_additional_workspace_folders():
folder_uri = pathlib.Path(abs_folder).as_uri()
folders.append({"uri": folder_uri, "name": os.path.basename(abs_folder)})
if len(folders) > 1:
log.info("LSP multi-root workspace: %d folders", len(folders))
return folders
def _activate_additional_workspaces(self) -> None:
"""Open a representative file from each additional workspace folder to
trigger project loading in the language server.
Many language servers only load projects on-demand when files are opened.
This method finds a source file in each additional workspace folder via
:meth:`_find_representative_source_file` and opens it, keeping it open
for the lifetime of this language server instance.
Subclasses must override :meth:`_find_representative_source_file` to
enable this feature; the default implementation raises ``NotImplementedError``.
"""
additional_folders = self._resolve_additional_workspace_folders()
if not additional_folders:
return
opened_count = 0
for folder in additional_folders:
source_file = self._find_representative_source_file(folder)
if source_file is None:
log.warning("No source file found in additional workspace folder: %s", folder)
continue
rel_path = os.path.relpath(source_file, self.repository_root_path)
log.info("Opening %s to trigger project loading for %s", rel_path, os.path.basename(folder))
abs_path = pathlib.Path(source_file).resolve()
uri = abs_path.as_uri()
language_id = self._get_language_id_for_file(rel_path)
self._signal_expect_indexing()
fb = LSPFileBuffer(
abs_path=abs_path,
uri=uri,
encoding=self._encoding,
version=0,
language_id=language_id,
ref_count=1,
language_server=self,
open_in_ls=True,
)
self.open_file_buffers[uri] = fb
opened_count += 1
if opened_count > 0:
log.info("Waiting for %d additional workspace(s) to index...", opened_count)
self._wait_for_additional_workspace_indexing()
def _find_representative_source_file(self, directory: str) -> str | None:
"""Find a source file suitable for triggering project loading in the given directory.
Must be overridden by subclasses that support ``additional_workspace_folders``.
Return ``None`` if no suitable file is found.
:param directory: Absolute path to the workspace folder.
:return: Absolute path to a source file, or None.
"""
raise NotImplementedError(
f"{type(self).__name__} does not yet support additional_workspace_folders. "
f"Override _find_representative_source_file() to enable this feature."
)
def _signal_expect_indexing(self) -> None:
"""Signal that new files are about to be opened and async indexing should be awaited.
Override in subclasses that track indexing progress (e.g. via $/progress).
Default implementation is a no-op.
"""
def _wait_for_additional_workspace_indexing(self) -> None:
"""Wait for additional workspace indexing to complete.
Override in subclasses that track indexing progress.
Default implementation is a no-op.
"""
def set_request_timeout(self, timeout: float | None) -> None:
"""
:param timeout: the timeout, in seconds, for requests to the language server.
@@ -751,6 +899,8 @@ class SolidLanguageServer(ABC):
raise SolidLSPException("Language Server not started")
absolute_file_path = Path(self.repository_root_path, relative_file_path)
if self._is_cross_workspace_path(relative_file_path):
absolute_file_path = absolute_file_path.resolve()
uri = absolute_file_path.as_uri()
if uri in self.open_file_buffers:
@@ -798,7 +948,7 @@ class SolidLanguageServer(ABC):
be opened in the LS later by calling the `ensure_open_in_ls` method on the returned LSPFileBuffer.
"""
if file_buffer is not None:
expected_uri = pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
expected_uri = self._resolve_file_uri(relative_file_path)
assert file_buffer.uri == expected_uri, f"Inconsistency between provided {file_buffer.uri=} and {expected_uri=}"
if open_in_ls:
file_buffer.ensure_open_in_ls()
@@ -821,8 +971,7 @@ class SolidLanguageServer(ABC):
log.error("insert_text_at_position called before Language Server started")
raise SolidLSPException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
uri = self._resolve_file_uri(relative_file_path)
# Ensure the file is open
assert uri in self.open_file_buffers
@@ -864,8 +1013,7 @@ class SolidLanguageServer(ABC):
log.error("delete_text_between_positions called before Language Server started")
raise SolidLSPException("Language Server not started")
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
uri = pathlib.Path(absolute_file_path).as_uri()
uri = self._resolve_file_uri(relative_file_path)
# Ensure the file is open
assert uri in self.open_file_buffers
@@ -1054,9 +1202,7 @@ class SolidLanguageServer(ABC):
return cast(
DefinitionParams,
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
},
LSPConstants.TEXT_DOCUMENT: {LSPConstants.URI: self._resolve_file_uri(relative_file_path)},
LSPConstants.POSITION: {
LSPConstants.LINE: line,
LSPConstants.CHARACTER: column,
@@ -1103,7 +1249,7 @@ class SolidLanguageServer(ABC):
def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
return self.server.send.references(
{
"textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))},
"textDocument": {"uri": self._resolve_file_uri(relative_file_path)},
"position": {"line": line, "character": column},
"context": {"includeDeclaration": False},
}
@@ -1210,7 +1356,7 @@ class SolidLanguageServer(ABC):
:return: A list of completions
"""
with self.open_file(relative_file_path):
open_file_buffer = self.open_file_buffers[pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()]
open_file_buffer = self.open_file_buffers[self._resolve_file_uri(relative_file_path)]
completion_params: LSPTypes.CompletionParams = {
"position": {"line": line, "character": column},
"textDocument": {"uri": open_file_buffer.uri},
@@ -1327,9 +1473,7 @@ class SolidLanguageServer(ABC):
# no cached result, query language server
log.debug(f"Requesting document symbols for {relative_file_path} from the Language Server")
response = self.server.send.document_symbol(
{"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}}
)
response = self.server.send.document_symbol({"textDocument": {"uri": self._resolve_file_uri(relative_file_path)}})
# Only cache non-empty results. An empty or None response can occur when the language server
# has not yet finished indexing or building the project (e.g. Lean 4 before `lake build`),
@@ -1768,7 +1912,7 @@ class SolidLanguageServer(ABC):
with self.open_file(relative_file_path):
response = self.server.send.signature_help(
{
"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()},
"textDocument": {"uri": self._resolve_file_uri(relative_file_path)},
"position": {
"line": line,
"character": column,
@@ -1879,10 +2023,13 @@ class SolidLanguageServer(ABC):
if containing_symbol is None and include_file_symbols:
log.warning(f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead")
fileRange = self._get_range_from_file_content(file_data.contents)
ref_abs_path = os.path.join(self.repository_root_path, ref_path)
if self._is_cross_workspace_path(ref_path):
ref_abs_path = str(pathlib.Path(ref_abs_path).resolve())
location = ls_types.Location(
uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()),
uri=self._resolve_file_uri(ref_path),
range=fileRange,
absolutePath=str(os.path.join(self.repository_root_path, ref_path)),
absolutePath=ref_abs_path,
relativePath=ref_path,
)
name = os.path.splitext(os.path.basename(ref_path))[0]
@@ -1987,7 +2134,9 @@ class SolidLanguageServer(ABC):
"""
# checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor
with self.open_file(relative_file_path):
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
absolute_file_path = os.path.join(self.repository_root_path, relative_file_path)
if self._is_cross_workspace_path(relative_file_path):
absolute_file_path = str(pathlib.Path(absolute_file_path).resolve())
content = FileUtils.read_file(absolute_file_path, self._encoding)
if content.split("\n")[line].strip() == "":
log.error(f"Passing empty lines to request_container_symbol is currently not supported, {relative_file_path=}, {line=}")
@@ -2102,7 +2251,9 @@ class SolidLanguageServer(ABC):
return definitions[0]
def _get_document_symbols_with_locations(self, relative_file_path: str) -> list[ls_types.UnifiedSymbolInformation]:
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
abs_path = os.path.join(self.repository_root_path, relative_file_path)
if self._is_cross_workspace_path(relative_file_path):
abs_path = str(pathlib.Path(abs_path).resolve())
document_symbols = self.request_document_symbols(relative_file_path)
symbols = list(document_symbols.iter_symbols())
@@ -2110,9 +2261,9 @@ class SolidLanguageServer(ABC):
# symbol exposes a normalized location/range in the current workspace.
for symbol in symbols:
location = symbol["location"]
location["absolutePath"] = absolute_file_path
location["absolutePath"] = abs_path
location["relativePath"] = relative_file_path
location["uri"] = Path(absolute_file_path).as_uri()
location["uri"] = self._resolve_file_uri(relative_file_path)
return symbols
@staticmethod
@@ -2540,9 +2691,7 @@ class SolidLanguageServer(ABC):
:return: A WorkspaceEdit containing the changes needed to rename the symbol, or None if rename is not supported
"""
params = RenameParams(
textDocument=ls_types.TextDocumentIdentifier(
uri=pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
),
textDocument=ls_types.TextDocumentIdentifier(uri=self._resolve_file_uri(relative_file_path)),
position=ls_types.Position(line=line, character=column),
newName=new_name,
)
+7
View File
@@ -34,6 +34,13 @@ class SolidLSPSettings:
Have a look at the docstring of the constructors of the corresponding LS implementations within solidlsp to see which options are available.
No documentation on options means no options are available.
"""
additional_workspace_folders: list[str] = field(default_factory=list)
"""
Additional workspace folder paths for cross-package reference support.
Paths can be absolute or relative to the project root. Each folder is added as a workspace
folder in the LSP initialization, enabling language servers to discover symbols and references
across package boundaries (e.g. in monorepos).
"""
def __post_init__(self) -> None:
os.makedirs(str(self.solidlsp_dir), exist_ok=True)
+6 -1
View File
@@ -57,6 +57,7 @@ def _create_ls(
ignored_paths: list[str] | None = None,
trace_lsp_communication: bool = False,
ls_specific_settings: dict[Language, dict[str, Any]] | None = None,
additional_workspace_folders: list[str] | None = None,
solidlsp_dir: Path | None = None,
) -> SolidLanguageServer:
ignored_paths = ignored_paths or []
@@ -79,6 +80,7 @@ def _create_ls(
solidlsp_dir=effective_solidlsp_dir,
project_data_path=project_data_path,
ls_specific_settings=ls_specific_settings or {},
additional_workspace_folders=additional_workspace_folders or [],
),
)
@@ -90,9 +92,12 @@ def start_ls_context(
ignored_paths: list[str] | None = None,
trace_lsp_communication: bool = False,
ls_specific_settings: dict[Language, dict[str, Any]] | None = None,
additional_workspace_folders: list[str] | None = None,
solidlsp_dir: Path | None = None,
) -> Iterator[SolidLanguageServer]:
ls = _create_ls(language, repo_path, ignored_paths, trace_lsp_communication, ls_specific_settings, solidlsp_dir)
ls = _create_ls(
language, repo_path, ignored_paths, trace_lsp_communication, ls_specific_settings, additional_workspace_folders, solidlsp_dir
)
log.info(f"Starting language server for {language} {repo_path}")
ls.start()
try:
@@ -0,0 +1,14 @@
export function sharedUtilityFunction(input: string): string {
return `processed: ${input}`;
}
export class SharedClass {
name: string;
constructor(name: string) {
this.name = name;
}
greet(): string {
return `Hello, ${this.name}`;
}
}
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["*.ts"]
}
@@ -0,0 +1,10 @@
import { sharedUtilityFunction, SharedClass } from "../cross_package_a/shared_utils";
export function consumeSharedUtil(): string {
return sharedUtilityFunction("test data");
}
export function consumeSharedClass(): string {
const instance = new SharedClass("World");
return instance.greet();
}
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"rootDir": ".",
"paths": {
"../cross_package_a/*": ["../cross_package_a/*"]
}
},
"include": ["*.ts"]
}
@@ -0,0 +1,99 @@
"""Tests for cross-package TypeScript references using additional_workspace_folders."""
from pathlib import Path
import pytest
from solidlsp.ls_config import Language
from test.conftest import start_ls_context
CROSS_PKG_DIR = Path(__file__).parent.parent.parent / "resources" / "repos" / "typescript"
PACKAGE_A = str(CROSS_PKG_DIR / "cross_package_a")
PACKAGE_B = str(CROSS_PKG_DIR / "cross_package_b")
@pytest.mark.typescript
class TestCrossPackageReferences:
"""Verify that find_referencing_symbols works across package boundaries
when additional_workspace_folders is configured.
"""
def test_cross_package_find_references(self) -> None:
"""Starting from package_a, with package_b as additional workspace,
references in package_b should be discovered.
"""
with start_ls_context(
Language.TYPESCRIPT,
repo_path=PACKAGE_A,
additional_workspace_folders=[PACKAGE_B],
) as ls:
symbols = ls.request_document_symbols("shared_utils.ts").get_all_symbols_and_roots()
shared_fn = None
for sym in symbols[0]:
if sym.get("name") == "sharedUtilityFunction":
shared_fn = sym
break
assert shared_fn is not None, "Could not find 'sharedUtilityFunction' in shared_utils.ts"
sel_start = shared_fn["selectionRange"]["start"]
refs = ls.request_references("shared_utils.ts", sel_start["line"], sel_start["character"])
ref_paths = [r.get("relativePath", "") for r in refs]
cross_package_refs = [p for p in ref_paths if "cross_package_b" in p or "consumer.ts" in p]
assert len(cross_package_refs) > 0, (
f"Expected cross-package reference from package_b/consumer.ts, but only found refs in: {ref_paths}"
)
def test_cross_package_referencing_symbols(self) -> None:
"""Test the higher-level request_referencing_symbols across packages."""
with start_ls_context(
Language.TYPESCRIPT,
repo_path=PACKAGE_A,
additional_workspace_folders=[PACKAGE_B],
) as ls:
symbols = ls.request_document_symbols("shared_utils.ts").get_all_symbols_and_roots()
shared_class = None
for sym in symbols[0]:
if sym.get("name") == "SharedClass":
shared_class = sym
break
assert shared_class is not None, "Could not find 'SharedClass' in shared_utils.ts"
sel_start = shared_class["selectionRange"]["start"]
ref_symbols = ls.request_referencing_symbols(
"shared_utils.ts",
sel_start["line"],
sel_start["character"],
include_imports=True,
include_file_symbols=True,
)
ref_files = [
r.symbol["location"]["relativePath"]
for r in ref_symbols
if "location" in r.symbol and "relativePath" in r.symbol["location"]
]
cross_refs = [p for p in ref_files if "cross_package_b" in p or "consumer.ts" in p]
assert len(cross_refs) > 0, f"Expected cross-package referencing symbol from package_b, but only found refs in: {ref_files}"
def test_without_additional_workspace_no_cross_refs(self) -> None:
"""Baseline: without additional_workspace_folders, cross-package refs should NOT appear."""
with start_ls_context(
Language.TYPESCRIPT,
repo_path=PACKAGE_A,
) as ls:
symbols = ls.request_document_symbols("shared_utils.ts").get_all_symbols_and_roots()
shared_fn = None
for sym in symbols[0]:
if sym.get("name") == "sharedUtilityFunction":
shared_fn = sym
break
assert shared_fn is not None
sel_start = shared_fn["selectionRange"]["start"]
refs = ls.request_references("shared_utils.ts", sel_start["line"], sel_start["character"])
ref_paths = [r.get("relativePath", "") for r in refs]
cross_package_refs = [p for p in ref_paths if "cross_package_b" in p or "consumer.ts" in p]
assert len(cross_package_refs) == 0, (
f"Without additional_workspace_folders, should NOT find cross-package refs, but found: {cross_package_refs}"
)