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