Skip to content

Commit e4a3c17

Browse files
committed
fix: add missing jedi scripts for document
1 parent 173518c commit e4a3c17

File tree

1 file changed

+124
-19
lines changed

1 file changed

+124
-19
lines changed

pylsp/server/workspace.py

Lines changed: 124 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
from __future__ import annotations
22

3+
import os
34
import typing as typ
45
from pathlib import Path
56

67
from collections.abc import Generator
78
from typing import Callable, Optional
89

10+
import jedi
11+
from jedi.api import environment as jedi_environment
912
from lsprotocol import types as lsptyp
1013
from lsprotocol.types import WorkspaceFolder
1114
from pygls import uris, workspace
1215
from pygls.workspace.text_document import TextDocument
1316

17+
from pylsp import _utils
18+
1419
if typ.TYPE_CHECKING:
1520
from pygls.server import LanguageServer
1621
from pylsp.server.settings import ServerConfig
1722

1823

24+
# Default auto-import modules for Jedi
25+
DEFAULT_AUTO_IMPORT_MODULES = ["numpy"]
26+
27+
1928
class Workspace(workspace.Workspace):
2029
"""Custom Workspace class for pylsp."""
2130

2231
def __init__(self, server: LanguageServer, *args, **kwargs):
2332
self._server = server
2433
super().__init__(*args, **kwargs)
2534
self._config = None
35+
# Cache jedi environments per configured path
36+
self._environments: dict[str, typ.Any] = {}
2637

2738
def get_document_folder(self, doc_uri: str) -> WorkspaceFolder | None:
2839
"""Get the workspace folder for a given document URI.
@@ -36,20 +47,42 @@ def get_document_folder(self, doc_uri: str) -> WorkspaceFolder | None:
3647
WorkspaceFolder | None: The workspace folder containing the document, or
3748
None if not found.
3849
"""
39-
best_match_len = float("inf")
50+
best_match_len = -1
4051
best_match = None
4152
document_path = Path(uris.to_fs_path(doc_uri) or "")
4253
for folder_uri, folder in self._folders.items():
4354
folder_path = Path(uris.to_fs_path(folder_uri) or "")
44-
if (
45-
match_len := len(document_path.relative_to(folder_path).parts)
46-
< best_match_len
47-
):
55+
try:
56+
document_path.relative_to(folder_path)
57+
except Exception:
58+
continue
59+
# prefer the longest matching folder
60+
match_len = len(str(folder_path))
61+
if match_len > best_match_len:
4862
best_match_len = match_len
4963
best_match = folder
5064

5165
return best_match
5266

67+
@property
68+
def config(self) -> ServerConfig | None:
69+
return self._config
70+
71+
def attach_config(self, config: ServerConfig):
72+
self._config = config
73+
74+
def source_roots(self, document_path: str) -> list[str]:
75+
"""Return source roots for the given document.
76+
77+
Searches for project files (setup.py, pyproject.toml) upwards and
78+
returns their directories, falling back to the workspace root.
79+
"""
80+
files = _utils.find_parents(
81+
self.root_path, document_path, ["setup.py", "pyproject.toml"]
82+
) or []
83+
roots = list({str(Path(f).parent) for f in files})
84+
return roots or [self.root_path]
85+
5386
def _create_text_document(
5487
self,
5588
doc_uri: str,
@@ -84,19 +117,6 @@ async def save(self, doc_uri: str) -> None:
84117
with open(path, "w", encoding="utf-8") as f:
85118
f.write(document.source)
86119

87-
# ---- pylsp plugin compatibility helpers ----
88-
89-
@property
90-
def config(self) -> ServerConfig:
91-
return self._config
92-
93-
@property
94-
def root_uri(self) -> str | None:
95-
return self._root_uri
96-
97-
def attach_config(self, config: ServerConfig) -> None:
98-
self._config = config
99-
100120
def report_progress(
101121
self,
102122
title: str,
@@ -136,11 +156,12 @@ def _progress(msg: str, pct: Optional[int] = None) -> None:
136156
self._server.progress(token, lsptyp.WorkDoneProgressEnd()) # type: ignore[attr-defined]
137157

138158

139-
140159
class Document(TextDocument):
141160
def __init__(self, workspace: Workspace, *args, **kwargs):
142161
self._workspace = workspace
143162
super().__init__(*args, **kwargs)
163+
# Extra data used by some plugins during resolve steps
164+
self.shared_data: dict[str, typ.Any] = {}
144165

145166
@property
146167
def workspace(self) -> Workspace:
@@ -149,3 +170,87 @@ def workspace(self) -> Workspace:
149170
@property
150171
def workspace_folder(self) -> WorkspaceFolder | None:
151172
return self._workspace.get_document_folder(self.uri)
173+
174+
@property
175+
def dot_path(self) -> str:
176+
return _utils.path_to_dot_name(self.path)
177+
178+
# ---- Jedi helpers ----
179+
180+
def jedi_names(
181+
self, all_scopes: bool = False, definitions: bool = True, references: bool = False
182+
):
183+
script = self.jedi_script()
184+
return script.get_names(
185+
all_scopes=all_scopes, definitions=definitions, references=references
186+
)
187+
188+
def jedi_script(
189+
self, use_document_path: bool = False
190+
) -> jedi.Script:
191+
extra_paths: list[str] = []
192+
environment_path: str | None = None
193+
env_vars: dict[str, str] | None = None
194+
prioritize_extra_paths = False
195+
196+
# Read Jedi-related settings
197+
cfg = self._workspace.config
198+
if cfg:
199+
jedi_settings = cfg.plugin_settings("jedi", document_path=self.path)
200+
jedi.settings.auto_import_modules = jedi_settings.get(
201+
"auto_import_modules", DEFAULT_AUTO_IMPORT_MODULES
202+
)
203+
environment_path = jedi_settings.get("environment")
204+
if environment_path and os.name != "nt":
205+
environment_path = os.path.expanduser(environment_path)
206+
extra_paths = jedi_settings.get("extra_paths") or []
207+
env_vars = jedi_settings.get("env_vars")
208+
prioritize_extra_paths = jedi_settings.get("prioritize_extra_paths", False)
209+
210+
# Ensure Jedi starts without PYTHONPATH collisions
211+
if env_vars is None:
212+
env_vars = os.environ.copy()
213+
env_vars.pop("PYTHONPATH", None)
214+
215+
environment = self.get_enviroment(environment_path, env_vars=env_vars)
216+
217+
# Build sys_path: project roots + environment + configured extra paths
218+
sys_path = [] # type: list[str]
219+
sys_path.extend(self._workspace.source_roots(self.path))
220+
sys_path.extend(environment.get_sys_path())
221+
if use_document_path:
222+
sys_path.append(os.path.normpath(os.path.dirname(self.path)))
223+
if prioritize_extra_paths:
224+
sys_path = list(extra_paths) + sys_path
225+
else:
226+
sys_path = sys_path + list(extra_paths)
227+
228+
# Determine project path
229+
project_path = self._workspace.root_path
230+
231+
kwargs: dict[str, typ.Any] = {
232+
"code": self.source,
233+
"path": self.path,
234+
"environment": environment if environment_path else None,
235+
"project": jedi.Project(path=project_path, sys_path=sys_path),
236+
}
237+
238+
# Position is passed to API calls (infer/complete), not Script ctor, so ignored here
239+
return jedi.Script(**kwargs)
240+
241+
def get_enviroment(
242+
self, environment_path: str | None = None, env_vars: dict[str, str] | None = None
243+
):
244+
"""Return a cached Jedi environment or create a new one.
245+
246+
Note: keep method name for backward compatibility (typo preserved).
247+
"""
248+
if environment_path is None:
249+
return jedi_environment.get_cached_default_environment()
250+
if environment_path in self._workspace._environments:
251+
return self._workspace._environments[environment_path]
252+
env = jedi_environment.create_environment(
253+
path=environment_path, safe=False, env_vars=env_vars
254+
)
255+
self._workspace._environments[environment_path] = env
256+
return env

0 commit comments

Comments
 (0)