Skip to content

Commit 74a6e89

Browse files
committed
feat: use Protocol for config change listener and support passing parameters
1 parent 9dc6548 commit 74a6e89

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-5
lines changed

src/lsp_client/capability/server_request/configuration.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,26 @@ def register_server_request_hooks(
8080
self, registry: ServerRequestHookRegistry
8181
) -> None:
8282
super().register_server_request_hooks(registry)
83+
84+
# Automatically bind change notification if both capabilities are present
85+
from lsp_client.capability.notification.did_change_configuration import (
86+
WithNotifyDidChangeConfiguration,
87+
)
88+
89+
if self.configuration_map and isinstance(
90+
self, WithNotifyDidChangeConfiguration
91+
):
92+
# We use a lambda to avoid sync/async issues if the notification
93+
# needs to be scheduled on an event loop
94+
import asyncer
95+
96+
def on_config_change(config_map: ConfigurationMap, **kwargs: Any):
97+
with logger.contextualize(method="didChangeConfiguration"):
98+
logger.debug("Configuration changed, notifying server")
99+
asyncer.runnify(self.notify_change_configuration)()
100+
101+
self.configuration_map.on_change(on_config_change)
102+
83103
registry.register(
84104
lsp_type.WORKSPACE_CONFIGURATION,
85105
ServerRequestHook(

src/lsp_client/utils/config.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import fnmatch
44
from copy import deepcopy
5-
from typing import Any
5+
from typing import Any, Protocol, runtime_checkable
66

77
from attrs import define, field
88
from loguru import logger
@@ -23,6 +23,15 @@ def deep_merge(base: dict[str, Any], update: dict[str, Any]) -> dict[str, Any]:
2323
return result
2424

2525

26+
@runtime_checkable
27+
class ConfigurationChangeListener(Protocol):
28+
"""
29+
Protocol for configuration change listeners.
30+
"""
31+
32+
def __call__(self, config_map: ConfigurationMap, **kwargs: Any) -> Any: ...
33+
34+
2635
@define
2736
class ConfigurationMap:
2837
"""
@@ -32,21 +41,49 @@ class ConfigurationMap:
3241

3342
_global_config: dict[str, Any] = field(factory=dict)
3443
_scoped_configs: list[tuple[str, dict[str, Any]]] = field(factory=list)
44+
_on_change_callbacks: list[ConfigurationChangeListener] = field(
45+
factory=list, init=False
46+
)
47+
48+
def on_change(self, callback: ConfigurationChangeListener) -> None:
49+
"""
50+
Register a callback to be called when the configuration changes.
51+
"""
52+
self._on_change_callbacks.append(callback)
53+
54+
def _notify_change(self, **kwargs: Any) -> None:
55+
for callback in self._on_change_callbacks:
56+
try:
57+
callback(self, **kwargs)
58+
except Exception as e:
59+
logger.error(f"Error in configuration change callback: {e}")
60+
61+
def update_global(
62+
self, config: dict[str, Any], merge: bool = True, **kwargs: Any
63+
) -> None:
64+
"""
65+
Update global configuration.
66+
"""
67+
if merge:
68+
self._global_config = deep_merge(self._global_config, config)
69+
else:
70+
self._global_config = deepcopy(config)
71+
self._notify_change(**kwargs)
3572

36-
def add_scope(self, pattern: str, config: dict[str, Any]) -> None:
73+
def add_scope(self, pattern: str, config: dict[str, Any], **kwargs: Any) -> None:
3774
"""
3875
Add a configuration override for a specific file pattern.
3976
4077
:param pattern: Glob pattern (e.g. "**/tests/**", "*.py")
4178
:param config: The configuration dict to merge for this scope
4279
"""
4380
self._scoped_configs.append((pattern, config))
81+
self._notify_change(**kwargs)
4482

4583
def _get_section(self, config: Any, section: str | None) -> Any:
4684
if not section:
4785
return config
4886

49-
# Traverse the config dictionary using the section path (e.g. "python.analysis")
5087
current = config
5188
for part in section.split("."):
5289
if isinstance(current, dict) and part in current:
@@ -56,10 +93,8 @@ def _get_section(self, config: Any, section: str | None) -> Any:
5693
return current
5794

5895
def get(self, scope_uri: str | None, section: str | None) -> Any:
59-
# Start with global config
6096
final_config = self._global_config
6197

62-
# If we have a scope, merge matching scoped configs
6398
if scope_uri:
6499
try:
65100
path_str = str(from_local_uri(scope_uri))

0 commit comments

Comments
 (0)