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 _: &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
501pub(crate) struct PythonToolchainProvider {
502 term: SharedString,
503}
504
505impl Default for PythonToolchainProvider {
506 fn default() -> Self {
507 Self {
508 term: SharedString::new_static("Virtual Environment"),
509 }
510 }
511}
512
513static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[
514 // Prioritize non-Conda environments.
515 PythonEnvironmentKind::Poetry,
516 PythonEnvironmentKind::Pipenv,
517 PythonEnvironmentKind::VirtualEnvWrapper,
518 PythonEnvironmentKind::Venv,
519 PythonEnvironmentKind::VirtualEnv,
520 PythonEnvironmentKind::Conda,
521 PythonEnvironmentKind::Pyenv,
522 PythonEnvironmentKind::GlobalPaths,
523 PythonEnvironmentKind::Homebrew,
524];
525
526fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
527 if let Some(kind) = kind {
528 ENV_PRIORITY_LIST
529 .iter()
530 .position(|blessed_env| blessed_env == &kind)
531 .unwrap_or(ENV_PRIORITY_LIST.len())
532 } else {
533 // Unknown toolchains are less useful than non-blessed ones.
534 ENV_PRIORITY_LIST.len() + 1
535 }
536}
537
538#[async_trait(?Send)]
539impl ToolchainLister for PythonToolchainProvider {
540 async fn list(
541 &self,
542 worktree_root: PathBuf,
543 project_env: Option<HashMap<String, String>>,
544 ) -> ToolchainList {
545 let env = project_env.unwrap_or_default();
546 let environment = EnvironmentApi::from_env(&env);
547 let locators = pet::locators::create_locators(
548 Arc::new(pet_conda::Conda::from(&environment)),
549 Arc::new(pet_poetry::Poetry::from(&environment)),
550 &environment,
551 );
552 let mut config = Configuration::default();
553 config.workspace_directories = Some(vec![worktree_root]);
554 for locator in locators.iter() {
555 locator.configure(&config);
556 }
557
558 let reporter = pet_reporter::collect::create_reporter();
559 pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
560
561 let mut toolchains = reporter
562 .environments
563 .lock()
564 .ok()
565 .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
566
567 toolchains.sort_by(|lhs, rhs| {
568 env_priority(lhs.kind)
569 .cmp(&env_priority(rhs.kind))
570 .then_with(|| {
571 if lhs.kind == Some(PythonEnvironmentKind::Conda) {
572 environment
573 .get_env_var("CONDA_PREFIX".to_string())
574 .map(|conda_prefix| {
575 let is_match = |exe: &Option<PathBuf>| {
576 exe.as_ref().map_or(false, |e| e.starts_with(&conda_prefix))
577 };
578 match (is_match(&lhs.executable), is_match(&rhs.executable)) {
579 (true, false) => Ordering::Less,
580 (false, true) => Ordering::Greater,
581 _ => Ordering::Equal,
582 }
583 })
584 .unwrap_or(Ordering::Equal)
585 } else {
586 Ordering::Equal
587 }
588 })
589 .then_with(|| lhs.executable.cmp(&rhs.executable))
590 });
591
592 let mut toolchains: Vec<_> = toolchains
593 .into_iter()
594 .filter_map(|toolchain| {
595 let name = if let Some(version) = &toolchain.version {
596 format!("Python {version} ({:?})", toolchain.kind?)
597 } else {
598 format!("{:?}", toolchain.kind?)
599 }
600 .into();
601 Some(Toolchain {
602 name,
603 path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
604 language_name: LanguageName::new("Python"),
605 as_json: serde_json::to_value(toolchain).ok()?,
606 })
607 })
608 .collect();
609 toolchains.dedup();
610 ToolchainList {
611 toolchains,
612 default: None,
613 groups: Default::default(),
614 }
615 }
616 fn term(&self) -> SharedString {
617 self.term.clone()
618 }
619}
620
621pub struct EnvironmentApi<'a> {
622 global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
623 project_env: &'a HashMap<String, String>,
624 pet_env: pet_core::os_environment::EnvironmentApi,
625}
626
627impl<'a> EnvironmentApi<'a> {
628 pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
629 let paths = project_env
630 .get("PATH")
631 .map(|p| std::env::split_paths(p).collect())
632 .unwrap_or_default();
633
634 EnvironmentApi {
635 global_search_locations: Arc::new(Mutex::new(paths)),
636 project_env,
637 pet_env: pet_core::os_environment::EnvironmentApi::new(),
638 }
639 }
640
641 fn user_home(&self) -> Option<PathBuf> {
642 self.project_env
643 .get("HOME")
644 .or_else(|| self.project_env.get("USERPROFILE"))
645 .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
646 .or_else(|| self.pet_env.get_user_home())
647 }
648}
649
650impl<'a> pet_core::os_environment::Environment for EnvironmentApi<'a> {
651 fn get_user_home(&self) -> Option<PathBuf> {
652 self.user_home()
653 }
654
655 fn get_root(&self) -> Option<PathBuf> {
656 None
657 }
658
659 fn get_env_var(&self, key: String) -> Option<String> {
660 self.project_env
661 .get(&key)
662 .cloned()
663 .or_else(|| self.pet_env.get_env_var(key))
664 }
665
666 fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
667 if self.global_search_locations.lock().unwrap().is_empty() {
668 let mut paths =
669 std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
670 .collect::<Vec<PathBuf>>();
671
672 log::trace!("Env PATH: {:?}", paths);
673 for p in self.pet_env.get_know_global_search_locations() {
674 if !paths.contains(&p) {
675 paths.push(p);
676 }
677 }
678
679 let mut paths = paths
680 .into_iter()
681 .filter(|p| p.exists())
682 .collect::<Vec<PathBuf>>();
683
684 self.global_search_locations
685 .lock()
686 .unwrap()
687 .append(&mut paths);
688 }
689 self.global_search_locations.lock().unwrap().clone()
690 }
691}
692
693pub(crate) struct PyLspAdapter {
694 python_venv_base: OnceCell<Result<Arc<Path>, String>>,
695}
696impl PyLspAdapter {
697 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
698 pub(crate) fn new() -> Self {
699 Self {
700 python_venv_base: OnceCell::new(),
701 }
702 }
703 async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
704 let python_path = Self::find_base_python(delegate)
705 .await
706 .ok_or_else(|| anyhow!("Could not find Python installation for PyLSP"))?;
707 let work_dir = delegate
708 .language_server_download_dir(&Self::SERVER_NAME)
709 .await
710 .ok_or_else(|| anyhow!("Could not get working directory for PyLSP"))?;
711 let mut path = PathBuf::from(work_dir.as_ref());
712 path.push("pylsp-venv");
713 if !path.exists() {
714 util::command::new_smol_command(python_path)
715 .arg("-m")
716 .arg("venv")
717 .arg("pylsp-venv")
718 .current_dir(work_dir)
719 .spawn()?
720 .output()
721 .await?;
722 }
723
724 Ok(path.into())
725 }
726 // Find "baseline", user python version from which we'll create our own venv.
727 async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
728 for path in ["python3", "python"] {
729 if let Some(path) = delegate.which(path.as_ref()).await {
730 return Some(path);
731 }
732 }
733 None
734 }
735
736 async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
737 self.python_venv_base
738 .get_or_init(move || async move {
739 Self::ensure_venv(delegate)
740 .await
741 .map_err(|e| format!("{e}"))
742 })
743 .await
744 .clone()
745 }
746}
747
748#[async_trait(?Send)]
749impl LspAdapter for PyLspAdapter {
750 fn name(&self) -> LanguageServerName {
751 Self::SERVER_NAME.clone()
752 }
753
754 async fn check_if_user_installed(
755 &self,
756 _: &dyn LspAdapterDelegate,
757 _: &AsyncAppContext,
758 ) -> Option<LanguageServerBinary> {
759 // We don't support user-provided pylsp, as global packages are discouraged in Python ecosystem.
760 None
761 }
762
763 async fn fetch_latest_server_version(
764 &self,
765 _: &dyn LspAdapterDelegate,
766 ) -> Result<Box<dyn 'static + Any + Send>> {
767 // let uri = "https://pypi.org/pypi/python-lsp-server/json";
768 // let mut root_manifest = delegate
769 // .http_client()
770 // .get(&uri, Default::default(), true)
771 // .await?;
772 // let mut body = Vec::new();
773 // root_manifest.body_mut().read_to_end(&mut body).await?;
774 // let as_str = String::from_utf8(body)?;
775 // let json = serde_json::Value::from_str(&as_str)?;
776 // let latest_version = json
777 // .get("info")
778 // .and_then(|info| info.get("version"))
779 // .and_then(|version| version.as_str().map(ToOwned::to_owned))
780 // .ok_or_else(|| {
781 // anyhow!("PyPI response did not contain version info for python-language-server")
782 // })?;
783 Ok(Box::new(()) as Box<_>)
784 }
785
786 async fn fetch_server_binary(
787 &self,
788 _: Box<dyn 'static + Send + Any>,
789 _: PathBuf,
790 delegate: &dyn LspAdapterDelegate,
791 ) -> Result<LanguageServerBinary> {
792 let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
793 let pip_path = venv.join("bin").join("pip3");
794 ensure!(
795 util::command::new_smol_command(pip_path.as_path())
796 .arg("install")
797 .arg("python-lsp-server")
798 .output()
799 .await?
800 .status
801 .success(),
802 "python-lsp-server installation failed"
803 );
804 ensure!(
805 util::command::new_smol_command(pip_path.as_path())
806 .arg("install")
807 .arg("python-lsp-server[all]")
808 .output()
809 .await?
810 .status
811 .success(),
812 "python-lsp-server[all] installation failed"
813 );
814 ensure!(
815 util::command::new_smol_command(pip_path)
816 .arg("install")
817 .arg("pylsp-mypy")
818 .output()
819 .await?
820 .status
821 .success(),
822 "pylsp-mypy installation failed"
823 );
824 let pylsp = venv.join("bin").join("pylsp");
825 Ok(LanguageServerBinary {
826 path: pylsp,
827 env: None,
828 arguments: vec![],
829 })
830 }
831
832 async fn cached_server_binary(
833 &self,
834 _: PathBuf,
835 delegate: &dyn LspAdapterDelegate,
836 ) -> Option<LanguageServerBinary> {
837 let venv = self.base_venv(delegate).await.ok()?;
838 let pylsp = venv.join("bin").join("pylsp");
839 Some(LanguageServerBinary {
840 path: pylsp,
841 env: None,
842 arguments: vec![],
843 })
844 }
845
846 async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {}
847
848 async fn label_for_completion(
849 &self,
850 item: &lsp::CompletionItem,
851 language: &Arc<language::Language>,
852 ) -> Option<language::CodeLabel> {
853 let label = &item.label;
854 let grammar = language.grammar()?;
855 let highlight_id = match item.kind? {
856 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
857 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
858 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
859 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
860 _ => return None,
861 };
862 Some(language::CodeLabel {
863 text: label.clone(),
864 runs: vec![(0..label.len(), highlight_id)],
865 filter_range: 0..label.len(),
866 })
867 }
868
869 async fn label_for_symbol(
870 &self,
871 name: &str,
872 kind: lsp::SymbolKind,
873 language: &Arc<language::Language>,
874 ) -> Option<language::CodeLabel> {
875 let (text, filter_range, display_range) = match kind {
876 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
877 let text = format!("def {}():\n", name);
878 let filter_range = 4..4 + name.len();
879 let display_range = 0..filter_range.end;
880 (text, filter_range, display_range)
881 }
882 lsp::SymbolKind::CLASS => {
883 let text = format!("class {}:", name);
884 let filter_range = 6..6 + name.len();
885 let display_range = 0..filter_range.end;
886 (text, filter_range, display_range)
887 }
888 lsp::SymbolKind::CONSTANT => {
889 let text = format!("{} = 0", name);
890 let filter_range = 0..name.len();
891 let display_range = 0..filter_range.end;
892 (text, filter_range, display_range)
893 }
894 _ => return None,
895 };
896
897 Some(language::CodeLabel {
898 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
899 text: text[display_range].to_string(),
900 filter_range,
901 })
902 }
903
904 async fn workspace_configuration(
905 self: Arc<Self>,
906 adapter: &Arc<dyn LspAdapterDelegate>,
907 toolchains: Arc<dyn LanguageToolchainStore>,
908 cx: &mut AsyncAppContext,
909 ) -> Result<Value> {
910 let toolchain = toolchains
911 .active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx)
912 .await;
913 cx.update(move |cx| {
914 let mut user_settings =
915 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
916 .and_then(|s| s.settings.clone())
917 .unwrap_or_else(|| {
918 json!({
919 "plugins": {
920 "rope_autoimport": {"enabled": true},
921 "mypy": {"enabled": true}
922 }
923 })
924 });
925
926 // If python.pythonPath is not set in user config, do so using our toolchain picker.
927 if let Some(toolchain) = toolchain {
928 if user_settings.is_null() {
929 user_settings = Value::Object(serde_json::Map::default());
930 }
931 let object = user_settings.as_object_mut().unwrap();
932 if let Some(python) = object
933 .entry("plugins")
934 .or_insert(Value::Object(serde_json::Map::default()))
935 .as_object_mut()
936 {
937 if let Some(jedi) = python
938 .entry("jedi")
939 .or_insert(Value::Object(serde_json::Map::default()))
940 .as_object_mut()
941 {
942 jedi.insert(
943 "environment".to_string(),
944 Value::String(toolchain.path.clone().into()),
945 );
946 }
947 if let Some(pylint) = python
948 .entry("mypy")
949 .or_insert(Value::Object(serde_json::Map::default()))
950 .as_object_mut()
951 {
952 pylint.insert(
953 "overrides".to_string(),
954 Value::Array(vec![
955 Value::String("--python-executable".into()),
956 Value::String(toolchain.path.into()),
957 ]),
958 );
959 }
960 }
961 }
962 user_settings = Value::Object(serde_json::Map::from_iter([(
963 "pylsp".to_string(),
964 user_settings,
965 )]));
966
967 user_settings
968 })
969 }
970}
971
972#[cfg(test)]
973mod tests {
974 use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
975 use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
976 use settings::SettingsStore;
977 use std::num::NonZeroU32;
978
979 #[gpui::test]
980 async fn test_python_autoindent(cx: &mut TestAppContext) {
981 cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
982 let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
983 cx.update(|cx| {
984 let test_settings = SettingsStore::test(cx);
985 cx.set_global(test_settings);
986 language::init(cx);
987 cx.update_global::<SettingsStore, _>(|store, cx| {
988 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
989 s.defaults.tab_size = NonZeroU32::new(2);
990 });
991 });
992 });
993
994 cx.new_model(|cx| {
995 let mut buffer = Buffer::local("", cx).with_language(language, cx);
996 let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
997 let ix = buffer.len();
998 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
999 };
1000
1001 // indent after "def():"
1002 append(&mut buffer, "def a():\n", cx);
1003 assert_eq!(buffer.text(), "def a():\n ");
1004
1005 // preserve indent after blank line
1006 append(&mut buffer, "\n ", cx);
1007 assert_eq!(buffer.text(), "def a():\n \n ");
1008
1009 // indent after "if"
1010 append(&mut buffer, "if a:\n ", cx);
1011 assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
1012
1013 // preserve indent after statement
1014 append(&mut buffer, "b()\n", cx);
1015 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
1016
1017 // preserve indent after statement
1018 append(&mut buffer, "else", cx);
1019 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
1020
1021 // dedent "else""
1022 append(&mut buffer, ":", cx);
1023 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
1024
1025 // indent lines after else
1026 append(&mut buffer, "\n", cx);
1027 assert_eq!(
1028 buffer.text(),
1029 "def a():\n \n if a:\n b()\n else:\n "
1030 );
1031
1032 // indent after an open paren. the closing paren is not indented
1033 // because there is another token before it on the same line.
1034 append(&mut buffer, "foo(\n1)", cx);
1035 assert_eq!(
1036 buffer.text(),
1037 "def a():\n \n if a:\n b()\n else:\n foo(\n 1)"
1038 );
1039
1040 // dedent the closing paren if it is shifted to the beginning of the line
1041 let argument_ix = buffer.text().find('1').unwrap();
1042 buffer.edit(
1043 [(argument_ix..argument_ix + 1, "")],
1044 Some(AutoindentMode::EachLine),
1045 cx,
1046 );
1047 assert_eq!(
1048 buffer.text(),
1049 "def a():\n \n if a:\n b()\n else:\n foo(\n )"
1050 );
1051
1052 // preserve indent after the close paren
1053 append(&mut buffer, "\n", cx);
1054 assert_eq!(
1055 buffer.text(),
1056 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
1057 );
1058
1059 // manually outdent the last line
1060 let end_whitespace_ix = buffer.len() - 4;
1061 buffer.edit(
1062 [(end_whitespace_ix..buffer.len(), "")],
1063 Some(AutoindentMode::EachLine),
1064 cx,
1065 );
1066 assert_eq!(
1067 buffer.text(),
1068 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
1069 );
1070
1071 // preserve the newly reduced indentation on the next newline
1072 append(&mut buffer, "\n", cx);
1073 assert_eq!(
1074 buffer.text(),
1075 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
1076 );
1077
1078 // reset to a simple if statement
1079 buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx);
1080
1081 // dedent "else" on the line after a closing paren
1082 append(&mut buffer, "\n else:\n", cx);
1083 assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n ");
1084
1085 buffer
1086 });
1087 }
1088}