Skip to content

Commit 20f516f

Browse files
committed
refactor: better support for multiple workspace folders
Signed-off-by: Jack Cherng <jfcherng@gmail.com>
1 parent 2ad481e commit 20f516f

File tree

7 files changed

+157
-135
lines changed

7 files changed

+157
-135
lines changed

plugin/__init__.py

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

3-
from .client import LspBasedpyrightPlugin
3+
from .client import LspBasedpyrightPlugin, ViewEventListener
44
from .commands import LspBasedpyrightCreateConfigurationCommand
55

66
__all__ = (
@@ -11,6 +11,7 @@
1111
"LspBasedpyrightCreateConfigurationCommand",
1212
# ...
1313
"LspBasedpyrightPlugin",
14+
"ViewEventListener",
1415
)
1516

1617

@@ -21,5 +22,5 @@ def plugin_loaded() -> None:
2122

2223
def plugin_unloaded() -> None:
2324
"""Executed when this plugin is unloaded."""
24-
LspBasedpyrightPlugin.window_attrs.clear()
25+
LspBasedpyrightPlugin.wf_attrs.clear()
2526
LspBasedpyrightPlugin.cleanup()

plugin/client.py

Lines changed: 39 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,36 @@
33
import json
44
import os
55
import re
6-
import shutil
7-
import weakref
8-
from dataclasses import dataclass
9-
from pathlib import Path
106
from typing import Any, cast
117

128
import jmespath
139
import sublime
10+
import sublime_plugin
1411
from LSP.plugin import ClientConfig, DottedDict, MarkdownLangMap, Response, WorkspaceFolder
1512
from LSP.plugin.core.protocol import CompletionItem, Hover, SignatureHelp
1613
from lsp_utils import NpmClientHandler
17-
from more_itertools import first_true
1814
from sublime_lib import ResourcePath
1915

2016
from .constants import PACKAGE_NAME, SERVER_SETTING_DEV_ENVIRONMENT
2117
from .dev_environment.helpers import get_dev_environment_handler
22-
from .log import log_error, log_info, log_warning
23-
from .template import load_string_template
24-
from .virtual_env.helpers import find_venv_by_finder_names, find_venv_by_python_executable
25-
from .virtual_env.venv_finder import BaseVenvInfo, get_finder_name_mapping
18+
from .log import log_error, log_warning
19+
from .utils_lsp import AbstractLspPythonPlugin, find_workspace_folder, update_view_status_bar_text, uri_to_file_path
20+
from .virtual_env.helpers import find_venv_by_finder_names
2621

27-
WindowId = int
2822

23+
class ViewEventListener(sublime_plugin.ViewEventListener):
24+
def on_activated(self) -> None:
25+
settings = self.view.settings()
2926

30-
@dataclass
31-
class WindowAttr:
32-
simple_python_executable: Path | None = None
33-
"""The path to the Python executable found by the `PATH` env variable."""
34-
venv_info: BaseVenvInfo | None = None
35-
"""The information of the virtual environment."""
27+
if settings.get("lsp_active"):
28+
update_view_status_bar_text(LspBasedpyrightPlugin, self.view)
3629

37-
@property
38-
def preferred_python_executable(self) -> Path | None:
39-
return self.venv_info.python_executable if self.venv_info else self.simple_python_executable
4030

41-
42-
class LspBasedpyrightPlugin(NpmClientHandler):
31+
class LspBasedpyrightPlugin(AbstractLspPythonPlugin, NpmClientHandler):
4332
package_name = PACKAGE_NAME
4433
server_directory = "language-server"
4534
server_binary_path = os.path.join(server_directory, "node_modules", "basedpyright", "langserver.index.js")
4635

47-
server_version = ""
48-
"""The version of the language server."""
49-
50-
window_attrs: weakref.WeakKeyDictionary[sublime.Window, WindowAttr] = weakref.WeakKeyDictionary()
51-
"""Per-window attributes. I.e., per-session attributes."""
52-
5336
@classmethod
5437
def required_node_version(cls) -> str:
5538
"""
@@ -83,8 +66,6 @@ def can_start(
8366
) -> str | None:
8467
if message := super().can_start(window, initiating_view, workspace_folders, configuration):
8568
return message
86-
87-
cls.window_attrs.setdefault(window, WindowAttr())
8869
return None
8970

9071
def on_settings_changed(self, settings: DottedDict) -> None:
@@ -104,24 +85,6 @@ def on_settings_changed(self, settings: DottedDict) -> None:
10485
except Exception as ex:
10586
log_error(f'Failed to update extra paths for dev environment "{dev_environment}": {ex}')
10687

107-
self.update_status_bar_text()
108-
109-
@classmethod
110-
def on_pre_start(
111-
cls,
112-
window: sublime.Window,
113-
initiating_view: sublime.View,
114-
workspace_folders: list[WorkspaceFolder],
115-
configuration: ClientConfig,
116-
) -> str | None:
117-
super().on_pre_start(window, initiating_view, workspace_folders, configuration)
118-
119-
cls.update_venv_info(configuration.settings, workspace_folders, window=window)
120-
if venv_info := cls.window_attrs[window].venv_info:
121-
log_info(f"Using python executable: {venv_info.python_executable}")
122-
configuration.settings.set("python.pythonPath", str(venv_info.python_executable))
123-
return None
124-
12588
@classmethod
12689
def install_or_update(cls) -> None:
12790
super().install_or_update()
@@ -156,6 +119,35 @@ def on_server_response_async(self, method: str, response: Response) -> None:
156119
documentation["value"] = self.patch_markdown_content(documentation["value"])
157120
return
158121

122+
def on_workspace_configuration(self, params: Any, configuration: dict[str, Any]) -> dict[str, Any]:
123+
# provide detected venv information from the workspace folder
124+
# note that `pyrightconfig.json` seems to be auto-prioritized by the server
125+
if (
126+
(session := self.weaksession())
127+
and (params["section"] == "python")
128+
and (scope_uri := params.get("scopeUri"))
129+
and (file_path := uri_to_file_path(scope_uri))
130+
and (wf_path := find_workspace_folder(session.window, file_path))
131+
and (venv_strategies := session.config.settings.get("venvStrategies"))
132+
and (venv_info := find_venv_by_finder_names(venv_strategies, project_dir=wf_path))
133+
):
134+
self.wf_attrs[wf_path].venv_info = venv_info
135+
# When ST just starts, server session hasn't been created yet.
136+
# So `on_activated` can't add full information for the initial view and hence we handle it here.
137+
if active_view := sublime.active_window().active_view():
138+
update_view_status_bar_text(self.__class__, active_view)
139+
140+
# modify configuration for the venv
141+
site_packages_dir = str(venv_info.site_packages_dir)
142+
conf_analysis: dict[str, Any] = configuration.setdefault("analysis", {})
143+
conf_analysis_extra_paths: list[str] = conf_analysis.setdefault("extraPaths", [])
144+
if site_packages_dir not in conf_analysis_extra_paths:
145+
conf_analysis_extra_paths.insert(0, site_packages_dir)
146+
if not configuration.get("pythonPath"):
147+
configuration["pythonPath"] = str(venv_info.python_executable)
148+
149+
return configuration
150+
159151
# -------------- #
160152
# custom methods #
161153
# -------------- #
@@ -173,32 +165,6 @@ def copy_overwrite_dirs(cls) -> None:
173165
except OSError:
174166
raise RuntimeError(f'Failed to copy overwrite dirs from "{dir_src}" to "{dir_dst}".')
175167

176-
def update_status_bar_text(self, extra_variables: dict[str, Any] | None = None) -> None:
177-
if not (session := self.weaksession()):
178-
return
179-
180-
variables: dict[str, Any] = {
181-
"server_version": self.server_version,
182-
}
183-
184-
if venv_info := self.window_attrs[session.window].venv_info:
185-
variables["venv"] = {
186-
"finder_name": venv_info.meta.finder_name,
187-
"python_version": venv_info.python_version,
188-
"venv_prompt": venv_info.prompt,
189-
}
190-
191-
if extra_variables:
192-
variables.update(extra_variables)
193-
194-
rendered_text = ""
195-
if template_text := str(session.config.settings.get("statusText") or ""):
196-
try:
197-
rendered_text = load_string_template(template_text).render(variables)
198-
except Exception as e:
199-
log_warning(f'Invalid "statusText" template: {e}')
200-
session.set_config_status_async(rendered_text)
201-
202168
def patch_markdown_content(self, content: str) -> str:
203169
# Add another linebreak before horizontal rule following fenced code block
204170
content = re.sub("```\n---", "```\n\n---", content)
@@ -220,40 +186,3 @@ def patch_markdown_content(self, content: str) -> str:
220186
def parse_server_version(cls) -> str:
221187
lock_file_content = sublime.load_resource(f"Packages/{PACKAGE_NAME}/language-server/package-lock.json")
222188
return jmespath.search("dependencies.basedpyright.version", json.loads(lock_file_content)) or ""
223-
224-
@classmethod
225-
def update_venv_info(
226-
cls,
227-
settings: DottedDict,
228-
workspace_folders: list[WorkspaceFolder],
229-
*,
230-
window: sublime.Window,
231-
) -> None:
232-
window_attr = cls.window_attrs[window]
233-
234-
def _update_venv_info() -> None:
235-
window_attr.venv_info = None
236-
237-
if python_path := settings.get("python.pythonPath"):
238-
window_attr.venv_info = find_venv_by_python_executable(python_path)
239-
return
240-
241-
supported_finder_names = tuple(get_finder_name_mapping().keys())
242-
finder_names: list[str] = settings.get("venvStrategies")
243-
if invalid_finder_names := sorted(set(finder_names) - set(supported_finder_names)):
244-
log_warning(f"The following finder names are not supported: {', '.join(invalid_finder_names)}")
245-
246-
if workspace_folders and (first_folder := Path(workspace_folders[0].path).resolve()):
247-
for folder in (first_folder, *first_folder.parents):
248-
if venv_info := find_venv_by_finder_names(finder_names, project_dir=folder):
249-
window_attr.venv_info = venv_info
250-
return
251-
252-
def _update_simple_python_path() -> None:
253-
window_attr.simple_python_executable = None
254-
255-
if python_path := first_true(("py", "python3", "python"), pred=shutil.which):
256-
window_attr.simple_python_executable = Path(python_path)
257-
258-
_update_simple_python_path()
259-
_update_venv_info()

plugin/dev_environment/helpers.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from more_itertools import first_true
77

8-
from ..virtual_env.venv_info import BaseVenvInfo
98
from .impl import (
109
BlenderDevEnvironmentHandler,
1110
GdbDevEnvironmentHandler,
@@ -28,14 +27,9 @@ def get_dev_environment_handler(
2827
*,
2928
server_dir: str | Path,
3029
workspace_folders: Sequence[str],
31-
venv_info: BaseVenvInfo | None = None,
3230
) -> BaseDevEnvironmentHandler | None:
3331
if handler_cls := find_dev_environment_handler_class(dev_environment):
34-
return handler_cls(
35-
server_dir=server_dir,
36-
workspace_folders=workspace_folders,
37-
venv_info=venv_info,
38-
)
32+
return handler_cls(server_dir=server_dir, workspace_folders=workspace_folders)
3933
return None
4034

4135

plugin/dev_environment/impl/sublime_text.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def python_version(self) -> tuple[int, int]:
1919
return (3, 3)
2020

2121
def handle_(self, *, settings: DottedDict) -> None:
22-
self._inject_extra_paths(settings=settings, paths=self.find_package_dependency_dirs(), operation="replace")
22+
self._inject_extra_paths(settings=settings, paths=self.find_package_dependency_dirs())
2323

2424
def find_package_dependency_dirs(self) -> list[str]:
2525
dep_dirs = sys.path.copy()

plugin/dev_environment/interfaces.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,19 @@
55
from typing import Any, Iterable, Literal, Sequence, final
66

77
from LSP.plugin.core.collections import DottedDict
8+
from more_itertools import unique_everseen
89

910
from ..constants import SERVER_SETTING_ANALYSIS_EXTRAPATHS, SERVER_SETTING_DEV_ENVIRONMENT
10-
from ..log import log_info
11+
from ..log import log_debug
1112
from ..utils import camel_to_snake, remove_suffix
12-
from ..virtual_env.venv_info import BaseVenvInfo
1313

1414

1515
class BaseDevEnvironmentHandler(ABC):
16-
def __init__(
17-
self,
18-
*,
19-
server_dir: str | Path,
20-
workspace_folders: Sequence[str],
21-
venv_info: BaseVenvInfo | None = None,
22-
) -> None:
16+
def __init__(self, *, server_dir: str | Path, workspace_folders: Sequence[str]) -> None:
2317
self.server_dir = Path(server_dir)
2418
"""The language server directory."""
2519
self.workspace_folders = workspace_folders
2620
"""The workspace folders."""
27-
self.venv_info = venv_info
28-
"""The virtual environment information."""
2921

3022
@classmethod
3123
def name(cls) -> str:
@@ -48,9 +40,6 @@ def handle(self, *, settings: DottedDict) -> None:
4840
"""Handle this environment."""
4941
self.handle_(settings=settings)
5042

51-
if self.venv_info:
52-
self._inject_extra_paths(settings=settings, paths=(self.venv_info.site_packages_dir,))
53-
5443
@abstractmethod
5544
def handle_(self, *, settings: DottedDict) -> None:
5645
"""Handle this environment. (subclass)"""
@@ -73,5 +62,7 @@ def _inject_extra_paths(
7362
next_paths = extra_paths
7463
else:
7564
raise ValueError(f"Invalid operation: {operation}")
76-
log_info(f"Modified extra analysis paths ({operation = }): {paths}")
65+
66+
next_paths = list(unique_everseen(next_paths, key=Path)) # deduplication
67+
log_debug(f'Due to "dev_environment", new "analysis.extraPaths" is ({operation = }): {next_paths}')
7768
settings.set(SERVER_SETTING_ANALYSIS_EXTRAPATHS, next_paths)

plugin/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ def get_default_startupinfo() -> Any:
6060
return None
6161

6262

63+
def to_resolved_posix_path(path: str | Path) -> str | None:
64+
try:
65+
return Path(path).resolve().as_posix()
66+
except Exception:
67+
return None
68+
69+
6370
def run_shell_command(
6471
command: str | Sequence[str],
6572
*,

0 commit comments

Comments
 (0)