1use anyhow::Result;
2use async_trait::async_trait;
3use collections::HashMap;
4use gpui::AppContext;
5use gpui::AsyncAppContext;
6use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
7use lsp::LanguageServerBinary;
8use node_runtime::NodeRuntime;
9use project::lsp_store::language_server_settings;
10use serde_json::Value;
11
12use std::{
13 any::Any,
14 borrow::Cow,
15 ffi::OsString,
16 path::{Path, PathBuf},
17 sync::Arc,
18};
19use task::{TaskTemplate, TaskTemplates, VariableName};
20use util::ResultExt;
21
22const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
23const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
24
25fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
26 vec![server_path.into(), "--stdio".into()]
27}
28
29pub struct PythonLspAdapter {
30 node: NodeRuntime,
31}
32
33impl PythonLspAdapter {
34 const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");
35
36 pub fn new(node: NodeRuntime) -> Self {
37 PythonLspAdapter { node }
38 }
39}
40
41#[async_trait(?Send)]
42impl LspAdapter for PythonLspAdapter {
43 fn name(&self) -> LanguageServerName {
44 Self::SERVER_NAME.clone()
45 }
46
47 async fn check_if_user_installed(
48 &self,
49 delegate: &dyn LspAdapterDelegate,
50 _: &AsyncAppContext,
51 ) -> Option<LanguageServerBinary> {
52 let node = delegate.which("node".as_ref()).await?;
53 let (node_modules_path, _) = delegate
54 .npm_package_installed_version(Self::SERVER_NAME.as_ref())
55 .await
56 .log_err()??;
57
58 let path = node_modules_path.join(NODE_MODULE_RELATIVE_SERVER_PATH);
59
60 Some(LanguageServerBinary {
61 path: node,
62 env: None,
63 arguments: server_binary_arguments(&path),
64 })
65 }
66
67 async fn fetch_latest_server_version(
68 &self,
69 _: &dyn LspAdapterDelegate,
70 ) -> Result<Box<dyn 'static + Any + Send>> {
71 Ok(Box::new(
72 self.node
73 .npm_package_latest_version(Self::SERVER_NAME.as_ref())
74 .await?,
75 ) as Box<_>)
76 }
77
78 async fn fetch_server_binary(
79 &self,
80 latest_version: Box<dyn 'static + Send + Any>,
81 container_dir: PathBuf,
82 _: &dyn LspAdapterDelegate,
83 ) -> Result<LanguageServerBinary> {
84 let latest_version = latest_version.downcast::<String>().unwrap();
85 let server_path = container_dir.join(SERVER_PATH);
86
87 let should_install_language_server = self
88 .node
89 .should_install_npm_package(
90 Self::SERVER_NAME.as_ref(),
91 &server_path,
92 &container_dir,
93 &latest_version,
94 )
95 .await;
96
97 if should_install_language_server {
98 self.node
99 .npm_install_packages(
100 &container_dir,
101 &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
102 )
103 .await?;
104 }
105
106 Ok(LanguageServerBinary {
107 path: self.node.binary_path().await?,
108 env: None,
109 arguments: server_binary_arguments(&server_path),
110 })
111 }
112
113 async fn cached_server_binary(
114 &self,
115 container_dir: PathBuf,
116 _: &dyn LspAdapterDelegate,
117 ) -> Option<LanguageServerBinary> {
118 get_cached_server_binary(container_dir, &self.node).await
119 }
120
121 async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
122 // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
123 // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
124 // and `name` is the symbol name itself.
125 //
126 // Because the symbol name is included, there generally are not ties when
127 // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
128 // into account. Here, we remove the symbol name from the sortText in order
129 // to allow our own fuzzy score to be used to break ties.
130 //
131 // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
132 for item in items {
133 let Some(sort_text) = &mut item.sort_text else {
134 continue;
135 };
136 let mut parts = sort_text.split('.');
137 let Some(first) = parts.next() else { continue };
138 let Some(second) = parts.next() else { continue };
139 let Some(_) = parts.next() else { continue };
140 sort_text.replace_range(first.len() + second.len() + 1.., "");
141 }
142 }
143
144 async fn label_for_completion(
145 &self,
146 item: &lsp::CompletionItem,
147 language: &Arc<language::Language>,
148 ) -> Option<language::CodeLabel> {
149 let label = &item.label;
150 let grammar = language.grammar()?;
151 let highlight_id = match item.kind? {
152 lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
153 lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
154 lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
155 lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
156 _ => return None,
157 };
158 Some(language::CodeLabel {
159 text: label.clone(),
160 runs: vec![(0..label.len(), highlight_id)],
161 filter_range: 0..label.len(),
162 })
163 }
164
165 async fn label_for_symbol(
166 &self,
167 name: &str,
168 kind: lsp::SymbolKind,
169 language: &Arc<language::Language>,
170 ) -> Option<language::CodeLabel> {
171 let (text, filter_range, display_range) = match kind {
172 lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
173 let text = format!("def {}():\n", name);
174 let filter_range = 4..4 + name.len();
175 let display_range = 0..filter_range.end;
176 (text, filter_range, display_range)
177 }
178 lsp::SymbolKind::CLASS => {
179 let text = format!("class {}:", name);
180 let filter_range = 6..6 + name.len();
181 let display_range = 0..filter_range.end;
182 (text, filter_range, display_range)
183 }
184 lsp::SymbolKind::CONSTANT => {
185 let text = format!("{} = 0", name);
186 let filter_range = 0..name.len();
187 let display_range = 0..filter_range.end;
188 (text, filter_range, display_range)
189 }
190 _ => return None,
191 };
192
193 Some(language::CodeLabel {
194 runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
195 text: text[display_range].to_string(),
196 filter_range,
197 })
198 }
199
200 async fn workspace_configuration(
201 self: Arc<Self>,
202 adapter: &Arc<dyn LspAdapterDelegate>,
203 cx: &mut AsyncAppContext,
204 ) -> Result<Value> {
205 cx.update(|cx| {
206 language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
207 .and_then(|s| s.settings.clone())
208 .unwrap_or_default()
209 })
210 }
211}
212
213async fn get_cached_server_binary(
214 container_dir: PathBuf,
215 node: &NodeRuntime,
216) -> Option<LanguageServerBinary> {
217 let server_path = container_dir.join(SERVER_PATH);
218 if server_path.exists() {
219 Some(LanguageServerBinary {
220 path: node.binary_path().await.log_err()?,
221 env: None,
222 arguments: server_binary_arguments(&server_path),
223 })
224 } else {
225 log::error!("missing executable in directory {:?}", server_path);
226 None
227 }
228}
229
230pub(crate) struct PythonContextProvider;
231
232const PYTHON_UNITTEST_TARGET_TASK_VARIABLE: VariableName =
233 VariableName::Custom(Cow::Borrowed("PYTHON_UNITTEST_TARGET"));
234
235impl ContextProvider for PythonContextProvider {
236 fn build_context(
237 &self,
238 variables: &task::TaskVariables,
239 _location: &project::Location,
240 _: Option<&HashMap<String, String>>,
241 _cx: &mut gpui::AppContext,
242 ) -> Result<task::TaskVariables> {
243 let python_module_name = python_module_name_from_relative_path(
244 variables.get(&VariableName::RelativeFile).unwrap_or(""),
245 );
246 let unittest_class_name =
247 variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
248 let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
249 "_unittest_method_name",
250 )));
251
252 let unittest_target_str = match (unittest_class_name, unittest_method_name) {
253 (Some(class_name), Some(method_name)) => {
254 format!("{}.{}.{}", python_module_name, class_name, method_name)
255 }
256 (Some(class_name), None) => format!("{}.{}", python_module_name, class_name),
257 (None, None) => python_module_name,
258 (None, Some(_)) => return Ok(task::TaskVariables::default()), // should never happen, a TestCase class is the unit of testing
259 };
260
261 let unittest_target = (
262 PYTHON_UNITTEST_TARGET_TASK_VARIABLE.clone(),
263 unittest_target_str,
264 );
265
266 Ok(task::TaskVariables::from_iter([unittest_target]))
267 }
268
269 fn associated_tasks(
270 &self,
271 _: Option<Arc<dyn language::File>>,
272 _: &AppContext,
273 ) -> Option<TaskTemplates> {
274 Some(TaskTemplates(vec![
275 TaskTemplate {
276 label: "execute selection".to_owned(),
277 command: "python3".to_owned(),
278 args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
279 ..TaskTemplate::default()
280 },
281 TaskTemplate {
282 label: format!("run '{}'", VariableName::File.template_value()),
283 command: "python3".to_owned(),
284 args: vec![VariableName::File.template_value()],
285 ..TaskTemplate::default()
286 },
287 TaskTemplate {
288 label: format!("unittest '{}'", VariableName::File.template_value()),
289 command: "python3".to_owned(),
290 args: vec![
291 "-m".to_owned(),
292 "unittest".to_owned(),
293 VariableName::File.template_value(),
294 ],
295 ..TaskTemplate::default()
296 },
297 TaskTemplate {
298 label: "unittest $ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(),
299 command: "python3".to_owned(),
300 args: vec![
301 "-m".to_owned(),
302 "unittest".to_owned(),
303 "$ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(),
304 ],
305 tags: vec![
306 "python-unittest-class".to_owned(),
307 "python-unittest-method".to_owned(),
308 ],
309 ..TaskTemplate::default()
310 },
311 ]))
312 }
313}
314
315fn python_module_name_from_relative_path(relative_path: &str) -> String {
316 let path_with_dots = relative_path.replace('/', ".");
317 path_with_dots
318 .strip_suffix(".py")
319 .unwrap_or(&path_with_dots)
320 .to_string()
321}
322
323#[cfg(test)]
324mod tests {
325 use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
326 use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
327 use settings::SettingsStore;
328 use std::num::NonZeroU32;
329
330 #[gpui::test]
331 async fn test_python_autoindent(cx: &mut TestAppContext) {
332 cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
333 let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
334 cx.update(|cx| {
335 let test_settings = SettingsStore::test(cx);
336 cx.set_global(test_settings);
337 language::init(cx);
338 cx.update_global::<SettingsStore, _>(|store, cx| {
339 store.update_user_settings::<AllLanguageSettings>(cx, |s| {
340 s.defaults.tab_size = NonZeroU32::new(2);
341 });
342 });
343 });
344
345 cx.new_model(|cx| {
346 let mut buffer = Buffer::local("", cx).with_language(language, cx);
347 let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
348 let ix = buffer.len();
349 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
350 };
351
352 // indent after "def():"
353 append(&mut buffer, "def a():\n", cx);
354 assert_eq!(buffer.text(), "def a():\n ");
355
356 // preserve indent after blank line
357 append(&mut buffer, "\n ", cx);
358 assert_eq!(buffer.text(), "def a():\n \n ");
359
360 // indent after "if"
361 append(&mut buffer, "if a:\n ", cx);
362 assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
363
364 // preserve indent after statement
365 append(&mut buffer, "b()\n", cx);
366 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
367
368 // preserve indent after statement
369 append(&mut buffer, "else", cx);
370 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
371
372 // dedent "else""
373 append(&mut buffer, ":", cx);
374 assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
375
376 // indent lines after else
377 append(&mut buffer, "\n", cx);
378 assert_eq!(
379 buffer.text(),
380 "def a():\n \n if a:\n b()\n else:\n "
381 );
382
383 // indent after an open paren. the closing paren is not indented
384 // because there is another token before it on the same line.
385 append(&mut buffer, "foo(\n1)", cx);
386 assert_eq!(
387 buffer.text(),
388 "def a():\n \n if a:\n b()\n else:\n foo(\n 1)"
389 );
390
391 // dedent the closing paren if it is shifted to the beginning of the line
392 let argument_ix = buffer.text().find('1').unwrap();
393 buffer.edit(
394 [(argument_ix..argument_ix + 1, "")],
395 Some(AutoindentMode::EachLine),
396 cx,
397 );
398 assert_eq!(
399 buffer.text(),
400 "def a():\n \n if a:\n b()\n else:\n foo(\n )"
401 );
402
403 // preserve indent after the close paren
404 append(&mut buffer, "\n", cx);
405 assert_eq!(
406 buffer.text(),
407 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
408 );
409
410 // manually outdent the last line
411 let end_whitespace_ix = buffer.len() - 4;
412 buffer.edit(
413 [(end_whitespace_ix..buffer.len(), "")],
414 Some(AutoindentMode::EachLine),
415 cx,
416 );
417 assert_eq!(
418 buffer.text(),
419 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
420 );
421
422 // preserve the newly reduced indentation on the next newline
423 append(&mut buffer, "\n", cx);
424 assert_eq!(
425 buffer.text(),
426 "def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
427 );
428
429 // reset to a simple if statement
430 buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx);
431
432 // dedent "else" on the line after a closing paren
433 append(&mut buffer, "\n else:\n", cx);
434 assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n ");
435
436 buffer
437 });
438 }
439}