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 "rope_autoimport": {"enabled": true},
740 "mypy": {"enabled": true}
741 }
742 })
743 });
744
745 // If python.pythonPath is not set in user config, do so using our toolchain picker.
746 if let Some(toolchain) = toolchain {
747 if user_settings.is_null() {
748 user_settings = Value::Object(serde_json::Map::default());
749 }
750 let object = user_settings.as_object_mut().unwrap();
751 if let Some(python) = object
752 .entry("plugins")
753 .or_insert(Value::Object(serde_json::Map::default()))
754 .as_object_mut()
755 {
756 if let Some(jedi) = python
757 .entry("jedi")
758 .or_insert(Value::Object(serde_json::Map::default()))
759 .as_object_mut()
760 {
761 jedi.insert(
762 "environment".to_string(),
763 Value::String(toolchain.path.clone().into()),
764 );
765 }
766 if let Some(pylint) = python
767 .entry("mypy")
768 .or_insert(Value::Object(serde_json::Map::default()))
769 .as_object_mut()
770 {
771 pylint.insert(
772 "overrides".to_string(),
773 Value::Array(vec![
774 Value::String("--python-executable".into()),
775 Value::String(toolchain.path.into()),
776 ]),
777 );
778 }
779 }
780 }
781 user_settings = Value::Object(serde_json::Map::from_iter([(
782 "pylsp".to_string(),
783 user_settings,
784 )]));
785
786 user_settings
787 })
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
794 use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
795 use settings::SettingsStore;
796 use std::num::NonZeroU32;
797
798 #[gpui::test]
799 async fn test_python_autoindent(cx: &mut TestAppContext) {
800 cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
801 let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
802 cx.update(|cx| {
803 let test_settings = SettingsStore::test(cx);
804 cx.set_global(test_settings);
805 language::init(cx);
806 cx.update_global::<SettingsStore, _>(|store, cx| {
807 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
808 s.defaults.tab_size = NonZeroU32::new(2);
809 });
810 });
811 });
812
813 cx.new_model(|cx| {
814 let mut buffer = Buffer::local("", cx).with_language(language, cx);
815 let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
816 let ix = buffer.len();
817 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
818 };
819
820 // indent after "def():"
821 append(&mut buffer, "def a():\n", cx);
822 assert_eq!(buffer.text(), "def a():\n ");
823
824 // preserve indent after blank line
825 append(&mut buffer, "\n ", cx);
826 assert_eq!(buffer.text(), "def a():\n \n ");
827
828 // indent after "if"
829 append(&mut buffer, "if a:\n ", cx);
830 assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
831
832 // preserve indent after statement
833 append(&mut buffer, "b()\n", cx);
834 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
835
836 // preserve indent after statement
837 append(&mut buffer, "else", cx);
838 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
839
840 // dedent "else""
841 append(&mut buffer, ":", cx);
842 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
843
844 // indent lines after else
845 append(&mut buffer, "\n", cx);
846 assert_eq!(
847 buffer.text(),
848 "def a():\n \n if a:\n b()\n else:\n "
849 );
850
851 // indent after an open paren. the closing paren is not indented
852 // because there is another token before it on the same line.
853 append(&mut buffer, "foo(\n1)", cx);
854 assert_eq!(
855 buffer.text(),
856 "def a():\n \n if a:\n b()\n else:\n foo(\n 1)"
857 );
858
859 // dedent the closing paren if it is shifted to the beginning of the line
860 let argument_ix = buffer.text().find('1').unwrap();
861 buffer.edit(
862 [(argument_ix..argument_ix + 1, "")],
863 Some(AutoindentMode::EachLine),
864 cx,
865 );
866 assert_eq!(
867 buffer.text(),
868 "def a():\n \n if a:\n b()\n else:\n foo(\n )"
869 );
870
871 // preserve indent after the close paren
872 append(&mut buffer, "\n", cx);
873 assert_eq!(
874 buffer.text(),
875 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
876 );
877
878 // manually outdent the last line
879 let end_whitespace_ix = buffer.len() - 4;
880 buffer.edit(
881 [(end_whitespace_ix..buffer.len(), "")],
882 Some(AutoindentMode::EachLine),
883 cx,
884 );
885 assert_eq!(
886 buffer.text(),
887 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
888 );
889
890 // preserve the newly reduced indentation on the next newline
891 append(&mut buffer, "\n", cx);
892 assert_eq!(
893 buffer.text(),
894 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
895 );
896
897 // reset to a simple if statement
898 buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx);
899
900 // dedent "else" on the line after a closing paren
901 append(&mut buffer, "\n else:\n", cx);
902 assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n ");
903
904 buffer
905 });
906 }
907}