Skip to content

Commit 9dc6548

Browse files
committed
feat: add ConfigurationMap for flexible LSP configuration management
1 parent 84c45ed commit 9dc6548

File tree

2 files changed

+79
-7
lines changed

2 files changed

+79
-7
lines changed

src/lsp_client/capability/server_request/configuration.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
from abc import abstractmethod
43
from collections.abc import Iterator
54
from typing import Any, Protocol, override, runtime_checkable
65

@@ -13,6 +12,7 @@
1312
ServerRequestHookRegistry,
1413
WorkspaceCapabilityProtocol,
1514
)
15+
from lsp_client.utils.config import ConfigurationMap
1616
from lsp_client.utils.types import lsp_type
1717

1818

@@ -27,6 +27,8 @@ class WithRespondConfigurationRequest(
2727
`workspace/configuration` - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration
2828
"""
2929

30+
configuration_map: ConfigurationMap | None = None
31+
3032
@override
3133
@classmethod
3234
def iter_methods(cls) -> Iterator[str]:
@@ -46,17 +48,15 @@ def register_workspace_capability(
4648
def check_server_capability(cls, cap: lsp_type.ServerCapabilities) -> None:
4749
super().check_server_capability(cap)
4850

49-
@abstractmethod
5051
def get_configuration(self, scope_uri: str | None, section: str | None) -> Any:
5152
"""
5253
Get the configuration value for the given scope URI and section.
5354
54-
If a client supports this capability, it's the user's responsibility to implement this method to return the appropriate configuration value.
55-
56-
:param scope_uri: The scope URI for which to get the configuration.
57-
:param section: The section of the configuration to get.
58-
:return: The configuration value.
55+
Default implementation uses `self.configuration_map` if available.
5956
"""
57+
if self.configuration_map:
58+
return self.configuration_map.get(scope_uri, section)
59+
return None
6060

6161
async def _respond_configuration(
6262
self, params: lsp_type.ConfigurationParams

src/lsp_client/utils/config.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
import fnmatch
4+
from copy import deepcopy
5+
from typing import Any
6+
7+
from attrs import define, field
8+
from loguru import logger
9+
10+
from lsp_client.utils.uri import from_local_uri
11+
12+
13+
def deep_merge(base: dict[str, Any], update: dict[str, Any]) -> dict[str, Any]:
14+
"""
15+
Recursively merge two dictionaries.
16+
"""
17+
result = deepcopy(base)
18+
for key, value in update.items():
19+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
20+
result[key] = deep_merge(result[key], value)
21+
else:
22+
result[key] = deepcopy(value)
23+
return result
24+
25+
26+
@define
27+
class ConfigurationMap:
28+
"""
29+
A helper class to manage LSP configuration.
30+
Supports global configuration and scope-specific overrides.
31+
"""
32+
33+
_global_config: dict[str, Any] = field(factory=dict)
34+
_scoped_configs: list[tuple[str, dict[str, Any]]] = field(factory=list)
35+
36+
def add_scope(self, pattern: str, config: dict[str, Any]) -> None:
37+
"""
38+
Add a configuration override for a specific file pattern.
39+
40+
:param pattern: Glob pattern (e.g. "**/tests/**", "*.py")
41+
:param config: The configuration dict to merge for this scope
42+
"""
43+
self._scoped_configs.append((pattern, config))
44+
45+
def _get_section(self, config: Any, section: str | None) -> Any:
46+
if not section:
47+
return config
48+
49+
# Traverse the config dictionary using the section path (e.g. "python.analysis")
50+
current = config
51+
for part in section.split("."):
52+
if isinstance(current, dict) and part in current:
53+
current = current[part]
54+
else:
55+
return None
56+
return current
57+
58+
def get(self, scope_uri: str | None, section: str | None) -> Any:
59+
# Start with global config
60+
final_config = self._global_config
61+
62+
# If we have a scope, merge matching scoped configs
63+
if scope_uri:
64+
try:
65+
path_str = str(from_local_uri(scope_uri))
66+
for pattern, scoped_config in self._scoped_configs:
67+
if fnmatch.fnmatch(path_str, pattern):
68+
final_config = deep_merge(final_config, scoped_config)
69+
except Exception:
70+
logger.warning(f"Failed to parse scope URI: {scope_uri}")
71+
72+
return self._get_section(final_config, section)

0 commit comments

Comments
 (0)