python.rs

  1use super::installation::{npm_install_packages, npm_package_latest_version};
  2use anyhow::{anyhow, Context, Result};
  3use client::http::HttpClient;
  4use futures::{future::BoxFuture, FutureExt, StreamExt};
  5use language::{LanguageServerName, LspAdapter};
  6use smol::fs;
  7use std::{
  8    any::Any,
  9    path::{Path, PathBuf},
 10    sync::Arc,
 11};
 12use util::{ResultExt, TryFutureExt};
 13
 14pub struct PythonLspAdapter;
 15
 16impl PythonLspAdapter {
 17    const BIN_PATH: &'static str = "node_modules/pyright/langserver.index.js";
 18}
 19
 20impl LspAdapter for PythonLspAdapter {
 21    fn name(&self) -> LanguageServerName {
 22        LanguageServerName("pyright".into())
 23    }
 24
 25    fn server_args(&self) -> &[&str] {
 26        &["--stdio"]
 27    }
 28
 29    fn fetch_latest_server_version(
 30        &self,
 31        _: Arc<dyn HttpClient>,
 32    ) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
 33        async move { Ok(Box::new(npm_package_latest_version("pyright").await?) as Box<_>) }.boxed()
 34    }
 35
 36    fn fetch_server_binary(
 37        &self,
 38        version: Box<dyn 'static + Send + Any>,
 39        _: Arc<dyn HttpClient>,
 40        container_dir: Arc<Path>,
 41    ) -> BoxFuture<'static, Result<PathBuf>> {
 42        let version = version.downcast::<String>().unwrap();
 43        async move {
 44            let version_dir = container_dir.join(version.as_str());
 45            fs::create_dir_all(&version_dir)
 46                .await
 47                .context("failed to create version directory")?;
 48            let binary_path = version_dir.join(Self::BIN_PATH);
 49
 50            if fs::metadata(&binary_path).await.is_err() {
 51                npm_install_packages([("pyright", version.as_str())], &version_dir).await?;
 52
 53                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
 54                    while let Some(entry) = entries.next().await {
 55                        if let Some(entry) = entry.log_err() {
 56                            let entry_path = entry.path();
 57                            if entry_path.as_path() != version_dir {
 58                                fs::remove_dir_all(&entry_path).await.log_err();
 59                            }
 60                        }
 61                    }
 62                }
 63            }
 64
 65            Ok(binary_path)
 66        }
 67        .boxed()
 68    }
 69
 70    fn cached_server_binary(
 71        &self,
 72        container_dir: Arc<Path>,
 73    ) -> BoxFuture<'static, Option<PathBuf>> {
 74        async move {
 75            let mut last_version_dir = None;
 76            let mut entries = fs::read_dir(&container_dir).await?;
 77            while let Some(entry) = entries.next().await {
 78                let entry = entry?;
 79                if entry.file_type().await?.is_dir() {
 80                    last_version_dir = Some(entry.path());
 81                }
 82            }
 83            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
 84            let bin_path = last_version_dir.join(Self::BIN_PATH);
 85            if bin_path.exists() {
 86                Ok(bin_path)
 87            } else {
 88                Err(anyhow!(
 89                    "missing executable in directory {:?}",
 90                    last_version_dir
 91                ))
 92            }
 93        }
 94        .log_err()
 95        .boxed()
 96    }
 97
 98    fn label_for_completion(
 99        &self,
100        item: &lsp::CompletionItem,
101        language: &language::Language,
102    ) -> Option<language::CodeLabel> {
103        let label = &item.label;
104        let grammar = language.grammar()?;
105        let highlight_id = match item.kind? {
106            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
107            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
108            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
109            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
110            _ => return None,
111        };
112        Some(language::CodeLabel {
113            text: label.clone(),
114            runs: vec![(0..label.len(), highlight_id)],
115            filter_range: 0..label.len(),
116        })
117    }
118
119    fn label_for_symbol(
120        &self,
121        name: &str,
122        kind: lsp::SymbolKind,
123        language: &language::Language,
124    ) -> Option<language::CodeLabel> {
125        let (text, filter_range, display_range) = match kind {
126            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
127                let text = format!("def {}():\n", name);
128                let filter_range = 4..4 + name.len();
129                let display_range = 0..filter_range.end;
130                (text, filter_range, display_range)
131            }
132            lsp::SymbolKind::CLASS => {
133                let text = format!("class {}:", name);
134                let filter_range = 6..6 + name.len();
135                let display_range = 0..filter_range.end;
136                (text, filter_range, display_range)
137            }
138            lsp::SymbolKind::CONSTANT => {
139                let text = format!("{} = 0", name);
140                let filter_range = 0..name.len();
141                let display_range = 0..filter_range.end;
142                (text, filter_range, display_range)
143            }
144            _ => return None,
145        };
146
147        Some(language::CodeLabel {
148            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
149            text: text[display_range].to_string(),
150            filter_range,
151        })
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use gpui::{ModelContext, MutableAppContext};
158    use language::{Buffer, IndentSize};
159    use std::sync::Arc;
160
161    #[gpui::test]
162    fn test_python_autoindent(cx: &mut MutableAppContext) {
163        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
164        let language = crate::languages::language("python", tree_sitter_python::language(), None);
165
166        cx.add_model(|cx| {
167            let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
168            let size = IndentSize::spaces(2);
169            let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
170                let ix = buffer.len();
171                buffer.edit_with_autoindent([(ix..ix, text)], size, cx);
172            };
173
174            // indent after "def():"
175            append(&mut buffer, "def a():\n", cx);
176            assert_eq!(buffer.text(), "def a():\n  ");
177
178            // preserve indent after blank line
179            append(&mut buffer, "\n  ", cx);
180            assert_eq!(buffer.text(), "def a():\n  \n  ");
181
182            // indent after "if"
183            append(&mut buffer, "if a:\n  ", cx);
184            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
185
186            // preserve indent after statement
187            append(&mut buffer, "b()\n", cx);
188            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
189
190            // preserve indent after statement
191            append(&mut buffer, "else", cx);
192            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
193
194            // dedent "else""
195            append(&mut buffer, ":", cx);
196            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
197
198            // indent lines after else
199            append(&mut buffer, "\n", cx);
200            assert_eq!(
201                buffer.text(),
202                "def a():\n  \n  if a:\n    b()\n  else:\n    "
203            );
204
205            // indent after an open paren. the closing  paren is not indented
206            // because there is another token before it on the same line.
207            append(&mut buffer, "foo(\n1)", cx);
208            assert_eq!(
209                buffer.text(),
210                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
211            );
212
213            // dedent the closing paren if it is shifted to the beginning of the line
214            let argument_ix = buffer.text().find("1").unwrap();
215            buffer.edit_with_autoindent([(argument_ix..argument_ix + 1, "")], size, cx);
216            assert_eq!(
217                buffer.text(),
218                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
219            );
220
221            // preserve indent after the close paren
222            append(&mut buffer, "\n", cx);
223            assert_eq!(
224                buffer.text(),
225                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
226            );
227
228            // manually outdent the last line
229            let end_whitespace_ix = buffer.len() - 4;
230            buffer.edit_with_autoindent([(end_whitespace_ix..buffer.len(), "")], size, cx);
231            assert_eq!(
232                buffer.text(),
233                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
234            );
235
236            // preserve the newly reduced indentation on the next newline
237            append(&mut buffer, "\n", cx);
238            assert_eq!(
239                buffer.text(),
240                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
241            );
242
243            // reset to a simple if statement
244            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], cx);
245
246            // dedent "else" on the line after a closing paren
247            append(&mut buffer, "\n  else:\n", cx);
248            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
249
250            buffer
251        });
252    }
253}