1use anyhow::ensure;
2use anyhow::{anyhow, Result};
3use async_trait::async_trait;
4use collections::HashMap;
5use gpui::{AppContext, Task};
6use gpui::{AsyncAppContext, SharedString};
7use language::language_settings::language_settings;
8use language::LanguageName;
9use language::LanguageToolchainStore;
10use language::Toolchain;
11use language::ToolchainList;
12use language::ToolchainLister;
13use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
14use lsp::LanguageServerBinary;
15use lsp::LanguageServerName;
16use node_runtime::NodeRuntime;
17use pet_core::os_environment::Environment;
18use pet_core::python_environment::PythonEnvironmentKind;
19use pet_core::Configuration;
20use project::lsp_store::language_server_settings;
21use serde_json::{json, Value};
22use smol::lock::OnceCell;
23use std::cmp::Ordering;
24
25use std::str::FromStr;
26use std::sync::Mutex;
27use std::{
28 any::Any,
29 borrow::Cow,
30 ffi::OsString,
31 path::{Path, PathBuf},
32 sync::Arc,
33};
34use task::{TaskTemplate, TaskTemplates, VariableName};
35use util::ResultExt;
36
37const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
38const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
39
40enum TestRunner {
41 UNITTEST,
42 PYTEST,
43}
44
45impl FromStr for TestRunner {
46 type Err = ();
47
48 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
49 match s {
50 "unittest" => Ok(Self::UNITTEST),
51 "pytest" => Ok(Self::PYTEST),
52 _ => Err(()),
53 }
54 }
55}
56
57fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
58 vec![server_path.into(), "--stdio".into()]
59}
60
61pub struct PythonLspAdapter {
62 node: NodeRuntime,
63}
64
65impl PythonLspAdapter {
66 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");
67
68 pub fn new(node: NodeRuntime) -> Self {
69 PythonLspAdapter { node }
70 }
71}
72
73#[async_trait(?Send)]
74impl LspAdapter for PythonLspAdapter {
75 fn name(&self) -> LanguageServerName {
76 Self::SERVER_NAME.clone()
77 }
78
79 async fn check_if_user_installed(
80 &self,
81 delegate: &dyn LspAdapterDelegate,
82 _: Arc<dyn LanguageToolchainStore>,
83 _: &AsyncAppContext,
84 ) -> Option<LanguageServerBinary> {
85 let node = delegate.which("node".as_ref()).await?;
86 let (node_modules_path, _) = delegate
87 .npm_package_installed_version(Self::SERVER_NAME.as_ref())
88 .await
89 .log_err()??;
90
91 let path = node_modules_path.join(NODE_MODULE_RELATIVE_SERVER_PATH);
92
93 Some(LanguageServerBinary {
94 path: node,
95 env: None,
96 arguments: server_binary_arguments(&path),
97 })
98 }
99
100 async fn fetch_latest_server_version(
101 &self,
102 _: &dyn LspAdapterDelegate,
103 ) -> Result<Box<dyn 'static + Any + Send>> {
104 Ok(Box::new(
105 self.node
106 .npm_package_latest_version(Self::SERVER_NAME.as_ref())
107 .await?,
108 ) as Box<_>)
109 }
110
111 async fn fetch_server_binary(
112 &self,
113 latest_version: Box<dyn 'static + Send + Any>,
114 container_dir: PathBuf,
115 _: &dyn LspAdapterDelegate,
116 ) -> Result<LanguageServerBinary> {
117 let latest_version = latest_version.downcast::<String>().unwrap();
118 let server_path = container_dir.join(SERVER_PATH);
119
120 let should_install_language_server = self
121 .node
122 .should_install_npm_package(
123 Self::SERVER_NAME.as_ref(),
124 &server_path,
125 &container_dir,
126 &latest_version,
127 )
128 .await;
129
130 if should_install_language_server {
131 self.node
132 .npm_install_packages(
133 &container_dir,
134 &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
135 )
136 .await?;
137 }
138
139 Ok(LanguageServerBinary {
140 path: self.node.binary_path().await?,
141 env: None,
142 arguments: server_binary_arguments(&server_path),
143 })
144 }
145
146 async fn cached_server_binary(
147 &self,
148 container_dir: PathBuf,
149 _: &dyn LspAdapterDelegate,
150 ) -> Option<LanguageServerBinary> {
151 get_cached_server_binary(container_dir, &self.node).await
152 }
153
154 async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
155 // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
156 // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
157 // and `name` is the symbol name itself.
158 //
159 // Because the symbol name is included, there generally are not ties when
160 // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
161 // into account. Here, we remove the symbol name from the sortText in order
162 // to allow our own fuzzy score to be used to break ties.
163 //
164 // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
165 for item in items {
166 let Some(sort_text) = &mut item.sort_text else {
167 continue;
168 };
169 let mut parts = sort_text.split('.');
170 let Some(first) = parts.next() else { continue };
171 let Some(second) = parts.next() else { continue };
172 let Some(_) = parts.next() else { continue };
173 sort_text.replace_range(first.len() + second.len() + 1.., "");
174 }
175 }
176
177 async fn label_for_completion(
178 &self,
179 item: &lsp::CompletionItem,
180 language: &Arc<language::Language>,
181 ) -> Option<language::CodeLabel> {
182 let label = &item.label;
183 let grammar = language.grammar()?;
184 let highlight_id = match item.kind? {
185 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
186 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
187 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
188 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
189 _ => return None,
190 };
191 Some(language::CodeLabel {
192 text: label.clone(),
193 runs: vec![(0..label.len(), highlight_id)],
194 filter_range: 0..label.len(),
195 })
196 }
197
198 async fn label_for_symbol(
199 &self,
200 name: &str,
201 kind: lsp::SymbolKind,
202 language: &Arc<language::Language>,
203 ) -> Option<language::CodeLabel> {
204 let (text, filter_range, display_range) = match kind {
205 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
206 let text = format!("def {}():\n", name);
207 let filter_range = 4..4 + name.len();
208 let display_range = 0..filter_range.end;
209 (text, filter_range, display_range)
210 }
211 lsp::SymbolKind::CLASS => {
212 let text = format!("class {}:", name);
213 let filter_range = 6..6 + name.len();
214 let display_range = 0..filter_range.end;
215 (text, filter_range, display_range)
216 }
217 lsp::SymbolKind::CONSTANT => {
218 let text = format!("{} = 0", name);
219 let filter_range = 0..name.len();
220 let display_range = 0..filter_range.end;
221 (text, filter_range, display_range)
222 }
223 _ => return None,
224 };
225
226 Some(language::CodeLabel {
227 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
228 text: text[display_range].to_string(),
229 filter_range,
230 })
231 }
232
233 async fn workspace_configuration(
234 self: Arc<Self>,
235 adapter: &Arc<dyn LspAdapterDelegate>,
236 toolchains: Arc<dyn LanguageToolchainStore>,
237 cx: &mut AsyncAppContext,
238 ) -> Result<Value> {
239 let toolchain = toolchains
240 .active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx)
241 .await;
242 cx.update(move |cx| {
243 let mut user_settings =
244 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
245 .and_then(|s| s.settings.clone())
246 .unwrap_or_default();
247
248 // If python.pythonPath is not set in user config, do so using our toolchain picker.
249 if let Some(toolchain) = toolchain {
250 if user_settings.is_null() {
251 user_settings = Value::Object(serde_json::Map::default());
252 }
253 let object = user_settings.as_object_mut().unwrap();
254 if let Some(python) = object
255 .entry("python")
256 .or_insert(Value::Object(serde_json::Map::default()))
257 .as_object_mut()
258 {
259 python
260 .entry("pythonPath")
261 .or_insert(Value::String(toolchain.path.into()));
262 }
263 }
264 user_settings
265 })
266 }
267}
268
269async fn get_cached_server_binary(
270 container_dir: PathBuf,
271 node: &NodeRuntime,
272) -> Option<LanguageServerBinary> {
273 let server_path = container_dir.join(SERVER_PATH);
274 if server_path.exists() {
275 Some(LanguageServerBinary {
276 path: node.binary_path().await.log_err()?,
277 env: None,
278 arguments: server_binary_arguments(&server_path),
279 })
280 } else {
281 log::error!("missing executable in directory {:?}", server_path);
282 None
283 }
284}
285
286pub(crate) struct PythonContextProvider;
287
288const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
289 VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET"));
290
291const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
292 VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
293impl ContextProvider for PythonContextProvider {
294 fn build_context(
295 &self,
296 variables: &task::TaskVariables,
297 location: &project::Location,
298 _: Option<HashMap<String, String>>,
299 toolchains: Arc<dyn LanguageToolchainStore>,
300 cx: &mut gpui::AppContext,
301 ) -> Task<Result<task::TaskVariables>> {
302 let test_target = {
303 let test_runner = selected_test_runner(location.buffer.read(cx).file(), cx);
304
305 let runner = match test_runner {
306 TestRunner::UNITTEST => self.build_unittest_target(variables),
307 TestRunner::PYTEST => self.build_pytest_target(variables),
308 };
309 runner
310 };
311
312 let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
313 cx.spawn(move |mut cx| async move {
314 let active_toolchain = if let Some(worktree_id) = worktree_id {
315 toolchains
316 .active_toolchain(worktree_id, "Python".into(), &mut cx)
317 .await
318 .map_or_else(
319 || "python3".to_owned(),
320 |toolchain| format!("\"{}\"", toolchain.path),
321 )
322 } else {
323 String::from("python3")
324 };
325 let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
326 Ok(task::TaskVariables::from_iter([test_target?, toolchain]))
327 })
328 }
329
330 fn associated_tasks(
331 &self,
332 file: Option<Arc<dyn language::File>>,
333 cx: &AppContext,
334 ) -> Option<TaskTemplates> {
335 let test_runner = selected_test_runner(file.as_ref(), cx);
336
337 let mut tasks = vec![
338 // Execute a selection
339 TaskTemplate {
340 label: "execute selection".to_owned(),
341 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
342 args: vec![
343 "-c".to_owned(),
344 VariableName::SelectedText.template_value_with_whitespace(),
345 ],
346 ..TaskTemplate::default()
347 },
348 // Execute an entire file
349 TaskTemplate {
350 label: format!("run '{}'", VariableName::File.template_value()),
351 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
352 args: vec![VariableName::File.template_value_with_whitespace()],
353 ..TaskTemplate::default()
354 },
355 ];
356
357 tasks.extend(match test_runner {
358 TestRunner::UNITTEST => {
359 [
360 // Run tests for an entire file
361 TaskTemplate {
362 label: format!("unittest '{}'", VariableName::File.template_value()),
363 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
364 args: vec![
365 "-m".to_owned(),
366 "unittest".to_owned(),
367 VariableName::File.template_value_with_whitespace(),
368 ],
369 ..TaskTemplate::default()
370 },
371 // Run test(s) for a specific target within a file
372 TaskTemplate {
373 label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
374 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
375 args: vec![
376 "-m".to_owned(),
377 "unittest".to_owned(),
378 PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace()
379 ],
380 tags: vec![
381 "python-unittest-class".to_owned(),
382 "python-unittest-method".to_owned(),
383 ],
384 ..TaskTemplate::default()
385 },
386 ]
387 }
388 TestRunner::PYTEST => {
389 [
390 // Run tests for an entire file
391 TaskTemplate {
392 label: format!("pytest '{}'", VariableName::File.template_value()),
393 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
394 args: vec![
395 "-m".to_owned(),
396 "pytest".to_owned(),
397 VariableName::File.template_value_with_whitespace(),
398 ],
399 ..TaskTemplate::default()
400 },
401 // Run test(s) for a specific target within a file
402 TaskTemplate {
403 label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
404 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
405 args: vec![
406 "-m".to_owned(),
407 "pytest".to_owned(),
408 PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
409 ],
410 tags: vec![
411 "python-pytest-class".to_owned(),
412 "python-pytest-method".to_owned(),
413 ],
414 ..TaskTemplate::default()
415 },
416 ]
417 }
418 });
419
420 Some(TaskTemplates(tasks))
421 }
422}
423
424fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &AppContext) -> TestRunner {
425 const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER";
426 language_settings(Some(LanguageName::new("Python")), location, cx)
427 .tasks
428 .variables
429 .get(TEST_RUNNER_VARIABLE)
430 .and_then(|val| TestRunner::from_str(val).ok())
431 .unwrap_or(TestRunner::PYTEST)
432}
433
434impl PythonContextProvider {
435 fn build_unittest_target(
436 &self,
437 variables: &task::TaskVariables,
438 ) -> Result<(VariableName, String)> {
439 let python_module_name = python_module_name_from_relative_path(
440 variables.get(&VariableName::RelativeFile).unwrap_or(""),
441 );
442
443 let unittest_class_name =
444 variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
445
446 let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
447 "_unittest_method_name",
448 )));
449
450 let unittest_target_str = match (unittest_class_name, unittest_method_name) {
451 (Some(class_name), Some(method_name)) => {
452 format!("{}.{}.{}", python_module_name, class_name, method_name)
453 }
454 (Some(class_name), None) => format!("{}.{}", python_module_name, class_name),
455 (None, None) => python_module_name,
456 (None, Some(_)) => return Ok((VariableName::Custom(Cow::Borrowed("")), String::new())), // should never happen, a TestCase class is the unit of testing
457 };
458
459 let unittest_target = (
460 PYTHON_TEST_TARGET_TASK_VARIABLE.clone(),
461 unittest_target_str,
462 );
463
464 Ok(unittest_target)
465 }
466
467 fn build_pytest_target(
468 &self,
469 variables: &task::TaskVariables,
470 ) -> Result<(VariableName, String)> {
471 let file_path = variables
472 .get(&VariableName::RelativeFile)
473 .ok_or_else(|| anyhow!("No file path given"))?;
474
475 let pytest_class_name =
476 variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name")));
477
478 let pytest_method_name =
479 variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name")));
480
481 let pytest_target_str = match (pytest_class_name, pytest_method_name) {
482 (Some(class_name), Some(method_name)) => {
483 format!("{}::{}::{}", file_path, class_name, method_name)
484 }
485 (Some(class_name), None) => {
486 format!("{}::{}", file_path, class_name)
487 }
488 (None, Some(method_name)) => {
489 format!("{}::{}", file_path, method_name)
490 }
491 (None, None) => file_path.to_string(),
492 };
493
494 let pytest_target = (PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str);
495
496 Ok(pytest_target)
497 }
498}
499
500fn python_module_name_from_relative_path(relative_path: &str) -> String {
501 let path_with_dots = relative_path.replace('/', ".");
502 path_with_dots
503 .strip_suffix(".py")
504 .unwrap_or(&path_with_dots)
505 .to_string()
506}
507
508pub(crate) struct PythonToolchainProvider {
509 term: SharedString,
510}
511
512impl Default for PythonToolchainProvider {
513 fn default() -> Self {
514 Self {
515 term: SharedString::new_static("Virtual Environment"),
516 }
517 }
518}
519
520static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[
521 // Prioritize non-Conda environments.
522 PythonEnvironmentKind::Poetry,
523 PythonEnvironmentKind::Pipenv,
524 PythonEnvironmentKind::VirtualEnvWrapper,
525 PythonEnvironmentKind::Venv,
526 PythonEnvironmentKind::VirtualEnv,
527 PythonEnvironmentKind::Conda,
528 PythonEnvironmentKind::Pyenv,
529 PythonEnvironmentKind::GlobalPaths,
530 PythonEnvironmentKind::Homebrew,
531];
532
533fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
534 if let Some(kind) = kind {
535 ENV_PRIORITY_LIST
536 .iter()
537 .position(|blessed_env| blessed_env == &kind)
538 .unwrap_or(ENV_PRIORITY_LIST.len())
539 } else {
540 // Unknown toolchains are less useful than non-blessed ones.
541 ENV_PRIORITY_LIST.len() + 1
542 }
543}
544
545#[async_trait]
546impl ToolchainLister for PythonToolchainProvider {
547 async fn list(
548 &self,
549 worktree_root: PathBuf,
550 project_env: Option<HashMap<String, String>>,
551 ) -> ToolchainList {
552 let env = project_env.unwrap_or_default();
553 let environment = EnvironmentApi::from_env(&env);
554 let locators = pet::locators::create_locators(
555 Arc::new(pet_conda::Conda::from(&environment)),
556 Arc::new(pet_poetry::Poetry::from(&environment)),
557 &environment,
558 );
559 let mut config = Configuration::default();
560 config.workspace_directories = Some(vec![worktree_root]);
561 for locator in locators.iter() {
562 locator.configure(&config);
563 }
564
565 let reporter = pet_reporter::collect::create_reporter();
566 pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
567
568 let mut toolchains = reporter
569 .environments
570 .lock()
571 .ok()
572 .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
573
574 toolchains.sort_by(|lhs, rhs| {
575 env_priority(lhs.kind)
576 .cmp(&env_priority(rhs.kind))
577 .then_with(|| {
578 if lhs.kind == Some(PythonEnvironmentKind::Conda) {
579 environment
580 .get_env_var("CONDA_PREFIX".to_string())
581 .map(|conda_prefix| {
582 let is_match = |exe: &Option<PathBuf>| {
583 exe.as_ref().map_or(false, |e| e.starts_with(&conda_prefix))
584 };
585 match (is_match(&lhs.executable), is_match(&rhs.executable)) {
586 (true, false) => Ordering::Less,
587 (false, true) => Ordering::Greater,
588 _ => Ordering::Equal,
589 }
590 })
591 .unwrap_or(Ordering::Equal)
592 } else {
593 Ordering::Equal
594 }
595 })
596 .then_with(|| lhs.executable.cmp(&rhs.executable))
597 });
598
599 let mut toolchains: Vec<_> = toolchains
600 .into_iter()
601 .filter_map(|toolchain| {
602 let name = if let Some(version) = &toolchain.version {
603 format!("Python {version} ({:?})", toolchain.kind?)
604 } else {
605 format!("{:?}", toolchain.kind?)
606 }
607 .into();
608 Some(Toolchain {
609 name,
610 path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
611 language_name: LanguageName::new("Python"),
612 as_json: serde_json::to_value(toolchain).ok()?,
613 })
614 })
615 .collect();
616 toolchains.dedup();
617 ToolchainList {
618 toolchains,
619 default: None,
620 groups: Default::default(),
621 }
622 }
623 fn term(&self) -> SharedString {
624 self.term.clone()
625 }
626}
627
628pub struct EnvironmentApi<'a> {
629 global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
630 project_env: &'a HashMap<String, String>,
631 pet_env: pet_core::os_environment::EnvironmentApi,
632}
633
634impl<'a> EnvironmentApi<'a> {
635 pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
636 let paths = project_env
637 .get("PATH")
638 .map(|p| std::env::split_paths(p).collect())
639 .unwrap_or_default();
640
641 EnvironmentApi {
642 global_search_locations: Arc::new(Mutex::new(paths)),
643 project_env,
644 pet_env: pet_core::os_environment::EnvironmentApi::new(),
645 }
646 }
647
648 fn user_home(&self) -> Option<PathBuf> {
649 self.project_env
650 .get("HOME")
651 .or_else(|| self.project_env.get("USERPROFILE"))
652 .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
653 .or_else(|| self.pet_env.get_user_home())
654 }
655}
656
657impl<'a> pet_core::os_environment::Environment for EnvironmentApi<'a> {
658 fn get_user_home(&self) -> Option<PathBuf> {
659 self.user_home()
660 }
661
662 fn get_root(&self) -> Option<PathBuf> {
663 None
664 }
665
666 fn get_env_var(&self, key: String) -> Option<String> {
667 self.project_env
668 .get(&key)
669 .cloned()
670 .or_else(|| self.pet_env.get_env_var(key))
671 }
672
673 fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
674 if self.global_search_locations.lock().unwrap().is_empty() {
675 let mut paths =
676 std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
677 .collect::<Vec<PathBuf>>();
678
679 log::trace!("Env PATH: {:?}", paths);
680 for p in self.pet_env.get_know_global_search_locations() {
681 if !paths.contains(&p) {
682 paths.push(p);
683 }
684 }
685
686 let mut paths = paths
687 .into_iter()
688 .filter(|p| p.exists())
689 .collect::<Vec<PathBuf>>();
690
691 self.global_search_locations
692 .lock()
693 .unwrap()
694 .append(&mut paths);
695 }
696 self.global_search_locations.lock().unwrap().clone()
697 }
698}
699
700pub(crate) struct PyLspAdapter {
701 python_venv_base: OnceCell<Result<Arc<Path>, String>>,
702}
703impl PyLspAdapter {
704 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
705 pub(crate) fn new() -> Self {
706 Self {
707 python_venv_base: OnceCell::new(),
708 }
709 }
710 async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
711 let python_path = Self::find_base_python(delegate)
712 .await
713 .ok_or_else(|| anyhow!("Could not find Python installation for PyLSP"))?;
714 let work_dir = delegate
715 .language_server_download_dir(&Self::SERVER_NAME)
716 .await
717 .ok_or_else(|| anyhow!("Could not get working directory for PyLSP"))?;
718 let mut path = PathBuf::from(work_dir.as_ref());
719 path.push("pylsp-venv");
720 if !path.exists() {
721 util::command::new_smol_command(python_path)
722 .arg("-m")
723 .arg("venv")
724 .arg("pylsp-venv")
725 .current_dir(work_dir)
726 .spawn()?
727 .output()
728 .await?;
729 }
730
731 Ok(path.into())
732 }
733 // Find "baseline", user python version from which we'll create our own venv.
734 async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
735 for path in ["python3", "python"] {
736 if let Some(path) = delegate.which(path.as_ref()).await {
737 return Some(path);
738 }
739 }
740 None
741 }
742
743 async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
744 self.python_venv_base
745 .get_or_init(move || async move {
746 Self::ensure_venv(delegate)
747 .await
748 .map_err(|e| format!("{e}"))
749 })
750 .await
751 .clone()
752 }
753}
754
755#[async_trait(?Send)]
756impl LspAdapter for PyLspAdapter {
757 fn name(&self) -> LanguageServerName {
758 Self::SERVER_NAME.clone()
759 }
760
761 async fn check_if_user_installed(
762 &self,
763 delegate: &dyn LspAdapterDelegate,
764 toolchains: Arc<dyn LanguageToolchainStore>,
765 cx: &AsyncAppContext,
766 ) -> Option<LanguageServerBinary> {
767 let venv = toolchains
768 .active_toolchain(
769 delegate.worktree_id(),
770 LanguageName::new("Python"),
771 &mut cx.clone(),
772 )
773 .await?;
774 let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp");
775 pylsp_path.exists().then(|| LanguageServerBinary {
776 path: venv.path.to_string().into(),
777 arguments: vec![pylsp_path.into()],
778 env: None,
779 })
780 }
781
782 async fn fetch_latest_server_version(
783 &self,
784 _: &dyn LspAdapterDelegate,
785 ) -> Result<Box<dyn 'static + Any + Send>> {
786 Ok(Box::new(()) as Box<_>)
787 }
788
789 async fn fetch_server_binary(
790 &self,
791 _: Box<dyn 'static + Send + Any>,
792 _: PathBuf,
793 delegate: &dyn LspAdapterDelegate,
794 ) -> Result<LanguageServerBinary> {
795 let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
796 let pip_path = venv.join("bin").join("pip3");
797 ensure!(
798 util::command::new_smol_command(pip_path.as_path())
799 .arg("install")
800 .arg("python-lsp-server")
801 .output()
802 .await?
803 .status
804 .success(),
805 "python-lsp-server installation failed"
806 );
807 ensure!(
808 util::command::new_smol_command(pip_path.as_path())
809 .arg("install")
810 .arg("python-lsp-server[all]")
811 .output()
812 .await?
813 .status
814 .success(),
815 "python-lsp-server[all] installation failed"
816 );
817 ensure!(
818 util::command::new_smol_command(pip_path)
819 .arg("install")
820 .arg("pylsp-mypy")
821 .output()
822 .await?
823 .status
824 .success(),
825 "pylsp-mypy installation failed"
826 );
827 let pylsp = venv.join("bin").join("pylsp");
828 Ok(LanguageServerBinary {
829 path: pylsp,
830 env: None,
831 arguments: vec![],
832 })
833 }
834
835 async fn cached_server_binary(
836 &self,
837 _: PathBuf,
838 delegate: &dyn LspAdapterDelegate,
839 ) -> Option<LanguageServerBinary> {
840 let venv = self.base_venv(delegate).await.ok()?;
841 let pylsp = venv.join("bin").join("pylsp");
842 Some(LanguageServerBinary {
843 path: pylsp,
844 env: None,
845 arguments: vec![],
846 })
847 }
848
849 async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {}
850
851 async fn label_for_completion(
852 &self,
853 item: &lsp::CompletionItem,
854 language: &Arc<language::Language>,
855 ) -> Option<language::CodeLabel> {
856 let label = &item.label;
857 let grammar = language.grammar()?;
858 let highlight_id = match item.kind? {
859 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
860 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
861 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
862 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
863 _ => return None,
864 };
865 Some(language::CodeLabel {
866 text: label.clone(),
867 runs: vec![(0..label.len(), highlight_id)],
868 filter_range: 0..label.len(),
869 })
870 }
871
872 async fn label_for_symbol(
873 &self,
874 name: &str,
875 kind: lsp::SymbolKind,
876 language: &Arc<language::Language>,
877 ) -> Option<language::CodeLabel> {
878 let (text, filter_range, display_range) = match kind {
879 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
880 let text = format!("def {}():\n", name);
881 let filter_range = 4..4 + name.len();
882 let display_range = 0..filter_range.end;
883 (text, filter_range, display_range)
884 }
885 lsp::SymbolKind::CLASS => {
886 let text = format!("class {}:", name);
887 let filter_range = 6..6 + name.len();
888 let display_range = 0..filter_range.end;
889 (text, filter_range, display_range)
890 }
891 lsp::SymbolKind::CONSTANT => {
892 let text = format!("{} = 0", name);
893 let filter_range = 0..name.len();
894 let display_range = 0..filter_range.end;
895 (text, filter_range, display_range)
896 }
897 _ => return None,
898 };
899
900 Some(language::CodeLabel {
901 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
902 text: text[display_range].to_string(),
903 filter_range,
904 })
905 }
906
907 async fn workspace_configuration(
908 self: Arc<Self>,
909 adapter: &Arc<dyn LspAdapterDelegate>,
910 toolchains: Arc<dyn LanguageToolchainStore>,
911 cx: &mut AsyncAppContext,
912 ) -> Result<Value> {
913 let toolchain = toolchains
914 .active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx)
915 .await;
916 cx.update(move |cx| {
917 let mut user_settings =
918 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
919 .and_then(|s| s.settings.clone())
920 .unwrap_or_else(|| {
921 json!({
922 "plugins": {
923 "pycodestyle": {"enabled": false},
924 "rope_autoimport": {"enabled": true, "memory": true},
925 "pylsp_mypy": {"enabled": false}
926 },
927 "rope": {
928 "ropeFolder": null
929 },
930 })
931 });
932
933 // If user did not explicitly modify their python venv, use one from picker.
934 if let Some(toolchain) = toolchain {
935 if user_settings.is_null() {
936 user_settings = Value::Object(serde_json::Map::default());
937 }
938 let object = user_settings.as_object_mut().unwrap();
939 if let Some(python) = object
940 .entry("plugins")
941 .or_insert(Value::Object(serde_json::Map::default()))
942 .as_object_mut()
943 {
944 if let Some(jedi) = python
945 .entry("jedi")
946 .or_insert(Value::Object(serde_json::Map::default()))
947 .as_object_mut()
948 {
949 jedi.entry("environment".to_string())
950 .or_insert_with(|| Value::String(toolchain.path.clone().into()));
951 }
952 if let Some(pylint) = python
953 .entry("pylsp_mypy")
954 .or_insert(Value::Object(serde_json::Map::default()))
955 .as_object_mut()
956 {
957 pylint.entry("overrides".to_string()).or_insert_with(|| {
958 Value::Array(vec![
959 Value::String("--python-executable".into()),
960 Value::String(toolchain.path.into()),
961 Value::String("--cache-dir=/dev/null".into()),
962 Value::Bool(true),
963 ])
964 });
965 }
966 }
967 }
968 user_settings = Value::Object(serde_json::Map::from_iter([(
969 "pylsp".to_string(),
970 user_settings,
971 )]));
972
973 user_settings
974 })
975 }
976}
977
978#[cfg(test)]
979mod tests {
980 use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
981 use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
982 use settings::SettingsStore;
983 use std::num::NonZeroU32;
984
985 #[gpui::test]
986 async fn test_python_autoindent(cx: &mut TestAppContext) {
987 cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
988 let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
989 cx.update(|cx| {
990 let test_settings = SettingsStore::test(cx);
991 cx.set_global(test_settings);
992 language::init(cx);
993 cx.update_global::<SettingsStore, _>(|store, cx| {
994 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
995 s.defaults.tab_size = NonZeroU32::new(2);
996 });
997 });
998 });
999
1000 cx.new_model(|cx| {
1001 let mut buffer = Buffer::local("", cx).with_language(language, cx);
1002 let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
1003 let ix = buffer.len();
1004 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
1005 };
1006
1007 // indent after "def():"
1008 append(&mut buffer, "def a():\n", cx);
1009 assert_eq!(buffer.text(), "def a():\n ");
1010
1011 // preserve indent after blank line
1012 append(&mut buffer, "\n ", cx);
1013 assert_eq!(buffer.text(), "def a():\n \n ");
1014
1015 // indent after "if"
1016 append(&mut buffer, "if a:\n ", cx);
1017 assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
1018
1019 // preserve indent after statement
1020 append(&mut buffer, "b()\n", cx);
1021 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
1022
1023 // preserve indent after statement
1024 append(&mut buffer, "else", cx);
1025 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
1026
1027 // dedent "else""
1028 append(&mut buffer, ":", cx);
1029 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
1030
1031 // indent lines after else
1032 append(&mut buffer, "\n", cx);
1033 assert_eq!(
1034 buffer.text(),
1035 "def a():\n \n if a:\n b()\n else:\n "
1036 );
1037
1038 // indent after an open paren. the closing paren is not indented
1039 // because there is another token before it on the same line.
1040 append(&mut buffer, "foo(\n1)", cx);
1041 assert_eq!(
1042 buffer.text(),
1043 "def a():\n \n if a:\n b()\n else:\n foo(\n 1)"
1044 );
1045
1046 // dedent the closing paren if it is shifted to the beginning of the line
1047 let argument_ix = buffer.text().find('1').unwrap();
1048 buffer.edit(
1049 [(argument_ix..argument_ix + 1, "")],
1050 Some(AutoindentMode::EachLine),
1051 cx,
1052 );
1053 assert_eq!(
1054 buffer.text(),
1055 "def a():\n \n if a:\n b()\n else:\n foo(\n )"
1056 );
1057
1058 // preserve indent after the close paren
1059 append(&mut buffer, "\n", cx);
1060 assert_eq!(
1061 buffer.text(),
1062 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
1063 );
1064
1065 // manually outdent the last line
1066 let end_whitespace_ix = buffer.len() - 4;
1067 buffer.edit(
1068 [(end_whitespace_ix..buffer.len(), "")],
1069 Some(AutoindentMode::EachLine),
1070 cx,
1071 );
1072 assert_eq!(
1073 buffer.text(),
1074 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
1075 );
1076
1077 // preserve the newly reduced indentation on the next newline
1078 append(&mut buffer, "\n", cx);
1079 assert_eq!(
1080 buffer.text(),
1081 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
1082 );
1083
1084 // reset to a simple if statement
1085 buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx);
1086
1087 // dedent "else" on the line after a closing paren
1088 append(&mut buffer, "\n else:\n", cx);
1089 assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n ");
1090
1091 buffer
1092 });
1093 }
1094}