From 209ff619ef3ff53d72b78bab0f11bc06914e037d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Jun 2022 16:26:01 -0700 Subject: [PATCH 01/12] Simplify interface of `latest_github_release` helper function --- crates/zed/src/languages/c.rs | 15 ++++++++++---- crates/zed/src/languages/installation.rs | 26 +++++++----------------- crates/zed/src/languages/rust.rs | 15 ++++++++++---- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 99358453f4..fd92e419a7 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -19,10 +19,17 @@ impl super::LspAdapter for CLspAdapter { http: Arc, ) -> BoxFuture<'static, Result>> { async move { - let version = latest_github_release("clangd/clangd", http, |release_name| { - format!("clangd-mac-{release_name}.zip") - }) - .await?; + let release = latest_github_release("clangd/clangd", http).await?; + let asset_name = format!("clangd-mac-{}.zip", release.name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }; Ok(Box::new(version) as Box<_>) } .boxed() diff --git a/crates/zed/src/languages/installation.rs b/crates/zed/src/languages/installation.rs index ede00e33d8..e0738a2fa5 100644 --- a/crates/zed/src/languages/installation.rs +++ b/crates/zed/src/languages/installation.rs @@ -25,14 +25,14 @@ struct NpmInfoDistTags { #[derive(Deserialize)] pub(crate) struct GithubRelease { - name: String, - assets: Vec, + pub name: String, + pub assets: Vec, } #[derive(Deserialize)] pub(crate) struct GithubReleaseAsset { - name: String, - browser_download_url: String, + pub name: String, + pub browser_download_url: String, } pub async fn npm_package_latest_version(name: &str) -> Result { @@ -78,11 +78,10 @@ pub async fn npm_install_packages( Ok(()) } -pub async fn latest_github_release( +pub(crate) async fn latest_github_release( repo_name_with_owner: &str, http: Arc, - asset_name: impl Fn(&str) -> String, -) -> Result { +) -> Result { let mut response = http .get( &format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"), @@ -91,24 +90,13 @@ pub async fn latest_github_release( ) .await .context("error fetching latest release")?; - let mut body = Vec::new(); response .body_mut() .read_to_end(&mut body) .await .context("error reading latest release")?; - let release: GithubRelease = serde_json::from_slice(body.as_slice()).context("error deserializing latest release")?; - let asset_name = asset_name(&release.name); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; - Ok(GitHubLspBinaryVersion { - name: release.name, - url: asset.browser_download_url.clone(), - }) + Ok(release) } diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 44ceeb4881..6cce615d81 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -22,10 +22,17 @@ impl LspAdapter for RustLspAdapter { http: Arc, ) -> BoxFuture<'static, Result>> { async move { - let version = latest_github_release("rust-analyzer/rust-analyzer", http, |_| { - format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH) - }) - .await?; + let release = latest_github_release("rust-analyzer/rust-analyzer", http).await?; + let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }; Ok(Box::new(version) as Box<_>) } .boxed() From 8f4387a252b42bd5ea60b0f91dc1c9c003bafaf9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 7 Jun 2022 18:38:37 -0700 Subject: [PATCH 02/12] Start work on installing gopls --- crates/language/src/language.rs | 14 +-- crates/zed/src/languages.rs | 1 + crates/zed/src/languages/c.rs | 13 ++- crates/zed/src/languages/go.rs | 140 +++++++++++++++++++++++++ crates/zed/src/languages/json.rs | 13 ++- crates/zed/src/languages/rust.rs | 16 ++- crates/zed/src/languages/typescript.rs | 13 ++- 7 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 crates/zed/src/languages/go.rs diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fbc8182d30..3bf57ad75b 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -75,9 +75,10 @@ pub trait LspAdapter: 'static + Send + Sync { &self, version: Box, http: Arc, - container_dir: PathBuf, + container_dir: Arc, ) -> BoxFuture<'static, Result>; - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option>; + fn cached_server_binary(&self, container_dir: Arc) + -> BoxFuture<'static, Option>; fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} @@ -362,7 +363,7 @@ async fn get_server_binary_path( download_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { - let container_dir = download_dir.join(adapter.name().0.as_ref()); + let container_dir: Arc = download_dir.join(adapter.name().0.as_ref()).into(); if !container_dir.exists() { smol::fs::create_dir_all(&container_dir) .await @@ -399,6 +400,7 @@ async fn fetch_latest_server_binary_path( container_dir: &Path, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { + let container_dir: Arc = container_dir.into(); lsp_binary_statuses_tx .broadcast(( language.clone(), @@ -412,7 +414,7 @@ async fn fetch_latest_server_binary_path( .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) .await?; let path = adapter - .fetch_server_binary(version_info, http_client, container_dir.to_path_buf()) + .fetch_server_binary(version_info, http_client, container_dir.clone()) .await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) @@ -657,12 +659,12 @@ impl LspAdapter for FakeLspAdapter { &self, _: Box, _: Arc, - _: PathBuf, + _: Arc, ) -> BoxFuture<'static, Result> { unreachable!(); } - fn cached_server_binary(&self, _: PathBuf) -> BoxFuture<'static, Option> { + fn cached_server_binary(&self, _: Arc) -> BoxFuture<'static, Option> { unreachable!(); } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 6fad623858..44c0d85566 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -4,6 +4,7 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, str, sync::Arc}; mod c; +mod go; mod installation; mod json; mod rust; diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index fd92e419a7..74b48ffee8 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -4,7 +4,11 @@ use client::http::HttpClient; use futures::{future::BoxFuture, FutureExt, StreamExt}; pub use language::*; use smol::fs::{self, File}; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{ + any::Any, + path::{Path, PathBuf}, + sync::Arc, +}; use util::{ResultExt, TryFutureExt}; pub struct CLspAdapter; @@ -39,7 +43,7 @@ impl super::LspAdapter for CLspAdapter { &self, version: Box, http: Arc, - container_dir: PathBuf, + container_dir: Arc, ) -> BoxFuture<'static, Result> { let version = version.downcast::().unwrap(); async move { @@ -88,7 +92,10 @@ impl super::LspAdapter for CLspAdapter { .boxed() } - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + fn cached_server_binary( + &self, + container_dir: Arc, + ) -> BoxFuture<'static, Option> { async move { let mut last_clangd_dir = None; let mut entries = fs::read_dir(&container_dir).await?; diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs new file mode 100644 index 0000000000..24f98b706c --- /dev/null +++ b/crates/zed/src/languages/go.rs @@ -0,0 +1,140 @@ +use super::installation::latest_github_release; +use anyhow::{anyhow, Result}; +use client::http::HttpClient; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +pub use language::*; +use lazy_static::lazy_static; +use regex::Regex; +use smol::{fs, process}; +use std::{ + any::Any, + path::{Path, PathBuf}, + str, + sync::Arc, +}; +use util::{ResultExt, TryFutureExt}; + +#[derive(Copy, Clone)] +pub struct GoLspAdapter; + +lazy_static! { + static ref GOPLS_VERSION_REGEX: Regex = Regex::new(r"\d+\.\d+\.\d+").unwrap(); +} + +impl super::LspAdapter for GoLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("gopls".into()) + } + + fn fetch_latest_server_version( + &self, + http: Arc, + ) -> BoxFuture<'static, Result>> { + async move { + let release = latest_github_release("golang/tools", http).await?; + let version: Option = release.name.strip_prefix("gopls/v").map(str::to_string); + if version.is_none() { + log::warn!( + "couldn't infer gopls version from github release name '{}'", + release.name + ); + } + Ok(Box::new(version) as Box<_>) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: Box, + _: Arc, + container_dir: Arc, + ) -> BoxFuture<'static, Result> { + let version = version.downcast::>().unwrap(); + let this = *self; + + async move { + if let Some(version) = *version { + let binary_path = container_dir.join(&format!("gopls_{version}")); + if let Ok(metadata) = fs::metadata(&binary_path).await { + if metadata.is_file() { + 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() != binary_path + && entry.file_name() != "gobin" + { + fs::remove_file(&entry_path).await.log_err(); + } + } + } + } + + return Ok(binary_path.to_path_buf()); + } + } + } else if let Some(path) = this.cached_server_binary(container_dir.clone()).await { + return Ok(path.to_path_buf()); + } + + let gobin_dir = container_dir.join("gobin"); + fs::create_dir_all(&gobin_dir).await?; + let install_output = process::Command::new("go") + .env("GO111MODULE", "on") + .env("GOBIN", &gobin_dir) + .args(["install", "golang.org/x/tools/gopls@latest"]) + .output() + .await?; + if !install_output.status.success() { + Err(anyhow!("failed to install gopls"))?; + } + + let installed_binary_path = gobin_dir.join("gopls"); + let version_output = process::Command::new(&installed_binary_path) + .arg("version") + .output() + .await + .map_err(|e| anyhow!("failed to run installed gopls binary {:?}", e))?; + let version_stdout = str::from_utf8(&version_output.stdout) + .map_err(|_| anyhow!("gopls version produced invalid utf8"))?; + let version = GOPLS_VERSION_REGEX + .shortest_match(version_stdout) + .ok_or_else(|| anyhow!("failed to parse gopls version output"))?; + let binary_path = container_dir.join(&format!("gopls_{version}")); + fs::rename(&installed_binary_path, &binary_path).await?; + + Ok(binary_path.to_path_buf()) + } + .boxed() + } + + fn cached_server_binary( + &self, + container_dir: Arc, + ) -> BoxFuture<'static, Option> { + async move { + let mut last_binary_path = 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_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name.starts_with("gopls_")) + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(path.to_path_buf()) + } else { + Err(anyhow!("no cached binary")) + } + } + .log_err() + .boxed() + } +} diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index dab6922c71..59e0652612 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -5,7 +5,11 @@ use language::{LanguageServerName, LspAdapter}; use serde::Deserialize; use serde_json::json; use smol::fs; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{ + any::Any, + path::{Path, PathBuf}, + sync::Arc, +}; use util::{ResultExt, TryFutureExt}; pub struct JsonLspAdapter; @@ -56,7 +60,7 @@ impl LspAdapter for JsonLspAdapter { &self, version: Box, _: Arc, - container_dir: PathBuf, + container_dir: Arc, ) -> BoxFuture<'static, Result> { let version = version.downcast::().unwrap(); async move { @@ -95,7 +99,10 @@ impl LspAdapter for JsonLspAdapter { .boxed() } - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + fn cached_server_binary( + &self, + container_dir: Arc, + ) -> BoxFuture<'static, Option> { async move { let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 6cce615d81..1abeeb097c 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -7,7 +7,14 @@ pub use language::*; use lazy_static::lazy_static; use regex::Regex; use smol::fs::{self, File}; -use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use std::{ + any::Any, + borrow::Cow, + env::consts, + path::{Path, PathBuf}, + str, + sync::Arc, +}; use util::{ResultExt, TryFutureExt}; pub struct RustLspAdapter; @@ -42,7 +49,7 @@ impl LspAdapter for RustLspAdapter { &self, version: Box, http: Arc, - container_dir: PathBuf, + container_dir: Arc, ) -> BoxFuture<'static, Result> { async move { let version = version.downcast::().unwrap(); @@ -79,7 +86,10 @@ impl LspAdapter for RustLspAdapter { .boxed() } - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + fn cached_server_binary( + &self, + container_dir: Arc, + ) -> BoxFuture<'static, Option> { async move { let mut last = None; let mut entries = fs::read_dir(&container_dir).await?; diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 96682d26d7..7b436ad79f 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -5,7 +5,11 @@ use futures::{future::BoxFuture, FutureExt, StreamExt}; use language::{LanguageServerName, LspAdapter}; use serde_json::json; use smol::fs; -use std::{any::Any, path::PathBuf, sync::Arc}; +use std::{ + any::Any, + path::{Path, PathBuf}, + sync::Arc, +}; use util::{ResultExt, TryFutureExt}; pub struct TypeScriptLspAdapter; @@ -45,7 +49,7 @@ impl LspAdapter for TypeScriptLspAdapter { &self, versions: Box, _: Arc, - container_dir: PathBuf, + container_dir: Arc, ) -> BoxFuture<'static, Result> { let versions = versions.downcast::().unwrap(); async move { @@ -88,7 +92,10 @@ impl LspAdapter for TypeScriptLspAdapter { .boxed() } - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + fn cached_server_binary( + &self, + container_dir: Arc, + ) -> BoxFuture<'static, Option> { async move { let mut last_version_dir = None; let mut entries = fs::read_dir(&container_dir).await?; From a41f164ffe6b167ef63be14e4716949948a8e914 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Jun 2022 10:25:58 -0700 Subject: [PATCH 03/12] Launch gopls with the right arguments Co-authored-by: Antonio Scandurra --- crates/zed/src/languages.rs | 5 +++++ crates/zed/src/languages/go.rs | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 44c0d85566..825925c07f 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -28,6 +28,11 @@ pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegi tree_sitter_cpp::language(), Some(Arc::new(c::CLspAdapter) as Arc), ), + ( + "go", + tree_sitter_go::language(), + Some(Arc::new(go::GoLspAdapter) as Arc), + ), ( "json", tree_sitter_json::language(), diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 24f98b706c..3a4c02446c 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -26,6 +26,10 @@ impl super::LspAdapter for GoLspAdapter { LanguageServerName("gopls".into()) } + fn server_args(&self) -> &[&str] { + &["-mode=stdio"] + } + fn fetch_latest_server_version( &self, http: Arc, @@ -99,8 +103,9 @@ impl super::LspAdapter for GoLspAdapter { let version_stdout = str::from_utf8(&version_output.stdout) .map_err(|_| anyhow!("gopls version produced invalid utf8"))?; let version = GOPLS_VERSION_REGEX - .shortest_match(version_stdout) - .ok_or_else(|| anyhow!("failed to parse gopls version output"))?; + .find(version_stdout) + .ok_or_else(|| anyhow!("failed to parse gopls version output"))? + .as_str(); let binary_path = container_dir.join(&format!("gopls_{version}")); fs::rename(&installed_binary_path, &binary_path).await?; From 36ce3eb5ef96b06581c67aa2bde671d9f6d52266 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Jun 2022 10:26:41 -0700 Subject: [PATCH 04/12] Handle messages associated with LSP `WorkDoneProgress::Begin` messages Co-authored-by: Antonio Scandurra --- crates/project/src/project.rs | 66 ++++++++++++++++++++++++++++------- crates/rpc/proto/zed.proto | 2 ++ 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 35bf4f2723..775b7f2ec2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1927,6 +1927,20 @@ impl Project { }) .detach(); + // Even though we don't have handling for these requests, respond to them to + // avoid stalling any language server like `gopls` which waits for a response + // to these requests when initializing. + language_server + .on_request::(|_, _| async { + Ok(()) + }) + .detach(); + language_server + .on_request::(|_, _| async { + Ok(()) + }) + .detach(); + language_server .on_request::({ let this = this.downgrade(); @@ -2159,7 +2173,7 @@ impl Project { return; }; match progress { - lsp::WorkDoneProgress::Begin(_) => { + lsp::WorkDoneProgress::Begin(report) => { if Some(token.as_str()) == disk_based_diagnostics_progress_token { language_server_status.pending_diagnostic_updates += 1; if language_server_status.pending_diagnostic_updates == 1 { @@ -2172,11 +2186,22 @@ impl Project { ); } } else { - self.on_lsp_work_start(server_id, token.clone(), cx); + self.on_lsp_work_start( + server_id, + token.clone(), + LanguageServerProgress { + message: report.message.clone(), + percentage: report.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, + cx, + ); self.broadcast_language_server_update( server_id, proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { token, + message: report.message, + percentage: report.percentage.map(|p| p as u32), }), ); } @@ -2234,17 +2259,11 @@ impl Project { &mut self, language_server_id: usize, token: String, + progress: LanguageServerProgress, cx: &mut ModelContext, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status.pending_work.insert( - token, - LanguageServerProgress { - message: None, - percentage: None, - last_update_at: Instant::now(), - }, - ); + status.pending_work.insert(token, progress); cx.notify(); } } @@ -2257,7 +2276,21 @@ impl Project { cx: &mut ModelContext, ) { if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status.pending_work.insert(token, progress); + let entry = status + .pending_work + .entry(token) + .or_insert(LanguageServerProgress { + message: Default::default(), + percentage: Default::default(), + last_update_at: progress.last_update_at, + }); + if progress.message.is_some() { + entry.message = progress.message; + } + if progress.percentage.is_some() { + entry.percentage = progress.percentage; + } + entry.last_update_at = progress.last_update_at; cx.notify(); } } @@ -4472,7 +4505,16 @@ impl Project { { proto::update_language_server::Variant::WorkStart(payload) => { this.update(&mut cx, |this, cx| { - this.on_lsp_work_start(language_server_id, payload.token, cx); + this.on_lsp_work_start( + language_server_id, + payload.token, + LanguageServerProgress { + message: payload.message, + percentage: payload.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, + cx, + ); }) } proto::update_language_server::Variant::WorkProgress(payload) => { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 60778bea32..7bece4c93c 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -555,6 +555,8 @@ message UpdateLanguageServer { message LspWorkStart { string token = 1; + optional string message = 2; + optional uint32 percentage = 3; } message LspWorkProgress { From 129fc515efbd7d5ba9c41e488174aeb73532412a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Jun 2022 10:44:13 -0700 Subject: [PATCH 05/12] Add parser and queries for go Co-authored-by: Antonio Scandurra --- Cargo.lock | 10 ++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages/go/brackets.scm | 3 + crates/zed/src/languages/go/config.toml | 11 +++ crates/zed/src/languages/go/highlights.scm | 107 +++++++++++++++++++++ crates/zed/src/languages/go/indents.scm | 9 ++ crates/zed/src/languages/go/outline.scm | 26 +++++ 7 files changed, 167 insertions(+) create mode 100644 crates/zed/src/languages/go/brackets.scm create mode 100644 crates/zed/src/languages/go/config.toml create mode 100644 crates/zed/src/languages/go/highlights.scm create mode 100644 crates/zed/src/languages/go/indents.scm create mode 100644 crates/zed/src/languages/go/outline.scm diff --git a/Cargo.lock b/Cargo.lock index e187738528..a9294e4adc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5297,6 +5297,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-go" +version = "0.19.1" +source = "git+https://github.com/tree-sitter/tree-sitter-go?rev=aeb2f33b366fd78d5789ff104956ce23508b85db#aeb2f33b366fd78d5789ff104956ce23508b85db" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-json" version = "0.19.0" @@ -6048,6 +6057,7 @@ dependencies = [ "tree-sitter", "tree-sitter-c", "tree-sitter-cpp", + "tree-sitter-go", "tree-sitter-json 0.20.0", "tree-sitter-markdown", "tree-sitter-rust", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d18adbd98e..2f1ab109af 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -89,6 +89,7 @@ toml = "0.5" tree-sitter = "0.20.6" 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" } diff --git a/crates/zed/src/languages/go/brackets.scm b/crates/zed/src/languages/go/brackets.scm new file mode 100644 index 0000000000..9e8c9cd93c --- /dev/null +++ b/crates/zed/src/languages/go/brackets.scm @@ -0,0 +1,3 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/go/config.toml b/crates/zed/src/languages/go/config.toml new file mode 100644 index 0000000000..fc6167e311 --- /dev/null +++ b/crates/zed/src/languages/go/config.toml @@ -0,0 +1,11 @@ +name = "Go" +path_suffixes = ["go"] +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 = true, newline = false }, +] diff --git a/crates/zed/src/languages/go/highlights.scm b/crates/zed/src/languages/go/highlights.scm new file mode 100644 index 0000000000..6a9be8aae0 --- /dev/null +++ b/crates/zed/src/languages/go/highlights.scm @@ -0,0 +1,107 @@ +(identifier) @variable +(type_identifier) @type +(field_identifier) @property + +(call_expression + function: (identifier) @function) + +(call_expression + function: (selector_expression + field: (field_identifier) @function.method)) + +(function_declaration + name: (identifier) @function) + +(method_declaration + name: (field_identifier) @function.method) + +[ + "--" + "-" + "-=" + ":=" + "!" + "!=" + "..." + "*" + "*" + "*=" + "/" + "/=" + "&" + "&&" + "&=" + "%" + "%=" + "^" + "^=" + "+" + "++" + "+=" + "<-" + "<" + "<<" + "<<=" + "<=" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "|" + "|=" + "||" + "~" +] @operator + +[ + "break" + "case" + "chan" + "const" + "continue" + "default" + "defer" + "else" + "fallthrough" + "for" + "func" + "go" + "goto" + "if" + "import" + "interface" + "map" + "package" + "range" + "return" + "select" + "struct" + "switch" + "type" + "var" +] @keyword + +[ + (interpreted_string_literal) + (raw_string_literal) + (rune_literal) +] @string + +(escape_sequence) @escape + +[ + (int_literal) + (float_literal) + (imaginary_literal) +] @number + +[ + (true) + (false) + (nil) + (iota) +] @constant.builtin + +(comment) @comment diff --git a/crates/zed/src/languages/go/indents.scm b/crates/zed/src/languages/go/indents.scm new file mode 100644 index 0000000000..abbb72eb37 --- /dev/null +++ b/crates/zed/src/languages/go/indents.scm @@ -0,0 +1,9 @@ +[ + (assignment_statement) + (call_expression) + (selector_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/go/outline.scm b/crates/zed/src/languages/go/outline.scm new file mode 100644 index 0000000000..1a754d2da5 --- /dev/null +++ b/crates/zed/src/languages/go/outline.scm @@ -0,0 +1,26 @@ +(type_declaration + "type" @context + (type_spec + name: (_) @name)) @item + +(function_declaration + "func" @context + name: (identifier) @name) @item + +(method_declaration + "func" @context + receiver: (parameter_list + (parameter_declaration + type: (_) @context)) + name: (field_identifier) @name) @item + +(const_declaration + "const" @context + (const_spec + name: (identifier) @name)) @item + +(source_file + (var_declaration + "var" @context + (var_spec + name: (identifier) @name)) @item) From f62fd3cddd408808059ead4739810db6f9e9651d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 8 Jun 2022 18:08:07 -0700 Subject: [PATCH 06/12] Add support for hard tabs * Add a `hard_tabs` setting that causes indentation to be performed using a tab instead of multiple spaces. * Change Buffer's indentation-related APIs to return an `IndentSize` struct with a length and a kind, instead of just a single u32. * Use hard tabs by default in Go. --- crates/editor/src/editor.rs | 71 +++--- crates/editor/src/movement.rs | 2 +- crates/editor/src/multi_buffer.rs | 33 ++- crates/language/src/buffer.rs | 347 ++++++++++++++++++------------ crates/language/src/tests.rs | 40 +++- crates/settings/src/settings.rs | 11 + crates/text/src/text.rs | 12 -- crates/zed/src/main.rs | 8 + 8 files changed, 324 insertions(+), 200 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 20bc53db37..14cd5dfe4c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -32,7 +32,8 @@ use gpui::{ pub use language::{char_kind, CharKind}; use language::{ BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, - Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, + TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ @@ -50,7 +51,7 @@ use std::{ any::TypeId, borrow::Cow, cmp::{self, Ordering, Reverse}, - iter, mem, + mem, ops::{Deref, DerefMut, Range, RangeInclusive}, sync::Arc, time::{Duration, Instant}, @@ -1923,9 +1924,8 @@ impl Editor { .iter() .map(|selection| { let start_point = selection.start.to_point(&buffer); - let indent = buffer - .indent_column_for_line(start_point.row) - .min(start_point.column); + let mut indent = buffer.indent_size_for_line(start_point.row); + indent.len = cmp::min(indent.len, start_point.column); let start = selection.start; let end = selection.end; @@ -1958,9 +1958,9 @@ impl Editor { }); } - let mut new_text = String::with_capacity(1 + indent as usize); + let mut new_text = String::with_capacity(1 + indent.len as usize); new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(indent as usize)); + new_text.extend(indent.chars()); if insert_extra_newline { new_text = new_text.repeat(2); } @@ -3061,14 +3061,21 @@ impl Editor { .buffer_snapshot .buffer_line_for_row(old_head.row) { - let indent_column = - buffer.indent_column_for_line(line_buffer_range.start.row); + let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); let language_name = buffer.language().map(|language| language.name()); - let indent = cx.global::().tab_size(language_name.as_deref()); - if old_head.column <= indent_column && old_head.column > 0 { + let indent_len = match indent_size.kind { + IndentKind::Space => { + cx.global::().tab_size(language_name.as_deref()) + } + IndentKind::Tab => 1, + }; + if old_head.column <= indent_size.len && old_head.column > 0 { new_head = cmp::min( new_head, - Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), + Point::new( + old_head.row, + ((old_head.column - 1) / indent_len) * indent_len, + ), ); } } @@ -3178,26 +3185,33 @@ impl Editor { } for row in start_row..end_row { - let indent_column = snapshot.indent_column_for_line(row); - let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); + let current_indent = snapshot.indent_size_for_line(row); + let indent_delta = match current_indent.kind { + IndentKind::Space => { + let columns_to_next_tab_stop = + tab_size - (current_indent.len % tab_size); + IndentSize::spaces(columns_to_next_tab_stop) + } + IndentKind::Tab => IndentSize::tab(), + }; let row_start = Point::new(row, 0); buffer.edit( [( row_start..row_start, - " ".repeat(columns_to_next_tab_stop as usize), + indent_delta.chars().collect::(), )], cx, ); // Update this selection's endpoints to reflect the indentation. if row == selection.start.row { - selection.start.column += columns_to_next_tab_stop as u32; + selection.start.column += indent_delta.len; } if row == selection.end.row { - selection.end.column += columns_to_next_tab_stop as u32; + selection.end.column += indent_delta.len as u32; } - last_indent = Some((row, columns_to_next_tab_stop as u32)); + last_indent = Some((row, indent_delta.len)); } } }); @@ -3230,12 +3244,19 @@ impl Editor { } for row in rows { - let column = snapshot.indent_column_for_line(row); - if column > 0 { - let mut deletion_len = column % tab_size; - if deletion_len == 0 { - deletion_len = tab_size; - } + let indent_size = snapshot.indent_size_for_line(row); + if indent_size.len > 0 { + let deletion_len = match indent_size.kind { + IndentKind::Space => { + let columns_to_prev_tab_stop = indent_size.len % tab_size; + if columns_to_prev_tab_stop == 0 { + tab_size + } else { + columns_to_prev_tab_stop + } + } + IndentKind::Tab => 1, + }; deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len)); last_outdent = Some(row); } @@ -4549,7 +4570,7 @@ impl Editor { continue; } - let start = Point::new(row, snapshot.indent_column_for_line(row)); + let start = Point::new(row, snapshot.indent_size_for_line(row).len); let mut line_bytes = snapshot .bytes_in_range(start..snapshot.max_point()) .flatten() diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 1f4486739b..7e9682ebe7 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -106,7 +106,7 @@ pub fn line_beginning( let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right); let indent_start = Point::new( point.row, - map.buffer_snapshot.indent_column_for_line(point.row), + map.buffer_snapshot.indent_size_for_line(point.row).len, ) .to_display_point(map); let line_start = map.prev_line_boundary(point).1; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 32e2021fd2..4523e21da1 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -8,8 +8,8 @@ use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, - Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, - ToPointUtf16 as _, TransactionId, + IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, + ToPoint as _, ToPointUtf16 as _, TransactionId, }; use settings::Settings; use smallvec::SmallVec; @@ -322,9 +322,14 @@ impl MultiBuffer { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| { - let language_name = buffer.language().map(|language| language.name()); - let indent_size = cx.global::().tab_size(language_name.as_deref()); if autoindent { + let language_name = buffer.language().map(|language| language.name()); + let settings = cx.global::(); + let indent_size = if settings.hard_tabs(language_name.as_deref()) { + IndentSize::tab() + } else { + IndentSize::spaces(settings.tab_size(language_name.as_deref())) + }; buffer.edit_with_autoindent(edits, indent_size, cx); } else { buffer.edit(edits, cx); @@ -426,9 +431,15 @@ impl MultiBuffer { } } let language_name = buffer.language().map(|l| l.name()); - let indent_size = cx.global::().tab_size(language_name.as_deref()); if autoindent { + let settings = cx.global::(); + let indent_size = if settings.hard_tabs(language_name.as_deref()) { + IndentSize::tab() + } else { + IndentSize::spaces(settings.tab_size(language_name.as_deref())) + }; + buffer.edit_with_autoindent(deletions, indent_size, cx); buffer.edit_with_autoindent(insertions, indent_size, cx); } else { @@ -1787,14 +1798,16 @@ impl MultiBufferSnapshot { } } - pub fn indent_column_for_line(&self, row: u32) -> u32 { + pub fn indent_size_for_line(&self, row: u32) -> IndentSize { if let Some((buffer, range)) = self.buffer_line_for_row(row) { - buffer - .indent_column_for_line(range.start.row) + let mut size = buffer.indent_size_for_line(range.start.row); + size.len = size + .len .min(range.end.column) - .saturating_sub(range.start.column) + .saturating_sub(range.start.column); + size } else { - 0 + IndentSize::spaces(0) } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 38dbaa29fd..a1b6bb2127 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -22,7 +22,7 @@ use std::{ collections::{BTreeMap, HashMap}, ffi::OsString, future::Future, - iter::{Iterator, Peekable}, + iter::{self, Iterator, Peekable}, mem, ops::{Deref, DerefMut, Range}, path::{Path, PathBuf}, @@ -82,6 +82,18 @@ pub struct BufferSnapshot { parse_count: usize, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct IndentSize { + pub len: u32, + pub kind: IndentKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IndentKind { + Space, + Tab, +} + #[derive(Clone, Debug)] struct SelectionSet { line_mode: bool, @@ -215,7 +227,7 @@ struct AutoindentRequest { before_edit: BufferSnapshot, edited: Vec, inserted: Option>>, - indent_size: u32, + indent_size: IndentSize, } #[derive(Debug)] @@ -723,18 +735,18 @@ impl Buffer { } fn request_autoindent(&mut self, cx: &mut ModelContext) { - if let Some(indent_columns) = self.compute_autoindents() { - let indent_columns = cx.background().spawn(indent_columns); + if let Some(indent_sizes) = self.compute_autoindents() { + let indent_sizes = cx.background().spawn(indent_sizes); match cx .background() - .block_with_timeout(Duration::from_micros(500), indent_columns) + .block_with_timeout(Duration::from_micros(500), indent_sizes) { - Ok(indent_columns) => self.apply_autoindents(indent_columns, cx), - Err(indent_columns) => { + Ok(indent_sizes) => self.apply_autoindents(indent_sizes, cx), + Err(indent_sizes) => { self.pending_autoindent = Some(cx.spawn(|this, mut cx| async move { - let indent_columns = indent_columns.await; + let indent_sizes = indent_sizes.await; this.update(&mut cx, |this, cx| { - this.apply_autoindents(indent_columns, cx); + this.apply_autoindents(indent_sizes, cx); }); })); } @@ -742,7 +754,7 @@ impl Buffer { } } - fn compute_autoindents(&self) -> Option>> { + fn compute_autoindents(&self) -> Option>> { let max_rows_between_yields = 100; let snapshot = self.snapshot(); if snapshot.language.is_none() @@ -754,7 +766,7 @@ impl Buffer { let autoindent_requests = self.autoindent_requests.clone(); Some(async move { - let mut indent_columns = BTreeMap::new(); + let mut indent_sizes = BTreeMap::new(); for request in autoindent_requests { let old_to_new_rows = request .edited @@ -768,7 +780,7 @@ impl Buffer { ) .collect::>(); - let mut old_suggestions = HashMap::::default(); + let mut old_suggestions = HashMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); for old_edited_range in old_edited_ranges { @@ -778,23 +790,19 @@ impl Buffer { .into_iter() .flatten(); for (old_row, suggestion) in old_edited_range.zip(suggestions) { - let indentation_basis = old_to_new_rows + let mut suggested_indent = old_to_new_rows .get(&suggestion.basis_row) .and_then(|from_row| old_suggestions.get(from_row).copied()) .unwrap_or_else(|| { request .before_edit - .indent_column_for_line(suggestion.basis_row) + .indent_size_for_line(suggestion.basis_row) }); - let delta = if suggestion.indent { - request.indent_size - } else { - 0 - }; - old_suggestions.insert( - *old_to_new_rows.get(&old_row).unwrap(), - indentation_basis + delta, - ); + if suggestion.indent { + suggested_indent += request.indent_size; + } + old_suggestions + .insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent); } yield_now().await; } @@ -809,23 +817,18 @@ impl Buffer { .into_iter() .flatten(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { - let delta = if suggestion.indent { - request.indent_size - } else { - 0 - }; - let new_indentation = indent_columns + let mut suggested_indent = indent_sizes .get(&suggestion.basis_row) .copied() - .unwrap_or_else(|| { - snapshot.indent_column_for_line(suggestion.basis_row) - }) - + delta; + .unwrap_or_else(|| snapshot.indent_size_for_line(suggestion.basis_row)); + if suggestion.indent { + suggested_indent += request.indent_size; + } if old_suggestions .get(&new_row) - .map_or(true, |old_indentation| new_indentation != *old_indentation) + .map_or(true, |old_indentation| suggested_indent != *old_indentation) { - indent_columns.insert(new_row, new_indentation); + indent_sizes.insert(new_row, suggested_indent); } } yield_now().await; @@ -845,56 +848,65 @@ impl Buffer { .into_iter() .flatten(); for (row, suggestion) in inserted_row_range.zip(suggestions) { - let delta = if suggestion.indent { - request.indent_size - } else { - 0 - }; - let new_indentation = indent_columns + let mut suggested_indent = indent_sizes .get(&suggestion.basis_row) .copied() .unwrap_or_else(|| { - snapshot.indent_column_for_line(suggestion.basis_row) - }) - + delta; - indent_columns.insert(row, new_indentation); + snapshot.indent_size_for_line(suggestion.basis_row) + }); + if suggestion.indent { + suggested_indent += request.indent_size; + } + indent_sizes.insert(row, suggested_indent); } yield_now().await; } } } - indent_columns + + indent_sizes }) } fn apply_autoindents( &mut self, - indent_columns: BTreeMap, + indent_sizes: BTreeMap, cx: &mut ModelContext, ) { self.autoindent_requests.clear(); self.start_transaction(); - for (row, indent_column) in &indent_columns { - self.set_indent_column_for_line(*row, *indent_column, cx); + for (row, indent_size) in &indent_sizes { + self.set_indent_size_for_line(*row, *indent_size, cx); } self.end_transaction(cx); } - fn set_indent_column_for_line(&mut self, row: u32, column: u32, cx: &mut ModelContext) { - let current_column = self.indent_column_for_line(row); - if column > current_column { + fn set_indent_size_for_line( + &mut self, + row: u32, + size: IndentSize, + cx: &mut ModelContext, + ) { + let current_size = indent_size_for_line(&self, row); + if size.kind != current_size.kind && current_size.len > 0 { + return; + } + + if size.len > current_size.len { let offset = Point::new(row, 0).to_offset(&*self); self.edit( [( offset..offset, - " ".repeat((column - current_column) as usize), + iter::repeat(size.char()) + .take((size.len - current_size.len) as usize) + .collect::(), )], cx, ); - } else if column < current_column { + } else if size.len < current_size.len { self.edit( [( - Point::new(row, 0)..Point::new(row, current_column - column), + Point::new(row, 0)..Point::new(row, current_size.len - size.len), "", )], cx, @@ -1084,7 +1096,7 @@ impl Buffer { pub fn edit_with_autoindent( &mut self, edits_iter: I, - indent_size: u32, + indent_size: IndentSize, cx: &mut ModelContext, ) -> Option where @@ -1098,7 +1110,7 @@ impl Buffer { pub fn edit_internal( &mut self, edits_iter: I, - autoindent_size: Option, + autoindent_size: Option, cx: &mut ModelContext, ) -> Option where @@ -1500,103 +1512,101 @@ impl Deref for Buffer { } impl BufferSnapshot { + pub fn indent_size_for_line(&self, row: u32) -> IndentSize { + indent_size_for_line(&self, row) + } + fn suggest_autoindents<'a>( &'a self, row_range: Range, ) -> Option + 'a> { + // Get the "indentation ranges" that intersect this row range. + let grammar = self.grammar()?; + let prev_non_blank_row = self.prev_non_blank_row(row_range.start); let mut query_cursor = QueryCursorHandle::new(); - if let Some((grammar, tree)) = self.grammar().zip(self.tree.as_ref()) { - let prev_non_blank_row = self.prev_non_blank_row(row_range.start); - - // Get the "indentation ranges" that intersect this row range. - let indent_capture_ix = grammar.indents_query.capture_index_for_name("indent"); - let end_capture_ix = grammar.indents_query.capture_index_for_name("end"); - query_cursor.set_point_range( - Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).to_ts_point() - ..Point::new(row_range.end, 0).to_ts_point(), - ); - let mut indentation_ranges = Vec::<(Range, &'static str)>::new(); - for mat in query_cursor.matches( - &grammar.indents_query, - tree.root_node(), - TextProvider(self.as_rope()), - ) { - let mut node_kind = ""; - let mut start: Option = None; - let mut end: Option = None; - for capture in mat.captures { - if Some(capture.index) == indent_capture_ix { - node_kind = capture.node.kind(); - start.get_or_insert(Point::from_ts_point(capture.node.start_position())); - end.get_or_insert(Point::from_ts_point(capture.node.end_position())); - } else if Some(capture.index) == end_capture_ix { - end = Some(Point::from_ts_point(capture.node.start_position().into())); - } - } - - if let Some((start, end)) = start.zip(end) { - if start.row == end.row { - continue; - } - - let range = start..end; - match indentation_ranges.binary_search_by_key(&range.start, |r| r.0.start) { - Err(ix) => indentation_ranges.insert(ix, (range, node_kind)), - Ok(ix) => { - let prev_range = &mut indentation_ranges[ix]; - prev_range.0.end = prev_range.0.end.max(range.end); - } - } + let indent_capture_ix = grammar.indents_query.capture_index_for_name("indent"); + let end_capture_ix = grammar.indents_query.capture_index_for_name("end"); + query_cursor.set_point_range( + Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).to_ts_point() + ..Point::new(row_range.end, 0).to_ts_point(), + ); + let mut indentation_ranges = Vec::>::new(); + for mat in query_cursor.matches( + &grammar.indents_query, + self.tree.as_ref()?.root_node(), + TextProvider(self.as_rope()), + ) { + let mut start: Option = None; + let mut end: Option = None; + for capture in mat.captures { + if Some(capture.index) == indent_capture_ix { + start.get_or_insert(Point::from_ts_point(capture.node.start_position())); + end.get_or_insert(Point::from_ts_point(capture.node.end_position())); + } else if Some(capture.index) == end_capture_ix { + end = Some(Point::from_ts_point(capture.node.start_position().into())); } } - let mut prev_row = prev_non_blank_row.unwrap_or(0); - Some(row_range.map(move |row| { - let row_start = Point::new(row, self.indent_column_for_line(row)); - - let mut indent_from_prev_row = false; - let mut outdent_to_row = u32::MAX; - for (range, _node_kind) in &indentation_ranges { - if range.start.row >= row { - break; - } - - if range.start.row == prev_row && range.end > row_start { - indent_from_prev_row = true; - } - if range.end.row >= prev_row && range.end <= row_start { - outdent_to_row = outdent_to_row.min(range.start.row); - } + if let Some((start, end)) = start.zip(end) { + if start.row == end.row { + continue; } - let suggestion = if outdent_to_row == prev_row { - IndentSuggestion { - basis_row: prev_row, - indent: false, + let range = start..end; + match indentation_ranges.binary_search_by_key(&range.start, |r| r.start) { + Err(ix) => indentation_ranges.insert(ix, range), + Ok(ix) => { + let prev_range = &mut indentation_ranges[ix]; + prev_range.end = prev_range.end.max(range.end); } - } else if indent_from_prev_row { - IndentSuggestion { - basis_row: prev_row, - indent: true, - } - } else if outdent_to_row < prev_row { - IndentSuggestion { - basis_row: outdent_to_row, - indent: false, - } - } else { - IndentSuggestion { - basis_row: prev_row, - indent: false, - } - }; - - prev_row = row; - suggestion - })) - } else { - None + } + } } + + let mut prev_row = prev_non_blank_row.unwrap_or(0); + Some(row_range.map(move |row| { + let row_start = Point::new(row, self.indent_size_for_line(row).len); + + let mut indent_from_prev_row = false; + let mut outdent_to_row = u32::MAX; + for range in &indentation_ranges { + if range.start.row >= row { + break; + } + + if range.start.row == prev_row && range.end > row_start { + indent_from_prev_row = true; + } + if range.end.row >= prev_row && range.end <= row_start { + outdent_to_row = outdent_to_row.min(range.start.row); + } + } + + let suggestion = if outdent_to_row == prev_row { + IndentSuggestion { + basis_row: prev_row, + indent: false, + } + } else if indent_from_prev_row { + IndentSuggestion { + basis_row: prev_row, + indent: true, + } + } else if outdent_to_row < prev_row { + IndentSuggestion { + basis_row: outdent_to_row, + indent: false, + } + } else { + IndentSuggestion { + basis_row: prev_row, + indent: false, + } + }; + + prev_row = row; + suggestion + })) } fn prev_non_blank_row(&self, mut row: u32) -> Option { @@ -1989,6 +1999,22 @@ impl BufferSnapshot { } } +pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { + let mut result = IndentSize::spaces(0); + for c in text.chars_at(Point::new(row, 0)) { + match (c, &result.kind) { + (' ', IndentKind::Space) | ('\t', IndentKind::Tab) => result.len += 1, + ('\t', IndentKind::Space) => { + if result.len == 0 { + result = IndentSize::tab(); + } + } + _ => break, + } + } + result +} + impl Clone for BufferSnapshot { fn clone(&self) -> Self { Self { @@ -2311,6 +2337,43 @@ impl Default for Diagnostic { } } +impl IndentSize { + pub fn spaces(len: u32) -> Self { + Self { + len, + kind: IndentKind::Space, + } + } + + pub fn tab() -> Self { + Self { + len: 1, + kind: IndentKind::Tab, + } + } + + pub fn chars(&self) -> impl Iterator { + iter::repeat(self.char()).take(self.len as usize) + } + + pub fn char(&self) -> char { + match self.kind { + IndentKind::Space => ' ', + IndentKind::Tab => '\t', + } + } +} + +impl std::ops::AddAssign for IndentSize { + fn add_assign(&mut self, other: IndentSize) { + if self.len == 0 { + *self = other; + } else if self.kind == other.kind { + self.len += other.len; + } + } +} + impl Completion { pub fn sort_key(&self) -> (usize, &str) { let kind_key = match self.lsp_completion.kind { diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 3bc9f4b9dc..7cd830d581 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -576,13 +576,21 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(8..8, "\n\n")], 4, cx); + buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::spaces(4), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); - buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 4), "b()\n")], 4, cx); + buffer.edit_with_autoindent( + [(Point::new(1, 4)..Point::new(1, 4), "b()\n")], + IndentSize::spaces(4), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); - buffer.edit_with_autoindent([(Point::new(2, 4)..Point::new(2, 4), ".c")], 4, cx); + buffer.edit_with_autoindent( + [(Point::new(2, 4)..Point::new(2, 4), ".c")], + IndentSize::spaces(4), + cx, + ); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); buffer @@ -609,7 +617,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "()"), (empty(Point::new(2, 1)), "()"), ], - 4, + IndentSize::spaces(4), cx, ); assert_eq!( @@ -630,7 +638,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta (empty(Point::new(1, 1)), "\n.f\n.g"), (empty(Point::new(2, 1)), "\n.f\n.g"), ], - 4, + IndentSize::spaces(4), cx, ); assert_eq!( @@ -653,13 +661,21 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta cx.add_model(|cx| { let text = "fn a() {\n {\n b()?\n }\n\n Ok(())\n}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(Point::new(3, 4)..Point::new(3, 5), "")], 4, cx); + buffer.edit_with_autoindent( + [(Point::new(3, 4)..Point::new(3, 5), "")], + IndentSize::spaces(4), + cx, + ); assert_eq!( buffer.text(), "fn a() {\n {\n b()?\n \n\n Ok(())\n}" ); - buffer.edit_with_autoindent([(Point::new(3, 0)..Point::new(3, 12), "")], 4, cx); + buffer.edit_with_autoindent( + [(Point::new(3, 0)..Point::new(3, 12), "")], + IndentSize::spaces(4), + cx, + ); assert_eq!( buffer.text(), "fn a() {\n {\n b()?\n\n\n Ok(())\n}" @@ -678,7 +694,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(5..5, "\nb")], 4, cx); + buffer.edit_with_autoindent([(5..5, "\nb")], IndentSize::spaces(4), cx); assert_eq!( buffer.text(), " @@ -690,7 +706,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte // The indentation suggestion changed because `@end` node (a close paren) // is now at the beginning of the line. - buffer.edit_with_autoindent([(Point::new(1, 4)..Point::new(1, 5), "")], 4, cx); + buffer.edit_with_autoindent( + [(Point::new(1, 4)..Point::new(1, 5), "")], + IndentSize::spaces(4), + cx, + ); assert_eq!( buffer.text(), " @@ -709,7 +729,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut MutableAppContext) { cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], 4, cx); + buffer.edit_with_autoindent([(0..1, "\n"), (2..3, "\n")], IndentSize::spaces(4), cx); assert_eq!(buffer.text(), "\n\n\n"); buffer }); diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 0efedbfd9b..08c153740a 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -25,6 +25,7 @@ pub struct Settings { pub default_buffer_font_size: f32, pub vim_mode: bool, pub tab_size: u32, + pub hard_tabs: bool, pub soft_wrap: SoftWrap, pub preferred_line_length: u32, pub format_on_save: bool, @@ -35,6 +36,7 @@ pub struct Settings { #[derive(Clone, Debug, Default, Deserialize, JsonSchema)] pub struct LanguageOverride { pub tab_size: Option, + pub hard_tabs: Option, pub soft_wrap: Option, pub preferred_line_length: Option, pub format_on_save: Option, @@ -80,6 +82,7 @@ impl Settings { default_buffer_font_size: 15., vim_mode: false, tab_size: 4, + hard_tabs: false, soft_wrap: SoftWrap::None, preferred_line_length: 80, language_overrides: Default::default(), @@ -106,6 +109,13 @@ impl Settings { .unwrap_or(self.tab_size) } + pub fn hard_tabs(&self, language: Option<&str>) -> bool { + language + .and_then(|language| self.language_overrides.get(language)) + .and_then(|settings| settings.hard_tabs) + .unwrap_or(self.hard_tabs) + } + pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap { language .and_then(|language| self.language_overrides.get(language)) @@ -135,6 +145,7 @@ impl Settings { default_buffer_font_size: 14., vim_mode: false, tab_size: 4, + hard_tabs: false, soft_wrap: SoftWrap::None, preferred_line_length: 80, format_on_save: true, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 8d37ad0c8b..2c8fc13313 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1642,18 +1642,6 @@ impl BufferSnapshot { .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) } - pub fn indent_column_for_line(&self, row: u32) -> u32 { - let mut result = 0; - for c in self.chars_at(Point::new(row, 0)) { - if c == ' ' { - result += 1; - } else { - break; - } - } - result - } - pub fn text_summary_for_range<'a, D, O: ToOffset>(&'a self, range: Range) -> D where D: TextDimension, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8706f1327a..276c08cc28 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -94,6 +94,14 @@ fn main() { ..Default::default() }, ) + .with_overrides( + "Go", + settings::LanguageOverride { + tab_size: Some(4), + hard_tabs: Some(true), + ..Default::default() + }, + ) .with_overrides( "Markdown", settings::LanguageOverride { From 77b9ab08855f24fa53a2d0345dd4cbfabd8848e7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Jun 2022 09:05:07 -0700 Subject: [PATCH 07/12] Add buffer test for autoindent with hard tabs --- crates/language/src/tests.rs | 51 +++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 7cd830d581..1cee723d12 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -571,7 +571,7 @@ fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) { } #[gpui::test] -fn test_edit_with_autoindent(cx: &mut MutableAppContext) { +fn test_autoindent_with_soft_tabs(cx: &mut MutableAppContext) { cx.add_model(|cx| { let text = "fn a() {}"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); @@ -586,6 +586,8 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) { ); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); + // Create a field expression on a new line, causing that line + // to be indented. buffer.edit_with_autoindent( [(Point::new(2, 4)..Point::new(2, 4), ".c")], IndentSize::spaces(4), @@ -593,6 +595,53 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) { ); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); + // Remove the dot so that the line is no longer a field expression, + // causing the line to be outdented. + buffer.edit_with_autoindent( + [(Point::new(2, 8)..Point::new(2, 9), "")], + IndentSize::spaces(4), + cx, + ); + assert_eq!(buffer.text(), "fn a() {\n b()\n c\n}"); + + buffer + }); +} + +#[gpui::test] +fn test_autoindent_with_hard_tabs(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = "fn a() {}"; + let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + + buffer.edit_with_autoindent([(8..8, "\n\n")], IndentSize::tab(), cx); + assert_eq!(buffer.text(), "fn a() {\n\t\n}"); + + buffer.edit_with_autoindent( + [(Point::new(1, 1)..Point::new(1, 1), "b()\n")], + IndentSize::tab(), + cx, + ); + assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\n}"); + + // Create a field expression on a new line, causing that line + // to be indented. + buffer.edit_with_autoindent( + [(Point::new(2, 1)..Point::new(2, 1), ".c")], + IndentSize::tab(), + cx, + ); + assert_eq!(buffer.text(), "fn a() {\n\tb()\n\t\t.c\n}"); + + // Remove the dot so that the line is no longer a field expression, + // causing the line to be outdented. + buffer.edit_with_autoindent( + [(Point::new(2, 2)..Point::new(2, 3), "")], + IndentSize::tab(), + cx, + ); + assert_eq!(buffer.text(), "fn a() {\n\tb()\n\tc\n}"); + buffer }); } From 7bb7187619fbd29f66475e0f8e8e7a5ba10b7ed0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Jun 2022 10:26:09 -0700 Subject: [PATCH 08/12] Add tests and fix bugs for editor indent/outdent commands w/ hard tabs --- crates/editor/src/editor.rs | 123 ++++++++++++++++++++++++++++++---- crates/editor/src/test.rs | 17 ++--- crates/language/src/buffer.rs | 14 ++-- crates/vim/src/vim.rs | 2 +- 4 files changed, 127 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 14cd5dfe4c..f141a9b6e3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3126,21 +3126,27 @@ impl Editor { for selection in &mut selections { let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let tab_size = cx.global::().tab_size(language_name.as_deref()); - let char_column = buffer - .read(cx) - .text_for_range(Point::new(selection.start.row, 0)..selection.start) - .flat_map(str::chars) - .count(); - let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); + let settings = cx.global::(); + let tab_size = if settings.hard_tabs(language_name.as_deref()) { + IndentSize::tab() + } else { + let tab_size = settings.tab_size(language_name.as_deref()); + let char_column = buffer + .read(cx) + .text_for_range(Point::new(selection.start.row, 0)..selection.start) + .flat_map(str::chars) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size); + IndentSize::spaces(chars_to_next_tab_stop) + }; buffer.edit( [( selection.start..selection.start, - " ".repeat(chars_to_next_tab_stop as usize), + tab_size.chars().collect::(), )], cx, ); - selection.start.column += chars_to_next_tab_stop; + selection.start.column += tab_size.len; selection.end = selection.start; } }); @@ -3161,7 +3167,14 @@ impl Editor { let snapshot = buffer.snapshot(cx); for selection in &mut selections { let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let tab_size = cx.global::().tab_size(language_name.as_deref()); + let settings = &cx.global::(); + let tab_size = settings.tab_size(language_name.as_deref()); + let indent_kind = if settings.hard_tabs(language_name.as_deref()) { + IndentKind::Tab + } else { + IndentKind::Space + }; + let mut start_row = selection.start.row; let mut end_row = selection.end.row + 1; @@ -3186,14 +3199,16 @@ impl Editor { for row in start_row..end_row { let current_indent = snapshot.indent_size_for_line(row); - let indent_delta = match current_indent.kind { - IndentKind::Space => { + let indent_delta = match (current_indent.kind, indent_kind) { + (IndentKind::Space, IndentKind::Space) => { let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size); IndentSize::spaces(columns_to_next_tab_stop) } - IndentKind::Tab => IndentSize::tab(), + (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size), + (_, IndentKind::Tab) => IndentSize::tab(), }; + let row_start = Point::new(row, 0); buffer.edit( [( @@ -7696,6 +7711,88 @@ mod tests { four"}); } + #[gpui::test] + async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + cx.update(|cx| { + cx.update_global::(|settings, _| { + settings.hard_tabs = true; + }); + }); + + // select two ranges on one line + cx.set_state(indoc! {" + [one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t[one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + \t\t[one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + \t[one} [two} + three + four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + [one} [two} + three + four"}); + + // select across a line ending + cx.set_state(indoc! {" + one two + t[hree + }four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt[hree + }four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \t\tt[hree + }four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + \tt[hree + }four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + t[hree + }four"}); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + cx.set_state(indoc! {" + one two + |three + four"}); + cx.assert_editor_state(indoc! {" + one two + |three + four"}); + cx.update_editor(|e, cx| e.tab(&Tab, cx)); + cx.assert_editor_state(indoc! {" + one two + \t|three + four"}); + cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); + cx.assert_editor_state(indoc! {" + one two + |three + four"}); + } + #[gpui::test] fn test_indent_outdent_with_excerpts(cx: &mut gpui::MutableAppContext) { cx.set_global( diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 4c9ceed9ae..0f49576936 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -109,9 +109,10 @@ impl<'a> EditorTestContext<'a> { self.editor.update(self.cx, update) } - pub fn editor_text(&mut self) -> String { - self.editor - .update(self.cx, |editor, cx| editor.snapshot(cx).text()) + pub fn buffer_text(&mut self) -> String { + self.editor.read_with(self.cx, |editor, cx| { + editor.buffer.read(cx).snapshot(cx).text() + }) } pub fn simulate_keystroke(&mut self, keystroke_text: &str) { @@ -171,10 +172,10 @@ impl<'a> EditorTestContext<'a> { &text, vec!['|'.into(), ('[', '}').into(), ('{', ']').into()], ); - let editor_text = self.editor_text(); + let buffer_text = self.buffer_text(); assert_eq!( - editor_text, unmarked_text, - "Unmarked text doesn't match editor text" + buffer_text, unmarked_text, + "Unmarked text doesn't match buffer text" ); let expected_empty_selections = selection_ranges.remove(&'|'.into()).unwrap_or_default(); @@ -254,7 +255,7 @@ impl<'a> EditorTestContext<'a> { let actual_selections = self.insert_markers(&empty_selections, &reverse_selections, &forward_selections); - let unmarked_text = self.editor_text(); + let unmarked_text = self.buffer_text(); let all_eq: Result<(), SetEqError> = set_eq!(expected_empty_selections, empty_selections) .map_err(|err| { @@ -322,7 +323,7 @@ impl<'a> EditorTestContext<'a> { reverse_selections: &Vec>, forward_selections: &Vec>, ) -> String { - let mut editor_text_with_selections = self.editor_text(); + let mut editor_text_with_selections = self.buffer_text(); let mut selection_marks = BTreeMap::new(); for range in empty_selections { selection_marks.insert(&range.start, '|'); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a1b6bb2127..d420722f49 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2002,15 +2002,15 @@ impl BufferSnapshot { pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { let mut result = IndentSize::spaces(0); for c in text.chars_at(Point::new(row, 0)) { - match (c, &result.kind) { - (' ', IndentKind::Space) | ('\t', IndentKind::Tab) => result.len += 1, - ('\t', IndentKind::Space) => { - if result.len == 0 { - result = IndentSize::tab(); - } - } + let kind = match c { + ' ' => IndentKind::Space, + '\t' => IndentKind::Tab, _ => break, + }; + if result.len == 0 { + result.kind = kind; } + result.len += 1; } result } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index b653f4198f..3f54954ed5 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -202,7 +202,7 @@ mod test { cx.enable_vim(); assert_eq!(cx.mode(), Mode::Normal); cx.simulate_keystrokes(["h", "h", "h", "l"]); - assert_eq!(cx.editor_text(), "hjkl".to_owned()); + assert_eq!(cx.buffer_text(), "hjkl".to_owned()); cx.assert_editor_state("h|jkl"); cx.simulate_keystrokes(["i", "T", "e", "s", "t"]); cx.assert_editor_state("hTest|jkl"); From 4ce4c0ef033f72c83e1f8647ce4b69499aa5a70c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Jun 2022 13:08:08 -0700 Subject: [PATCH 09/12] Ignore completions from gopls that we can't yet handle We only support additionalEdits if they are provided when resolving the completion, not if they are provided immediately. --- crates/project/src/project.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 775b7f2ec2..be79b8b3e6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3054,6 +3054,16 @@ impl Project { Ok(completions .into_iter() .filter_map(|lsp_completion| { + // For now, we can only handle additional edits if they are returned + // when resolving the completion, not if they are present initially. + if lsp_completion + .additional_text_edits + .as_ref() + .map_or(false, |edits| !edits.is_empty()) + { + return None; + } + let (old_range, new_text) = match lsp_completion.text_edit.as_ref() { // If the language server provides a range to overwrite, then // check that the range is valid. From 861f4c72309dbe06586e27a84017154e21ca5e0b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Jun 2022 13:08:18 -0700 Subject: [PATCH 10/12] Add syntax highlighting for Go completions --- crates/zed/src/languages/go.rs | 197 +++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 3a4c02446c..3879ebbc27 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -8,6 +8,7 @@ use regex::Regex; use smol::{fs, process}; use std::{ any::Any, + ops::Range, path::{Path, PathBuf}, str, sync::Arc, @@ -142,4 +143,200 @@ impl super::LspAdapter for GoLspAdapter { .log_err() .boxed() } + + fn label_for_completion( + &self, + completion: &lsp::CompletionItem, + language: &Language, + ) -> Option { + let label = &completion.label; + + // Gopls returns nested fields and methods as completions. + // To syntax highlight these, combine their final component + // with their detail. + let name_offset = label.rfind(".").unwrap_or(0); + + match completion.kind.zip(completion.detail.as_ref()) { + Some((lsp::CompletionItemKind::MODULE, detail)) => { + let text = format!("{label} {detail}"); + let source = Rope::from(format!("import {text}").as_str()); + let runs = language.highlight_text(&source, 7..7 + text.len()); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some(( + lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE, + detail, + )) => { + let text = format!("{label} {detail}"); + let source = + Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 4..4 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some((lsp::CompletionItemKind::STRUCT, _)) => { + let text = format!("{label} struct {{}}"); + let source = Rope::from(format!("type {}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 5..5 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some((lsp::CompletionItemKind::INTERFACE, _)) => { + let text = format!("{label} interface {{}}"); + let source = Rope::from(format!("type {}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 5..5 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some((lsp::CompletionItemKind::FIELD, detail)) => { + let text = format!("{label} {detail}"); + let source = + Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 16..16 + text.len()), + ); + return Some(CodeLabel { + text, + runs, + filter_range: 0..label.len(), + }); + } + Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => { + if let Some(signature) = detail.strip_prefix("func") { + let text = format!("{label}{signature}"); + let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str()); + let runs = adjust_runs( + name_offset, + language.highlight_text(&source, 5..5 + text.len()), + ); + return Some(CodeLabel { + filter_range: 0..label.len(), + text, + runs, + }); + } + } + _ => {} + } + None + } +} + +fn adjust_runs( + delta: usize, + mut runs: Vec<(Range, HighlightId)>, +) -> Vec<(Range, HighlightId)> { + for (range, _) in &mut runs { + range.start += delta; + range.end += delta; + } + runs +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::languages::language; + use gpui::color::Color; + use theme::SyntaxTheme; + + #[test] + fn test_go_label_for_completion() { + let language = language( + "go", + tree_sitter_go::language(), + Some(Arc::new(GoLspAdapter)), + ); + + let theme = SyntaxTheme::new(vec![ + ("type".into(), Color::green().into()), + ("keyword".into(), Color::blue().into()), + ("function".into(), Color::red().into()), + ("number".into(), Color::yellow().into()), + ("property".into(), Color::white().into()), + ]); + language.set_theme(&theme); + + let grammar = language.grammar().unwrap(); + let highlight_function = grammar.highlight_id_for_name("function").unwrap(); + let highlight_type = grammar.highlight_id_for_name("type").unwrap(); + let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap(); + let highlight_number = grammar.highlight_id_for_name("number").unwrap(); + let highlight_field = grammar.highlight_id_for_name("property").unwrap(); + + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FUNCTION), + label: "Hello".to_string(), + detail: Some("func(a B) c.D".to_string()), + ..Default::default() + }), + Some(CodeLabel { + text: "Hello(a B) c.D".to_string(), + filter_range: 0..5, + runs: vec![ + (0..5, highlight_function), + (8..9, highlight_type), + (13..14, highlight_type), + ], + }) + ); + + // Nested methods + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::METHOD), + label: "one.two.Three".to_string(), + detail: Some("func() [3]interface{}".to_string()), + ..Default::default() + }), + Some(CodeLabel { + text: "one.two.Three() [3]interface{}".to_string(), + filter_range: 0..13, + runs: vec![ + (8..13, highlight_function), + (17..18, highlight_number), + (19..28, highlight_keyword), + ], + }) + ); + + // Nested fields + assert_eq!( + language.label_for_completion(&lsp::CompletionItem { + kind: Some(lsp::CompletionItemKind::FIELD), + label: "two.Three".to_string(), + detail: Some("a.Bcd".to_string()), + ..Default::default() + }), + Some(CodeLabel { + text: "two.Three a.Bcd".to_string(), + filter_range: 0..9, + runs: vec![(4..9, highlight_field), (12..15, highlight_type)], + }) + ); + } } From 94957174217c05d70359ee6949ff344fe14a0966 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Jun 2022 13:16:08 -0700 Subject: [PATCH 11/12] Add project symbol labels for Go --- crates/zed/src/languages/go.rs | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 3879ebbc27..9a8895266c 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -243,6 +243,65 @@ impl super::LspAdapter for GoLspAdapter { } None } + + fn label_for_symbol( + &self, + name: &str, + kind: lsp::SymbolKind, + language: &Language, + ) -> Option { + let (text, filter_range, display_range) = match kind { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { + let text = format!("func {} () {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::STRUCT => { + let text = format!("type {} struct {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..text.len(); + (text, filter_range, display_range) + } + lsp::SymbolKind::INTERFACE => { + let text = format!("type {} interface {{}}", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..text.len(); + (text, filter_range, display_range) + } + lsp::SymbolKind::CLASS => { + let text = format!("type {} T", name); + let filter_range = 5..5 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::CONSTANT => { + let text = format!("const {} = nil", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::VARIABLE => { + let text = format!("var {} = nil", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::MODULE => { + let text = format!("package {}", name); + let filter_range = 8..8 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } } fn adjust_runs( From 924e9648e99b401caa5d89207e870ec98df75ad0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Jun 2022 13:31:30 -0700 Subject: [PATCH 12/12] Tweak golang outline query --- crates/zed/src/languages/go/outline.scm | 28 ++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/zed/src/languages/go/outline.scm b/crates/zed/src/languages/go/outline.scm index 1a754d2da5..849d2bdebf 100644 --- a/crates/zed/src/languages/go/outline.scm +++ b/crates/zed/src/languages/go/outline.scm @@ -5,22 +5,40 @@ (function_declaration "func" @context - name: (identifier) @name) @item + name: (identifier) @name + parameters: (parameter_list + "(" @context + ")" @context)) @item (method_declaration "func" @context receiver: (parameter_list + "(" @context (parameter_declaration - type: (_) @context)) - name: (field_identifier) @name) @item + type: (_) @context) + ")" @context) + name: (field_identifier) @name + parameters: (parameter_list + "(" @context + ")" @context)) @item (const_declaration "const" @context (const_spec - name: (identifier) @name)) @item + name: (identifier) @name) @item) (source_file (var_declaration "var" @context (var_spec - name: (identifier) @name)) @item) + name: (identifier) @name) @item)) + +(method_spec + name: (_) @name + parameters: (parameter_list + "(" @context + ")" @context)) @item + +(field_declaration + name: (_) @name + type: (_) @context) @item \ No newline at end of file