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