Skip to content

Commit a42291a

Browse files
committed
Answer workspace/workspaceFolders regardless of client capability
tombi sends this server-to-client request from its `initialized` handling even though LspService declares workspace.workspaceFolders false, and with no handler registered the client answered "Method not found", which tombi treats as a fatal handshake error. - LspService now always registers a workspace/workspaceFolders handler and answers it with the folders passed at initialize (fixed for the lifetime of a session, so there is nothing dynamic to report). - Store those folders on the service so the handler can return them, clearing them on reset like the other per-session LSP state. - Update the stale comment in tombi_lsp_service.py: the capability flag no longer decides whether the request arrives, only whether tombi is told support for dynamic updates exists.
1 parent 2948775 commit a42291a

3 files changed

Lines changed: 48 additions & 5 deletions

File tree

‎extensions/fine_toml_tombi/fine_toml_tombi/tombi_lsp_service.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@
3838
},
3939
},
4040
"workspace": {
41-
# workspaceFolders must stay False: LspService has no workspace/workspaceFolders
42-
# request handler. Declaring True would tell tombi we support the pull-based
43-
# request, but we pass folders once in initialize — no dynamic updates needed.
41+
# tombi sends workspace/workspaceFolders from its `initialized` handling
42+
# regardless of this flag, so the flag no longer decides whether the
43+
# request arrives; LspService answers it with the folders passed in
44+
# initialize. Kept False because the folders never change dynamically.
4445
"workspaceFolders": False,
4546
"configuration": True,
4647
},

‎finecode_extension_api/src/finecode_extension_api/contrib/lsp_service.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ def __init__(
181181
# whether the server asked to be told about workspace file changes via
182182
# client/registerCapability; gate `workspace/didChangeWatchedFiles`.
183183
self._registered_watched_files = False
184+
# folders sent in initialize; a server may ask for them again via
185+
# `workspace/workspaceFolders`, so they are kept to answer that request
186+
self._workspace_folders: list[dict[str, str]] = []
184187

185188
@override
186189
async def init(self) -> None:
@@ -213,6 +216,7 @@ async def _async_dispose(self) -> None:
213216
self._server_capabilities = {}
214217
self._root_path = None
215218
self._registered_watched_files = False
219+
self._workspace_folders = []
216220

217221
async def ensure_started(
218222
self,
@@ -227,10 +231,11 @@ async def start(
227231
self,
228232
root_uri: str,
229233
) -> None:
234+
workspace_folders = [{"uri": root_uri, "name": root_uri}]
230235
session = self._lsp_client.session(
231236
cmd=self._cmd,
232237
root_uri=root_uri,
233-
workspace_folders=[{"uri": root_uri, "name": root_uri}],
238+
workspace_folders=workspace_folders,
234239
initialization_options=(
235240
{"settings": self._settings} if self._settings else None
236241
),
@@ -241,6 +246,7 @@ async def start(
241246
self._session = session
242247
self._root_path = resource_uri_to_path(root_uri)
243248
self._server_capabilities = session.server_capabilities
249+
self._workspace_folders = workspace_folders
244250
self._session.on_notification(
245251
"textDocument/publishDiagnostics",
246252
self._handle_diagnostics,
@@ -267,6 +273,13 @@ async def start(
267273
"workspace/inlayHint/refresh",
268274
self._handle_inlay_hint_refresh,
269275
)
276+
# Some servers (e.g. tombi) send workspace/workspaceFolders from their
277+
# `initialized` handling regardless of the client capability, so this
278+
# needs a handler even when `workspace.workspaceFolders` is false.
279+
self._session.on_request(
280+
"workspace/workspaceFolders",
281+
self._handle_workspace_folders_request,
282+
)
270283

271284
# some LSP servers read settings from didChangeConfiguration (e.g. pyrefly)
272285
if self._settings:
@@ -1418,6 +1431,16 @@ async def _handle_inlay_hint_refresh(self, params: dict[str, Any] | None) -> Non
14181431
"""
14191432
return
14201433

1434+
async def _handle_workspace_folders_request(
1435+
self, params: dict[str, Any] | None
1436+
) -> list[dict[str, str]]:
1437+
"""Handle the workspace/workspaceFolders pull request.
1438+
1439+
The folders are the fixed set this session was started with; there is
1440+
no dynamic change to report.
1441+
"""
1442+
return self._workspace_folders
1443+
14211444
async def _handle_configuration_request(
14221445
self, params: dict[str, Any] | None
14231446
) -> list[dict[str, Any]]:

‎finecode_extension_api/tests/test_lsp_service.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ def __init__(self) -> None:
6060
# What the server advertised in its initialize result. Declaring
6161
# `diagnosticProvider` here is what puts the service on the pull path.
6262
self.capabilities: dict[str, Any] = {}
63+
# Handlers registered with on_request, keyed by method, so a test can
64+
# invoke the client side of a server-to-client request directly.
65+
self.request_handlers: dict[str, Any] = {}
6366

6467
async def __aenter__(self) -> "_FakeLspSession":
6568
return self
@@ -106,7 +109,7 @@ def on_notification(self, method: str, handler: Any) -> None:
106109
pass
107110

108111
def on_request(self, method: str, handler: Any) -> None:
109-
pass
112+
self.request_handlers[method] = handler
110113

111114
@property
112115
def server_capabilities(self) -> dict[str, Any]:
@@ -1210,6 +1213,22 @@ async def test_unregistered_service_drops_a_nonempty_watched_file_batch(
12101213
]
12111214

12121215

1216+
async def test_workspace_folders_request_is_answered_with_initial_folders(
1217+
tmp_path: Path,
1218+
) -> None:
1219+
"""Some servers (tombi) pull the workspace folders regardless of the
1220+
workspace.workspaceFolders capability. The request has to be answered with
1221+
the folder passed at initialize, not Method not found."""
1222+
subject = tmp_path / "subject.py"
1223+
1224+
async with _running_service(subject, "x = 1\n") as (_, session, _):
1225+
handler = session.request_handlers["workspace/workspaceFolders"]
1226+
result = await handler(None)
1227+
1228+
root_uri = subject.parent.as_uri()
1229+
assert result == [{"uri": root_uri, "name": root_uri}]
1230+
1231+
12131232
async def test_watched_file_create_and_delete_after_registration(
12141233
tmp_path: Path,
12151234
) -> None:

0 commit comments

Comments
 (0)