python.rs

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