You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have made sure it's an actual issue, not a question (use GitHub Discussions instead).
I have consulted the user guide and verified that the issue cannot be resolved by adjusting configuration/following recommended workflows.
I have looked for similar issues and discussions, including closed ones.
Issue details:
I have provided a meaningful title and description.
I have explained how the issue arose and, where possible, added instructions on how to reproduce it.
I have added details on my setup: Serena version, MCP client, OS, the programming language(s), relevant configuration adjustments and project specifics.
If the issue relates to an application of Serena to an open-source project, I have added the link.
Summary
In a repository that holds more than one Scala build below its root, find_referencing_symbols / request_references silently return nothing for every build but one. Serena sends Metals a single workspace folder — the repository root — and Metals serves one build per workspace folder, so the rest are left with no build target and answered by the fallback presentation compiler, which cannot see across files.
Setup
Serena main @ 9a9d07e83d8c1cba3458992707f440c624446c6d; also reproduces on released 1.6.1, with LanguageServerConfig(code_language=Language.SCALA) in place of the ls_id= line below (the Refactoring: Language vs language server semantics #1739 rename)
Metals 1.6.4 (Serena's bundled default), Scala 3.3.6, sbt 1.11.7 (pinned by the fixture), Coursier 2.1.24
No MCP client involved — driven directly through SolidLanguageServer, see below
No configuration adjustments
Reproduction
Save this as sscce.py at the root of a Serena checkout and run it against that checkout's environment (uv sync --all-extras first if you haven't):
uv run python sscce.py /tmp/mono
Needs java, sbt and cs on PATH. It writes two independent, self-contained sbt builds under /tmp/mono, then asks for the references to Lib.greet in each, waiting 4 minutes for a response.
"""Serena + Metals in a monorepo: two independent sbt builds below the repository root."""importosimportshutilimportsysimporttimefromsolidlsp.language_servers.scala_language_serverimportScalaLanguageServerfromsolidlsp.ls_configimportLanguageServerConfig, LanguageServerIdfromsolidlsp.settingsimportSolidLSPSettingsREPO=os.path.abspath(sys.argv[1])
# --- fixture: <repo>/alpha and <repo>/beta, each a self-contained sbt build -------------shutil.rmtree(REPO, ignore_errors=True)
fornamein ("alpha", "beta"):
files= {
"build.sbt": 'ThisBuild / scalaVersion := "3.3.6"\n\nlazy val %s = (project in file("."))\n'%name,
"project/build.properties": "sbt.version=1.11.7\n",
f"src/main/scala/{name}/Lib.scala": f"package {name}\n\nobject Lib {{\n def greet(who: String): String = s\"hello $who\"\n}}\n",
f"src/main/scala/{name}/App.scala": f'package {name}\n\nobject App {{\n def main(args: Array[String]): Unit = println(Lib.greet("world"))\n}}\n',
}
forrel, contentinfiles.items():
path=os.path.join(REPO, name, rel)
os.makedirs(os.path.dirname(path), exist_ok=True)
withopen(path, "w") asf:
f.write(content)
# --- run Serena's Scala language server over it ----------------------------------------ls=ScalaLanguageServer(LanguageServerConfig(ls_id=LanguageServerId.SCALA), REPO, SolidLSPSettings())
defshow_message_request(params):
"""Answer Metals' "would you like to import the build?" prompt. Serena has no handler for window/showMessageRequest, and without one Metals aborts initialization ("Unexpected error initializing server ... method 'window/showMessageRequest' not handled on client") and imports nothing at all. That is a separate gap; this handler removes it from the picture so the workspace folders are the only variable. There is precedent for such a handler in src/solidlsp/language_servers/ansible_language_server.py. """actions=params.get("actions") or []
foractioninactions:
ifaction.get("title") =="Import build":
returnactionreturnactions[0] ifactionselseNonels.server.on_request("window/showMessageRequest", show_message_request)
withls.start_server_context():
folders=ls._create_initialize_params().get("workspaceFolders", [])
print("workspaceFolders:", [f["uri"] forfinfolders])
results: dict[str, list] = {}
deadline=time.time() +240whiletime.time() <deadline:
fornamein ("alpha", "beta"):
ifnotresults.get(name):
# Lib.scala line 3 (0-based) is ` def greet(...)`; column 6 is on `greet`.results[name] =ls.request_references(os.path.join(name, "src", "main", "scala", name, "Lib.scala"), 3, 6)
ifall(results.values()):
breaktime.sleep(10)
forname, refsinsorted(results.items()):
print(f"references to {name}.Lib.greet: {[r['relativePath'] forrinrefs]}")
Expected
Both builds resolve their own cross-file reference: alpha/src/main/scala/alpha/App.scala and beta/src/main/scala/beta/App.scala.
Actual
workspaceFolders: ['file:///tmp/mono']
references to alpha.Lib.greet: ['alpha/src/main/scala/alpha/App.scala']
references to beta.Lib.greet: []
beta gets nothing, and keeps getting nothing for the full four minutes the script waits. /tmp/mono/.metals/metals.log says why:
INFO Started: Metals version 1.6.4 in folders '/tmp/mono' for client Serena .
[…]
INFO Connected to Build server: Bloop v2.0.17
INFO no build target found for /tmp/mono/beta/src/main/scala/beta/Lib.scala. Using presentation compiler with project's scala-library version: 3.3.6
(Excerpt.) alpha gets that same line, but only before the build server connects, never after — it is the build that got imported. beta's repeats on every attempt for the full four minutes.
Which of the two wins is not something you can rely on either; it falls out of a collectFirst over a directory listing.
Analysis
Three things line up:
DefaultInitializeParamsBuilder._apply_updates sets workspaceFolders from LanguageServerConfig.workspace_folders (src/solidlsp/initialize_params.py:59-69), which defaults to ["."] (src/solidlsp/ls_config.py:902) — the repository root. The Scala server does not override this; its _create_base_initialize_params sets only locale, initializationOptions and capabilities.
Metals builds one Folder per entry of the workspaceFolders it is given (MetalsLanguageServer.initialize, metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala:134-153, falling back to rootUri only when the list is empty) and creates one ProjectMetalsLspService per folder (WorkspaceFolders.initServices, .../internal/metals/WorkspaceFolders.scala:28-45). One folder, one service, one build.
Given a workspace that is not itself a build root, Metals does look one level down for a build — but with collectFirst, so it stops at the first (BuildTools.searchForBuildTool, .../internal/builds/BuildTools.scala:195-212). That is the fallback the single-build-in-a-subdirectory case survives on, and it is exactly why the second build cannot.
(Metals references are at scalameta/metals @ 987b0cdfa11905abeee575de0968293f9837936e.)
On ls_workspace_folders
To be straight about the workaround: for a repository that is only Scala, ls_workspace_folders: [alpha, beta] does fix the repro above — those folders are passed straight through as workspaceFolders, and Metals gives each its own service. So this is not "no configuration can help"; it is that the default is wrong and the escape hatch does not reach far enough.
Where it stops reaching is a polyglot repository, which is the shape that put me here. ls_workspace_folders is one project-level list applied identically to every language server (src/serena/ls_manager.py:49-56, src/serena/config/serena_config.py:326) and it also sets what SolidLSP indexes. If another server needs the repository root as its folder — a TypeScript project with package.json at the top, say — then including . puts the root back in front of Metals and widens indexing again, and omitting it cuts that server off from its own sources. There is no value that suits both.
Even in the pure-Scala case it asks the user to know that Metals is one-service-per-folder, work out which directories are builds, and keep that list current by hand — for something Serena can see for itself.
Metals' own metals.customProjectRoot is no help either: it names a single root, and it lives in Metals' UserConfiguration rather than its initialization options, so it would arrive by workspace/didChangeConfiguration, which Serena never sends.
Incoming PR I have working, modelled on #1444 and hanging off the InitializeParamsBuilder mechanism from #1631. With it, the script above prints:
workspaceFolders: ['file:///tmp/mono/alpha', 'file:///tmp/mono/beta']
references to alpha.Lib.greet: ['alpha/src/main/scala/alpha/App.scala']
references to beta.Lib.greet: ['beta/src/main/scala/beta/App.scala']
Drafted with the help of Claude Code. I reproduced the bug myself, verified the source citations and the log output, and edited the text; questions are mine to answer.
Preconditions:
Issue details:
Summary
In a repository that holds more than one Scala build below its root,
find_referencing_symbols/request_referencessilently return nothing for every build but one. Serena sends Metals a single workspace folder — the repository root — and Metals serves one build per workspace folder, so the rest are left with no build target and answered by the fallback presentation compiler, which cannot see across files.Setup
main@9a9d07e83d8c1cba3458992707f440c624446c6d; also reproduces on released 1.6.1, withLanguageServerConfig(code_language=Language.SCALA)in place of thels_id=line below (the Refactoring: Language vs language server semantics #1739 rename)SolidLanguageServer, see belowReproduction
Save this as
sscce.pyat the root of a Serena checkout and run it against that checkout's environment (uv sync --all-extrasfirst if you haven't):Needs
java,sbtandcsonPATH. It writes two independent, self-contained sbt builds under/tmp/mono, then asks for the references toLib.greetin each, waiting 4 minutes for a response.Expected
Both builds resolve their own cross-file reference:
alpha/src/main/scala/alpha/App.scalaandbeta/src/main/scala/beta/App.scala.Actual
betagets nothing, and keeps getting nothing for the full four minutes the script waits./tmp/mono/.metals/metals.logsays why:(Excerpt.)
alphagets that same line, but only before the build server connects, never after — it is the build that got imported.beta's repeats on every attempt for the full four minutes.Which of the two wins is not something you can rely on either; it falls out of a
collectFirstover a directory listing.Analysis
Three things line up:
DefaultInitializeParamsBuilder._apply_updatessetsworkspaceFoldersfromLanguageServerConfig.workspace_folders(src/solidlsp/initialize_params.py:59-69), which defaults to["."](src/solidlsp/ls_config.py:902) — the repository root. The Scala server does not override this; its_create_base_initialize_paramssets onlylocale,initializationOptionsandcapabilities.Folderper entry of theworkspaceFoldersit is given (MetalsLanguageServer.initialize,metals/src/main/scala/scala/meta/metals/MetalsLanguageServer.scala:134-153, falling back torootUrionly when the list is empty) and creates oneProjectMetalsLspServiceper folder (WorkspaceFolders.initServices,.../internal/metals/WorkspaceFolders.scala:28-45). One folder, one service, one build.collectFirst, so it stops at the first (BuildTools.searchForBuildTool,.../internal/builds/BuildTools.scala:195-212). That is the fallback the single-build-in-a-subdirectory case survives on, and it is exactly why the second build cannot.(Metals references are at
scalameta/metals@987b0cdfa11905abeee575de0968293f9837936e.)On
ls_workspace_foldersTo be straight about the workaround: for a repository that is only Scala,
ls_workspace_folders: [alpha, beta]does fix the repro above — those folders are passed straight through asworkspaceFolders, and Metals gives each its own service. So this is not "no configuration can help"; it is that the default is wrong and the escape hatch does not reach far enough.Where it stops reaching is a polyglot repository, which is the shape that put me here.
ls_workspace_foldersis one project-level list applied identically to every language server (src/serena/ls_manager.py:49-56,src/serena/config/serena_config.py:326) and it also sets what SolidLSP indexes. If another server needs the repository root as its folder — a TypeScript project withpackage.jsonat the top, say — then including.puts the root back in front of Metals and widens indexing again, and omitting it cuts that server off from its own sources. There is no value that suits both.Even in the pure-Scala case it asks the user to know that Metals is one-service-per-folder, work out which directories are builds, and keep that list current by hand — for something Serena can see for itself.
Metals' own
metals.customProjectRootis no help either: it names a single root, and it lives in Metals'UserConfigurationrather than its initialization options, so it would arrive byworkspace/didChangeConfiguration, which Serena never sends.Related
ls_workspace_folders, but for scoping what gets indexed in an umbrella repo. This is a different axis: which directories are builds, per language server.mix.exs.Fix
Incoming PR I have working, modelled on #1444 and hanging off the
InitializeParamsBuildermechanism from #1631. With it, the script above prints:Drafted with the help of Claude Code. I reproduced the bug myself, verified the source citations and the log output, and edited the text; questions are mine to answer.