1use anyhow::{Context as _, ensure};
2use anyhow::{Result, anyhow};
3use async_trait::async_trait;
4use collections::HashMap;
5use futures::future::BoxFuture;
6use futures::lock::OwnedMutexGuard;
7use futures::{AsyncBufReadExt, StreamExt as _};
8use gpui::{App, AsyncApp, Entity, SharedString, Task};
9use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
10use language::language_settings::LanguageSettings;
11use language::{
12 Buffer, ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol,
13};
14use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
15use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
16use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};
17use lsp::{LanguageServerBinary, Uri};
18use lsp::{LanguageServerBinaryOptions, LanguageServerName};
19use node_runtime::{NodeRuntime, VersionStrategy};
20use pet_core::Configuration;
21use pet_core::os_environment::Environment;
22use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
23use pet_virtualenv::is_virtualenv_dir;
24use project::Fs;
25use project::lsp_store::language_server_settings;
26use semver::Version;
27use serde::{Deserialize, Serialize};
28use serde_json::{Value, json};
29use settings::{SemanticTokenRules, Settings};
30use terminal::terminal_settings::TerminalSettings;
31
32use smol::lock::OnceCell;
33use std::cmp::{Ordering, Reverse};
34use std::env::consts;
35use util::command::Stdio;
36
37use util::command::new_command;
38use util::fs::{make_file_executable, remove_matching};
39use util::paths::PathStyle;
40use util::rel_path::RelPath;
41
42use crate::LanguageDir;
43use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
44use parking_lot::Mutex;
45use std::str::FromStr;
46use std::{
47 borrow::Cow,
48 fmt::Write,
49 path::{Path, PathBuf},
50 sync::Arc,
51};
52use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
53use util::{ResultExt, maybe};
54
55pub(crate) fn semantic_token_rules() -> SemanticTokenRules {
56 let content = LanguageDir::get("python/semantic_token_rules.json")
57 .expect("missing python/semantic_token_rules.json");
58 let json = std::str::from_utf8(&content.data).expect("invalid utf-8 in semantic_token_rules");
59 settings::parse_json_with_comments::<SemanticTokenRules>(json)
60 .expect("failed to parse python semantic_token_rules.json")
61}
62
63#[derive(Debug, Serialize, Deserialize)]
64pub(crate) struct PythonToolchainData {
65 #[serde(flatten)]
66 environment: PythonEnvironment,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 activation_scripts: Option<HashMap<ShellKind, PathBuf>>,
69}
70
71pub(crate) struct PyprojectTomlManifestProvider;
72
73impl ManifestProvider for PyprojectTomlManifestProvider {
74 fn name(&self) -> ManifestName {
75 SharedString::new_static("pyproject.toml").into()
76 }
77
78 fn search(
79 &self,
80 ManifestQuery {
81 path,
82 depth,
83 delegate,
84 }: ManifestQuery,
85 ) -> Option<Arc<RelPath>> {
86 for path in path.ancestors().take(depth) {
87 let p = path.join(RelPath::unix("pyproject.toml").unwrap());
88 if delegate.exists(&p, Some(false)) {
89 return Some(path.into());
90 }
91 }
92
93 None
94 }
95}
96
97enum TestRunner {
98 UNITTEST,
99 PYTEST,
100}
101
102impl FromStr for TestRunner {
103 type Err = ();
104
105 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
106 match s {
107 "unittest" => Ok(Self::UNITTEST),
108 "pytest" => Ok(Self::PYTEST),
109 _ => Err(()),
110 }
111 }
112}
113
114/// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
115/// Where `XX` is the sorting category, `YYYY` is based on most recent usage,
116/// and `name` is the symbol name itself.
117///
118/// The problem with it is that Pyright adjusts the sort text based on previous resolutions (items for which we've issued `completion/resolve` call have their sortText adjusted),
119/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail.
120/// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
121///
122/// upd 02.12.25:
123/// Decided to ignore Pyright's sortText() completely and to manually sort all entries
124fn process_pyright_completions(items: &mut [lsp::CompletionItem]) {
125 for item in items {
126 let is_named_argument = item.label.ends_with('=');
127
128 let is_dunder = item.label.starts_with("__") && item.label.ends_with("__");
129
130 let visibility_priority = if is_dunder {
131 '3'
132 } else if item.label.starts_with("__") {
133 '2' // private non-dunder
134 } else if item.label.starts_with('_') {
135 '1' // protected
136 } else {
137 '0' // public
138 };
139
140 let is_external = item
141 .detail
142 .as_ref()
143 .is_some_and(|detail| detail == "Auto-import");
144
145 let source_priority = if is_external { '1' } else { '0' };
146
147 // Kind priority within same visibility level
148 let kind_priority = match item.kind {
149 Some(lsp::CompletionItemKind::KEYWORD) => '0',
150 Some(lsp::CompletionItemKind::ENUM_MEMBER) => '1',
151 Some(lsp::CompletionItemKind::FIELD) => '2',
152 Some(lsp::CompletionItemKind::PROPERTY) => '3',
153 Some(lsp::CompletionItemKind::VARIABLE) => '4',
154 Some(lsp::CompletionItemKind::CONSTANT) => '5',
155 Some(lsp::CompletionItemKind::METHOD) => '6',
156 Some(lsp::CompletionItemKind::FUNCTION) => '6',
157 Some(lsp::CompletionItemKind::CLASS) => '7',
158 Some(lsp::CompletionItemKind::MODULE) => '8',
159
160 _ => 'z',
161 };
162
163 // Named arguments get higher priority
164 let argument_priority = if is_named_argument { '0' } else { '1' };
165
166 item.sort_text = Some(format!(
167 "{}{}{}{}{}",
168 argument_priority, source_priority, visibility_priority, kind_priority, item.label
169 ));
170 }
171}
172
173fn label_for_pyright_completion(
174 item: &lsp::CompletionItem,
175 language: &Arc<language::Language>,
176) -> Option<language::CodeLabel> {
177 let label = &item.label;
178 let label_len = label.len();
179 let grammar = language.grammar()?;
180 let highlight_id = match item.kind? {
181 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
182 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
183 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
184 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
185 lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
186 _ => {
187 return None;
188 }
189 };
190 let mut text = label.clone();
191 if let Some(completion_details) = item
192 .label_details
193 .as_ref()
194 .and_then(|details| details.description.as_ref())
195 {
196 write!(&mut text, " {}", completion_details).ok();
197 }
198 Some(language::CodeLabel::filtered(
199 text,
200 label_len,
201 item.filter_text.as_deref(),
202 highlight_id
203 .map(|id| (0..label_len, id))
204 .into_iter()
205 .collect(),
206 ))
207}
208
209fn label_for_python_symbol(
210 symbol: &Symbol,
211 language: &Arc<language::Language>,
212) -> Option<language::CodeLabel> {
213 let name = &symbol.name;
214 let (text, filter_range, display_range) = match symbol.kind {
215 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
216 let text = format!("def {}():\n", name);
217 let filter_range = 4..4 + name.len();
218 let display_range = 0..filter_range.end;
219 (text, filter_range, display_range)
220 }
221 lsp::SymbolKind::CLASS => {
222 let text = format!("class {}:", name);
223 let filter_range = 6..6 + name.len();
224 let display_range = 0..filter_range.end;
225 (text, filter_range, display_range)
226 }
227 lsp::SymbolKind::CONSTANT => {
228 let text = format!("{} = 0", name);
229 let filter_range = 0..name.len();
230 let display_range = 0..filter_range.end;
231 (text, filter_range, display_range)
232 }
233 _ => return None,
234 };
235 Some(language::CodeLabel::new(
236 text[display_range.clone()].to_string(),
237 filter_range,
238 language.highlight_text(&text.as_str().into(), display_range),
239 ))
240}
241
242pub struct TyLspAdapter {
243 fs: Arc<dyn Fs>,
244}
245
246#[cfg(target_os = "macos")]
247impl TyLspAdapter {
248 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
249 const ARCH_SERVER_NAME: &str = "apple-darwin";
250}
251
252#[cfg(target_os = "linux")]
253impl TyLspAdapter {
254 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
255 const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
256}
257
258#[cfg(target_os = "freebsd")]
259impl TyLspAdapter {
260 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
261 const ARCH_SERVER_NAME: &str = "unknown-freebsd";
262}
263
264#[cfg(target_os = "windows")]
265impl TyLspAdapter {
266 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
267 const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
268}
269
270impl TyLspAdapter {
271 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ty");
272
273 pub fn new(fs: Arc<dyn Fs>) -> TyLspAdapter {
274 TyLspAdapter { fs }
275 }
276
277 fn build_asset_name() -> Result<(String, String)> {
278 let arch = match consts::ARCH {
279 "x86" => "i686",
280 _ => consts::ARCH,
281 };
282 let os = Self::ARCH_SERVER_NAME;
283 let suffix = match consts::OS {
284 "windows" => "zip",
285 _ => "tar.gz",
286 };
287 let asset_name = format!("ty-{arch}-{os}.{suffix}");
288 let asset_stem = format!("ty-{arch}-{os}");
289 Ok((asset_stem, asset_name))
290 }
291}
292
293#[async_trait(?Send)]
294impl LspAdapter for TyLspAdapter {
295 fn name(&self) -> LanguageServerName {
296 Self::SERVER_NAME
297 }
298
299 async fn label_for_completion(
300 &self,
301 item: &lsp::CompletionItem,
302 language: &Arc<language::Language>,
303 ) -> Option<language::CodeLabel> {
304 let label = &item.label;
305 let label_len = label.len();
306 let grammar = language.grammar()?;
307 let highlight_id = match item.kind? {
308 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
309 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
310 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
311 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
312 lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
313 _ => {
314 return None;
315 }
316 };
317
318 let mut text = label.clone();
319 if let Some(completion_details) = item
320 .label_details
321 .as_ref()
322 .and_then(|details| details.detail.as_ref())
323 {
324 write!(&mut text, " {}", completion_details).ok();
325 }
326
327 Some(language::CodeLabel::filtered(
328 text,
329 label_len,
330 item.filter_text.as_deref(),
331 highlight_id
332 .map(|id| (0..label_len, id))
333 .into_iter()
334 .collect(),
335 ))
336 }
337
338 async fn label_for_symbol(
339 &self,
340 symbol: &language::Symbol,
341 language: &Arc<language::Language>,
342 ) -> Option<language::CodeLabel> {
343 label_for_python_symbol(symbol, language)
344 }
345
346 async fn workspace_configuration(
347 self: Arc<Self>,
348 delegate: &Arc<dyn LspAdapterDelegate>,
349 toolchain: Option<Toolchain>,
350 _: Option<Uri>,
351 cx: &mut AsyncApp,
352 ) -> Result<Value> {
353 let mut ret = cx
354 .update(|cx| {
355 language_server_settings(delegate.as_ref(), &self.name(), cx)
356 .and_then(|s| s.settings.clone())
357 })
358 .unwrap_or_else(|| json!({}));
359 if let Some(toolchain) = toolchain.and_then(|toolchain| {
360 serde_json::from_value::<PythonToolchainData>(toolchain.as_json).ok()
361 }) {
362 _ = maybe!({
363 let uri =
364 url::Url::from_file_path(toolchain.environment.executable.as_ref()?).ok()?;
365 let sys_prefix = toolchain.environment.prefix.clone()?;
366 let environment = json!({
367 "executable": {
368 "uri": uri,
369 "sysPrefix": sys_prefix
370 }
371 });
372 ret.as_object_mut()?
373 .entry("pythonExtension")
374 .or_insert_with(|| json!({ "activeEnvironment": environment }));
375 Some(())
376 });
377 }
378 Ok(json!({"ty": ret}))
379 }
380}
381
382impl LspInstaller for TyLspAdapter {
383 type BinaryVersion = GitHubLspBinaryVersion;
384 async fn fetch_latest_server_version(
385 &self,
386 delegate: &dyn LspAdapterDelegate,
387 _: bool,
388 _: &mut AsyncApp,
389 ) -> Result<Self::BinaryVersion> {
390 let release =
391 latest_github_release("astral-sh/ty", true, false, delegate.http_client()).await?;
392 let (_, asset_name) = Self::build_asset_name()?;
393 let asset = release
394 .assets
395 .into_iter()
396 .find(|asset| asset.name == asset_name)
397 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
398 Ok(GitHubLspBinaryVersion {
399 name: release.tag_name,
400 url: asset.browser_download_url,
401 digest: asset.digest,
402 })
403 }
404
405 async fn check_if_user_installed(
406 &self,
407 delegate: &dyn LspAdapterDelegate,
408 toolchain: Option<Toolchain>,
409 _: &AsyncApp,
410 ) -> Option<LanguageServerBinary> {
411 let ty_in_venv = if let Some(toolchain) = toolchain
412 && toolchain.language_name.as_ref() == "Python"
413 {
414 Path::new(toolchain.path.as_str())
415 .parent()
416 .map(|path| path.join("ty"))
417 } else {
418 None
419 };
420
421 for path in ty_in_venv.into_iter().chain(["ty".into()]) {
422 if let Some(ty_bin) = delegate.which(path.as_os_str()).await {
423 let env = delegate.shell_env().await;
424 return Some(LanguageServerBinary {
425 path: ty_bin,
426 env: Some(env),
427 arguments: vec!["server".into()],
428 });
429 }
430 }
431
432 None
433 }
434
435 async fn fetch_server_binary(
436 &self,
437 latest_version: Self::BinaryVersion,
438 container_dir: PathBuf,
439 delegate: &dyn LspAdapterDelegate,
440 ) -> Result<LanguageServerBinary> {
441 let GitHubLspBinaryVersion {
442 name,
443 url,
444 digest: expected_digest,
445 } = latest_version;
446 let destination_path = container_dir.join(format!("ty-{name}"));
447
448 async_fs::create_dir_all(&destination_path).await?;
449
450 let server_path = match Self::GITHUB_ASSET_KIND {
451 AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path
452 .join(Self::build_asset_name()?.0)
453 .join("ty"),
454 AssetKind::Zip => destination_path.clone().join("ty.exe"),
455 };
456
457 let binary = LanguageServerBinary {
458 path: server_path.clone(),
459 env: None,
460 arguments: vec!["server".into()],
461 };
462
463 let metadata_path = destination_path.with_extension("metadata");
464 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
465 .await
466 .ok();
467 if let Some(metadata) = metadata {
468 let validity_check = async || {
469 delegate
470 .try_exec(LanguageServerBinary {
471 path: server_path.clone(),
472 arguments: vec!["--version".into()],
473 env: None,
474 })
475 .await
476 .inspect_err(|err| {
477 log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
478 })
479 };
480 if let (Some(actual_digest), Some(expected_digest)) =
481 (&metadata.digest, &expected_digest)
482 {
483 if actual_digest == expected_digest {
484 if validity_check().await.is_ok() {
485 return Ok(binary);
486 }
487 } else {
488 log::info!(
489 "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
490 );
491 }
492 } else if validity_check().await.is_ok() {
493 return Ok(binary);
494 }
495 }
496
497 download_server_binary(
498 &*delegate.http_client(),
499 &url,
500 expected_digest.as_deref(),
501 &destination_path,
502 Self::GITHUB_ASSET_KIND,
503 )
504 .await?;
505 make_file_executable(&server_path).await?;
506 remove_matching(&container_dir, |path| path != destination_path).await;
507 GithubBinaryMetadata::write_to_file(
508 &GithubBinaryMetadata {
509 metadata_version: 1,
510 digest: expected_digest,
511 },
512 &metadata_path,
513 )
514 .await?;
515
516 Ok(LanguageServerBinary {
517 path: server_path,
518 env: None,
519 arguments: vec!["server".into()],
520 })
521 }
522
523 async fn cached_server_binary(
524 &self,
525 container_dir: PathBuf,
526 _: &dyn LspAdapterDelegate,
527 ) -> Option<LanguageServerBinary> {
528 maybe!(async {
529 let mut last = None;
530 let mut entries = self.fs.read_dir(&container_dir).await?;
531 while let Some(entry) = entries.next().await {
532 let path = entry?;
533 if path.extension().is_some_and(|ext| ext == "metadata") {
534 continue;
535 }
536 last = Some(path);
537 }
538
539 let path = last.context("no cached binary")?;
540 let path = match TyLspAdapter::GITHUB_ASSET_KIND {
541 AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => {
542 path.join(Self::build_asset_name()?.0).join("ty")
543 }
544 AssetKind::Zip => path.join("ty.exe"),
545 };
546
547 anyhow::Ok(LanguageServerBinary {
548 path,
549 env: None,
550 arguments: vec!["server".into()],
551 })
552 })
553 .await
554 .log_err()
555 }
556}
557
558pub struct PyrightLspAdapter {
559 node: NodeRuntime,
560}
561
562impl PyrightLspAdapter {
563 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");
564 const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
565 const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
566
567 pub fn new(node: NodeRuntime) -> Self {
568 PyrightLspAdapter { node }
569 }
570
571 async fn get_cached_server_binary(
572 container_dir: PathBuf,
573 node: &NodeRuntime,
574 ) -> Option<LanguageServerBinary> {
575 let server_path = container_dir.join(Self::SERVER_PATH);
576 if server_path.exists() {
577 Some(LanguageServerBinary {
578 path: node.binary_path().await.log_err()?,
579 env: None,
580 arguments: vec![server_path.into(), "--stdio".into()],
581 })
582 } else {
583 log::error!("missing executable in directory {:?}", server_path);
584 None
585 }
586 }
587}
588
589#[async_trait(?Send)]
590impl LspAdapter for PyrightLspAdapter {
591 fn name(&self) -> LanguageServerName {
592 Self::SERVER_NAME
593 }
594
595 async fn initialization_options(
596 self: Arc<Self>,
597 _: &Arc<dyn LspAdapterDelegate>,
598 _: &mut AsyncApp,
599 ) -> Result<Option<Value>> {
600 // Provide minimal initialization options
601 // Virtual environment configuration will be handled through workspace configuration
602 Ok(Some(json!({
603 "python": {
604 "analysis": {
605 "autoSearchPaths": true,
606 "useLibraryCodeForTypes": true,
607 "autoImportCompletions": true
608 }
609 }
610 })))
611 }
612
613 async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
614 process_pyright_completions(items);
615 }
616
617 async fn label_for_completion(
618 &self,
619 item: &lsp::CompletionItem,
620 language: &Arc<language::Language>,
621 ) -> Option<language::CodeLabel> {
622 label_for_pyright_completion(item, language)
623 }
624
625 async fn label_for_symbol(
626 &self,
627 symbol: &language::Symbol,
628 language: &Arc<language::Language>,
629 ) -> Option<language::CodeLabel> {
630 label_for_python_symbol(symbol, language)
631 }
632
633 async fn workspace_configuration(
634 self: Arc<Self>,
635 adapter: &Arc<dyn LspAdapterDelegate>,
636 toolchain: Option<Toolchain>,
637 _: Option<Uri>,
638 cx: &mut AsyncApp,
639 ) -> Result<Value> {
640 Ok(cx.update(move |cx| {
641 let mut user_settings =
642 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
643 .and_then(|s| s.settings.clone())
644 .unwrap_or_default();
645
646 // If we have a detected toolchain, configure Pyright to use it
647 if let Some(toolchain) = toolchain
648 && let Ok(env) =
649 serde_json::from_value::<PythonToolchainData>(toolchain.as_json.clone())
650 {
651 if !user_settings.is_object() {
652 user_settings = Value::Object(serde_json::Map::default());
653 }
654 let object = user_settings.as_object_mut().unwrap();
655
656 let interpreter_path = toolchain.path.to_string();
657 if let Some(venv_dir) = &env.environment.prefix {
658 // Set venvPath and venv at the root level
659 // This matches the format of a pyrightconfig.json file
660 if let Some(parent) = venv_dir.parent() {
661 // Use relative path if the venv is inside the workspace
662 let venv_path = if parent == adapter.worktree_root_path() {
663 ".".to_string()
664 } else {
665 parent.to_string_lossy().into_owned()
666 };
667 object.insert("venvPath".to_string(), Value::String(venv_path));
668 }
669
670 if let Some(venv_name) = venv_dir.file_name() {
671 object.insert(
672 "venv".to_owned(),
673 Value::String(venv_name.to_string_lossy().into_owned()),
674 );
675 }
676 }
677
678 // Always set the python interpreter path
679 // Get or create the python section
680 let python = object
681 .entry("python")
682 .and_modify(|v| {
683 if !v.is_object() {
684 *v = Value::Object(serde_json::Map::default());
685 }
686 })
687 .or_insert(Value::Object(serde_json::Map::default()));
688 let python = python.as_object_mut().unwrap();
689
690 // Set both pythonPath and defaultInterpreterPath for compatibility
691 python.insert(
692 "pythonPath".to_owned(),
693 Value::String(interpreter_path.clone()),
694 );
695 python.insert(
696 "defaultInterpreterPath".to_owned(),
697 Value::String(interpreter_path),
698 );
699 }
700
701 user_settings
702 }))
703 }
704}
705
706impl LspInstaller for PyrightLspAdapter {
707 type BinaryVersion = Version;
708
709 async fn fetch_latest_server_version(
710 &self,
711 _: &dyn LspAdapterDelegate,
712 _: bool,
713 _: &mut AsyncApp,
714 ) -> Result<Self::BinaryVersion> {
715 self.node
716 .npm_package_latest_version(Self::SERVER_NAME.as_ref())
717 .await
718 }
719
720 async fn check_if_user_installed(
721 &self,
722 delegate: &dyn LspAdapterDelegate,
723 _: Option<Toolchain>,
724 _: &AsyncApp,
725 ) -> Option<LanguageServerBinary> {
726 if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
727 let env = delegate.shell_env().await;
728 Some(LanguageServerBinary {
729 path: pyright_bin,
730 env: Some(env),
731 arguments: vec!["--stdio".into()],
732 })
733 } else {
734 let node = delegate.which("node".as_ref()).await?;
735 let (node_modules_path, _) = delegate
736 .npm_package_installed_version(Self::SERVER_NAME.as_ref())
737 .await
738 .log_err()??;
739
740 let path = node_modules_path.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
741
742 let env = delegate.shell_env().await;
743 Some(LanguageServerBinary {
744 path: node,
745 env: Some(env),
746 arguments: vec![path.into(), "--stdio".into()],
747 })
748 }
749 }
750
751 async fn fetch_server_binary(
752 &self,
753 latest_version: Self::BinaryVersion,
754 container_dir: PathBuf,
755 delegate: &dyn LspAdapterDelegate,
756 ) -> Result<LanguageServerBinary> {
757 let server_path = container_dir.join(Self::SERVER_PATH);
758 let latest_version = latest_version.to_string();
759
760 self.node
761 .npm_install_packages(
762 &container_dir,
763 &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
764 )
765 .await?;
766
767 let env = delegate.shell_env().await;
768 Ok(LanguageServerBinary {
769 path: self.node.binary_path().await?,
770 env: Some(env),
771 arguments: vec![server_path.into(), "--stdio".into()],
772 })
773 }
774
775 async fn check_if_version_installed(
776 &self,
777 version: &Self::BinaryVersion,
778 container_dir: &PathBuf,
779 delegate: &dyn LspAdapterDelegate,
780 ) -> Option<LanguageServerBinary> {
781 let server_path = container_dir.join(Self::SERVER_PATH);
782
783 let should_install_language_server = self
784 .node
785 .should_install_npm_package(
786 Self::SERVER_NAME.as_ref(),
787 &server_path,
788 container_dir,
789 VersionStrategy::Latest(version),
790 )
791 .await;
792
793 if should_install_language_server {
794 None
795 } else {
796 let env = delegate.shell_env().await;
797 Some(LanguageServerBinary {
798 path: self.node.binary_path().await.ok()?,
799 env: Some(env),
800 arguments: vec![server_path.into(), "--stdio".into()],
801 })
802 }
803 }
804
805 async fn cached_server_binary(
806 &self,
807 container_dir: PathBuf,
808 delegate: &dyn LspAdapterDelegate,
809 ) -> Option<LanguageServerBinary> {
810 let mut binary = Self::get_cached_server_binary(container_dir, &self.node).await?;
811 binary.env = Some(delegate.shell_env().await);
812 Some(binary)
813 }
814}
815
816pub(crate) struct PythonContextProvider;
817
818const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
819 VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET"));
820
821const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
822 VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
823
824const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
825 VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
826
827impl ContextProvider for PythonContextProvider {
828 fn build_context(
829 &self,
830 variables: &task::TaskVariables,
831 location: ContextLocation<'_>,
832 _: Option<HashMap<String, String>>,
833 toolchains: Arc<dyn LanguageToolchainStore>,
834 cx: &mut gpui::App,
835 ) -> Task<Result<task::TaskVariables>> {
836 let test_target = match selected_test_runner(Some(&location.file_location.buffer), cx) {
837 TestRunner::UNITTEST => self.build_unittest_target(variables),
838 TestRunner::PYTEST => self.build_pytest_target(variables),
839 };
840
841 let module_target = self.build_module_target(variables);
842 let location_file = location.file_location.buffer.read(cx).file().cloned();
843 let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx));
844
845 cx.spawn(async move |cx| {
846 let active_toolchain = if let Some(worktree_id) = worktree_id {
847 let file_path = location_file
848 .as_ref()
849 .and_then(|f| f.path().parent())
850 .map(Arc::from)
851 .unwrap_or_else(|| RelPath::empty().into());
852
853 toolchains
854 .active_toolchain(worktree_id, file_path, "Python".into(), cx)
855 .await
856 .map_or_else(
857 || String::from("python3"),
858 |toolchain| toolchain.path.to_string(),
859 )
860 } else {
861 String::from("python3")
862 };
863
864 let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
865
866 Ok(task::TaskVariables::from_iter(
867 test_target
868 .into_iter()
869 .chain(module_target.into_iter())
870 .chain([toolchain]),
871 ))
872 })
873 }
874
875 fn associated_tasks(
876 &self,
877 buffer: Option<Entity<Buffer>>,
878 cx: &App,
879 ) -> Task<Option<TaskTemplates>> {
880 let test_runner = selected_test_runner(buffer.as_ref(), cx);
881
882 let mut tasks = vec![
883 // Execute a selection
884 TaskTemplate {
885 label: "execute selection".to_owned(),
886 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
887 args: vec![
888 "-c".to_owned(),
889 VariableName::SelectedText.template_value_with_whitespace(),
890 ],
891 cwd: Some(VariableName::WorktreeRoot.template_value()),
892 ..TaskTemplate::default()
893 },
894 // Execute an entire file
895 TaskTemplate {
896 label: format!("run '{}'", VariableName::File.template_value()),
897 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
898 args: vec![VariableName::File.template_value_with_whitespace()],
899 cwd: Some(VariableName::WorktreeRoot.template_value()),
900 ..TaskTemplate::default()
901 },
902 // Execute a file as module
903 TaskTemplate {
904 label: format!("run module '{}'", VariableName::File.template_value()),
905 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
906 args: vec![
907 "-m".to_owned(),
908 PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
909 ],
910 cwd: Some(VariableName::WorktreeRoot.template_value()),
911 tags: vec!["python-module-main-method".to_owned()],
912 ..TaskTemplate::default()
913 },
914 ];
915
916 tasks.extend(match test_runner {
917 TestRunner::UNITTEST => {
918 [
919 // Run tests for an entire file
920 TaskTemplate {
921 label: format!("unittest '{}'", VariableName::File.template_value()),
922 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
923 args: vec![
924 "-m".to_owned(),
925 "unittest".to_owned(),
926 VariableName::File.template_value_with_whitespace(),
927 ],
928 cwd: Some(VariableName::WorktreeRoot.template_value()),
929 ..TaskTemplate::default()
930 },
931 // Run test(s) for a specific target within a file
932 TaskTemplate {
933 label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
934 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
935 args: vec![
936 "-m".to_owned(),
937 "unittest".to_owned(),
938 PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
939 ],
940 tags: vec![
941 "python-unittest-class".to_owned(),
942 "python-unittest-method".to_owned(),
943 ],
944 cwd: Some(VariableName::WorktreeRoot.template_value()),
945 ..TaskTemplate::default()
946 },
947 ]
948 }
949 TestRunner::PYTEST => {
950 [
951 // Run tests for an entire file
952 TaskTemplate {
953 label: format!("pytest '{}'", VariableName::File.template_value()),
954 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
955 args: vec![
956 "-m".to_owned(),
957 "pytest".to_owned(),
958 VariableName::File.template_value_with_whitespace(),
959 ],
960 cwd: Some(VariableName::WorktreeRoot.template_value()),
961 ..TaskTemplate::default()
962 },
963 // Run test(s) for a specific target within a file
964 TaskTemplate {
965 label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
966 command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
967 args: vec![
968 "-m".to_owned(),
969 "pytest".to_owned(),
970 PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
971 ],
972 cwd: Some(VariableName::WorktreeRoot.template_value()),
973 tags: vec![
974 "python-pytest-class".to_owned(),
975 "python-pytest-method".to_owned(),
976 ],
977 ..TaskTemplate::default()
978 },
979 ]
980 }
981 });
982
983 Task::ready(Some(TaskTemplates(tasks)))
984 }
985}
986
987fn selected_test_runner(location: Option<&Entity<Buffer>>, cx: &App) -> TestRunner {
988 const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER";
989 let language = LanguageName::new_static("Python");
990 let settings = LanguageSettings::resolve(location.map(|b| b.read(cx)), Some(&language), cx);
991 settings
992 .tasks
993 .variables
994 .get(TEST_RUNNER_VARIABLE)
995 .and_then(|val| TestRunner::from_str(val).ok())
996 .unwrap_or(TestRunner::PYTEST)
997}
998
999impl PythonContextProvider {
1000 fn build_unittest_target(
1001 &self,
1002 variables: &task::TaskVariables,
1003 ) -> Option<(VariableName, String)> {
1004 let python_module_name =
1005 python_module_name_from_relative_path(variables.get(&VariableName::RelativeFile)?)?;
1006
1007 let unittest_class_name =
1008 variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
1009
1010 let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
1011 "_unittest_method_name",
1012 )));
1013
1014 let unittest_target_str = match (unittest_class_name, unittest_method_name) {
1015 (Some(class_name), Some(method_name)) => {
1016 format!("{python_module_name}.{class_name}.{method_name}")
1017 }
1018 (Some(class_name), None) => format!("{python_module_name}.{class_name}"),
1019 (None, None) => python_module_name,
1020 // should never happen, a TestCase class is the unit of testing
1021 (None, Some(_)) => return None,
1022 };
1023
1024 Some((
1025 PYTHON_TEST_TARGET_TASK_VARIABLE.clone(),
1026 unittest_target_str,
1027 ))
1028 }
1029
1030 fn build_pytest_target(
1031 &self,
1032 variables: &task::TaskVariables,
1033 ) -> Option<(VariableName, String)> {
1034 let file_path = variables.get(&VariableName::RelativeFile)?;
1035
1036 let pytest_class_name =
1037 variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name")));
1038
1039 let pytest_method_name =
1040 variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name")));
1041
1042 let pytest_target_str = match (pytest_class_name, pytest_method_name) {
1043 (Some(class_name), Some(method_name)) => {
1044 format!("{file_path}::{class_name}::{method_name}")
1045 }
1046 (Some(class_name), None) => {
1047 format!("{file_path}::{class_name}")
1048 }
1049 (None, Some(method_name)) => {
1050 format!("{file_path}::{method_name}")
1051 }
1052 (None, None) => file_path.to_string(),
1053 };
1054
1055 Some((PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str))
1056 }
1057
1058 fn build_module_target(
1059 &self,
1060 variables: &task::TaskVariables,
1061 ) -> Result<(VariableName, String)> {
1062 let python_module_name = variables
1063 .get(&VariableName::RelativeFile)
1064 .and_then(|module| python_module_name_from_relative_path(module))
1065 .unwrap_or_default();
1066
1067 let module_target = (PYTHON_MODULE_NAME_TASK_VARIABLE.clone(), python_module_name);
1068
1069 Ok(module_target)
1070 }
1071}
1072
1073fn python_module_name_from_relative_path(relative_path: &str) -> Option<String> {
1074 let rel_path = RelPath::new(relative_path.as_ref(), PathStyle::local()).ok()?;
1075 let path_with_dots = rel_path.display(PathStyle::Posix).replace('/', ".");
1076 Some(
1077 path_with_dots
1078 .strip_suffix(".py")
1079 .map(ToOwned::to_owned)
1080 .unwrap_or(path_with_dots),
1081 )
1082}
1083
1084fn is_python_env_global(k: &PythonEnvironmentKind) -> bool {
1085 matches!(
1086 k,
1087 PythonEnvironmentKind::Homebrew
1088 | PythonEnvironmentKind::Pyenv
1089 | PythonEnvironmentKind::GlobalPaths
1090 | PythonEnvironmentKind::MacPythonOrg
1091 | PythonEnvironmentKind::MacCommandLineTools
1092 | PythonEnvironmentKind::LinuxGlobal
1093 | PythonEnvironmentKind::MacXCode
1094 | PythonEnvironmentKind::WindowsStore
1095 | PythonEnvironmentKind::WindowsRegistry
1096 )
1097}
1098
1099fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
1100 match k {
1101 PythonEnvironmentKind::Conda => "Conda",
1102 PythonEnvironmentKind::Pixi => "pixi",
1103 PythonEnvironmentKind::Homebrew => "Homebrew",
1104 PythonEnvironmentKind::Pyenv => "global (Pyenv)",
1105 PythonEnvironmentKind::GlobalPaths => "global",
1106 PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
1107 PythonEnvironmentKind::Pipenv => "Pipenv",
1108 PythonEnvironmentKind::Poetry => "Poetry",
1109 PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
1110 PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
1111 PythonEnvironmentKind::LinuxGlobal => "global",
1112 PythonEnvironmentKind::MacXCode => "global (Xcode)",
1113 PythonEnvironmentKind::Venv => "venv",
1114 PythonEnvironmentKind::VirtualEnv => "virtualenv",
1115 PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
1116 PythonEnvironmentKind::WinPython => "WinPython",
1117 PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
1118 PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
1119 PythonEnvironmentKind::Uv => "uv",
1120 PythonEnvironmentKind::UvWorkspace => "uv (Workspace)",
1121 }
1122}
1123
1124pub(crate) struct PythonToolchainProvider;
1125
1126static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
1127 // Prioritize non-Conda environments.
1128 PythonEnvironmentKind::UvWorkspace,
1129 PythonEnvironmentKind::Uv,
1130 PythonEnvironmentKind::Poetry,
1131 PythonEnvironmentKind::Pipenv,
1132 PythonEnvironmentKind::VirtualEnvWrapper,
1133 PythonEnvironmentKind::Venv,
1134 PythonEnvironmentKind::VirtualEnv,
1135 PythonEnvironmentKind::PyenvVirtualEnv,
1136 PythonEnvironmentKind::Pixi,
1137 PythonEnvironmentKind::Conda,
1138 PythonEnvironmentKind::Pyenv,
1139 PythonEnvironmentKind::GlobalPaths,
1140 PythonEnvironmentKind::Homebrew,
1141];
1142
1143fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
1144 if let Some(kind) = kind {
1145 ENV_PRIORITY_LIST
1146 .iter()
1147 .position(|blessed_env| blessed_env == &kind)
1148 .unwrap_or(ENV_PRIORITY_LIST.len())
1149 } else {
1150 // Unknown toolchains are less useful than non-blessed ones.
1151 ENV_PRIORITY_LIST.len() + 1
1152 }
1153}
1154
1155/// Return the name of environment declared in <worktree-root/.venv.
1156///
1157/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
1158async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
1159 let file = async_fs::File::open(worktree_root.join(".venv"))
1160 .await
1161 .ok()?;
1162 let mut venv_name = String::new();
1163 smol::io::BufReader::new(file)
1164 .read_line(&mut venv_name)
1165 .await
1166 .ok()?;
1167 Some(venv_name.trim().to_string())
1168}
1169
1170fn get_venv_parent_dir(env: &PythonEnvironment) -> Option<PathBuf> {
1171 // If global, we aren't a virtual environment
1172 if let Some(kind) = env.kind
1173 && is_python_env_global(&kind)
1174 {
1175 return None;
1176 }
1177
1178 // Check to be sure we are a virtual environment using pet's most generic
1179 // virtual environment type, VirtualEnv
1180 let venv = env
1181 .executable
1182 .as_ref()
1183 .and_then(|p| p.parent())
1184 .and_then(|p| p.parent())
1185 .filter(|p| is_virtualenv_dir(p))?;
1186
1187 venv.parent().map(|parent| parent.to_path_buf())
1188}
1189
1190// How far is this venv from the root of our current project?
1191#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1192enum SubprojectDistance {
1193 WithinSubproject(Reverse<usize>),
1194 WithinWorktree(Reverse<usize>),
1195 NotInWorktree,
1196}
1197
1198fn wr_distance(
1199 wr: &PathBuf,
1200 subroot_relative_path: &RelPath,
1201 venv: Option<&PathBuf>,
1202) -> SubprojectDistance {
1203 if let Some(venv) = venv
1204 && let Ok(p) = venv.strip_prefix(wr)
1205 {
1206 if subroot_relative_path.components().next().is_some()
1207 && let Ok(distance) = p
1208 .strip_prefix(subroot_relative_path.as_std_path())
1209 .map(|p| p.components().count())
1210 {
1211 SubprojectDistance::WithinSubproject(Reverse(distance))
1212 } else {
1213 SubprojectDistance::WithinWorktree(Reverse(p.components().count()))
1214 }
1215 } else {
1216 SubprojectDistance::NotInWorktree
1217 }
1218}
1219
1220fn micromamba_shell_name(kind: ShellKind) -> &'static str {
1221 match kind {
1222 ShellKind::Csh => "csh",
1223 ShellKind::Fish => "fish",
1224 ShellKind::Nushell => "nu",
1225 ShellKind::PowerShell => "powershell",
1226 ShellKind::Cmd => "cmd.exe",
1227 // default / catch-all:
1228 _ => "posix",
1229 }
1230}
1231
1232#[async_trait]
1233impl ToolchainLister for PythonToolchainProvider {
1234 async fn list(
1235 &self,
1236 worktree_root: PathBuf,
1237 subroot_relative_path: Arc<RelPath>,
1238 project_env: Option<HashMap<String, String>>,
1239 fs: &dyn Fs,
1240 ) -> ToolchainList {
1241 let env = project_env.unwrap_or_default();
1242 let environment = EnvironmentApi::from_env(&env);
1243 let locators = pet::locators::create_locators(
1244 Arc::new(pet_conda::Conda::from(&environment)),
1245 Arc::new(pet_poetry::Poetry::from(&environment)),
1246 &environment,
1247 );
1248 let mut config = Configuration::default();
1249
1250 // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
1251 // worktree root as the workspace directory.
1252 config.workspace_directories = Some(
1253 subroot_relative_path
1254 .ancestors()
1255 .map(|ancestor| {
1256 // remove trailing separator as it alters the environment name hash used by Poetry.
1257 let path = worktree_root.join(ancestor.as_std_path());
1258 let path_str = path.to_string_lossy();
1259 if path_str.ends_with(std::path::MAIN_SEPARATOR) && path_str.len() > 1 {
1260 PathBuf::from(path_str.trim_end_matches(std::path::MAIN_SEPARATOR))
1261 } else {
1262 path
1263 }
1264 })
1265 .collect(),
1266 );
1267 for locator in locators.iter() {
1268 locator.configure(&config);
1269 }
1270
1271 let reporter = pet_reporter::collect::create_reporter();
1272 pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
1273
1274 let mut toolchains = reporter
1275 .environments
1276 .lock()
1277 .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
1278
1279 let wr = worktree_root;
1280 let wr_venv = get_worktree_venv_declaration(&wr).await;
1281 // Sort detected environments by:
1282 // environment name matching activation file (<workdir>/.venv)
1283 // environment project dir matching worktree_root
1284 // general env priority
1285 // environment path matching the CONDA_PREFIX env var
1286 // executable path
1287 toolchains.sort_by(|lhs, rhs| {
1288 // Compare venv names against worktree .venv file
1289 let venv_ordering =
1290 wr_venv
1291 .as_ref()
1292 .map_or(Ordering::Equal, |venv| match (&lhs.name, &rhs.name) {
1293 (Some(l), Some(r)) => (r == venv).cmp(&(l == venv)),
1294 (Some(l), None) if l == venv => Ordering::Less,
1295 (None, Some(r)) if r == venv => Ordering::Greater,
1296 _ => Ordering::Equal,
1297 });
1298
1299 // Compare project paths against worktree root
1300 let proj_ordering =
1301 || {
1302 let lhs_project = lhs.project.clone().or_else(|| get_venv_parent_dir(lhs));
1303 let rhs_project = rhs.project.clone().or_else(|| get_venv_parent_dir(rhs));
1304 wr_distance(&wr, &subroot_relative_path, lhs_project.as_ref()).cmp(
1305 &wr_distance(&wr, &subroot_relative_path, rhs_project.as_ref()),
1306 )
1307 };
1308
1309 // Compare environment priorities
1310 let priority_ordering = || env_priority(lhs.kind).cmp(&env_priority(rhs.kind));
1311
1312 // Compare conda prefixes
1313 let conda_ordering = || {
1314 if lhs.kind == Some(PythonEnvironmentKind::Conda) {
1315 environment
1316 .get_env_var("CONDA_PREFIX".to_string())
1317 .map(|conda_prefix| {
1318 let is_match = |exe: &Option<PathBuf>| {
1319 exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
1320 };
1321 match (is_match(&lhs.executable), is_match(&rhs.executable)) {
1322 (true, false) => Ordering::Less,
1323 (false, true) => Ordering::Greater,
1324 _ => Ordering::Equal,
1325 }
1326 })
1327 .unwrap_or(Ordering::Equal)
1328 } else {
1329 Ordering::Equal
1330 }
1331 };
1332
1333 // Compare Python executables
1334 let exe_ordering = || lhs.executable.cmp(&rhs.executable);
1335
1336 venv_ordering
1337 .then_with(proj_ordering)
1338 .then_with(priority_ordering)
1339 .then_with(conda_ordering)
1340 .then_with(exe_ordering)
1341 });
1342
1343 let mut out_toolchains = Vec::new();
1344 for toolchain in toolchains {
1345 let Some(toolchain) = venv_to_toolchain(toolchain, fs).await else {
1346 continue;
1347 };
1348 out_toolchains.push(toolchain);
1349 }
1350 out_toolchains.dedup();
1351 ToolchainList {
1352 toolchains: out_toolchains,
1353 default: None,
1354 groups: Default::default(),
1355 }
1356 }
1357 fn meta(&self) -> ToolchainMetadata {
1358 ToolchainMetadata {
1359 term: SharedString::new_static("Virtual Environment"),
1360 new_toolchain_placeholder: SharedString::new_static(
1361 "A path to the python3 executable within a virtual environment, or path to virtual environment itself",
1362 ),
1363 manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
1364 }
1365 }
1366
1367 async fn resolve(
1368 &self,
1369 path: PathBuf,
1370 env: Option<HashMap<String, String>>,
1371 fs: &dyn Fs,
1372 ) -> anyhow::Result<Toolchain> {
1373 let env = env.unwrap_or_default();
1374 let environment = EnvironmentApi::from_env(&env);
1375 let locators = pet::locators::create_locators(
1376 Arc::new(pet_conda::Conda::from(&environment)),
1377 Arc::new(pet_poetry::Poetry::from(&environment)),
1378 &environment,
1379 );
1380 let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
1381 .context("Could not find a virtual environment in provided path")?;
1382 let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
1383 venv_to_toolchain(venv, fs)
1384 .await
1385 .context("Could not convert a venv into a toolchain")
1386 }
1387
1388 fn activation_script(
1389 &self,
1390 toolchain: &Toolchain,
1391 shell: ShellKind,
1392 cx: &App,
1393 ) -> BoxFuture<'static, Vec<String>> {
1394 let settings = TerminalSettings::get_global(cx);
1395 let conda_manager = settings
1396 .detect_venv
1397 .as_option()
1398 .map(|venv| venv.conda_manager)
1399 .unwrap_or(settings::CondaManager::Auto);
1400
1401 let toolchain_clone = toolchain.clone();
1402 Box::pin(async move {
1403 let Ok(toolchain) =
1404 serde_json::from_value::<PythonToolchainData>(toolchain_clone.as_json.clone())
1405 else {
1406 return vec![];
1407 };
1408
1409 log::debug!("(Python) Composing activation script for toolchain {toolchain:?}");
1410
1411 let mut activation_script = vec![];
1412
1413 match toolchain.environment.kind {
1414 Some(PythonEnvironmentKind::Conda) => {
1415 if toolchain.environment.manager.is_none() {
1416 return vec![];
1417 };
1418
1419 let manager = match conda_manager {
1420 settings::CondaManager::Conda => "conda",
1421 settings::CondaManager::Mamba => "mamba",
1422 settings::CondaManager::Micromamba => "micromamba",
1423 settings::CondaManager::Auto => toolchain
1424 .environment
1425 .manager
1426 .as_ref()
1427 .and_then(|m| m.executable.file_name())
1428 .and_then(|name| name.to_str())
1429 .filter(|name| matches!(*name, "conda" | "mamba" | "micromamba"))
1430 .unwrap_or("conda"),
1431 };
1432
1433 // Activate micromamba shell in the child shell
1434 // [required for micromamba]
1435 if manager == "micromamba" {
1436 let shell = micromamba_shell_name(shell);
1437 activation_script
1438 .push(format!(r#"eval "$({manager} shell hook --shell {shell})""#));
1439 }
1440
1441 if let Some(name) = &toolchain.environment.name {
1442 if let Some(quoted_name) = shell.try_quote(name) {
1443 activation_script.push(format!("{manager} activate {quoted_name}"));
1444 } else {
1445 log::warn!(
1446 "Could not safely quote environment name {:?}, falling back to base",
1447 name
1448 );
1449 activation_script.push(format!("{manager} activate base"));
1450 }
1451 } else {
1452 activation_script.push(format!("{manager} activate base"));
1453 }
1454 }
1455 Some(
1456 PythonEnvironmentKind::Venv
1457 | PythonEnvironmentKind::VirtualEnv
1458 | PythonEnvironmentKind::Uv
1459 | PythonEnvironmentKind::UvWorkspace
1460 | PythonEnvironmentKind::Poetry,
1461 ) => {
1462 if let Some(activation_scripts) = &toolchain.activation_scripts {
1463 if let Some(activate_script_path) = activation_scripts.get(&shell) {
1464 let activate_keyword = shell.activate_keyword();
1465 if let Some(quoted) =
1466 shell.try_quote(&activate_script_path.to_string_lossy())
1467 {
1468 activation_script.push(format!("{activate_keyword} {quoted}"));
1469 }
1470 }
1471 }
1472 }
1473 Some(PythonEnvironmentKind::Pyenv) => {
1474 let Some(manager) = &toolchain.environment.manager else {
1475 return vec![];
1476 };
1477 let version = toolchain.environment.version.as_deref().unwrap_or("system");
1478 let pyenv = &manager.executable;
1479 let pyenv = pyenv.display();
1480 activation_script.extend(match shell {
1481 ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
1482 ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
1483 ShellKind::Nushell => Some(format!("^\"{pyenv}\" shell - nu {version}")),
1484 ShellKind::PowerShell | ShellKind::Pwsh => None,
1485 ShellKind::Csh => None,
1486 ShellKind::Tcsh => None,
1487 ShellKind::Cmd => None,
1488 ShellKind::Rc => None,
1489 ShellKind::Xonsh => None,
1490 ShellKind::Elvish => None,
1491 })
1492 }
1493 _ => {}
1494 }
1495 activation_script
1496 })
1497 }
1498}
1499
1500async fn venv_to_toolchain(venv: PythonEnvironment, fs: &dyn Fs) -> Option<Toolchain> {
1501 let mut name = String::from("Python");
1502 if let Some(ref version) = venv.version {
1503 _ = write!(name, " {version}");
1504 }
1505
1506 let name_and_kind = match (&venv.name, &venv.kind) {
1507 (Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
1508 (Some(name), None) => Some(format!("({name})")),
1509 (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
1510 (None, None) => None,
1511 };
1512
1513 if let Some(nk) = name_and_kind {
1514 _ = write!(name, " {nk}");
1515 }
1516
1517 let mut activation_scripts = HashMap::default();
1518 match venv.kind {
1519 Some(
1520 PythonEnvironmentKind::Venv
1521 | PythonEnvironmentKind::VirtualEnv
1522 | PythonEnvironmentKind::Uv
1523 | PythonEnvironmentKind::UvWorkspace
1524 | PythonEnvironmentKind::Poetry,
1525 ) => resolve_venv_activation_scripts(&venv, fs, &mut activation_scripts).await,
1526 _ => {}
1527 }
1528 let data = PythonToolchainData {
1529 environment: venv,
1530 activation_scripts: Some(activation_scripts),
1531 };
1532
1533 Some(Toolchain {
1534 name: name.into(),
1535 path: data
1536 .environment
1537 .executable
1538 .as_ref()?
1539 .to_str()?
1540 .to_owned()
1541 .into(),
1542 language_name: LanguageName::new_static("Python"),
1543 as_json: serde_json::to_value(data).ok()?,
1544 })
1545}
1546
1547async fn resolve_venv_activation_scripts(
1548 venv: &PythonEnvironment,
1549 fs: &dyn Fs,
1550 activation_scripts: &mut HashMap<ShellKind, PathBuf>,
1551) {
1552 log::debug!("(Python) Resolving activation scripts for venv toolchain {venv:?}");
1553 if let Some(prefix) = &venv.prefix {
1554 for (shell_kind, script_name) in &[
1555 (ShellKind::Posix, "activate"),
1556 (ShellKind::Rc, "activate"),
1557 (ShellKind::Csh, "activate.csh"),
1558 (ShellKind::Tcsh, "activate.csh"),
1559 (ShellKind::Fish, "activate.fish"),
1560 (ShellKind::Nushell, "activate.nu"),
1561 (ShellKind::PowerShell, "activate.ps1"),
1562 (ShellKind::Pwsh, "activate.ps1"),
1563 (ShellKind::Cmd, "activate.bat"),
1564 (ShellKind::Xonsh, "activate.xsh"),
1565 ] {
1566 let path = prefix.join(BINARY_DIR).join(script_name);
1567
1568 log::debug!("Trying path: {}", path.display());
1569
1570 if fs.is_file(&path).await {
1571 activation_scripts.insert(*shell_kind, path);
1572 }
1573 }
1574 }
1575}
1576
1577pub struct EnvironmentApi<'a> {
1578 global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
1579 project_env: &'a HashMap<String, String>,
1580 pet_env: pet_core::os_environment::EnvironmentApi,
1581}
1582
1583impl<'a> EnvironmentApi<'a> {
1584 pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
1585 let paths = project_env
1586 .get("PATH")
1587 .map(|p| std::env::split_paths(p).collect())
1588 .unwrap_or_default();
1589
1590 EnvironmentApi {
1591 global_search_locations: Arc::new(Mutex::new(paths)),
1592 project_env,
1593 pet_env: pet_core::os_environment::EnvironmentApi::new(),
1594 }
1595 }
1596
1597 fn user_home(&self) -> Option<PathBuf> {
1598 self.project_env
1599 .get("HOME")
1600 .or_else(|| self.project_env.get("USERPROFILE"))
1601 .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
1602 .or_else(|| self.pet_env.get_user_home())
1603 }
1604}
1605
1606impl pet_core::os_environment::Environment for EnvironmentApi<'_> {
1607 fn get_user_home(&self) -> Option<PathBuf> {
1608 self.user_home()
1609 }
1610
1611 fn get_root(&self) -> Option<PathBuf> {
1612 None
1613 }
1614
1615 fn get_env_var(&self, key: String) -> Option<String> {
1616 self.project_env
1617 .get(&key)
1618 .cloned()
1619 .or_else(|| self.pet_env.get_env_var(key))
1620 }
1621
1622 fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
1623 if self.global_search_locations.lock().is_empty() {
1624 let mut paths = std::env::split_paths(
1625 &self
1626 .get_env_var("PATH".to_string())
1627 .or_else(|| self.get_env_var("Path".to_string()))
1628 .unwrap_or_default(),
1629 )
1630 .collect::<Vec<PathBuf>>();
1631
1632 log::trace!("Env PATH: {:?}", paths);
1633 for p in self.pet_env.get_know_global_search_locations() {
1634 if !paths.contains(&p) {
1635 paths.push(p);
1636 }
1637 }
1638
1639 let mut paths = paths
1640 .into_iter()
1641 .filter(|p| p.exists())
1642 .collect::<Vec<PathBuf>>();
1643
1644 self.global_search_locations.lock().append(&mut paths);
1645 }
1646 self.global_search_locations.lock().clone()
1647 }
1648}
1649
1650pub(crate) struct PyLspAdapter {
1651 python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1652}
1653impl PyLspAdapter {
1654 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
1655 pub(crate) fn new() -> Self {
1656 Self {
1657 python_venv_base: OnceCell::new(),
1658 }
1659 }
1660 async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1661 let python_path = Self::find_base_python(delegate)
1662 .await
1663 .with_context(|| {
1664 let mut message = "Could not find Python installation for PyLSP".to_owned();
1665 if cfg!(windows){
1666 message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
1667 }
1668 message
1669 })?;
1670 let work_dir = delegate
1671 .language_server_download_dir(&Self::SERVER_NAME)
1672 .await
1673 .context("Could not get working directory for PyLSP")?;
1674 let mut path = PathBuf::from(work_dir.as_ref());
1675 path.push("pylsp-venv");
1676 if !path.exists() {
1677 util::command::new_command(python_path)
1678 .arg("-m")
1679 .arg("venv")
1680 .arg("pylsp-venv")
1681 .current_dir(work_dir)
1682 .spawn()?
1683 .output()
1684 .await?;
1685 }
1686
1687 Ok(path.into())
1688 }
1689 // Find "baseline", user python version from which we'll create our own venv.
1690 async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1691 for path in ["python3", "python"] {
1692 let Some(path) = delegate.which(path.as_ref()).await else {
1693 continue;
1694 };
1695 // Try to detect situations where `python3` exists but is not a real Python interpreter.
1696 // Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
1697 // when run with no arguments, and just fails otherwise.
1698 let Some(output) = new_command(&path)
1699 .args(["-c", "print(1 + 2)"])
1700 .output()
1701 .await
1702 .ok()
1703 else {
1704 continue;
1705 };
1706 if output.stdout.trim_ascii() != b"3" {
1707 continue;
1708 }
1709 return Some(path);
1710 }
1711 None
1712 }
1713
1714 async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1715 self.python_venv_base
1716 .get_or_init(move || async move {
1717 Self::ensure_venv(delegate)
1718 .await
1719 .map_err(|e| format!("{e}"))
1720 })
1721 .await
1722 .clone()
1723 }
1724}
1725
1726const BINARY_DIR: &str = if cfg!(target_os = "windows") {
1727 "Scripts"
1728} else {
1729 "bin"
1730};
1731
1732#[async_trait(?Send)]
1733impl LspAdapter for PyLspAdapter {
1734 fn name(&self) -> LanguageServerName {
1735 Self::SERVER_NAME
1736 }
1737
1738 async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
1739 for item in items {
1740 let is_named_argument = item.label.ends_with('=');
1741 let priority = if is_named_argument { '0' } else { '1' };
1742 let sort_text = item.sort_text.take().unwrap_or_else(|| item.label.clone());
1743 item.sort_text = Some(format!("{}{}", priority, sort_text));
1744 }
1745 }
1746
1747 async fn label_for_completion(
1748 &self,
1749 item: &lsp::CompletionItem,
1750 language: &Arc<language::Language>,
1751 ) -> Option<language::CodeLabel> {
1752 let label = &item.label;
1753 let label_len = label.len();
1754 let grammar = language.grammar()?;
1755 let highlight_id = match item.kind? {
1756 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
1757 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
1758 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
1759 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
1760 _ => return None,
1761 };
1762 Some(language::CodeLabel::filtered(
1763 label.clone(),
1764 label_len,
1765 item.filter_text.as_deref(),
1766 vec![(0..label.len(), highlight_id)],
1767 ))
1768 }
1769
1770 async fn label_for_symbol(
1771 &self,
1772 symbol: &language::Symbol,
1773 language: &Arc<language::Language>,
1774 ) -> Option<language::CodeLabel> {
1775 label_for_python_symbol(symbol, language)
1776 }
1777
1778 async fn workspace_configuration(
1779 self: Arc<Self>,
1780 adapter: &Arc<dyn LspAdapterDelegate>,
1781 toolchain: Option<Toolchain>,
1782 _: Option<Uri>,
1783 cx: &mut AsyncApp,
1784 ) -> Result<Value> {
1785 Ok(cx.update(move |cx| {
1786 let mut user_settings =
1787 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1788 .and_then(|s| s.settings.clone())
1789 .unwrap_or_else(|| {
1790 json!({
1791 "plugins": {
1792 "pycodestyle": {"enabled": false},
1793 "rope_autoimport": {"enabled": true, "memory": true},
1794 "pylsp_mypy": {"enabled": false}
1795 },
1796 "rope": {
1797 "ropeFolder": null
1798 },
1799 })
1800 });
1801
1802 // If user did not explicitly modify their python venv, use one from picker.
1803 if let Some(toolchain) = toolchain {
1804 if !user_settings.is_object() {
1805 user_settings = Value::Object(serde_json::Map::default());
1806 }
1807 let object = user_settings.as_object_mut().unwrap();
1808 if let Some(python) = object
1809 .entry("plugins")
1810 .or_insert(Value::Object(serde_json::Map::default()))
1811 .as_object_mut()
1812 {
1813 if let Some(jedi) = python
1814 .entry("jedi")
1815 .or_insert(Value::Object(serde_json::Map::default()))
1816 .as_object_mut()
1817 {
1818 jedi.entry("environment".to_string())
1819 .or_insert_with(|| Value::String(toolchain.path.clone().into()));
1820 }
1821 if let Some(pylint) = python
1822 .entry("pylsp_mypy")
1823 .or_insert(Value::Object(serde_json::Map::default()))
1824 .as_object_mut()
1825 {
1826 pylint.entry("overrides".to_string()).or_insert_with(|| {
1827 Value::Array(vec![
1828 Value::String("--python-executable".into()),
1829 Value::String(toolchain.path.into()),
1830 Value::String("--cache-dir=/dev/null".into()),
1831 Value::Bool(true),
1832 ])
1833 });
1834 }
1835 }
1836 }
1837 user_settings = Value::Object(serde_json::Map::from_iter([(
1838 "pylsp".to_string(),
1839 user_settings,
1840 )]));
1841
1842 user_settings
1843 }))
1844 }
1845}
1846
1847impl LspInstaller for PyLspAdapter {
1848 type BinaryVersion = ();
1849 async fn check_if_user_installed(
1850 &self,
1851 delegate: &dyn LspAdapterDelegate,
1852 toolchain: Option<Toolchain>,
1853 _: &AsyncApp,
1854 ) -> Option<LanguageServerBinary> {
1855 if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
1856 let env = delegate.shell_env().await;
1857 delegate
1858 .try_exec(LanguageServerBinary {
1859 path: pylsp_bin.clone(),
1860 arguments: vec!["--version".into()],
1861 env: Some(env.clone()),
1862 })
1863 .await
1864 .inspect_err(|err| {
1865 log::warn!("failed to validate user-installed pylsp at {pylsp_bin:?}: {err:#}")
1866 })
1867 .ok()?;
1868 Some(LanguageServerBinary {
1869 path: pylsp_bin,
1870 env: Some(env),
1871 arguments: vec![],
1872 })
1873 } else {
1874 let toolchain = toolchain?;
1875 let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp");
1876 if !pylsp_path.exists() {
1877 return None;
1878 }
1879 delegate
1880 .try_exec(LanguageServerBinary {
1881 path: toolchain.path.to_string().into(),
1882 arguments: vec![pylsp_path.clone().into(), "--version".into()],
1883 env: None,
1884 })
1885 .await
1886 .inspect_err(|err| {
1887 log::warn!("failed to validate toolchain pylsp at {pylsp_path:?}: {err:#}")
1888 })
1889 .ok()?;
1890 Some(LanguageServerBinary {
1891 path: toolchain.path.to_string().into(),
1892 arguments: vec![pylsp_path.into()],
1893 env: None,
1894 })
1895 }
1896 }
1897
1898 async fn fetch_latest_server_version(
1899 &self,
1900 _: &dyn LspAdapterDelegate,
1901 _: bool,
1902 _: &mut AsyncApp,
1903 ) -> Result<()> {
1904 Ok(())
1905 }
1906
1907 async fn fetch_server_binary(
1908 &self,
1909 _: (),
1910 _: PathBuf,
1911 delegate: &dyn LspAdapterDelegate,
1912 ) -> Result<LanguageServerBinary> {
1913 let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1914 let pip_path = venv.join(BINARY_DIR).join("pip3");
1915 ensure!(
1916 util::command::new_command(pip_path.as_path())
1917 .arg("install")
1918 .arg("python-lsp-server[all]")
1919 .arg("--upgrade")
1920 .output()
1921 .await?
1922 .status
1923 .success(),
1924 "python-lsp-server[all] installation failed"
1925 );
1926 ensure!(
1927 util::command::new_command(pip_path)
1928 .arg("install")
1929 .arg("pylsp-mypy")
1930 .arg("--upgrade")
1931 .output()
1932 .await?
1933 .status
1934 .success(),
1935 "pylsp-mypy installation failed"
1936 );
1937 let pylsp = venv.join(BINARY_DIR).join("pylsp");
1938 ensure!(
1939 delegate.which(pylsp.as_os_str()).await.is_some(),
1940 "pylsp installation was incomplete"
1941 );
1942 Ok(LanguageServerBinary {
1943 path: pylsp,
1944 env: None,
1945 arguments: vec![],
1946 })
1947 }
1948
1949 async fn cached_server_binary(
1950 &self,
1951 _: PathBuf,
1952 delegate: &dyn LspAdapterDelegate,
1953 ) -> Option<LanguageServerBinary> {
1954 let venv = self.base_venv(delegate).await.ok()?;
1955 let pylsp = venv.join(BINARY_DIR).join("pylsp");
1956 delegate.which(pylsp.as_os_str()).await?;
1957 Some(LanguageServerBinary {
1958 path: pylsp,
1959 env: None,
1960 arguments: vec![],
1961 })
1962 }
1963}
1964
1965pub(crate) struct BasedPyrightLspAdapter {
1966 node: NodeRuntime,
1967}
1968
1969impl BasedPyrightLspAdapter {
1970 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
1971 const BINARY_NAME: &'static str = "basedpyright-langserver";
1972 const SERVER_PATH: &str = "node_modules/basedpyright/langserver.index.js";
1973 const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "basedpyright/langserver.index.js";
1974
1975 pub(crate) fn new(node: NodeRuntime) -> Self {
1976 BasedPyrightLspAdapter { node }
1977 }
1978
1979 async fn get_cached_server_binary(
1980 container_dir: PathBuf,
1981 node: &NodeRuntime,
1982 ) -> Option<LanguageServerBinary> {
1983 let server_path = container_dir.join(Self::SERVER_PATH);
1984 if server_path.exists() {
1985 Some(LanguageServerBinary {
1986 path: node.binary_path().await.log_err()?,
1987 env: None,
1988 arguments: vec![server_path.into(), "--stdio".into()],
1989 })
1990 } else {
1991 log::error!("missing executable in directory {:?}", server_path);
1992 None
1993 }
1994 }
1995}
1996
1997#[async_trait(?Send)]
1998impl LspAdapter for BasedPyrightLspAdapter {
1999 fn name(&self) -> LanguageServerName {
2000 Self::SERVER_NAME
2001 }
2002
2003 async fn initialization_options(
2004 self: Arc<Self>,
2005 _: &Arc<dyn LspAdapterDelegate>,
2006 _: &mut AsyncApp,
2007 ) -> Result<Option<Value>> {
2008 // Provide minimal initialization options
2009 // Virtual environment configuration will be handled through workspace configuration
2010 Ok(Some(json!({
2011 "python": {
2012 "analysis": {
2013 "autoSearchPaths": true,
2014 "useLibraryCodeForTypes": true,
2015 "autoImportCompletions": true
2016 }
2017 }
2018 })))
2019 }
2020
2021 async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
2022 process_pyright_completions(items);
2023 }
2024
2025 async fn label_for_completion(
2026 &self,
2027 item: &lsp::CompletionItem,
2028 language: &Arc<language::Language>,
2029 ) -> Option<language::CodeLabel> {
2030 label_for_pyright_completion(item, language)
2031 }
2032
2033 async fn label_for_symbol(
2034 &self,
2035 symbol: &Symbol,
2036 language: &Arc<language::Language>,
2037 ) -> Option<language::CodeLabel> {
2038 label_for_python_symbol(symbol, language)
2039 }
2040
2041 async fn workspace_configuration(
2042 self: Arc<Self>,
2043 adapter: &Arc<dyn LspAdapterDelegate>,
2044 toolchain: Option<Toolchain>,
2045 _: Option<Uri>,
2046 cx: &mut AsyncApp,
2047 ) -> Result<Value> {
2048 Ok(cx.update(move |cx| {
2049 let mut user_settings =
2050 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
2051 .and_then(|s| s.settings.clone())
2052 .unwrap_or_default();
2053
2054 // If we have a detected toolchain, configure Pyright to use it
2055 if let Some(toolchain) = toolchain
2056 && let Ok(env) = serde_json::from_value::<
2057 pet_core::python_environment::PythonEnvironment,
2058 >(toolchain.as_json.clone())
2059 {
2060 if !user_settings.is_object() {
2061 user_settings = Value::Object(serde_json::Map::default());
2062 }
2063 let object = user_settings.as_object_mut().unwrap();
2064
2065 let interpreter_path = toolchain.path.to_string();
2066 if let Some(venv_dir) = env.prefix {
2067 // Set venvPath and venv at the root level
2068 // This matches the format of a pyrightconfig.json file
2069 if let Some(parent) = venv_dir.parent() {
2070 // Use relative path if the venv is inside the workspace
2071 let venv_path = if parent == adapter.worktree_root_path() {
2072 ".".to_string()
2073 } else {
2074 parent.to_string_lossy().into_owned()
2075 };
2076 object.insert("venvPath".to_string(), Value::String(venv_path));
2077 }
2078
2079 if let Some(venv_name) = venv_dir.file_name() {
2080 object.insert(
2081 "venv".to_owned(),
2082 Value::String(venv_name.to_string_lossy().into_owned()),
2083 );
2084 }
2085 }
2086
2087 // Set both pythonPath and defaultInterpreterPath for compatibility
2088 if let Some(python) = object
2089 .entry("python")
2090 .or_insert(Value::Object(serde_json::Map::default()))
2091 .as_object_mut()
2092 {
2093 python.insert(
2094 "pythonPath".to_owned(),
2095 Value::String(interpreter_path.clone()),
2096 );
2097 python.insert(
2098 "defaultInterpreterPath".to_owned(),
2099 Value::String(interpreter_path),
2100 );
2101 }
2102 // Basedpyright by default uses `strict` type checking, we tone it down as to not surpris users
2103 maybe!({
2104 let analysis = object
2105 .entry("basedpyright.analysis")
2106 .or_insert(Value::Object(serde_json::Map::default()));
2107 if let serde_json::map::Entry::Vacant(v) =
2108 analysis.as_object_mut()?.entry("typeCheckingMode")
2109 {
2110 v.insert(Value::String("standard".to_owned()));
2111 }
2112 Some(())
2113 });
2114 // Disable basedpyright's organizeImports so ruff handles it instead
2115 if let serde_json::map::Entry::Vacant(v) =
2116 object.entry("basedpyright.disableOrganizeImports")
2117 {
2118 v.insert(Value::Bool(true));
2119 }
2120 }
2121
2122 user_settings
2123 }))
2124 }
2125}
2126
2127impl LspInstaller for BasedPyrightLspAdapter {
2128 type BinaryVersion = Version;
2129
2130 async fn fetch_latest_server_version(
2131 &self,
2132 _: &dyn LspAdapterDelegate,
2133 _: bool,
2134 _: &mut AsyncApp,
2135 ) -> Result<Self::BinaryVersion> {
2136 self.node
2137 .npm_package_latest_version(Self::SERVER_NAME.as_ref())
2138 .await
2139 }
2140
2141 async fn check_if_user_installed(
2142 &self,
2143 delegate: &dyn LspAdapterDelegate,
2144 _: Option<Toolchain>,
2145 _: &AsyncApp,
2146 ) -> Option<LanguageServerBinary> {
2147 if let Some(path) = delegate.which(Self::BINARY_NAME.as_ref()).await {
2148 let env = delegate.shell_env().await;
2149 Some(LanguageServerBinary {
2150 path,
2151 env: Some(env),
2152 arguments: vec!["--stdio".into()],
2153 })
2154 } else {
2155 // TODO shouldn't this be self.node.binary_path()?
2156 let node = delegate.which("node".as_ref()).await?;
2157 let (node_modules_path, _) = delegate
2158 .npm_package_installed_version(Self::SERVER_NAME.as_ref())
2159 .await
2160 .log_err()??;
2161
2162 let path = node_modules_path.join(Self::NODE_MODULE_RELATIVE_SERVER_PATH);
2163
2164 let env = delegate.shell_env().await;
2165 Some(LanguageServerBinary {
2166 path: node,
2167 env: Some(env),
2168 arguments: vec![path.into(), "--stdio".into()],
2169 })
2170 }
2171 }
2172
2173 async fn fetch_server_binary(
2174 &self,
2175 latest_version: Self::BinaryVersion,
2176 container_dir: PathBuf,
2177 delegate: &dyn LspAdapterDelegate,
2178 ) -> Result<LanguageServerBinary> {
2179 let server_path = container_dir.join(Self::SERVER_PATH);
2180 let latest_version = latest_version.to_string();
2181
2182 self.node
2183 .npm_install_packages(
2184 &container_dir,
2185 &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
2186 )
2187 .await?;
2188
2189 let env = delegate.shell_env().await;
2190 Ok(LanguageServerBinary {
2191 path: self.node.binary_path().await?,
2192 env: Some(env),
2193 arguments: vec![server_path.into(), "--stdio".into()],
2194 })
2195 }
2196
2197 async fn check_if_version_installed(
2198 &self,
2199 version: &Self::BinaryVersion,
2200 container_dir: &PathBuf,
2201 delegate: &dyn LspAdapterDelegate,
2202 ) -> Option<LanguageServerBinary> {
2203 let server_path = container_dir.join(Self::SERVER_PATH);
2204
2205 let should_install_language_server = self
2206 .node
2207 .should_install_npm_package(
2208 Self::SERVER_NAME.as_ref(),
2209 &server_path,
2210 container_dir,
2211 VersionStrategy::Latest(version),
2212 )
2213 .await;
2214
2215 if should_install_language_server {
2216 None
2217 } else {
2218 let env = delegate.shell_env().await;
2219 Some(LanguageServerBinary {
2220 path: self.node.binary_path().await.ok()?,
2221 env: Some(env),
2222 arguments: vec![server_path.into(), "--stdio".into()],
2223 })
2224 }
2225 }
2226
2227 async fn cached_server_binary(
2228 &self,
2229 container_dir: PathBuf,
2230 delegate: &dyn LspAdapterDelegate,
2231 ) -> Option<LanguageServerBinary> {
2232 let mut binary = Self::get_cached_server_binary(container_dir, &self.node).await?;
2233 binary.env = Some(delegate.shell_env().await);
2234 Some(binary)
2235 }
2236}
2237
2238pub(crate) struct RuffLspAdapter {
2239 fs: Arc<dyn Fs>,
2240}
2241
2242impl RuffLspAdapter {
2243 fn convert_ruff_schema(raw_schema: &serde_json::Value) -> serde_json::Value {
2244 let Some(schema_object) = raw_schema.as_object() else {
2245 return raw_schema.clone();
2246 };
2247
2248 let mut root_properties = serde_json::Map::new();
2249
2250 for (key, value) in schema_object {
2251 let parts: Vec<&str> = key.split('.').collect();
2252
2253 if parts.is_empty() {
2254 continue;
2255 }
2256
2257 let mut current = &mut root_properties;
2258
2259 for (i, part) in parts.iter().enumerate() {
2260 let is_last = i == parts.len() - 1;
2261
2262 if is_last {
2263 let mut schema_entry = serde_json::Map::new();
2264
2265 if let Some(doc) = value.get("doc").and_then(|d| d.as_str()) {
2266 schema_entry.insert(
2267 "markdownDescription".to_string(),
2268 serde_json::Value::String(doc.to_string()),
2269 );
2270 }
2271
2272 if let Some(default_val) = value.get("default") {
2273 schema_entry.insert("default".to_string(), default_val.clone());
2274 }
2275
2276 if let Some(value_type) = value.get("value_type").and_then(|v| v.as_str()) {
2277 if value_type.contains('|') {
2278 let enum_values: Vec<serde_json::Value> = value_type
2279 .split('|')
2280 .map(|s| s.trim().trim_matches('"'))
2281 .filter(|s| !s.is_empty())
2282 .map(|s| serde_json::Value::String(s.to_string()))
2283 .collect();
2284
2285 if !enum_values.is_empty() {
2286 schema_entry
2287 .insert("type".to_string(), serde_json::json!("string"));
2288 schema_entry.insert(
2289 "enum".to_string(),
2290 serde_json::Value::Array(enum_values),
2291 );
2292 }
2293 } else if value_type.starts_with("list[") {
2294 schema_entry.insert("type".to_string(), serde_json::json!("array"));
2295 if let Some(item_type) = value_type
2296 .strip_prefix("list[")
2297 .and_then(|s| s.strip_suffix(']'))
2298 {
2299 let json_type = match item_type {
2300 "str" => "string",
2301 "int" => "integer",
2302 "bool" => "boolean",
2303 _ => "string",
2304 };
2305 schema_entry.insert(
2306 "items".to_string(),
2307 serde_json::json!({"type": json_type}),
2308 );
2309 }
2310 } else if value_type.starts_with("dict[") {
2311 schema_entry.insert("type".to_string(), serde_json::json!("object"));
2312 } else {
2313 let json_type = match value_type {
2314 "bool" => "boolean",
2315 "int" | "usize" => "integer",
2316 "str" => "string",
2317 _ => "string",
2318 };
2319 schema_entry.insert(
2320 "type".to_string(),
2321 serde_json::Value::String(json_type.to_string()),
2322 );
2323 }
2324 }
2325
2326 current.insert(part.to_string(), serde_json::Value::Object(schema_entry));
2327 } else {
2328 let next_current = current
2329 .entry(part.to_string())
2330 .or_insert_with(|| {
2331 serde_json::json!({
2332 "type": "object",
2333 "properties": {}
2334 })
2335 })
2336 .as_object_mut()
2337 .expect("should be an object")
2338 .entry("properties")
2339 .or_insert_with(|| serde_json::json!({}))
2340 .as_object_mut()
2341 .expect("properties should be an object");
2342
2343 current = next_current;
2344 }
2345 }
2346 }
2347
2348 serde_json::json!({
2349 "type": "object",
2350 "properties": root_properties
2351 })
2352 }
2353}
2354
2355#[cfg(target_os = "macos")]
2356impl RuffLspAdapter {
2357 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2358 const ARCH_SERVER_NAME: &str = "apple-darwin";
2359}
2360
2361#[cfg(target_os = "linux")]
2362impl RuffLspAdapter {
2363 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2364 const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
2365}
2366
2367#[cfg(target_os = "freebsd")]
2368impl RuffLspAdapter {
2369 const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
2370 const ARCH_SERVER_NAME: &str = "unknown-freebsd";
2371}
2372
2373#[cfg(target_os = "windows")]
2374impl RuffLspAdapter {
2375 const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
2376 const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
2377}
2378
2379impl RuffLspAdapter {
2380 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ruff");
2381
2382 pub fn new(fs: Arc<dyn Fs>) -> RuffLspAdapter {
2383 RuffLspAdapter { fs }
2384 }
2385
2386 fn build_asset_name() -> Result<(String, String)> {
2387 let arch = match consts::ARCH {
2388 "x86" => "i686",
2389 _ => consts::ARCH,
2390 };
2391 let os = Self::ARCH_SERVER_NAME;
2392 let suffix = match consts::OS {
2393 "windows" => "zip",
2394 _ => "tar.gz",
2395 };
2396 let asset_name = format!("ruff-{arch}-{os}.{suffix}");
2397 let asset_stem = format!("ruff-{arch}-{os}");
2398 Ok((asset_stem, asset_name))
2399 }
2400}
2401
2402#[async_trait(?Send)]
2403impl LspAdapter for RuffLspAdapter {
2404 fn name(&self) -> LanguageServerName {
2405 Self::SERVER_NAME
2406 }
2407
2408 async fn initialization_options_schema(
2409 self: Arc<Self>,
2410 delegate: &Arc<dyn LspAdapterDelegate>,
2411 cached_binary: OwnedMutexGuard<Option<(bool, LanguageServerBinary)>>,
2412 cx: &mut AsyncApp,
2413 ) -> Option<serde_json::Value> {
2414 let binary = self
2415 .get_language_server_command(
2416 delegate.clone(),
2417 None,
2418 LanguageServerBinaryOptions {
2419 allow_path_lookup: true,
2420 allow_binary_download: false,
2421 pre_release: false,
2422 },
2423 cached_binary,
2424 cx.clone(),
2425 )
2426 .await
2427 .0
2428 .ok()?;
2429
2430 let mut command = util::command::new_command(&binary.path);
2431 command
2432 .args(&["config", "--output-format", "json"])
2433 .stdout(Stdio::piped())
2434 .stderr(Stdio::piped());
2435 let cmd = command
2436 .spawn()
2437 .map_err(|e| log::debug!("failed to spawn command {command:?}: {e}"))
2438 .ok()?;
2439 let output = cmd
2440 .output()
2441 .await
2442 .map_err(|e| log::debug!("failed to execute command {command:?}: {e}"))
2443 .ok()?;
2444 if !output.status.success() {
2445 return None;
2446 }
2447
2448 let raw_schema: serde_json::Value = serde_json::from_slice(output.stdout.as_slice())
2449 .map_err(|e| log::debug!("failed to parse ruff's JSON schema output: {e}"))
2450 .ok()?;
2451
2452 let converted_schema = Self::convert_ruff_schema(&raw_schema);
2453 Some(converted_schema)
2454 }
2455}
2456
2457impl LspInstaller for RuffLspAdapter {
2458 type BinaryVersion = GitHubLspBinaryVersion;
2459 async fn check_if_user_installed(
2460 &self,
2461 delegate: &dyn LspAdapterDelegate,
2462 toolchain: Option<Toolchain>,
2463 _: &AsyncApp,
2464 ) -> Option<LanguageServerBinary> {
2465 let ruff_in_venv = if let Some(toolchain) = toolchain
2466 && toolchain.language_name.as_ref() == "Python"
2467 {
2468 Path::new(toolchain.path.as_str())
2469 .parent()
2470 .map(|path| path.join("ruff"))
2471 } else {
2472 None
2473 };
2474
2475 for path in ruff_in_venv.into_iter().chain(["ruff".into()]) {
2476 if let Some(ruff_bin) = delegate.which(path.as_os_str()).await {
2477 let env = delegate.shell_env().await;
2478 return Some(LanguageServerBinary {
2479 path: ruff_bin,
2480 env: Some(env),
2481 arguments: vec!["server".into()],
2482 });
2483 }
2484 }
2485
2486 None
2487 }
2488
2489 async fn fetch_latest_server_version(
2490 &self,
2491 delegate: &dyn LspAdapterDelegate,
2492 _: bool,
2493 _: &mut AsyncApp,
2494 ) -> Result<GitHubLspBinaryVersion> {
2495 let release =
2496 latest_github_release("astral-sh/ruff", true, false, delegate.http_client()).await?;
2497 let (_, asset_name) = Self::build_asset_name()?;
2498 let asset = release
2499 .assets
2500 .into_iter()
2501 .find(|asset| asset.name == asset_name)
2502 .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
2503 Ok(GitHubLspBinaryVersion {
2504 name: release.tag_name,
2505 url: asset.browser_download_url,
2506 digest: asset.digest,
2507 })
2508 }
2509
2510 async fn fetch_server_binary(
2511 &self,
2512 latest_version: GitHubLspBinaryVersion,
2513 container_dir: PathBuf,
2514 delegate: &dyn LspAdapterDelegate,
2515 ) -> Result<LanguageServerBinary> {
2516 let GitHubLspBinaryVersion {
2517 name,
2518 url,
2519 digest: expected_digest,
2520 } = latest_version;
2521 let destination_path = container_dir.join(format!("ruff-{name}"));
2522 let server_path = match Self::GITHUB_ASSET_KIND {
2523 AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => destination_path
2524 .join(Self::build_asset_name()?.0)
2525 .join("ruff"),
2526 AssetKind::Zip => destination_path.clone().join("ruff.exe"),
2527 };
2528
2529 let binary = LanguageServerBinary {
2530 path: server_path.clone(),
2531 env: None,
2532 arguments: vec!["server".into()],
2533 };
2534
2535 let metadata_path = destination_path.with_extension("metadata");
2536 let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
2537 .await
2538 .ok();
2539 if let Some(metadata) = metadata {
2540 let validity_check = async || {
2541 delegate
2542 .try_exec(LanguageServerBinary {
2543 path: server_path.clone(),
2544 arguments: vec!["--version".into()],
2545 env: None,
2546 })
2547 .await
2548 .inspect_err(|err| {
2549 log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)
2550 })
2551 };
2552 if let (Some(actual_digest), Some(expected_digest)) =
2553 (&metadata.digest, &expected_digest)
2554 {
2555 if actual_digest == expected_digest {
2556 if validity_check().await.is_ok() {
2557 return Ok(binary);
2558 }
2559 } else {
2560 log::info!(
2561 "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
2562 );
2563 }
2564 } else if validity_check().await.is_ok() {
2565 return Ok(binary);
2566 }
2567 }
2568
2569 download_server_binary(
2570 &*delegate.http_client(),
2571 &url,
2572 expected_digest.as_deref(),
2573 &destination_path,
2574 Self::GITHUB_ASSET_KIND,
2575 )
2576 .await?;
2577 make_file_executable(&server_path).await?;
2578 remove_matching(&container_dir, |path| path != destination_path).await;
2579 GithubBinaryMetadata::write_to_file(
2580 &GithubBinaryMetadata {
2581 metadata_version: 1,
2582 digest: expected_digest,
2583 },
2584 &metadata_path,
2585 )
2586 .await?;
2587
2588 Ok(LanguageServerBinary {
2589 path: server_path,
2590 env: None,
2591 arguments: vec!["server".into()],
2592 })
2593 }
2594
2595 async fn cached_server_binary(
2596 &self,
2597 container_dir: PathBuf,
2598 _: &dyn LspAdapterDelegate,
2599 ) -> Option<LanguageServerBinary> {
2600 maybe!(async {
2601 let mut last = None;
2602 let mut entries = self.fs.read_dir(&container_dir).await?;
2603 while let Some(entry) = entries.next().await {
2604 let path = entry?;
2605 if path.extension().is_some_and(|ext| ext == "metadata") {
2606 continue;
2607 }
2608 last = Some(path);
2609 }
2610
2611 let path = last.context("no cached binary")?;
2612 let path = match Self::GITHUB_ASSET_KIND {
2613 AssetKind::TarGz | AssetKind::TarBz2 | AssetKind::Gz => {
2614 path.join(Self::build_asset_name()?.0).join("ruff")
2615 }
2616 AssetKind::Zip => path.join("ruff.exe"),
2617 };
2618
2619 anyhow::Ok(LanguageServerBinary {
2620 path,
2621 env: None,
2622 arguments: vec!["server".into()],
2623 })
2624 })
2625 .await
2626 .log_err()
2627 }
2628}
2629
2630#[cfg(test)]
2631mod tests {
2632 use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
2633 use language::{AutoindentMode, Buffer};
2634 use settings::SettingsStore;
2635 use std::num::NonZeroU32;
2636
2637 use crate::python::python_module_name_from_relative_path;
2638
2639 #[gpui::test]
2640 async fn test_conda_activation_script_injection(cx: &mut TestAppContext) {
2641 use language::{LanguageName, Toolchain, ToolchainLister};
2642 use settings::{CondaManager, VenvSettings};
2643 use task::ShellKind;
2644
2645 use crate::python::PythonToolchainProvider;
2646
2647 cx.executor().allow_parking();
2648
2649 cx.update(|cx| {
2650 let test_settings = SettingsStore::test(cx);
2651 cx.set_global(test_settings);
2652 cx.update_global::<SettingsStore, _>(|store, cx| {
2653 store.update_user_settings(cx, |s| {
2654 s.terminal
2655 .get_or_insert_with(Default::default)
2656 .project
2657 .detect_venv = Some(VenvSettings::On {
2658 activate_script: None,
2659 venv_name: None,
2660 directories: None,
2661 conda_manager: Some(CondaManager::Conda),
2662 });
2663 });
2664 });
2665 });
2666
2667 let provider = PythonToolchainProvider;
2668 let malicious_name = "foo; rm -rf /";
2669
2670 let manager_executable = std::env::current_exe().unwrap();
2671
2672 let data = serde_json::json!({
2673 "name": malicious_name,
2674 "kind": "Conda",
2675 "executable": "/tmp/conda/bin/python",
2676 "version": serde_json::Value::Null,
2677 "prefix": serde_json::Value::Null,
2678 "arch": serde_json::Value::Null,
2679 "displayName": serde_json::Value::Null,
2680 "project": serde_json::Value::Null,
2681 "symlinks": serde_json::Value::Null,
2682 "manager": {
2683 "executable": manager_executable,
2684 "version": serde_json::Value::Null,
2685 "tool": "Conda",
2686 },
2687 });
2688
2689 let toolchain = Toolchain {
2690 name: "test".into(),
2691 path: "/tmp/conda".into(),
2692 language_name: LanguageName::new_static("Python"),
2693 as_json: data,
2694 };
2695
2696 let script = cx
2697 .update(|cx| provider.activation_script(&toolchain, ShellKind::Posix, cx))
2698 .await;
2699
2700 assert!(
2701 script
2702 .iter()
2703 .any(|s| s.contains("conda activate 'foo; rm -rf /'")),
2704 "Script should contain quoted malicious name, actual: {:?}",
2705 script
2706 );
2707 }
2708
2709 #[gpui::test]
2710 async fn test_python_autoindent(cx: &mut TestAppContext) {
2711 cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
2712 let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
2713 cx.update(|cx| {
2714 let test_settings = SettingsStore::test(cx);
2715 cx.set_global(test_settings);
2716 cx.update_global::<SettingsStore, _>(|store, cx| {
2717 store.update_user_settings(cx, |s| {
2718 s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
2719 });
2720 });
2721 });
2722
2723 cx.new(|cx| {
2724 let mut buffer = Buffer::local("", cx).with_language(language, cx);
2725 let append = |buffer: &mut Buffer, text: &str, cx: &mut Context<Buffer>| {
2726 let ix = buffer.len();
2727 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
2728 };
2729
2730 // indent after "def():"
2731 append(&mut buffer, "def a():\n", cx);
2732 assert_eq!(buffer.text(), "def a():\n ");
2733
2734 // preserve indent after blank line
2735 append(&mut buffer, "\n ", cx);
2736 assert_eq!(buffer.text(), "def a():\n \n ");
2737
2738 // indent after "if"
2739 append(&mut buffer, "if a:\n ", cx);
2740 assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
2741
2742 // preserve indent after statement
2743 append(&mut buffer, "b()\n", cx);
2744 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
2745
2746 // preserve indent after statement
2747 append(&mut buffer, "else", cx);
2748 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
2749
2750 // dedent "else""
2751 append(&mut buffer, ":", cx);
2752 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
2753
2754 // indent lines after else
2755 append(&mut buffer, "\n", cx);
2756 assert_eq!(
2757 buffer.text(),
2758 "def a():\n \n if a:\n b()\n else:\n "
2759 );
2760
2761 // indent after an open paren. the closing paren is not indented
2762 // because there is another token before it on the same line.
2763 append(&mut buffer, "foo(\n1)", cx);
2764 assert_eq!(
2765 buffer.text(),
2766 "def a():\n \n if a:\n b()\n else:\n foo(\n 1)"
2767 );
2768
2769 // dedent the closing paren if it is shifted to the beginning of the line
2770 let argument_ix = buffer.text().find('1').unwrap();
2771 buffer.edit(
2772 [(argument_ix..argument_ix + 1, "")],
2773 Some(AutoindentMode::EachLine),
2774 cx,
2775 );
2776 assert_eq!(
2777 buffer.text(),
2778 "def a():\n \n if a:\n b()\n else:\n foo(\n )"
2779 );
2780
2781 // preserve indent after the close paren
2782 append(&mut buffer, "\n", cx);
2783 assert_eq!(
2784 buffer.text(),
2785 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
2786 );
2787
2788 // manually outdent the last line
2789 let end_whitespace_ix = buffer.len() - 4;
2790 buffer.edit(
2791 [(end_whitespace_ix..buffer.len(), "")],
2792 Some(AutoindentMode::EachLine),
2793 cx,
2794 );
2795 assert_eq!(
2796 buffer.text(),
2797 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
2798 );
2799
2800 // preserve the newly reduced indentation on the next newline
2801 append(&mut buffer, "\n", cx);
2802 assert_eq!(
2803 buffer.text(),
2804 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
2805 );
2806
2807 // reset to a for loop statement
2808 let statement = "for i in range(10):\n print(i)\n";
2809 buffer.edit([(0..buffer.len(), statement)], None, cx);
2810
2811 // insert single line comment after each line
2812 let eol_ixs = statement
2813 .char_indices()
2814 .filter_map(|(ix, c)| if c == '\n' { Some(ix) } else { None })
2815 .collect::<Vec<usize>>();
2816 let editions = eol_ixs
2817 .iter()
2818 .enumerate()
2819 .map(|(i, &eol_ix)| (eol_ix..eol_ix, format!(" # comment {}", i + 1)))
2820 .collect::<Vec<(std::ops::Range<usize>, String)>>();
2821 buffer.edit(editions, Some(AutoindentMode::EachLine), cx);
2822 assert_eq!(
2823 buffer.text(),
2824 "for i in range(10): # comment 1\n print(i) # comment 2\n"
2825 );
2826
2827 // reset to a simple if statement
2828 buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx);
2829
2830 // dedent "else" on the line after a closing paren
2831 append(&mut buffer, "\n else:\n", cx);
2832 assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n ");
2833
2834 buffer
2835 });
2836 }
2837
2838 #[test]
2839 fn test_python_module_name_from_relative_path() {
2840 assert_eq!(
2841 python_module_name_from_relative_path("foo/bar.py"),
2842 Some("foo.bar".to_string())
2843 );
2844 assert_eq!(
2845 python_module_name_from_relative_path("foo/bar"),
2846 Some("foo.bar".to_string())
2847 );
2848 if cfg!(windows) {
2849 assert_eq!(
2850 python_module_name_from_relative_path("foo\\bar.py"),
2851 Some("foo.bar".to_string())
2852 );
2853 assert_eq!(
2854 python_module_name_from_relative_path("foo\\bar"),
2855 Some("foo.bar".to_string())
2856 );
2857 } else {
2858 assert_eq!(
2859 python_module_name_from_relative_path("foo\\bar.py"),
2860 Some("foo\\bar".to_string())
2861 );
2862 assert_eq!(
2863 python_module_name_from_relative_path("foo\\bar"),
2864 Some("foo\\bar".to_string())
2865 );
2866 }
2867 }
2868
2869 #[test]
2870 fn test_convert_ruff_schema() {
2871 use super::RuffLspAdapter;
2872
2873 let raw_schema = serde_json::json!({
2874 "line-length": {
2875 "doc": "The line length to use when enforcing long-lines violations",
2876 "default": "88",
2877 "value_type": "int",
2878 "scope": null,
2879 "example": "line-length = 120",
2880 "deprecated": null
2881 },
2882 "lint.select": {
2883 "doc": "A list of rule codes or prefixes to enable",
2884 "default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
2885 "value_type": "list[RuleSelector]",
2886 "scope": null,
2887 "example": "select = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
2888 "deprecated": null
2889 },
2890 "lint.isort.case-sensitive": {
2891 "doc": "Sort imports taking into account case sensitivity.",
2892 "default": "false",
2893 "value_type": "bool",
2894 "scope": null,
2895 "example": "case-sensitive = true",
2896 "deprecated": null
2897 },
2898 "format.quote-style": {
2899 "doc": "Configures the preferred quote character for strings.",
2900 "default": "\"double\"",
2901 "value_type": "\"double\" | \"single\" | \"preserve\"",
2902 "scope": null,
2903 "example": "quote-style = \"single\"",
2904 "deprecated": null
2905 }
2906 });
2907
2908 let converted = RuffLspAdapter::convert_ruff_schema(&raw_schema);
2909
2910 assert!(converted.is_object());
2911 assert_eq!(
2912 converted.get("type").and_then(|v| v.as_str()),
2913 Some("object")
2914 );
2915
2916 let properties = converted
2917 .get("properties")
2918 .expect("should have properties")
2919 .as_object()
2920 .expect("properties should be an object");
2921
2922 assert!(properties.contains_key("line-length"));
2923 assert!(properties.contains_key("lint"));
2924 assert!(properties.contains_key("format"));
2925
2926 let line_length = properties
2927 .get("line-length")
2928 .expect("should have line-length")
2929 .as_object()
2930 .expect("line-length should be an object");
2931
2932 assert_eq!(
2933 line_length.get("type").and_then(|v| v.as_str()),
2934 Some("integer")
2935 );
2936 assert_eq!(
2937 line_length.get("default").and_then(|v| v.as_str()),
2938 Some("88")
2939 );
2940
2941 let lint = properties
2942 .get("lint")
2943 .expect("should have lint")
2944 .as_object()
2945 .expect("lint should be an object");
2946
2947 let lint_props = lint
2948 .get("properties")
2949 .expect("lint should have properties")
2950 .as_object()
2951 .expect("lint properties should be an object");
2952
2953 assert!(lint_props.contains_key("select"));
2954 assert!(lint_props.contains_key("isort"));
2955
2956 let select = lint_props.get("select").expect("should have select");
2957 assert_eq!(select.get("type").and_then(|v| v.as_str()), Some("array"));
2958
2959 let isort = lint_props
2960 .get("isort")
2961 .expect("should have isort")
2962 .as_object()
2963 .expect("isort should be an object");
2964
2965 let isort_props = isort
2966 .get("properties")
2967 .expect("isort should have properties")
2968 .as_object()
2969 .expect("isort properties should be an object");
2970
2971 let case_sensitive = isort_props
2972 .get("case-sensitive")
2973 .expect("should have case-sensitive");
2974
2975 assert_eq!(
2976 case_sensitive.get("type").and_then(|v| v.as_str()),
2977 Some("boolean")
2978 );
2979 assert!(case_sensitive.get("markdownDescription").is_some());
2980
2981 let format = properties
2982 .get("format")
2983 .expect("should have format")
2984 .as_object()
2985 .expect("format should be an object");
2986
2987 let format_props = format
2988 .get("properties")
2989 .expect("format should have properties")
2990 .as_object()
2991 .expect("format properties should be an object");
2992
2993 let quote_style = format_props
2994 .get("quote-style")
2995 .expect("should have quote-style");
2996
2997 assert_eq!(
2998 quote_style.get("type").and_then(|v| v.as_str()),
2999 Some("string")
3000 );
3001
3002 let enum_values = quote_style
3003 .get("enum")
3004 .expect("should have enum")
3005 .as_array()
3006 .expect("enum should be an array");
3007
3008 assert_eq!(enum_values.len(), 3);
3009 assert!(enum_values.contains(&serde_json::json!("double")));
3010 assert!(enum_values.contains(&serde_json::json!("single")));
3011 assert!(enum_values.contains(&serde_json::json!("preserve")));
3012 }
3013}