1use anyhow::{Context as _, ensure};
2use anyhow::{Result, anyhow};
3use async_trait::async_trait;
4use collections::HashMap;
5use futures::AsyncBufReadExt;
6use gpui::{App, Task};
7use gpui::{AsyncApp, SharedString};
8use language::Toolchain;
9use language::ToolchainList;
10use language::ToolchainLister;
11use language::language_settings::language_settings;
12use language::{ContextLocation, LanguageToolchainStore};
13use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
14use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
15use lsp::LanguageServerBinary;
16use lsp::LanguageServerName;
17use node_runtime::{NodeRuntime, VersionStrategy};
18use pet_core::Configuration;
19use pet_core::os_environment::Environment;
20use pet_core::python_environment::PythonEnvironmentKind;
21use project::Fs;
22use project::lsp_store::language_server_settings;
23use serde_json::{Value, json};
24use smol::lock::OnceCell;
25use std::cmp::Ordering;
26
27use parking_lot::Mutex;
28use std::str::FromStr;
29use std::{
30 any::Any,
31 borrow::Cow,
32 ffi::OsString,
33 fmt::Write,
34 path::{Path, PathBuf},
35 sync::Arc,
36};
37use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
38use util::ResultExt;
39
40pub(crate) struct PyprojectTomlManifestProvider;
41
42impl ManifestProvider for PyprojectTomlManifestProvider {
43 fn name(&self) -> ManifestName {
44 SharedString::new_static("pyproject.toml").into()
45 }
46
47 fn search(
48 &self,
49 ManifestQuery {
50 path,
51 depth,
52 delegate,
53 }: ManifestQuery,
54 ) -> Option<Arc<Path>> {
55 for path in path.ancestors().take(depth) {
56 let p = path.join("pyproject.toml");
57 if delegate.exists(&p, Some(false)) {
58 return Some(path.into());
59 }
60 }
61
62 None
63 }
64}
65
66const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
67const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
68
69enum TestRunner {
70 UNITTEST,
71 PYTEST,
72}
73
74impl FromStr for TestRunner {
75 type Err = ();
76
77 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
78 match s {
79 "unittest" => Ok(Self::UNITTEST),
80 "pytest" => Ok(Self::PYTEST),
81 _ => Err(()),
82 }
83 }
84}
85
86fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
87 vec![server_path.into(), "--stdio".into()]
88}
89
90pub struct PythonLspAdapter {
91 node: NodeRuntime,
92}
93
94impl PythonLspAdapter {
95 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");
96
97 pub fn new(node: NodeRuntime) -> Self {
98 PythonLspAdapter { node }
99 }
100}
101
102#[async_trait(?Send)]
103impl LspAdapter for PythonLspAdapter {
104 fn name(&self) -> LanguageServerName {
105 Self::SERVER_NAME
106 }
107
108 async fn initialization_options(
109 self: Arc<Self>,
110 _: &dyn Fs,
111 _: &Arc<dyn LspAdapterDelegate>,
112 ) -> Result<Option<Value>> {
113 // Provide minimal initialization options
114 // Virtual environment configuration will be handled through workspace configuration
115 Ok(Some(json!({
116 "python": {
117 "analysis": {
118 "autoSearchPaths": true,
119 "useLibraryCodeForTypes": true,
120 "autoImportCompletions": true
121 }
122 }
123 })))
124 }
125
126 async fn check_if_user_installed(
127 &self,
128 delegate: &dyn LspAdapterDelegate,
129 _: Option<Toolchain>,
130 _: &AsyncApp,
131 ) -> Option<LanguageServerBinary> {
132 if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
133 let env = delegate.shell_env().await;
134 Some(LanguageServerBinary {
135 path: pyright_bin,
136 env: Some(env),
137 arguments: vec!["--stdio".into()],
138 })
139 } else {
140 let node = delegate.which("node".as_ref()).await?;
141 let (node_modules_path, _) = delegate
142 .npm_package_installed_version(Self::SERVER_NAME.as_ref())
143 .await
144 .log_err()??;
145
146 let path = node_modules_path.join(NODE_MODULE_RELATIVE_SERVER_PATH);
147
148 let env = delegate.shell_env().await;
149 Some(LanguageServerBinary {
150 path: node,
151 env: Some(env),
152 arguments: server_binary_arguments(&path),
153 })
154 }
155 }
156
157 async fn fetch_latest_server_version(
158 &self,
159 _: &dyn LspAdapterDelegate,
160 ) -> Result<Box<dyn 'static + Any + Send>> {
161 Ok(Box::new(
162 self.node
163 .npm_package_latest_version(Self::SERVER_NAME.as_ref())
164 .await?,
165 ) as Box<_>)
166 }
167
168 async fn fetch_server_binary(
169 &self,
170 latest_version: Box<dyn 'static + Send + Any>,
171 container_dir: PathBuf,
172 delegate: &dyn LspAdapterDelegate,
173 ) -> Result<LanguageServerBinary> {
174 let latest_version = latest_version.downcast::<String>().unwrap();
175 let server_path = container_dir.join(SERVER_PATH);
176
177 self.node
178 .npm_install_packages(
179 &container_dir,
180 &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
181 )
182 .await?;
183
184 let env = delegate.shell_env().await;
185 Ok(LanguageServerBinary {
186 path: self.node.binary_path().await?,
187 env: Some(env),
188 arguments: server_binary_arguments(&server_path),
189 })
190 }
191
192 async fn check_if_version_installed(
193 &self,
194 version: &(dyn 'static + Send + Any),
195 container_dir: &PathBuf,
196 delegate: &dyn LspAdapterDelegate,
197 ) -> Option<LanguageServerBinary> {
198 let version = version.downcast_ref::<String>().unwrap();
199 let server_path = container_dir.join(SERVER_PATH);
200
201 let should_install_language_server = self
202 .node
203 .should_install_npm_package(
204 Self::SERVER_NAME.as_ref(),
205 &server_path,
206 container_dir,
207 VersionStrategy::Latest(version),
208 )
209 .await;
210
211 if should_install_language_server {
212 None
213 } else {
214 let env = delegate.shell_env().await;
215 Some(LanguageServerBinary {
216 path: self.node.binary_path().await.ok()?,
217 env: Some(env),
218 arguments: server_binary_arguments(&server_path),
219 })
220 }
221 }
222
223 async fn cached_server_binary(
224 &self,
225 container_dir: PathBuf,
226 delegate: &dyn LspAdapterDelegate,
227 ) -> Option<LanguageServerBinary> {
228 let mut binary = get_cached_server_binary(container_dir, &self.node).await?;
229 binary.env = Some(delegate.shell_env().await);
230 Some(binary)
231 }
232
233 async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
234 // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
235 // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
236 // and `name` is the symbol name itself.
237 //
238 // Because the symbol name is included, there generally are not ties when
239 // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
240 // into account. Here, we remove the symbol name from the sortText in order
241 // to allow our own fuzzy score to be used to break ties.
242 //
243 // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
244 for item in items {
245 let Some(sort_text) = &mut item.sort_text else {
246 continue;
247 };
248 let mut parts = sort_text.split('.');
249 let Some(first) = parts.next() else { continue };
250 let Some(second) = parts.next() else { continue };
251 let Some(_) = parts.next() else { continue };
252 sort_text.replace_range(first.len() + second.len() + 1.., "");
253 }
254 }
255
256 async fn label_for_completion(
257 &self,
258 item: &lsp::CompletionItem,
259 language: &Arc<language::Language>,
260 ) -> Option<language::CodeLabel> {
261 let label = &item.label;
262 let grammar = language.grammar()?;
263 let highlight_id = match item.kind? {
264 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
265 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
266 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
267 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
268 _ => return None,
269 };
270 let filter_range = item
271 .filter_text
272 .as_deref()
273 .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
274 .unwrap_or(0..label.len());
275 Some(language::CodeLabel {
276 text: label.clone(),
277 runs: vec![(0..label.len(), highlight_id)],
278 filter_range,
279 })
280 }
281
282 async fn label_for_symbol(
283 &self,
284 name: &str,
285 kind: lsp::SymbolKind,
286 language: &Arc<language::Language>,
287 ) -> Option<language::CodeLabel> {
288 let (text, filter_range, display_range) = match kind {
289 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
290 let text = format!("def {}():\n", name);
291 let filter_range = 4..4 + name.len();
292 let display_range = 0..filter_range.end;
293 (text, filter_range, display_range)
294 }
295 lsp::SymbolKind::CLASS => {
296 let text = format!("class {}:", name);
297 let filter_range = 6..6 + name.len();
298 let display_range = 0..filter_range.end;
299 (text, filter_range, display_range)
300 }
301 lsp::SymbolKind::CONSTANT => {
302 let text = format!("{} = 0", name);
303 let filter_range = 0..name.len();
304 let display_range = 0..filter_range.end;
305 (text, filter_range, display_range)
306 }
307 _ => return None,
308 };
309
310 Some(language::CodeLabel {
311 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
312 text: text[display_range].to_string(),
313 filter_range,
314 })
315 }
316
317 async fn workspace_configuration(
318 self: Arc<Self>,
319 _: &dyn Fs,
320 adapter: &Arc<dyn LspAdapterDelegate>,
321 toolchain: Option<Toolchain>,
322 cx: &mut AsyncApp,
323 ) -> Result<Value> {
324 cx.update(move |cx| {
325 let mut user_settings =
326 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
327 .and_then(|s| s.settings.clone())
328 .unwrap_or_default();
329
330 // If we have a detected toolchain, configure Pyright to use it
331 if let Some(toolchain) = toolchain
332 && let Ok(env) = serde_json::from_value::<
333 pet_core::python_environment::PythonEnvironment,
334 >(toolchain.as_json.clone())
335 {
336 if user_settings.is_null() {
337 user_settings = Value::Object(serde_json::Map::default());
338 }
339 let object = user_settings.as_object_mut().unwrap();
340
341 let interpreter_path = toolchain.path.to_string();
342 if let Some(venv_dir) = env.prefix {
343 // Set venvPath and venv at the root level
344 // This matches the format of a pyrightconfig.json file
345 if let Some(parent) = venv_dir.parent() {
346 // Use relative path if the venv is inside the workspace
347 let venv_path = if parent == adapter.worktree_root_path() {
348 ".".to_string()
349 } else {
350 parent.to_string_lossy().into_owned()
351 };
352 object.insert("venvPath".to_string(), Value::String(venv_path));
353 }
354
355 if let Some(venv_name) = venv_dir.file_name() {
356 object.insert(
357 "venv".to_owned(),
358 Value::String(venv_name.to_string_lossy().into_owned()),
359 );
360 }
361 }
362
363 // Always set the python interpreter path
364 // Get or create the python section
365 let python = object
366 .entry("python")
367 .or_insert(Value::Object(serde_json::Map::default()))
368 .as_object_mut()
369 .unwrap();
370
371 // Set both pythonPath and defaultInterpreterPath for compatibility
372 python.insert(
373 "pythonPath".to_owned(),
374 Value::String(interpreter_path.clone()),
375 );
376 python.insert(
377 "defaultInterpreterPath".to_owned(),
378 Value::String(interpreter_path),
379 );
380 }
381
382 user_settings
383 })
384 }
385}
386
387async fn get_cached_server_binary(
388 container_dir: PathBuf,
389 node: &NodeRuntime,
390) -> Option<LanguageServerBinary> {
391 let server_path = container_dir.join(SERVER_PATH);
392 if server_path.exists() {
393 Some(LanguageServerBinary {
394 path: node.binary_path().await.log_err()?,
395 env: None,
396 arguments: server_binary_arguments(&server_path),
397 })
398 } else {
399 log::error!("missing executable in directory {:?}", server_path);
400 None
401 }
402}
403
404pub(crate) struct PythonContextProvider;
405
406const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
407 VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET"));
408
409const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
410 VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
411
412const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName =
413 VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW"));
414
415const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
416 VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
417
418impl ContextProvider for PythonContextProvider {
419 fn build_context(
420 &self,
421 variables: &task::TaskVariables,
422 location: ContextLocation<'_>,
423 _: Option<HashMap<String, String>>,
424 toolchains: Arc<dyn LanguageToolchainStore>,
425 cx: &mut gpui::App,
426 ) -> Task<Result<task::TaskVariables>> {
427 let test_target =
428 match selected_test_runner(location.file_location.buffer.read(cx).file(), cx) {
429 TestRunner::UNITTEST => self.build_unittest_target(variables),
430 TestRunner::PYTEST => self.build_pytest_target(variables),
431 };
432
433 let module_target = self.build_module_target(variables);
434 let location_file = location.file_location.buffer.read(cx).file().cloned();
435 let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx));
436
437 cx.spawn(async move |cx| {
438 let raw_toolchain = if let Some(worktree_id) = worktree_id {
439 let file_path = location_file
440 .as_ref()
441 .and_then(|f| f.path().parent())
442 .map(Arc::from)
443 .unwrap_or_else(|| Arc::from("".as_ref()));
444
445 toolchains
446 .active_toolchain(worktree_id, file_path, "Python".into(), cx)
447 .await
448 .map_or_else(
449 || String::from("python3"),
450 |toolchain| toolchain.path.to_string(),
451 )
452 } else {
453 String::from("python3")
454 };
455
456 let active_toolchain = format!("\"{raw_toolchain}\"");
457 let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
458 let raw_toolchain_var = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
459
460 Ok(task::TaskVariables::from_iter(
461 test_target
462 .into_iter()
463 .chain(module_target.into_iter())
464 .chain([toolchain, raw_toolchain_var]),
465 ))
466 })
467 }
468
469 fn associated_tasks(
470 &self,
471 _: Arc<dyn Fs>,
472 file: Option<Arc<dyn language::File>>,
473 cx: &App,
474 ) -> Task<Option<TaskTemplates>> {
475 let test_runner = selected_test_runner(file.as_ref(), cx);
476
477 let mut tasks = vec![
478 // Execute a selection
479 TaskTemplate {
480 label: "execute selection".to_owned(),
481 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
482 args: vec![
483 "-c".to_owned(),
484 VariableName::SelectedText.template_value_with_whitespace(),
485 ],
486 cwd: Some("$ZED_WORKTREE_ROOT".into()),
487 ..TaskTemplate::default()
488 },
489 // Execute an entire file
490 TaskTemplate {
491 label: format!("run '{}'", VariableName::File.template_value()),
492 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
493 args: vec![VariableName::File.template_value_with_whitespace()],
494 cwd: Some("$ZED_WORKTREE_ROOT".into()),
495 ..TaskTemplate::default()
496 },
497 // Execute a file as module
498 TaskTemplate {
499 label: format!("run module '{}'", VariableName::File.template_value()),
500 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
501 args: vec![
502 "-m".to_owned(),
503 PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
504 ],
505 cwd: Some("$ZED_WORKTREE_ROOT".into()),
506 tags: vec!["python-module-main-method".to_owned()],
507 ..TaskTemplate::default()
508 },
509 ];
510
511 tasks.extend(match test_runner {
512 TestRunner::UNITTEST => {
513 [
514 // Run tests for an entire file
515 TaskTemplate {
516 label: format!("unittest '{}'", VariableName::File.template_value()),
517 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
518 args: vec![
519 "-m".to_owned(),
520 "unittest".to_owned(),
521 VariableName::File.template_value_with_whitespace(),
522 ],
523 cwd: Some("$ZED_WORKTREE_ROOT".into()),
524 ..TaskTemplate::default()
525 },
526 // Run test(s) for a specific target within a file
527 TaskTemplate {
528 label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
529 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
530 args: vec![
531 "-m".to_owned(),
532 "unittest".to_owned(),
533 PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
534 ],
535 tags: vec![
536 "python-unittest-class".to_owned(),
537 "python-unittest-method".to_owned(),
538 ],
539 cwd: Some("$ZED_WORKTREE_ROOT".into()),
540 ..TaskTemplate::default()
541 },
542 ]
543 }
544 TestRunner::PYTEST => {
545 [
546 // Run tests for an entire file
547 TaskTemplate {
548 label: format!("pytest '{}'", VariableName::File.template_value()),
549 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
550 args: vec![
551 "-m".to_owned(),
552 "pytest".to_owned(),
553 VariableName::File.template_value_with_whitespace(),
554 ],
555 cwd: Some("$ZED_WORKTREE_ROOT".into()),
556 ..TaskTemplate::default()
557 },
558 // Run test(s) for a specific target within a file
559 TaskTemplate {
560 label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
561 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
562 args: vec![
563 "-m".to_owned(),
564 "pytest".to_owned(),
565 PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
566 ],
567 cwd: Some("$ZED_WORKTREE_ROOT".into()),
568 tags: vec![
569 "python-pytest-class".to_owned(),
570 "python-pytest-method".to_owned(),
571 ],
572 ..TaskTemplate::default()
573 },
574 ]
575 }
576 });
577
578 Task::ready(Some(TaskTemplates(tasks)))
579 }
580}
581
582fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner {
583 const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER";
584 language_settings(Some(LanguageName::new("Python")), location, cx)
585 .tasks
586 .variables
587 .get(TEST_RUNNER_VARIABLE)
588 .and_then(|val| TestRunner::from_str(val).ok())
589 .unwrap_or(TestRunner::PYTEST)
590}
591
592impl PythonContextProvider {
593 fn build_unittest_target(
594 &self,
595 variables: &task::TaskVariables,
596 ) -> Option<(VariableName, String)> {
597 let python_module_name =
598 python_module_name_from_relative_path(variables.get(&VariableName::RelativeFile)?);
599
600 let unittest_class_name =
601 variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
602
603 let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
604 "_unittest_method_name",
605 )));
606
607 let unittest_target_str = match (unittest_class_name, unittest_method_name) {
608 (Some(class_name), Some(method_name)) => {
609 format!("{python_module_name}.{class_name}.{method_name}")
610 }
611 (Some(class_name), None) => format!("{python_module_name}.{class_name}"),
612 (None, None) => python_module_name,
613 // should never happen, a TestCase class is the unit of testing
614 (None, Some(_)) => return None,
615 };
616
617 Some((
618 PYTHON_TEST_TARGET_TASK_VARIABLE.clone(),
619 unittest_target_str,
620 ))
621 }
622
623 fn build_pytest_target(
624 &self,
625 variables: &task::TaskVariables,
626 ) -> Option<(VariableName, String)> {
627 let file_path = variables.get(&VariableName::RelativeFile)?;
628
629 let pytest_class_name =
630 variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name")));
631
632 let pytest_method_name =
633 variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name")));
634
635 let pytest_target_str = match (pytest_class_name, pytest_method_name) {
636 (Some(class_name), Some(method_name)) => {
637 format!("{file_path}::{class_name}::{method_name}")
638 }
639 (Some(class_name), None) => {
640 format!("{file_path}::{class_name}")
641 }
642 (None, Some(method_name)) => {
643 format!("{file_path}::{method_name}")
644 }
645 (None, None) => file_path.to_string(),
646 };
647
648 Some((PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str))
649 }
650
651 fn build_module_target(
652 &self,
653 variables: &task::TaskVariables,
654 ) -> Result<(VariableName, String)> {
655 let python_module_name = python_module_name_from_relative_path(
656 variables.get(&VariableName::RelativeFile).unwrap_or(""),
657 );
658
659 let module_target = (PYTHON_MODULE_NAME_TASK_VARIABLE.clone(), python_module_name);
660
661 Ok(module_target)
662 }
663}
664
665fn python_module_name_from_relative_path(relative_path: &str) -> String {
666 let path_with_dots = relative_path.replace('/', ".");
667 path_with_dots
668 .strip_suffix(".py")
669 .unwrap_or(&path_with_dots)
670 .to_string()
671}
672
673fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
674 match k {
675 PythonEnvironmentKind::Conda => "Conda",
676 PythonEnvironmentKind::Pixi => "pixi",
677 PythonEnvironmentKind::Homebrew => "Homebrew",
678 PythonEnvironmentKind::Pyenv => "global (Pyenv)",
679 PythonEnvironmentKind::GlobalPaths => "global",
680 PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
681 PythonEnvironmentKind::Pipenv => "Pipenv",
682 PythonEnvironmentKind::Poetry => "Poetry",
683 PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
684 PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
685 PythonEnvironmentKind::LinuxGlobal => "global",
686 PythonEnvironmentKind::MacXCode => "global (Xcode)",
687 PythonEnvironmentKind::Venv => "venv",
688 PythonEnvironmentKind::VirtualEnv => "virtualenv",
689 PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
690 PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
691 PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
692 }
693}
694
695pub(crate) struct PythonToolchainProvider {
696 term: SharedString,
697}
698
699impl Default for PythonToolchainProvider {
700 fn default() -> Self {
701 Self {
702 term: SharedString::new_static("Virtual Environment"),
703 }
704 }
705}
706
707static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
708 // Prioritize non-Conda environments.
709 PythonEnvironmentKind::Poetry,
710 PythonEnvironmentKind::Pipenv,
711 PythonEnvironmentKind::VirtualEnvWrapper,
712 PythonEnvironmentKind::Venv,
713 PythonEnvironmentKind::VirtualEnv,
714 PythonEnvironmentKind::PyenvVirtualEnv,
715 PythonEnvironmentKind::Pixi,
716 PythonEnvironmentKind::Conda,
717 PythonEnvironmentKind::Pyenv,
718 PythonEnvironmentKind::GlobalPaths,
719 PythonEnvironmentKind::Homebrew,
720];
721
722fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
723 if let Some(kind) = kind {
724 ENV_PRIORITY_LIST
725 .iter()
726 .position(|blessed_env| blessed_env == &kind)
727 .unwrap_or(ENV_PRIORITY_LIST.len())
728 } else {
729 // Unknown toolchains are less useful than non-blessed ones.
730 ENV_PRIORITY_LIST.len() + 1
731 }
732}
733
734/// Return the name of environment declared in <worktree-root/.venv.
735///
736/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
737async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
738 let file = async_fs::File::open(worktree_root.join(".venv"))
739 .await
740 .ok()?;
741 let mut venv_name = String::new();
742 smol::io::BufReader::new(file)
743 .read_line(&mut venv_name)
744 .await
745 .ok()?;
746 Some(venv_name.trim().to_string())
747}
748
749#[async_trait]
750impl ToolchainLister for PythonToolchainProvider {
751 fn manifest_name(&self) -> language::ManifestName {
752 ManifestName::from(SharedString::new_static("pyproject.toml"))
753 }
754 async fn list(
755 &self,
756 worktree_root: PathBuf,
757 subroot_relative_path: Arc<Path>,
758 project_env: Option<HashMap<String, String>>,
759 ) -> ToolchainList {
760 let env = project_env.unwrap_or_default();
761 let environment = EnvironmentApi::from_env(&env);
762 let locators = pet::locators::create_locators(
763 Arc::new(pet_conda::Conda::from(&environment)),
764 Arc::new(pet_poetry::Poetry::from(&environment)),
765 &environment,
766 );
767 let mut config = Configuration::default();
768
769 debug_assert!(subroot_relative_path.is_relative());
770 // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
771 // worktree root as the workspace directory.
772 config.workspace_directories = Some(
773 subroot_relative_path
774 .ancestors()
775 .map(|ancestor| worktree_root.join(ancestor))
776 .collect(),
777 );
778 for locator in locators.iter() {
779 locator.configure(&config);
780 }
781
782 let reporter = pet_reporter::collect::create_reporter();
783 pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
784
785 let mut toolchains = reporter
786 .environments
787 .lock()
788 .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
789
790 let wr = worktree_root;
791 let wr_venv = get_worktree_venv_declaration(&wr).await;
792 // Sort detected environments by:
793 // environment name matching activation file (<workdir>/.venv)
794 // environment project dir matching worktree_root
795 // general env priority
796 // environment path matching the CONDA_PREFIX env var
797 // executable path
798 toolchains.sort_by(|lhs, rhs| {
799 // Compare venv names against worktree .venv file
800 let venv_ordering =
801 wr_venv
802 .as_ref()
803 .map_or(Ordering::Equal, |venv| match (&lhs.name, &rhs.name) {
804 (Some(l), Some(r)) => (r == venv).cmp(&(l == venv)),
805 (Some(l), None) if l == venv => Ordering::Less,
806 (None, Some(r)) if r == venv => Ordering::Greater,
807 _ => Ordering::Equal,
808 });
809
810 // Compare project paths against worktree root
811 let proj_ordering = || match (&lhs.project, &rhs.project) {
812 (Some(l), Some(r)) => (r == &wr).cmp(&(l == &wr)),
813 (Some(l), None) if l == &wr => Ordering::Less,
814 (None, Some(r)) if r == &wr => Ordering::Greater,
815 _ => Ordering::Equal,
816 };
817
818 // Compare environment priorities
819 let priority_ordering = || env_priority(lhs.kind).cmp(&env_priority(rhs.kind));
820
821 // Compare conda prefixes
822 let conda_ordering = || {
823 if lhs.kind == Some(PythonEnvironmentKind::Conda) {
824 environment
825 .get_env_var("CONDA_PREFIX".to_string())
826 .map(|conda_prefix| {
827 let is_match = |exe: &Option<PathBuf>| {
828 exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
829 };
830 match (is_match(&lhs.executable), is_match(&rhs.executable)) {
831 (true, false) => Ordering::Less,
832 (false, true) => Ordering::Greater,
833 _ => Ordering::Equal,
834 }
835 })
836 .unwrap_or(Ordering::Equal)
837 } else {
838 Ordering::Equal
839 }
840 };
841
842 // Compare Python executables
843 let exe_ordering = || lhs.executable.cmp(&rhs.executable);
844
845 venv_ordering
846 .then_with(proj_ordering)
847 .then_with(priority_ordering)
848 .then_with(conda_ordering)
849 .then_with(exe_ordering)
850 });
851
852 let mut toolchains: Vec<_> = toolchains
853 .into_iter()
854 .filter_map(|toolchain| {
855 let mut name = String::from("Python");
856 if let Some(version) = &toolchain.version {
857 _ = write!(name, " {version}");
858 }
859
860 let name_and_kind = match (&toolchain.name, &toolchain.kind) {
861 (Some(name), Some(kind)) => {
862 Some(format!("({name}; {})", python_env_kind_display(kind)))
863 }
864 (Some(name), None) => Some(format!("({name})")),
865 (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
866 (None, None) => None,
867 };
868
869 if let Some(nk) = name_and_kind {
870 _ = write!(name, " {nk}");
871 }
872
873 Some(Toolchain {
874 name: name.into(),
875 path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
876 language_name: LanguageName::new("Python"),
877 as_json: serde_json::to_value(toolchain.clone()).ok()?,
878 })
879 })
880 .collect();
881 toolchains.dedup();
882 ToolchainList {
883 toolchains,
884 default: None,
885 groups: Default::default(),
886 }
887 }
888 fn term(&self) -> SharedString {
889 self.term.clone()
890 }
891 async fn activation_script(
892 &self,
893 toolchain: &Toolchain,
894 shell: ShellKind,
895 fs: &dyn Fs,
896 ) -> Vec<String> {
897 let Ok(toolchain) = serde_json::from_value::<pet_core::python_environment::PythonEnvironment>(
898 toolchain.as_json.clone(),
899 ) else {
900 return vec![];
901 };
902 let mut activation_script = vec![];
903
904 match toolchain.kind {
905 Some(PythonEnvironmentKind::Pixi) => {
906 let env = toolchain.name.as_deref().unwrap_or("default");
907 activation_script.push(format!("pixi shell -e {env}"))
908 }
909 Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
910 if let Some(prefix) = &toolchain.prefix {
911 let activate_keyword = match shell {
912 ShellKind::Cmd => ".",
913 ShellKind::Nushell => "overlay use",
914 ShellKind::Powershell => ".",
915 ShellKind::Fish => "source",
916 ShellKind::Csh => "source",
917 ShellKind::Posix => "source",
918 };
919 let activate_script_name = match shell {
920 ShellKind::Posix => "activate",
921 ShellKind::Csh => "activate.csh",
922 ShellKind::Fish => "activate.fish",
923 ShellKind::Nushell => "activate.nu",
924 ShellKind::Powershell => "activate.ps1",
925 ShellKind::Cmd => "activate.bat",
926 };
927 let path = prefix.join(BINARY_DIR).join(activate_script_name);
928 if fs.is_file(&path).await {
929 activation_script
930 .push(format!("{activate_keyword} \"{}\"", path.display()));
931 }
932 }
933 }
934 Some(PythonEnvironmentKind::Pyenv) => {
935 let Some(manager) = toolchain.manager else {
936 return vec![];
937 };
938 let version = toolchain.version.as_deref().unwrap_or("system");
939 let pyenv = manager.executable;
940 let pyenv = pyenv.display();
941 activation_script.extend(match shell {
942 ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
943 ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
944 ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")),
945 ShellKind::Powershell => None,
946 ShellKind::Csh => None,
947 ShellKind::Cmd => None,
948 })
949 }
950 _ => {}
951 }
952 activation_script
953 }
954}
955
956pub struct EnvironmentApi<'a> {
957 global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
958 project_env: &'a HashMap<String, String>,
959 pet_env: pet_core::os_environment::EnvironmentApi,
960}
961
962impl<'a> EnvironmentApi<'a> {
963 pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
964 let paths = project_env
965 .get("PATH")
966 .map(|p| std::env::split_paths(p).collect())
967 .unwrap_or_default();
968
969 EnvironmentApi {
970 global_search_locations: Arc::new(Mutex::new(paths)),
971 project_env,
972 pet_env: pet_core::os_environment::EnvironmentApi::new(),
973 }
974 }
975
976 fn user_home(&self) -> Option<PathBuf> {
977 self.project_env
978 .get("HOME")
979 .or_else(|| self.project_env.get("USERPROFILE"))
980 .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
981 .or_else(|| self.pet_env.get_user_home())
982 }
983}
984
985impl pet_core::os_environment::Environment for EnvironmentApi<'_> {
986 fn get_user_home(&self) -> Option<PathBuf> {
987 self.user_home()
988 }
989
990 fn get_root(&self) -> Option<PathBuf> {
991 None
992 }
993
994 fn get_env_var(&self, key: String) -> Option<String> {
995 self.project_env
996 .get(&key)
997 .cloned()
998 .or_else(|| self.pet_env.get_env_var(key))
999 }
1000
1001 fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
1002 if self.global_search_locations.lock().is_empty() {
1003 let mut paths =
1004 std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
1005 .collect::<Vec<PathBuf>>();
1006
1007 log::trace!("Env PATH: {:?}", paths);
1008 for p in self.pet_env.get_know_global_search_locations() {
1009 if !paths.contains(&p) {
1010 paths.push(p);
1011 }
1012 }
1013
1014 let mut paths = paths
1015 .into_iter()
1016 .filter(|p| p.exists())
1017 .collect::<Vec<PathBuf>>();
1018
1019 self.global_search_locations.lock().append(&mut paths);
1020 }
1021 self.global_search_locations.lock().clone()
1022 }
1023}
1024
1025pub(crate) struct PyLspAdapter {
1026 python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1027}
1028impl PyLspAdapter {
1029 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
1030 pub(crate) fn new() -> Self {
1031 Self {
1032 python_venv_base: OnceCell::new(),
1033 }
1034 }
1035 async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1036 let python_path = Self::find_base_python(delegate)
1037 .await
1038 .context("Could not find Python installation for PyLSP")?;
1039 let work_dir = delegate
1040 .language_server_download_dir(&Self::SERVER_NAME)
1041 .await
1042 .context("Could not get working directory for PyLSP")?;
1043 let mut path = PathBuf::from(work_dir.as_ref());
1044 path.push("pylsp-venv");
1045 if !path.exists() {
1046 util::command::new_smol_command(python_path)
1047 .arg("-m")
1048 .arg("venv")
1049 .arg("pylsp-venv")
1050 .current_dir(work_dir)
1051 .spawn()?
1052 .output()
1053 .await?;
1054 }
1055
1056 Ok(path.into())
1057 }
1058 // Find "baseline", user python version from which we'll create our own venv.
1059 async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1060 for path in ["python3", "python"] {
1061 if let Some(path) = delegate.which(path.as_ref()).await {
1062 return Some(path);
1063 }
1064 }
1065 None
1066 }
1067
1068 async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1069 self.python_venv_base
1070 .get_or_init(move || async move {
1071 Self::ensure_venv(delegate)
1072 .await
1073 .map_err(|e| format!("{e}"))
1074 })
1075 .await
1076 .clone()
1077 }
1078}
1079
1080const BINARY_DIR: &str = if cfg!(target_os = "windows") {
1081 "Scripts"
1082} else {
1083 "bin"
1084};
1085
1086#[async_trait(?Send)]
1087impl LspAdapter for PyLspAdapter {
1088 fn name(&self) -> LanguageServerName {
1089 Self::SERVER_NAME
1090 }
1091
1092 async fn check_if_user_installed(
1093 &self,
1094 delegate: &dyn LspAdapterDelegate,
1095 toolchain: Option<Toolchain>,
1096 _: &AsyncApp,
1097 ) -> Option<LanguageServerBinary> {
1098 if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
1099 let env = delegate.shell_env().await;
1100 Some(LanguageServerBinary {
1101 path: pylsp_bin,
1102 env: Some(env),
1103 arguments: vec![],
1104 })
1105 } else {
1106 let toolchain = toolchain?;
1107 let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp");
1108 pylsp_path.exists().then(|| LanguageServerBinary {
1109 path: toolchain.path.to_string().into(),
1110 arguments: vec![pylsp_path.into()],
1111 env: None,
1112 })
1113 }
1114 }
1115
1116 async fn fetch_latest_server_version(
1117 &self,
1118 _: &dyn LspAdapterDelegate,
1119 ) -> Result<Box<dyn 'static + Any + Send>> {
1120 Ok(Box::new(()) as Box<_>)
1121 }
1122
1123 async fn fetch_server_binary(
1124 &self,
1125 _: Box<dyn 'static + Send + Any>,
1126 _: PathBuf,
1127 delegate: &dyn LspAdapterDelegate,
1128 ) -> Result<LanguageServerBinary> {
1129 let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1130 let pip_path = venv.join(BINARY_DIR).join("pip3");
1131 ensure!(
1132 util::command::new_smol_command(pip_path.as_path())
1133 .arg("install")
1134 .arg("python-lsp-server")
1135 .arg("-U")
1136 .output()
1137 .await?
1138 .status
1139 .success(),
1140 "python-lsp-server installation failed"
1141 );
1142 ensure!(
1143 util::command::new_smol_command(pip_path.as_path())
1144 .arg("install")
1145 .arg("python-lsp-server[all]")
1146 .arg("-U")
1147 .output()
1148 .await?
1149 .status
1150 .success(),
1151 "python-lsp-server[all] installation failed"
1152 );
1153 ensure!(
1154 util::command::new_smol_command(pip_path)
1155 .arg("install")
1156 .arg("pylsp-mypy")
1157 .arg("-U")
1158 .output()
1159 .await?
1160 .status
1161 .success(),
1162 "pylsp-mypy installation failed"
1163 );
1164 let pylsp = venv.join(BINARY_DIR).join("pylsp");
1165 Ok(LanguageServerBinary {
1166 path: pylsp,
1167 env: None,
1168 arguments: vec![],
1169 })
1170 }
1171
1172 async fn cached_server_binary(
1173 &self,
1174 _: PathBuf,
1175 delegate: &dyn LspAdapterDelegate,
1176 ) -> Option<LanguageServerBinary> {
1177 let venv = self.base_venv(delegate).await.ok()?;
1178 let pylsp = venv.join(BINARY_DIR).join("pylsp");
1179 Some(LanguageServerBinary {
1180 path: pylsp,
1181 env: None,
1182 arguments: vec![],
1183 })
1184 }
1185
1186 async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {}
1187
1188 async fn label_for_completion(
1189 &self,
1190 item: &lsp::CompletionItem,
1191 language: &Arc<language::Language>,
1192 ) -> Option<language::CodeLabel> {
1193 let label = &item.label;
1194 let grammar = language.grammar()?;
1195 let highlight_id = match item.kind? {
1196 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
1197 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
1198 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
1199 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
1200 _ => return None,
1201 };
1202 let filter_range = item
1203 .filter_text
1204 .as_deref()
1205 .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
1206 .unwrap_or(0..label.len());
1207 Some(language::CodeLabel {
1208 text: label.clone(),
1209 runs: vec![(0..label.len(), highlight_id)],
1210 filter_range,
1211 })
1212 }
1213
1214 async fn label_for_symbol(
1215 &self,
1216 name: &str,
1217 kind: lsp::SymbolKind,
1218 language: &Arc<language::Language>,
1219 ) -> Option<language::CodeLabel> {
1220 let (text, filter_range, display_range) = match kind {
1221 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1222 let text = format!("def {}():\n", name);
1223 let filter_range = 4..4 + name.len();
1224 let display_range = 0..filter_range.end;
1225 (text, filter_range, display_range)
1226 }
1227 lsp::SymbolKind::CLASS => {
1228 let text = format!("class {}:", name);
1229 let filter_range = 6..6 + name.len();
1230 let display_range = 0..filter_range.end;
1231 (text, filter_range, display_range)
1232 }
1233 lsp::SymbolKind::CONSTANT => {
1234 let text = format!("{} = 0", name);
1235 let filter_range = 0..name.len();
1236 let display_range = 0..filter_range.end;
1237 (text, filter_range, display_range)
1238 }
1239 _ => return None,
1240 };
1241
1242 Some(language::CodeLabel {
1243 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
1244 text: text[display_range].to_string(),
1245 filter_range,
1246 })
1247 }
1248
1249 async fn workspace_configuration(
1250 self: Arc<Self>,
1251 _: &dyn Fs,
1252 adapter: &Arc<dyn LspAdapterDelegate>,
1253 toolchain: Option<Toolchain>,
1254 cx: &mut AsyncApp,
1255 ) -> Result<Value> {
1256 cx.update(move |cx| {
1257 let mut user_settings =
1258 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1259 .and_then(|s| s.settings.clone())
1260 .unwrap_or_else(|| {
1261 json!({
1262 "plugins": {
1263 "pycodestyle": {"enabled": false},
1264 "rope_autoimport": {"enabled": true, "memory": true},
1265 "pylsp_mypy": {"enabled": false}
1266 },
1267 "rope": {
1268 "ropeFolder": null
1269 },
1270 })
1271 });
1272
1273 // If user did not explicitly modify their python venv, use one from picker.
1274 if let Some(toolchain) = toolchain {
1275 if user_settings.is_null() {
1276 user_settings = Value::Object(serde_json::Map::default());
1277 }
1278 let object = user_settings.as_object_mut().unwrap();
1279 if let Some(python) = object
1280 .entry("plugins")
1281 .or_insert(Value::Object(serde_json::Map::default()))
1282 .as_object_mut()
1283 {
1284 if let Some(jedi) = python
1285 .entry("jedi")
1286 .or_insert(Value::Object(serde_json::Map::default()))
1287 .as_object_mut()
1288 {
1289 jedi.entry("environment".to_string())
1290 .or_insert_with(|| Value::String(toolchain.path.clone().into()));
1291 }
1292 if let Some(pylint) = python
1293 .entry("pylsp_mypy")
1294 .or_insert(Value::Object(serde_json::Map::default()))
1295 .as_object_mut()
1296 {
1297 pylint.entry("overrides".to_string()).or_insert_with(|| {
1298 Value::Array(vec![
1299 Value::String("--python-executable".into()),
1300 Value::String(toolchain.path.into()),
1301 Value::String("--cache-dir=/dev/null".into()),
1302 Value::Bool(true),
1303 ])
1304 });
1305 }
1306 }
1307 }
1308 user_settings = Value::Object(serde_json::Map::from_iter([(
1309 "pylsp".to_string(),
1310 user_settings,
1311 )]));
1312
1313 user_settings
1314 })
1315 }
1316}
1317
1318pub(crate) struct BasedPyrightLspAdapter {
1319 python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1320}
1321
1322impl BasedPyrightLspAdapter {
1323 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
1324 const BINARY_NAME: &'static str = "basedpyright-langserver";
1325
1326 pub(crate) fn new() -> Self {
1327 Self {
1328 python_venv_base: OnceCell::new(),
1329 }
1330 }
1331
1332 async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1333 let python_path = Self::find_base_python(delegate)
1334 .await
1335 .context("Could not find Python installation for basedpyright")?;
1336 let work_dir = delegate
1337 .language_server_download_dir(&Self::SERVER_NAME)
1338 .await
1339 .context("Could not get working directory for basedpyright")?;
1340 let mut path = PathBuf::from(work_dir.as_ref());
1341 path.push("basedpyright-venv");
1342 if !path.exists() {
1343 util::command::new_smol_command(python_path)
1344 .arg("-m")
1345 .arg("venv")
1346 .arg("basedpyright-venv")
1347 .current_dir(work_dir)
1348 .spawn()?
1349 .output()
1350 .await?;
1351 }
1352
1353 Ok(path.into())
1354 }
1355
1356 // Find "baseline", user python version from which we'll create our own venv.
1357 async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1358 for path in ["python3", "python"] {
1359 if let Some(path) = delegate.which(path.as_ref()).await {
1360 return Some(path);
1361 }
1362 }
1363 None
1364 }
1365
1366 async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1367 self.python_venv_base
1368 .get_or_init(move || async move {
1369 Self::ensure_venv(delegate)
1370 .await
1371 .map_err(|e| format!("{e}"))
1372 })
1373 .await
1374 .clone()
1375 }
1376}
1377
1378#[async_trait(?Send)]
1379impl LspAdapter for BasedPyrightLspAdapter {
1380 fn name(&self) -> LanguageServerName {
1381 Self::SERVER_NAME
1382 }
1383
1384 async fn initialization_options(
1385 self: Arc<Self>,
1386 _: &dyn Fs,
1387 _: &Arc<dyn LspAdapterDelegate>,
1388 ) -> Result<Option<Value>> {
1389 // Provide minimal initialization options
1390 // Virtual environment configuration will be handled through workspace configuration
1391 Ok(Some(json!({
1392 "python": {
1393 "analysis": {
1394 "autoSearchPaths": true,
1395 "useLibraryCodeForTypes": true,
1396 "autoImportCompletions": true
1397 }
1398 }
1399 })))
1400 }
1401
1402 async fn check_if_user_installed(
1403 &self,
1404 delegate: &dyn LspAdapterDelegate,
1405 toolchain: Option<Toolchain>,
1406 _: &AsyncApp,
1407 ) -> Option<LanguageServerBinary> {
1408 if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await {
1409 let env = delegate.shell_env().await;
1410 Some(LanguageServerBinary {
1411 path: bin,
1412 env: Some(env),
1413 arguments: vec!["--stdio".into()],
1414 })
1415 } else {
1416 let path = Path::new(toolchain?.path.as_ref())
1417 .parent()?
1418 .join(Self::BINARY_NAME);
1419 path.exists().then(|| LanguageServerBinary {
1420 path,
1421 arguments: vec!["--stdio".into()],
1422 env: None,
1423 })
1424 }
1425 }
1426
1427 async fn fetch_latest_server_version(
1428 &self,
1429 _: &dyn LspAdapterDelegate,
1430 ) -> Result<Box<dyn 'static + Any + Send>> {
1431 Ok(Box::new(()) as Box<_>)
1432 }
1433
1434 async fn fetch_server_binary(
1435 &self,
1436 _latest_version: Box<dyn 'static + Send + Any>,
1437 _container_dir: PathBuf,
1438 delegate: &dyn LspAdapterDelegate,
1439 ) -> Result<LanguageServerBinary> {
1440 let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1441 let pip_path = venv.join(BINARY_DIR).join("pip3");
1442 ensure!(
1443 util::command::new_smol_command(pip_path.as_path())
1444 .arg("install")
1445 .arg("basedpyright")
1446 .arg("-U")
1447 .output()
1448 .await?
1449 .status
1450 .success(),
1451 "basedpyright installation failed"
1452 );
1453 let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
1454 Ok(LanguageServerBinary {
1455 path: pylsp,
1456 env: None,
1457 arguments: vec!["--stdio".into()],
1458 })
1459 }
1460
1461 async fn cached_server_binary(
1462 &self,
1463 _container_dir: PathBuf,
1464 delegate: &dyn LspAdapterDelegate,
1465 ) -> Option<LanguageServerBinary> {
1466 let venv = self.base_venv(delegate).await.ok()?;
1467 let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
1468 Some(LanguageServerBinary {
1469 path: pylsp,
1470 env: None,
1471 arguments: vec!["--stdio".into()],
1472 })
1473 }
1474
1475 async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
1476 // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
1477 // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
1478 // and `name` is the symbol name itself.
1479 //
1480 // Because the symbol name is included, there generally are not ties when
1481 // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
1482 // into account. Here, we remove the symbol name from the sortText in order
1483 // to allow our own fuzzy score to be used to break ties.
1484 //
1485 // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
1486 for item in items {
1487 let Some(sort_text) = &mut item.sort_text else {
1488 continue;
1489 };
1490 let mut parts = sort_text.split('.');
1491 let Some(first) = parts.next() else { continue };
1492 let Some(second) = parts.next() else { continue };
1493 let Some(_) = parts.next() else { continue };
1494 sort_text.replace_range(first.len() + second.len() + 1.., "");
1495 }
1496 }
1497
1498 async fn label_for_completion(
1499 &self,
1500 item: &lsp::CompletionItem,
1501 language: &Arc<language::Language>,
1502 ) -> Option<language::CodeLabel> {
1503 let label = &item.label;
1504 let grammar = language.grammar()?;
1505 let highlight_id = match item.kind? {
1506 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
1507 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
1508 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
1509 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
1510 _ => return None,
1511 };
1512 let filter_range = item
1513 .filter_text
1514 .as_deref()
1515 .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
1516 .unwrap_or(0..label.len());
1517 Some(language::CodeLabel {
1518 text: label.clone(),
1519 runs: vec![(0..label.len(), highlight_id)],
1520 filter_range,
1521 })
1522 }
1523
1524 async fn label_for_symbol(
1525 &self,
1526 name: &str,
1527 kind: lsp::SymbolKind,
1528 language: &Arc<language::Language>,
1529 ) -> Option<language::CodeLabel> {
1530 let (text, filter_range, display_range) = match kind {
1531 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1532 let text = format!("def {}():\n", name);
1533 let filter_range = 4..4 + name.len();
1534 let display_range = 0..filter_range.end;
1535 (text, filter_range, display_range)
1536 }
1537 lsp::SymbolKind::CLASS => {
1538 let text = format!("class {}:", name);
1539 let filter_range = 6..6 + name.len();
1540 let display_range = 0..filter_range.end;
1541 (text, filter_range, display_range)
1542 }
1543 lsp::SymbolKind::CONSTANT => {
1544 let text = format!("{} = 0", name);
1545 let filter_range = 0..name.len();
1546 let display_range = 0..filter_range.end;
1547 (text, filter_range, display_range)
1548 }
1549 _ => return None,
1550 };
1551
1552 Some(language::CodeLabel {
1553 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
1554 text: text[display_range].to_string(),
1555 filter_range,
1556 })
1557 }
1558
1559 async fn workspace_configuration(
1560 self: Arc<Self>,
1561 _: &dyn Fs,
1562 adapter: &Arc<dyn LspAdapterDelegate>,
1563 toolchain: Option<Toolchain>,
1564 cx: &mut AsyncApp,
1565 ) -> Result<Value> {
1566 cx.update(move |cx| {
1567 let mut user_settings =
1568 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1569 .and_then(|s| s.settings.clone())
1570 .unwrap_or_default();
1571
1572 // If we have a detected toolchain, configure Pyright to use it
1573 if let Some(toolchain) = toolchain
1574 && let Ok(env) = serde_json::from_value::<
1575 pet_core::python_environment::PythonEnvironment,
1576 >(toolchain.as_json.clone())
1577 {
1578 if user_settings.is_null() {
1579 user_settings = Value::Object(serde_json::Map::default());
1580 }
1581 let object = user_settings.as_object_mut().unwrap();
1582
1583 let interpreter_path = toolchain.path.to_string();
1584 if let Some(venv_dir) = env.prefix {
1585 // Set venvPath and venv at the root level
1586 // This matches the format of a pyrightconfig.json file
1587 if let Some(parent) = venv_dir.parent() {
1588 // Use relative path if the venv is inside the workspace
1589 let venv_path = if parent == adapter.worktree_root_path() {
1590 ".".to_string()
1591 } else {
1592 parent.to_string_lossy().into_owned()
1593 };
1594 object.insert("venvPath".to_string(), Value::String(venv_path));
1595 }
1596
1597 if let Some(venv_name) = venv_dir.file_name() {
1598 object.insert(
1599 "venv".to_owned(),
1600 Value::String(venv_name.to_string_lossy().into_owned()),
1601 );
1602 }
1603 }
1604
1605 // Always set the python interpreter path
1606 // Get or create the python section
1607 let python = object
1608 .entry("python")
1609 .or_insert(Value::Object(serde_json::Map::default()))
1610 .as_object_mut()
1611 .unwrap();
1612
1613 // Set both pythonPath and defaultInterpreterPath for compatibility
1614 python.insert(
1615 "pythonPath".to_owned(),
1616 Value::String(interpreter_path.clone()),
1617 );
1618 python.insert(
1619 "defaultInterpreterPath".to_owned(),
1620 Value::String(interpreter_path),
1621 );
1622 }
1623
1624 user_settings
1625 })
1626 }
1627}
1628
1629#[cfg(test)]
1630mod tests {
1631 use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
1632 use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings};
1633 use settings::SettingsStore;
1634 use std::num::NonZeroU32;
1635
1636 #[gpui::test]
1637 async fn test_python_autoindent(cx: &mut TestAppContext) {
1638 cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
1639 let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
1640 cx.update(|cx| {
1641 let test_settings = SettingsStore::test(cx);
1642 cx.set_global(test_settings);
1643 language::init(cx);
1644 cx.update_global::<SettingsStore, _>(|store, cx| {
1645 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
1646 s.defaults.tab_size = NonZeroU32::new(2);
1647 });
1648 });
1649 });
1650
1651 cx.new(|cx| {
1652 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1653 let append = |buffer: &mut Buffer, text: &str, cx: &mut Context<Buffer>| {
1654 let ix = buffer.len();
1655 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
1656 };
1657
1658 // indent after "def():"
1659 append(&mut buffer, "def a():\n", cx);
1660 assert_eq!(buffer.text(), "def a():\n ");
1661
1662 // preserve indent after blank line
1663 append(&mut buffer, "\n ", cx);
1664 assert_eq!(buffer.text(), "def a():\n \n ");
1665
1666 // indent after "if"
1667 append(&mut buffer, "if a:\n ", cx);
1668 assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
1669
1670 // preserve indent after statement
1671 append(&mut buffer, "b()\n", cx);
1672 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
1673
1674 // preserve indent after statement
1675 append(&mut buffer, "else", cx);
1676 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
1677
1678 // dedent "else""
1679 append(&mut buffer, ":", cx);
1680 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
1681
1682 // indent lines after else
1683 append(&mut buffer, "\n", cx);
1684 assert_eq!(
1685 buffer.text(),
1686 "def a():\n \n if a:\n b()\n else:\n "
1687 );
1688
1689 // indent after an open paren. the closing paren is not indented
1690 // because there is another token before it on the same line.
1691 append(&mut buffer, "foo(\n1)", cx);
1692 assert_eq!(
1693 buffer.text(),
1694 "def a():\n \n if a:\n b()\n else:\n foo(\n 1)"
1695 );
1696
1697 // dedent the closing paren if it is shifted to the beginning of the line
1698 let argument_ix = buffer.text().find('1').unwrap();
1699 buffer.edit(
1700 [(argument_ix..argument_ix + 1, "")],
1701 Some(AutoindentMode::EachLine),
1702 cx,
1703 );
1704 assert_eq!(
1705 buffer.text(),
1706 "def a():\n \n if a:\n b()\n else:\n foo(\n )"
1707 );
1708
1709 // preserve indent after the close paren
1710 append(&mut buffer, "\n", cx);
1711 assert_eq!(
1712 buffer.text(),
1713 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
1714 );
1715
1716 // manually outdent the last line
1717 let end_whitespace_ix = buffer.len() - 4;
1718 buffer.edit(
1719 [(end_whitespace_ix..buffer.len(), "")],
1720 Some(AutoindentMode::EachLine),
1721 cx,
1722 );
1723 assert_eq!(
1724 buffer.text(),
1725 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
1726 );
1727
1728 // preserve the newly reduced indentation on the next newline
1729 append(&mut buffer, "\n", cx);
1730 assert_eq!(
1731 buffer.text(),
1732 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
1733 );
1734
1735 // reset to a simple if statement
1736 buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx);
1737
1738 // dedent "else" on the line after a closing paren
1739 append(&mut buffer, "\n else:\n", cx);
1740 assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n ");
1741
1742 buffer
1743 });
1744 }
1745}