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