python.rs

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