python.rs

  1use anyhow::{anyhow, Result};
  2use async_trait::async_trait;
  3use futures::StreamExt;
  4use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
  5use node_runtime::NodeRuntime;
  6use smol::fs;
  7use std::{
  8    any::Any,
  9    ffi::OsString,
 10    path::{Path, PathBuf},
 11    sync::Arc,
 12};
 13use util::ResultExt;
 14
 15fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 16    vec![server_path.into(), "--stdio".into()]
 17}
 18
 19pub struct PythonLspAdapter {
 20    node: Arc<NodeRuntime>,
 21}
 22
 23impl PythonLspAdapter {
 24    const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
 25
 26    pub fn new(node: Arc<NodeRuntime>) -> Self {
 27        PythonLspAdapter { node }
 28    }
 29}
 30
 31#[async_trait]
 32impl LspAdapter for PythonLspAdapter {
 33    async fn name(&self) -> LanguageServerName {
 34        LanguageServerName("pyright".into())
 35    }
 36
 37    async fn fetch_latest_server_version(
 38        &self,
 39        _: &dyn LspAdapterDelegate,
 40    ) -> Result<Box<dyn 'static + Any + Send>> {
 41        Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
 42    }
 43
 44    async fn fetch_server_binary(
 45        &self,
 46        version: Box<dyn 'static + Send + Any>,
 47        container_dir: PathBuf,
 48        _: &dyn LspAdapterDelegate,
 49    ) -> Result<LanguageServerBinary> {
 50        let version = version.downcast::<String>().unwrap();
 51        let server_path = container_dir.join(Self::SERVER_PATH);
 52
 53        if fs::metadata(&server_path).await.is_err() {
 54            self.node
 55                .npm_install_packages(&container_dir, [("pyright", version.as_str())])
 56                .await?;
 57        }
 58
 59        Ok(LanguageServerBinary {
 60            path: self.node.binary_path().await?,
 61            arguments: server_binary_arguments(&server_path),
 62        })
 63    }
 64
 65    async fn cached_server_binary(
 66        &self,
 67        container_dir: PathBuf,
 68        _: &dyn LspAdapterDelegate,
 69    ) -> Option<LanguageServerBinary> {
 70        (|| async move {
 71            let mut last_version_dir = None;
 72            let mut entries = fs::read_dir(&container_dir).await?;
 73            while let Some(entry) = entries.next().await {
 74                let entry = entry?;
 75                if entry.file_type().await?.is_dir() {
 76                    last_version_dir = Some(entry.path());
 77                }
 78            }
 79            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
 80            let server_path = last_version_dir.join(Self::SERVER_PATH);
 81            if server_path.exists() {
 82                Ok(LanguageServerBinary {
 83                    path: self.node.binary_path().await?,
 84                    arguments: server_binary_arguments(&server_path),
 85                })
 86            } else {
 87                Err(anyhow!(
 88                    "missing executable in directory {:?}",
 89                    last_version_dir
 90                ))
 91            }
 92        })()
 93        .await
 94        .log_err()
 95    }
 96
 97    async fn process_completion(&self, item: &mut lsp::CompletionItem) {
 98        // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
 99        // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
100        // and `name` is the symbol name itself.
101        //
102        // Because the the symbol name is included, there generally are not ties when
103        // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
104        // into account. Here, we remove the symbol name from the sortText in order
105        // to allow our own fuzzy score to be used to break ties.
106        //
107        // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
108        let Some(sort_text) = &mut item.sort_text else { return };
109        let mut parts = sort_text.split('.');
110        let Some(first) = parts.next() else { return };
111        let Some(second) = parts.next() else { return };
112        let Some(_) = parts.next() else { return };
113        sort_text.replace_range(first.len() + second.len() + 1.., "");
114    }
115
116    async fn label_for_completion(
117        &self,
118        item: &lsp::CompletionItem,
119        language: &Arc<language::Language>,
120    ) -> Option<language::CodeLabel> {
121        let label = &item.label;
122        let grammar = language.grammar()?;
123        let highlight_id = match item.kind? {
124            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
125            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
126            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
127            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
128            _ => return None,
129        };
130        Some(language::CodeLabel {
131            text: label.clone(),
132            runs: vec![(0..label.len(), highlight_id)],
133            filter_range: 0..label.len(),
134        })
135    }
136
137    async fn label_for_symbol(
138        &self,
139        name: &str,
140        kind: lsp::SymbolKind,
141        language: &Arc<language::Language>,
142    ) -> Option<language::CodeLabel> {
143        let (text, filter_range, display_range) = match kind {
144            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
145                let text = format!("def {}():\n", name);
146                let filter_range = 4..4 + name.len();
147                let display_range = 0..filter_range.end;
148                (text, filter_range, display_range)
149            }
150            lsp::SymbolKind::CLASS => {
151                let text = format!("class {}:", name);
152                let filter_range = 6..6 + name.len();
153                let display_range = 0..filter_range.end;
154                (text, filter_range, display_range)
155            }
156            lsp::SymbolKind::CONSTANT => {
157                let text = format!("{} = 0", name);
158                let filter_range = 0..name.len();
159                let display_range = 0..filter_range.end;
160                (text, filter_range, display_range)
161            }
162            _ => return None,
163        };
164
165        Some(language::CodeLabel {
166            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
167            text: text[display_range].to_string(),
168            filter_range,
169        })
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use gpui::{ModelContext, TestAppContext};
176    use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
177    use settings::SettingsStore;
178    use std::num::NonZeroU32;
179
180    #[gpui::test]
181    async fn test_python_autoindent(cx: &mut TestAppContext) {
182        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
183        let language =
184            crate::languages::language("python", tree_sitter_python::language(), None).await;
185        cx.update(|cx| {
186            cx.set_global(SettingsStore::test(cx));
187            language::init(cx);
188            cx.update_global::<SettingsStore, _, _>(|store, cx| {
189                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
190                    s.defaults.tab_size = NonZeroU32::new(2);
191                });
192            });
193        });
194
195        cx.add_model(|cx| {
196            let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
197            let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
198                let ix = buffer.len();
199                buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
200            };
201
202            // indent after "def():"
203            append(&mut buffer, "def a():\n", cx);
204            assert_eq!(buffer.text(), "def a():\n  ");
205
206            // preserve indent after blank line
207            append(&mut buffer, "\n  ", cx);
208            assert_eq!(buffer.text(), "def a():\n  \n  ");
209
210            // indent after "if"
211            append(&mut buffer, "if a:\n  ", cx);
212            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
213
214            // preserve indent after statement
215            append(&mut buffer, "b()\n", cx);
216            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
217
218            // preserve indent after statement
219            append(&mut buffer, "else", cx);
220            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
221
222            // dedent "else""
223            append(&mut buffer, ":", cx);
224            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
225
226            // indent lines after else
227            append(&mut buffer, "\n", cx);
228            assert_eq!(
229                buffer.text(),
230                "def a():\n  \n  if a:\n    b()\n  else:\n    "
231            );
232
233            // indent after an open paren. the closing  paren is not indented
234            // because there is another token before it on the same line.
235            append(&mut buffer, "foo(\n1)", cx);
236            assert_eq!(
237                buffer.text(),
238                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
239            );
240
241            // dedent the closing paren if it is shifted to the beginning of the line
242            let argument_ix = buffer.text().find('1').unwrap();
243            buffer.edit(
244                [(argument_ix..argument_ix + 1, "")],
245                Some(AutoindentMode::EachLine),
246                cx,
247            );
248            assert_eq!(
249                buffer.text(),
250                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
251            );
252
253            // preserve indent after the close paren
254            append(&mut buffer, "\n", cx);
255            assert_eq!(
256                buffer.text(),
257                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
258            );
259
260            // manually outdent the last line
261            let end_whitespace_ix = buffer.len() - 4;
262            buffer.edit(
263                [(end_whitespace_ix..buffer.len(), "")],
264                Some(AutoindentMode::EachLine),
265                cx,
266            );
267            assert_eq!(
268                buffer.text(),
269                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
270            );
271
272            // preserve the newly reduced indentation on the next newline
273            append(&mut buffer, "\n", cx);
274            assert_eq!(
275                buffer.text(),
276                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
277            );
278
279            // reset to a simple if statement
280            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], None, cx);
281
282            // dedent "else" on the line after a closing paren
283            append(&mut buffer, "\n  else:\n", cx);
284            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
285
286            buffer
287        });
288    }
289}