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