Merge pull request #1228 from zed-industries/python

Max Brunsfeld created

Add Python support

Change summary

Cargo.lock                                     |  15 +
crates/language/src/buffer.rs                  |   2 
crates/lsp/src/lsp.rs                          |   7 
crates/zed/Cargo.toml                          |   3 
crates/zed/src/languages.rs                    |   6 
crates/zed/src/languages/python.rs             | 153 ++++++++++++++++++++
crates/zed/src/languages/python/brackets.scm   |   3 
crates/zed/src/languages/python/config.toml    |  11 +
crates/zed/src/languages/python/highlights.scm | 125 ++++++++++++++++
crates/zed/src/languages/python/indents.scm    |   4 
crates/zed/src/languages/python/outline.scm    |   9 +
styles/src/themes/common/base16.ts             |   4 
12 files changed, 336 insertions(+), 6 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5214,9 +5214,9 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter"
-version = "0.20.6"
+version = "0.20.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09b3b781640108d29892e8b9684642d2cda5ea05951fd58f0fea1db9edeb9b71"
+checksum = "549a9faf45679ad50b7f603253635598cf5e007d8ceb806a23f95355938f76a0"
 dependencies = [
  "cc",
  "regex",
@@ -5279,6 +5279,16 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-python"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713170684ba94376b784b0c6dd23693461e15f96a806ed1848e40996e3cda7c7"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-rust"
 version = "0.20.1"
@@ -6032,6 +6042,7 @@ dependencies = [
  "tree-sitter-go",
  "tree-sitter-json 0.20.0",
  "tree-sitter-markdown",
+ "tree-sitter-python",
  "tree-sitter-rust",
  "tree-sitter-toml",
  "tree-sitter-typescript",

crates/language/src/buffer.rs 🔗

@@ -1902,7 +1902,7 @@ impl BufferSnapshot {
                 }
 
                 while stack.last().map_or(false, |prev_range| {
-                    !prev_range.contains(&item_range.start) || !prev_range.contains(&item_range.end)
+                    prev_range.start > item_range.start || prev_range.end < item_range.end
                 }) {
                     stack.pop();
                 }

crates/lsp/src/lsp.rs 🔗

@@ -251,7 +251,7 @@ impl LanguageServer {
         let params = InitializeParams {
             process_id: Default::default(),
             root_path: Default::default(),
-            root_uri: Some(root_uri),
+            root_uri: Some(root_uri.clone()),
             initialization_options: options,
             capabilities: ClientCapabilities {
                 workspace: Some(WorkspaceClientCapabilities {
@@ -312,7 +312,10 @@ impl LanguageServer {
                 ..Default::default()
             },
             trace: Default::default(),
-            workspace_folders: Default::default(),
+            workspace_folders: Some(vec![WorkspaceFolder {
+                uri: root_uri,
+                name: Default::default(),
+            }]),
             client_info: Default::default(),
             locale: Default::default(),
         };

crates/zed/Cargo.toml 🔗

@@ -87,13 +87,14 @@ tempdir = { version = "0.3.7" }
 thiserror = "1.0.29"
 tiny_http = "0.8"
 toml = "0.5"
-tree-sitter = "0.20.6"
+tree-sitter = "0.20.7"
 tree-sitter-c = "0.20.1"
 tree-sitter-cpp = "0.20.0"
 tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
 tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
 tree-sitter-rust = "0.20.1"
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
+tree-sitter-python = "0.20.1"
 tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
 tree-sitter-typescript = "0.20.1"
 url = "2.2"

crates/zed/src/languages.rs 🔗

@@ -7,6 +7,7 @@ mod c;
 mod go;
 mod installation;
 mod json;
+mod python;
 mod rust;
 mod typescript;
 
@@ -43,6 +44,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi
             tree_sitter_markdown::language(),
             None, //
         ),
+        (
+            "python",
+            tree_sitter_python::language(),
+            Some(Arc::new(python::PythonLspAdapter)),
+        ),
         (
             "rust",
             tree_sitter_rust::language(),

crates/zed/src/languages/python.rs 🔗

@@ -0,0 +1,153 @@
+use super::installation::{npm_install_packages, npm_package_latest_version};
+use anyhow::{anyhow, Context, Result};
+use client::http::HttpClient;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use language::{LanguageServerName, LspAdapter};
+use smol::fs;
+use std::{
+    any::Any,
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+use util::{ResultExt, TryFutureExt};
+
+pub struct PythonLspAdapter;
+
+impl PythonLspAdapter {
+    const BIN_PATH: &'static str = "node_modules/pyright/langserver.index.js";
+}
+
+impl LspAdapter for PythonLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("pyright".into())
+    }
+
+    fn server_args(&self) -> &[&str] {
+        &["--stdio"]
+    }
+
+    fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
+        async move { Ok(Box::new(npm_package_latest_version("pyright").await?) as Box<_>) }.boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        container_dir: Arc<Path>,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        let version = version.downcast::<String>().unwrap();
+        async move {
+            let version_dir = container_dir.join(version.as_str());
+            fs::create_dir_all(&version_dir)
+                .await
+                .context("failed to create version directory")?;
+            let binary_path = version_dir.join(Self::BIN_PATH);
+
+            if fs::metadata(&binary_path).await.is_err() {
+                npm_install_packages([("pyright", version.as_str())], &version_dir).await?;
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(
+        &self,
+        container_dir: Arc<Path>,
+    ) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let mut last_version_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_version_dir = Some(entry.path());
+                }
+            }
+            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let bin_path = last_version_dir.join(Self::BIN_PATH);
+            if bin_path.exists() {
+                Ok(bin_path)
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    last_version_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn label_for_completion(
+        &self,
+        item: &lsp::CompletionItem,
+        language: &language::Language,
+    ) -> Option<language::CodeLabel> {
+        let label = &item.label;
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
+            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
+            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
+            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
+            _ => return None,
+        };
+        Some(language::CodeLabel {
+            text: label.clone(),
+            runs: vec![(0..label.len(), highlight_id)],
+            filter_range: 0..label.len(),
+        })
+    }
+
+    fn label_for_symbol(
+        &self,
+        name: &str,
+        kind: lsp::SymbolKind,
+        language: &language::Language,
+    ) -> Option<language::CodeLabel> {
+        let (text, filter_range, display_range) = match kind {
+            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
+                let text = format!("def {}():\n", name);
+                let filter_range = 4..4 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::CLASS => {
+                let text = format!("class {}:", name);
+                let filter_range = 6..6 + name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            lsp::SymbolKind::CONSTANT => {
+                let text = format!("{} = 0", name);
+                let filter_range = 0..name.len();
+                let display_range = 0..filter_range.end;
+                (text, filter_range, display_range)
+            }
+            _ => return None,
+        };
+
+        Some(language::CodeLabel {
+            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
+            text: text[display_range].to_string(),
+            filter_range,
+        })
+    }
+}

crates/zed/src/languages/python/config.toml 🔗

@@ -0,0 +1,11 @@
+name = "Python"
+path_suffixes = ["py", "pyi"]
+line_comment = "# "
+autoclose_before = ";:.,=}])>"
+brackets = [
+  { start = "{", end = "}", close = true, newline = true },
+  { start = "[", end = "]", close = true, newline = true },
+  { start = "(", end = ")", close = true, newline = true },
+  { start = "\"", end = "\"", close = true, newline = false },
+  { start = "'", end = "'", close = false, newline = false },
+]

crates/zed/src/languages/python/highlights.scm 🔗

@@ -0,0 +1,125 @@
+(attribute attribute: (identifier) @property)
+(type (identifier) @type)
+
+; Function calls
+
+(decorator) @function
+
+(call
+  function: (attribute attribute: (identifier) @function.method))
+(call
+  function: (identifier) @function)
+
+; Function definitions
+
+(function_definition
+  name: (identifier) @function)
+
+; Identifier naming conventions
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+((identifier) @constant
+ (#match? @constant "^[A-Z][A-Z_]*$"))
+
+; Builtin functions
+
+((call
+  function: (identifier) @function.builtin)
+ (#match?
+   @function.builtin
+   "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
+
+; Literals
+
+[
+  (none)
+  (true)
+  (false)
+] @constant.builtin
+
+[
+  (integer)
+  (float)
+] @number
+
+(comment) @comment
+(string) @string
+(escape_sequence) @escape
+
+(interpolation
+  "{" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+[
+  "-"
+  "-="
+  "!="
+  "*"
+  "**"
+  "**="
+  "*="
+  "/"
+  "//"
+  "//="
+  "/="
+  "&"
+  "%"
+  "%="
+  "^"
+  "+"
+  "->"
+  "+="
+  "<"
+  "<<"
+  "<="
+  "<>"
+  "="
+  ":="
+  "=="
+  ">"
+  ">="
+  ">>"
+  "|"
+  "~"
+  "and"
+  "in"
+  "is"
+  "not"
+  "or"
+] @operator
+
+[
+  "as"
+  "assert"
+  "async"
+  "await"
+  "break"
+  "class"
+  "continue"
+  "def"
+  "del"
+  "elif"
+  "else"
+  "except"
+  "exec"
+  "finally"
+  "for"
+  "from"
+  "global"
+  "if"
+  "import"
+  "lambda"
+  "nonlocal"
+  "pass"
+  "print"
+  "raise"
+  "return"
+  "try"
+  "while"
+  "with"
+  "yield"
+  "match"
+  "case"
+] @keyword

styles/src/themes/common/base16.ts 🔗

@@ -171,6 +171,10 @@ export function createTheme(
       color: sample(ramps.cyan, 0.5),
       weight: fontWeights.normal,
     },
+    constructor: {
+      color: sample(ramps.blue, 0.5),
+      weight: fontWeights.normal,
+    },
     variant: {
       color: sample(ramps.blue, 0.5),
       weight: fontWeights.normal,