From a48995c782da8b5db09645240eea5821d1f776cb Mon Sep 17 00:00:00 2001 From: Isaac Clayton Date: Fri, 22 Jul 2022 12:25:07 +0200 Subject: [PATCH 01/15] Basic html highlighting + lsp support --- Cargo.lock | 11 ++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 6 ++ crates/zed/src/languages/html.rs | 101 +++++++++++++++++++ crates/zed/src/languages/html/brackets.scm | 2 + crates/zed/src/languages/html/config.toml | 10 ++ crates/zed/src/languages/html/highlights.scm | 11 ++ crates/zed/src/languages/html/indents.scm | 1 + crates/zed/src/languages/html/outline.scm | 0 9 files changed, 143 insertions(+) create mode 100644 crates/zed/src/languages/html.rs create mode 100644 crates/zed/src/languages/html/brackets.scm create mode 100644 crates/zed/src/languages/html/config.toml create mode 100644 crates/zed/src/languages/html/highlights.scm create mode 100644 crates/zed/src/languages/html/indents.scm create mode 100644 crates/zed/src/languages/html/outline.scm diff --git a/Cargo.lock b/Cargo.lock index 363ee93c14..5023de1f75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6043,6 +6043,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-html" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "184e6b77953a354303dc87bf5fe36558c83569ce92606e7b382a0dc1b7443443" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-json" version = "0.19.0" @@ -7229,6 +7239,7 @@ dependencies = [ "tree-sitter-cpp", "tree-sitter-elixir", "tree-sitter-go", + "tree-sitter-html", "tree-sitter-json 0.20.0", "tree-sitter-markdown", "tree-sitter-python", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index dc2b0abd03..f84c8836bd 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -100,6 +100,7 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", tree-sitter-python = "0.20.2" tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" } tree-sitter-typescript = "0.20.1" +tree-sitter-html = "0.19.0" url = "2.2" [dev-dependencies] diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 6e57106e87..ba1945f316 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -7,6 +7,7 @@ use std::{borrow::Cow, str, sync::Arc}; mod c; mod elixir; mod go; +mod html; mod installation; mod json; mod language_plugin; @@ -96,6 +97,11 @@ pub async fn init(languages: Arc, _executor: Arc) tree_sitter_typescript::language_tsx(), Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await), ), + ( + "html", + tree_sitter_html::language(), + Some(CachedLspAdapter::new(html::HtmlLspAdapter).await), + ), ] { languages.add(Arc::new(language(name, grammar, lsp_adapter))); } diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs new file mode 100644 index 0000000000..5497841d88 --- /dev/null +++ b/crates/zed/src/languages/html.rs @@ -0,0 +1,101 @@ +use super::installation::{npm_install_packages, npm_package_latest_version}; +use anyhow::{anyhow, Context, Result}; +use async_trait::async_trait; +use client::http::HttpClient; +use futures::StreamExt; +use language::{LanguageServerName, LspAdapter}; +use serde_json::json; +use smol::fs; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::ResultExt; + +pub struct HtmlLspAdapter; + +impl HtmlLspAdapter { + const BIN_PATH: &'static str = + "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server"; +} + +#[async_trait] +impl LspAdapter for HtmlLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-html-language-server".into()) + } + + async fn server_args(&self) -> Vec { + vec!["--stdio".into()] + } + + async fn fetch_latest_server_version( + &self, + _: Arc, + ) -> Result> { + Ok(Box::new(npm_package_latest_version("vscode-langservers-extracted").await?) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + _: Arc, + container_dir: PathBuf, + ) -> Result { + let version = version.downcast::().unwrap(); + 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( + [("vscode-langservers-extracted", 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) + } + + async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + (|| 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 + )) + } + })() + .await + .log_err() + } + + async fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} diff --git a/crates/zed/src/languages/html/brackets.scm b/crates/zed/src/languages/html/brackets.scm new file mode 100644 index 0000000000..2d12b17daa --- /dev/null +++ b/crates/zed/src/languages/html/brackets.scm @@ -0,0 +1,2 @@ +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml new file mode 100644 index 0000000000..0680717b2c --- /dev/null +++ b/crates/zed/src/languages/html/config.toml @@ -0,0 +1,10 @@ +name = "HTML" +path_suffixes = ["html"] +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/html/highlights.scm b/crates/zed/src/languages/html/highlights.scm new file mode 100644 index 0000000000..d6d361ca49 --- /dev/null +++ b/crates/zed/src/languages/html/highlights.scm @@ -0,0 +1,11 @@ +(tag_name) @keyword +(doctype) @constant +(attribute_name) @property +(attribute_value) @string +(comment) @comment + +[ + "<" + ">" + " Date: Tue, 30 Aug 2022 16:52:41 -0700 Subject: [PATCH 02/15] Add JavaScript language injection in HTML --- crates/zed/src/languages/html/injections.scm | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 crates/zed/src/languages/html/injections.scm diff --git a/crates/zed/src/languages/html/injections.scm b/crates/zed/src/languages/html/injections.scm new file mode 100644 index 0000000000..6d2b8e9377 --- /dev/null +++ b/crates/zed/src/languages/html/injections.scm @@ -0,0 +1,4 @@ +(script_element + (raw_text) @content + (#set! "language" "javascript")) + From 21fb2b9bf1eea8ac3f6042478dfd31f78497c3f8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 30 Aug 2022 17:23:09 -0700 Subject: [PATCH 03/15] Tweak HTML indents and highlights --- crates/language/src/buffer.rs | 2 ++ crates/language/src/language.rs | 4 ++++ crates/zed/src/languages/html/highlights.scm | 4 ++++ crates/zed/src/languages/html/indents.scm | 7 ++++++- crates/zed/src/languages/html/injections.scm | 1 - 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 08843aacfe..c8730be452 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1631,6 +1631,8 @@ impl BufferSnapshot { if capture.index == config.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) == config.start_capture_ix { + start = Some(Point::from_ts_point(capture.node.end_position())); } else if Some(capture.index) == config.end_capture_ix { end = Some(Point::from_ts_point(capture.node.start_position())); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 780f6e75b5..0366bdf669 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -304,6 +304,7 @@ pub struct Grammar { struct IndentConfig { query: Query, indent_capture_ix: u32, + start_capture_ix: Option, end_capture_ix: Option, } @@ -661,11 +662,13 @@ impl Language { let grammar = self.grammar_mut(); let query = Query::new(grammar.ts_language, source)?; let mut indent_capture_ix = None; + let mut start_capture_ix = None; let mut end_capture_ix = None; get_capture_indices( &query, &mut [ ("indent", &mut indent_capture_ix), + ("start", &mut start_capture_ix), ("end", &mut end_capture_ix), ], ); @@ -673,6 +676,7 @@ impl Language { grammar.indents_config = Some(IndentConfig { query, indent_capture_ix, + start_capture_ix, end_capture_ix, }); } diff --git a/crates/zed/src/languages/html/highlights.scm b/crates/zed/src/languages/html/highlights.scm index d6d361ca49..0ce535fad4 100644 --- a/crates/zed/src/languages/html/highlights.scm +++ b/crates/zed/src/languages/html/highlights.scm @@ -1,11 +1,15 @@ (tag_name) @keyword +(erroneous_end_tag_name) @keyword (doctype) @constant (attribute_name) @property (attribute_value) @string (comment) @comment +"=" @operator + [ "<" ">" "" ] @punctuation.bracket \ No newline at end of file diff --git a/crates/zed/src/languages/html/indents.scm b/crates/zed/src/languages/html/indents.scm index a1560bfea7..436663dba3 100644 --- a/crates/zed/src/languages/html/indents.scm +++ b/crates/zed/src/languages/html/indents.scm @@ -1 +1,6 @@ -(tag_name) @indent +(start_tag ">" @end) @indent +(self_closing_tag "/>" @end) @indent + +(element + (start_tag) @start + (end_tag)? @end) @indent diff --git a/crates/zed/src/languages/html/injections.scm b/crates/zed/src/languages/html/injections.scm index 6d2b8e9377..60688f599f 100644 --- a/crates/zed/src/languages/html/injections.scm +++ b/crates/zed/src/languages/html/injections.scm @@ -1,4 +1,3 @@ (script_element (raw_text) @content (#set! "language" "javascript")) - From a2e57e8d71e00bcb9059e6c79854937eeb488047 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 Aug 2022 11:53:52 -0700 Subject: [PATCH 04/15] Add basic syntax highlighting for CSS --- Cargo.lock | 10 +++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 5 ++ crates/zed/src/languages/css/brackets.scm | 3 + crates/zed/src/languages/css/config.toml | 9 +++ crates/zed/src/languages/css/highlights.scm | 76 ++++++++++++++++++++ crates/zed/src/languages/css/indents.scm | 1 + crates/zed/src/languages/html/injections.scm | 8 ++- 8 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 crates/zed/src/languages/css/brackets.scm create mode 100644 crates/zed/src/languages/css/config.toml create mode 100644 crates/zed/src/languages/css/highlights.scm create mode 100644 crates/zed/src/languages/css/indents.scm diff --git a/Cargo.lock b/Cargo.lock index 5023de1f75..3d74213062 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6025,6 +6025,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-css" +version = "0.19.0" +source = "git+https://github.com/tree-sitter/tree-sitter-css?rev=769203d0f9abe1a9a691ac2b9fe4bb4397a73c51#769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-elixir" version = "0.19.0" @@ -7237,6 +7246,7 @@ dependencies = [ "tree-sitter", "tree-sitter-c", "tree-sitter-cpp", + "tree-sitter-css", "tree-sitter-elixir", "tree-sitter-go", "tree-sitter-html", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f84c8836bd..27c8477355 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -92,6 +92,7 @@ toml = "0.5" tree-sitter = "0.20" tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" +tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" } 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" } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index ba1945f316..71a85af0c8 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -47,6 +47,11 @@ pub async fn init(languages: Arc, _executor: Arc) tree_sitter_cpp::language(), Some(CachedLspAdapter::new(c::CLspAdapter).await), ), + ( + "css", + tree_sitter_css::language(), + None, // + ), ( "elixir", tree_sitter_elixir::language(), diff --git a/crates/zed/src/languages/css/brackets.scm b/crates/zed/src/languages/css/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed/src/languages/css/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed/src/languages/css/config.toml b/crates/zed/src/languages/css/config.toml new file mode 100644 index 0000000000..28def3abd5 --- /dev/null +++ b/crates/zed/src/languages/css/config.toml @@ -0,0 +1,9 @@ +name = "CSS" +path_suffixes = ["css"] +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 } +] diff --git a/crates/zed/src/languages/css/highlights.scm b/crates/zed/src/languages/css/highlights.scm new file mode 100644 index 0000000000..3638837af7 --- /dev/null +++ b/crates/zed/src/languages/css/highlights.scm @@ -0,0 +1,76 @@ +(comment) @comment + +[ + (tag_name) + (nesting_selector) + (universal_selector) +] @tag + +[ + "~" + ">" + "+" + "-" + "*" + "/" + "=" + "^=" + "|=" + "~=" + "$=" + "*=" + "and" + "or" + "not" + "only" +] @operator + +(attribute_selector (plain_value) @string) + +(attribute_name) @attribute +(pseudo_element_selector (tag_name) @attribute) +(pseudo_class_selector (class_name) @attribute) + +[ + (class_name) + (id_name) + (namespace_name) + (property_name) + (feature_name) +] @property + +(function_name) @function + +((property_name) @variable + (#match? @variable "^--")) +((plain_value) @variable + (#match? @variable "^--")) + +[ + "@media" + "@import" + "@charset" + "@namespace" + "@supports" + "@keyframes" + (at_keyword) + (to) + (from) + (important) +] @keyword + +(string_value) @string +(color_value) @string.special + +[ + (integer_value) + (float_value) +] @number + +(unit) @type + +[ + "#" + "," + ":" +] @punctuation.delimiter diff --git a/crates/zed/src/languages/css/indents.scm b/crates/zed/src/languages/css/indents.scm new file mode 100644 index 0000000000..e975469092 --- /dev/null +++ b/crates/zed/src/languages/css/indents.scm @@ -0,0 +1 @@ +(_ "{" "}" @end) @indent diff --git a/crates/zed/src/languages/html/injections.scm b/crates/zed/src/languages/html/injections.scm index 60688f599f..9084e373f2 100644 --- a/crates/zed/src/languages/html/injections.scm +++ b/crates/zed/src/languages/html/injections.scm @@ -1,3 +1,7 @@ (script_element - (raw_text) @content - (#set! "language" "javascript")) + (raw_text) @content + (#set! "language" "javascript")) + +(style_element + (raw_text) @content + (#set! "language" "css")) From 67e188a015cc95dc4c84440bd9a63cb4903789be Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 31 Aug 2022 16:50:44 -0700 Subject: [PATCH 05/15] Add Buffer::language_at, update MultiBuffer to use it Co-authored-by: Julia Risley --- Cargo.lock | 12 ++++ crates/editor/Cargo.toml | 4 ++ crates/editor/src/editor.rs | 94 ++++++++++++++++++++++++-- crates/editor/src/multi_buffer.rs | 27 +++++++- crates/language/src/buffer.rs | 21 +++++- crates/language/src/language.rs | 29 ++++---- crates/language/src/syntax_map.rs | 91 ++++++++++++++++--------- crates/language/src/tests.rs | 2 +- crates/zed/src/languages.rs | 6 +- crates/zed/src/languages/c.rs | 7 +- crates/zed/src/languages/elixir.rs | 4 +- crates/zed/src/languages/go.rs | 4 +- crates/zed/src/languages/python.rs | 7 +- crates/zed/src/languages/rust.rs | 6 +- crates/zed/src/languages/typescript.rs | 8 +-- crates/zed/src/zed.rs | 4 +- 16 files changed, 245 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d74213062..79eae80258 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1719,6 +1719,8 @@ dependencies = [ "text", "theme", "tree-sitter", + "tree-sitter-html", + "tree-sitter-javascript", "tree-sitter-rust", "unindent", "util", @@ -6062,6 +6064,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-javascript" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2490fab08630b2c8943c320f7b63473cbf65511c8d83aec551beb9b4375906ed" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-json" version = "0.19.0" diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index dfd4938742..cfe244d4fa 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -51,6 +51,8 @@ serde = { version = "1.0", features = ["derive", "rc"] } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" tree-sitter-rust = { version = "*", optional = true } +tree-sitter-html = { version = "*", optional = true } +tree-sitter-javascript = { version = "*", optional = true } [dev-dependencies] text = { path = "../text", features = ["test-support"] } @@ -67,3 +69,5 @@ rand = "0.8" unindent = "0.1.7" tree-sitter = "0.20" tree-sitter-rust = "0.20" +tree-sitter-html = "0.19" +tree-sitter-javascript = "0.20" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c6cfd887db..b313d2de55 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1116,7 +1116,7 @@ impl Editor { &self, point: T, cx: &'a AppContext, - ) -> Option<&'a Arc> { + ) -> Option> { self.buffer.read(cx).language_at(point, cx) } @@ -4501,9 +4501,9 @@ impl Editor { // as that portion won't be used for detecting if a line is a comment. let full_comment_prefix: Arc = if let Some(prefix) = buffer .language_at(selection.start, cx) - .and_then(|l| l.line_comment_prefix()) + .and_then(|l| l.line_comment_prefix().map(|p| p.into())) { - prefix.into() + prefix } else { return; }; @@ -6713,7 +6713,7 @@ mod tests { platform::{WindowBounds, WindowOptions}, }; use indoc::indoc; - use language::{FakeLspAdapter, LanguageConfig}; + use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry}; use project::FakeFs; use settings::EditorSettings; use std::{cell::RefCell, rc::Rc, time::Instant}; @@ -9792,6 +9792,92 @@ mod tests { }); } + #[gpui::test] + async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx).await; + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + brackets: vec![BracketPair { + start: "<".to_string(), + end: ">".to_string(), + close: true, + newline: true, + }], + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + brackets: vec![BracketPair { + start: "/*".to_string(), + end: "*/".to_string(), + close: true, + newline: true, + }], + autoclose_before: "})]".to_string(), + ..Default::default() + }, + Some(tree_sitter_javascript::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); + + cx.set_state( + &r#" + ˇ + + + "# + .unindent(), + ); + + let cursors = cx.update_editor(|editor, cx| editor.selections.ranges::(cx)); + cx.update_buffer(|buffer, _| { + let snapshot = buffer.snapshot(); + assert_eq!( + snapshot + .language_at(cursors[0].start) + .unwrap() + .name() + .as_ref(), + "HTML" + ); + assert_eq!( + snapshot + .language_at(cursors[1].start) + .unwrap() + .name() + .as_ref(), + "JavaScript" + ); + }); + } + #[gpui::test] async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { cx.update(|cx| cx.set_global(Settings::test(cx))); diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4ee9526a67..3b43f99ca0 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1212,9 +1212,9 @@ impl MultiBuffer { &self, point: T, cx: &'a AppContext, - ) -> Option<&'a Arc> { + ) -> Option> { self.point_to_buffer_offset(point, cx) - .and_then(|(buffer, _)| buffer.read(cx).language()) + .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) } pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> { @@ -1940,6 +1940,24 @@ impl MultiBufferSnapshot { } } + pub fn point_to_buffer_offset( + &self, + point: T, + ) -> Option<(&BufferSnapshot, usize)> { + let offset = point.to_offset(&self); + let mut cursor = self.excerpts.cursor::(); + cursor.seek(&offset, Bias::Right, &()); + if cursor.item().is_none() { + cursor.prev(&()); + } + + cursor.item().map(|excerpt| { + let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); + let buffer_point = excerpt_start + offset - *cursor.start(); + (&excerpt.buffer, buffer_point) + }) + } + pub fn suggested_indents( &self, rows: impl IntoIterator, @@ -2490,6 +2508,11 @@ impl MultiBufferSnapshot { .and_then(|excerpt| excerpt.buffer.language()) } + pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { + self.point_to_buffer_offset(point) + .and_then(|(buffer, offset)| buffer.language_at(offset)) + } + pub fn is_dirty(&self) -> bool { self.is_dirty } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index c8730be452..372f77cf20 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -641,6 +641,15 @@ impl Buffer { self.language.as_ref() } + pub fn language_at(&self, position: D) -> Option> { + let offset = position.to_offset(self); + self.syntax_map + .lock() + .layers_for_range(offset..offset, &self.text) + .last() + .map(|info| info.language.clone()) + } + pub fn parse_count(&self) -> usize { self.parse_count } @@ -1826,6 +1835,14 @@ impl BufferSnapshot { self.language.as_ref() } + pub fn language_at(&self, position: D) -> Option<&Arc> { + let offset = position.to_offset(self); + self.syntax + .layers_for_range(offset..offset, &self.text) + .last() + .map(|info| info.language) + } + pub fn surrounding_word(&self, start: T) -> (Range, Option) { let mut start = start.to_offset(self); let mut end = start; @@ -1858,8 +1875,8 @@ impl BufferSnapshot { pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut result: Option> = None; - 'outer: for (_, _, node) in self.syntax.layers_for_range(range.clone(), &self.text) { - let mut cursor = node.walk(); + 'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) { + let mut cursor = layer.node.walk(); // Descend to the first leaf that touches the start of the range, // and if the range is non-empty, extends beyond the start. diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 0366bdf669..341f70bff9 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -135,7 +135,7 @@ impl CachedLspAdapter { pub async fn label_for_completion( &self, completion_item: &lsp::CompletionItem, - language: &Language, + language: &Arc, ) -> Option { self.adapter .label_for_completion(completion_item, language) @@ -146,7 +146,7 @@ impl CachedLspAdapter { &self, name: &str, kind: lsp::SymbolKind, - language: &Language, + language: &Arc, ) -> Option { self.adapter.label_for_symbol(name, kind, language).await } @@ -175,7 +175,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn label_for_completion( &self, _: &lsp::CompletionItem, - _: &Language, + _: &Arc, ) -> Option { None } @@ -184,7 +184,7 @@ pub trait LspAdapter: 'static + Send + Sync { &self, _: &str, _: lsp::SymbolKind, - _: &Language, + _: &Arc, ) -> Option { None } @@ -793,7 +793,7 @@ impl Language { } pub async fn label_for_completion( - &self, + self: &Arc, completion: &lsp::CompletionItem, ) -> Option { self.adapter @@ -802,7 +802,11 @@ impl Language { .await } - pub async fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option { + pub async fn label_for_symbol( + self: &Arc, + name: &str, + kind: lsp::SymbolKind, + ) -> Option { self.adapter .as_ref()? .label_for_symbol(name, kind, self) @@ -810,20 +814,17 @@ impl Language { } pub fn highlight_text<'a>( - &'a self, + self: &'a Arc, text: &'a Rope, range: Range, ) -> Vec<(Range, HighlightId)> { let mut result = Vec::new(); if let Some(grammar) = &self.grammar { let tree = grammar.parse_text(text, None); - let captures = SyntaxSnapshot::single_tree_captures( - range.clone(), - text, - &tree, - grammar, - |grammar| grammar.highlights_query.as_ref(), - ); + let captures = + SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| { + grammar.highlights_query.as_ref() + }); let highlight_maps = vec![grammar.highlight_map()]; let mut offset = 0; for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index a8cac76ac7..a7d9101d7b 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -92,6 +92,12 @@ struct SyntaxLayer { language: Arc, } +pub struct SyntaxLayerInfo<'a> { + pub depth: usize, + pub node: Node<'a>, + pub language: &'a Arc, +} + #[derive(Debug, Clone)] struct SyntaxLayerSummary { min_depth: usize, @@ -473,13 +479,18 @@ impl SyntaxSnapshot { range: Range, text: &'a Rope, tree: &'a Tree, - grammar: &'a Grammar, + language: &'a Arc, query: fn(&Grammar) -> Option<&Query>, ) -> SyntaxMapCaptures<'a> { SyntaxMapCaptures::new( range.clone(), text, - [(grammar, 0, tree.root_node())].into_iter(), + [SyntaxLayerInfo { + language, + depth: 0, + node: tree.root_node(), + }] + .into_iter(), query, ) } @@ -513,7 +524,7 @@ impl SyntaxSnapshot { } #[cfg(test)] - pub fn layers(&self, buffer: &BufferSnapshot) -> Vec<(&Grammar, usize, Node)> { + pub fn layers(&self, buffer: &BufferSnapshot) -> Vec { self.layers_for_range(0..buffer.len(), buffer) } @@ -521,7 +532,7 @@ impl SyntaxSnapshot { &self, range: Range, buffer: &BufferSnapshot, - ) -> Vec<(&Grammar, usize, Node)> { + ) -> Vec { let start = buffer.anchor_before(range.start.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer)); @@ -538,16 +549,14 @@ impl SyntaxSnapshot { let mut result = Vec::new(); cursor.next(buffer); while let Some(layer) = cursor.item() { - if let Some(grammar) = &layer.language.grammar { - result.push(( - grammar.as_ref(), - layer.depth, - layer.tree.root_node_with_offset( - layer.range.start.to_offset(buffer), - layer.range.start.to_point(buffer).to_ts_point(), - ), - )); - } + result.push(SyntaxLayerInfo { + language: &layer.language, + depth: layer.depth, + node: layer.tree.root_node_with_offset( + layer.range.start.to_offset(buffer), + layer.range.start.to_point(buffer).to_ts_point(), + ), + }); cursor.next(buffer) } @@ -559,7 +568,7 @@ impl<'a> SyntaxMapCaptures<'a> { fn new( range: Range, text: &'a Rope, - layers: impl Iterator)>, + layers: impl Iterator>, query: fn(&Grammar) -> Option<&Query>, ) -> Self { let mut result = Self { @@ -567,11 +576,19 @@ impl<'a> SyntaxMapCaptures<'a> { grammars: Vec::new(), active_layer_count: 0, }; - for (grammar, depth, node) in layers { - let query = if let Some(query) = query(grammar) { - query - } else { - continue; + for SyntaxLayerInfo { + language, + depth, + node, + } in layers + { + let grammar = match &language.grammar { + Some(grammer) => grammer, + None => continue, + }; + let query = match query(&grammar) { + Some(query) => query, + None => continue, }; let mut query_cursor = QueryCursorHandle::new(); @@ -678,15 +695,23 @@ impl<'a> SyntaxMapMatches<'a> { fn new( range: Range, text: &'a Rope, - layers: impl Iterator)>, + layers: impl Iterator>, query: fn(&Grammar) -> Option<&Query>, ) -> Self { let mut result = Self::default(); - for (grammar, depth, node) in layers { - let query = if let Some(query) = query(grammar) { - query - } else { - continue; + for SyntaxLayerInfo { + language, + depth, + node, + } in layers + { + let grammar = match &language.grammar { + Some(grammer) => grammer, + None => continue, + }; + let query = match query(&grammar) { + Some(query) => query, + None => continue, }; let mut query_cursor = QueryCursorHandle::new(); @@ -1624,8 +1649,8 @@ mod tests { let reference_layers = reference_syntax_map.layers(&buffer); for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) { - assert_eq!(edited_layer.2.to_sexp(), reference_layer.2.to_sexp()); - assert_eq!(edited_layer.2.range(), reference_layer.2.range()); + assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp()); + assert_eq!(edited_layer.node.range(), reference_layer.node.range()); } } @@ -1770,13 +1795,13 @@ mod tests { mutated_layers.into_iter().zip(reference_layers.into_iter()) { assert_eq!( - edited_layer.2.to_sexp(), - reference_layer.2.to_sexp(), + edited_layer.node.to_sexp(), + reference_layer.node.to_sexp(), "different layer at step {i}" ); assert_eq!( - edited_layer.2.range(), - reference_layer.2.range(), + edited_layer.node.range(), + reference_layer.node.range(), "different layer at step {i}" ); } @@ -1828,7 +1853,7 @@ mod tests { expected_layers.len(), "wrong number of layers" ); - for (i, ((_, _, node), expected_s_exp)) in + for (i, (SyntaxLayerInfo { node, .. }, expected_s_exp)) in layers.iter().zip(expected_layers.iter()).enumerate() { let actual_s_exp = node.to_sexp(); diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 821bbc9968..8f56f3287e 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -1449,7 +1449,7 @@ fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> Str buffer.read_with(cx, |buffer, _| { let snapshot = buffer.snapshot(); let layers = snapshot.syntax.layers(buffer.as_text_snapshot()); - layers[0].2.to_sexp() + layers[0].node.to_sexp() }) } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 71a85af0c8..2745fa824a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -108,7 +108,7 @@ pub async fn init(languages: Arc, _executor: Arc) Some(CachedLspAdapter::new(html::HtmlLspAdapter).await), ), ] { - languages.add(Arc::new(language(name, grammar, lsp_adapter))); + languages.add(language(name, grammar, lsp_adapter)); } } @@ -116,7 +116,7 @@ pub(crate) fn language( name: &str, grammar: tree_sitter::Language, lsp_adapter: Option>, -) -> Language { +) -> Arc { let config = toml::from_slice( &LanguageDir::get(&format!("{}/config.toml", name)) .unwrap() @@ -153,7 +153,7 @@ pub(crate) fn language( if let Some(lsp_adapter) = lsp_adapter { language = language.with_lsp_adapter(lsp_adapter) } - language + Arc::new(language) } fn load_query(name: &str, filename_prefix: &str) -> Option> { diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 6aa750f6a0..712e87101b 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -112,7 +112,7 @@ impl super::LspAdapter for CLspAdapter { async fn label_for_completion( &self, completion: &lsp::CompletionItem, - language: &Language, + language: &Arc, ) -> Option { let label = completion .label @@ -190,7 +190,7 @@ impl super::LspAdapter for CLspAdapter { &self, name: &str, kind: lsp::SymbolKind, - language: &Language, + language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { @@ -251,7 +251,6 @@ mod tests { use gpui::MutableAppContext; use language::{AutoindentMode, Buffer}; use settings::Settings; - use std::sync::Arc; #[gpui::test] fn test_c_autoindent(cx: &mut MutableAppContext) { @@ -262,7 +261,7 @@ mod tests { let language = crate::languages::language("c", tree_sitter_c::language(), None); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); + let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); // empty function buffer.edit([(0..0, "int main() {}")], None, cx); diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index 4959338522..75b35bb630 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -113,7 +113,7 @@ impl LspAdapter for ElixirLspAdapter { async fn label_for_completion( &self, completion: &lsp::CompletionItem, - language: &Language, + language: &Arc, ) -> Option { match completion.kind.zip(completion.detail.as_ref()) { Some((_, detail)) if detail.starts_with("(function)") => { @@ -168,7 +168,7 @@ impl LspAdapter for ElixirLspAdapter { &self, name: &str, kind: SymbolKind, - language: &Language, + language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { SymbolKind::METHOD | SymbolKind::FUNCTION => { diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index 729d39b513..19692fdf44 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -134,7 +134,7 @@ impl super::LspAdapter for GoLspAdapter { async fn label_for_completion( &self, completion: &lsp::CompletionItem, - language: &Language, + language: &Arc, ) -> Option { let label = &completion.label; @@ -235,7 +235,7 @@ impl super::LspAdapter for GoLspAdapter { &self, name: &str, kind: lsp::SymbolKind, - language: &Language, + language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 274fc3216c..e6e55eeac4 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -90,7 +90,7 @@ impl LspAdapter for PythonLspAdapter { async fn label_for_completion( &self, item: &lsp::CompletionItem, - language: &language::Language, + language: &Arc, ) -> Option { let label = &item.label; let grammar = language.grammar()?; @@ -112,7 +112,7 @@ impl LspAdapter for PythonLspAdapter { &self, name: &str, kind: lsp::SymbolKind, - language: &language::Language, + language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { @@ -149,7 +149,6 @@ mod tests { use gpui::{ModelContext, MutableAppContext}; use language::{AutoindentMode, Buffer}; use settings::Settings; - use std::sync::Arc; #[gpui::test] fn test_python_autoindent(cx: &mut MutableAppContext) { @@ -160,7 +159,7 @@ mod tests { cx.set_global(settings); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); + let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index adbe431279..f5776f3420 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -119,7 +119,7 @@ impl LspAdapter for RustLspAdapter { async fn label_for_completion( &self, completion: &lsp::CompletionItem, - language: &Language, + language: &Arc, ) -> Option { match completion.kind { Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => { @@ -196,7 +196,7 @@ impl LspAdapter for RustLspAdapter { &self, name: &str, kind: lsp::SymbolKind, - language: &Language, + language: &Arc, ) -> Option { let (text, filter_range, display_range) = match kind { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { @@ -439,7 +439,7 @@ mod tests { cx.set_global(settings); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); + let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); // indent between braces buffer.set_text("fn a() {}", cx); diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 85a1bd6400..95f56bce5b 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -115,7 +115,7 @@ impl LspAdapter for TypeScriptLspAdapter { async fn label_for_completion( &self, item: &lsp::CompletionItem, - language: &language::Language, + language: &Arc, ) -> Option { use lsp::CompletionItemKind as Kind; let len = item.label.len(); @@ -144,7 +144,6 @@ impl LspAdapter for TypeScriptLspAdapter { #[cfg(test)] mod tests { - use std::sync::Arc; use gpui::MutableAppContext; use unindent::Unindent; @@ -172,9 +171,8 @@ mod tests { "# .unindent(); - let buffer = cx.add_model(|cx| { - language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx) - }); + let buffer = + cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); let outline = buffer.read(cx).snapshot().outline(None).unwrap(); assert_eq!( outline diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 76bc62e4cb..f86022e39c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1133,7 +1133,7 @@ mod tests { assert!(!editor.is_dirty(cx)); assert_eq!(editor.title(cx), "untitled"); assert!(Arc::ptr_eq( - editor.language_at(0, cx).unwrap(), + &editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); editor.handle_input("hi", cx); @@ -1220,7 +1220,7 @@ mod tests { editor.update(cx, |editor, cx| { assert!(Arc::ptr_eq( - editor.language_at(0, cx).unwrap(), + &editor.language_at(0, cx).unwrap(), &languages::PLAIN_TEXT )); editor.handle_input("hi", cx); From 2b0794f5ae76d1f1fdd6a8d16c3a0db17ddc9cb6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 9 Sep 2022 17:40:34 -0700 Subject: [PATCH 06/15] Restructure autoclosing to account for multi-language documents --- crates/editor/src/editor.rs | 572 +++++++++++----------- crates/zed/src/languages/html/config.toml | 2 +- 2 files changed, 281 insertions(+), 293 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b313d2de55..055b73d7dc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -410,7 +410,7 @@ pub struct Editor { add_selections_state: Option, select_next_state: Option, selection_history: SelectionHistory, - autoclose_stack: InvalidationStack, + autoclose_regions: Vec, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, @@ -569,8 +569,9 @@ struct SelectNextState { done: bool, } -struct BracketPairState { - ranges: Vec>, +struct AutocloseRegion { + selection_id: usize, + range: Range, pair: BracketPair, } @@ -1010,7 +1011,7 @@ impl Editor { add_selections_state: None, select_next_state: None, selection_history: Default::default(), - autoclose_stack: Default::default(), + autoclose_regions: Default::default(), snippet_stack: Default::default(), select_larger_syntax_node_stack: Vec::new(), ime_transaction: Default::default(), @@ -1401,8 +1402,7 @@ impl Editor { self.add_selections_state = None; self.select_next_state = None; self.select_larger_syntax_node_stack.clear(); - self.autoclose_stack - .invalidate(&self.selections.disjoint_anchors(), buffer); + self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); self.snippet_stack .invalidate(&self.selections.disjoint_anchors(), buffer); self.take_rename(false, cx); @@ -1849,15 +1849,158 @@ impl Editor { return; } - if !self.skip_autoclose_end(text, cx) { - self.transact(cx, |this, cx| { - if !this.surround_with_bracket_pair(text, cx) { - this.insert(text, cx); - this.autoclose_bracket_pairs(cx); + let text: Arc = text.into(); + let selections = self.selections.all_adjusted(cx); + let mut edits = Vec::new(); + let mut new_selections = Vec::with_capacity(selections.len()); + let mut new_autoclose_regions = Vec::new(); + let snapshot = self.buffer.read(cx).read(cx); + + for (selection, autoclose_region) in + self.selections_with_autoclose_regions(selections, &snapshot) + { + if let Some(language) = snapshot.language_at(selection.head()) { + // Determine if the inserted text matches the opening or closing + // bracket of any of this language's bracket pairs. + let mut bracket_pair = None; + let mut is_bracket_pair_start = false; + for pair in language.brackets() { + if pair.start.ends_with(text.as_ref()) { + bracket_pair = Some(pair.clone()); + is_bracket_pair_start = true; + break; + } else if pair.end.as_str() == text.as_ref() { + bracket_pair = Some(pair.clone()); + break; + } } - }); - self.trigger_completion_on_input(text, cx); + + if let Some(bracket_pair) = bracket_pair { + if selection.is_empty() { + if is_bracket_pair_start { + let prefix_len = bracket_pair.start.len() - text.len(); + + // If the inserted text is a suffix of an opening bracket and the + // selection is preceded by the rest of the opening bracket, then + // insert the closing bracket. + let should_autoclose = selection.start.column > (prefix_len as u32) + && snapshot.contains_str_at( + Point::new( + selection.start.row, + selection.start.column - (prefix_len as u32), + ), + &bracket_pair.start[..prefix_len], + ) + && snapshot + .chars_at(selection.start) + .next() + .map_or(true, |c| language.should_autoclose_before(c)); + if should_autoclose { + let anchor = snapshot.anchor_before(selection.end); + new_selections + .push((selection.map(|_| anchor.clone()), text.len())); + new_autoclose_regions.push(( + anchor.clone(), + text.len(), + selection.id, + bracket_pair.clone(), + )); + edits.push(( + selection.range(), + format!("{}{}", text, bracket_pair.end).into(), + )); + continue; + } + } else if let Some(region) = autoclose_region { + // If the selection is followed by an auto-inserted closing bracket, + // then don't insert anything else; just move the selection past the + // closing bracket. + let should_skip = selection.end == region.range.end.to_point(&snapshot); + if should_skip { + let anchor = snapshot.anchor_after(selection.end); + new_selections.push(( + selection.map(|_| anchor.clone()), + region.pair.end.len(), + )); + continue; + } + } + } + // If an opening bracket is typed while text is selected, then + // surround that text with the bracket pair. + else if is_bracket_pair_start { + edits.push((selection.start..selection.start, text.clone())); + edits.push(( + selection.end..selection.end, + bracket_pair.end.as_str().into(), + )); + new_selections.push(( + Selection { + id: selection.id, + start: snapshot.anchor_after(selection.start), + end: snapshot.anchor_before(selection.end), + reversed: selection.reversed, + goal: selection.goal, + }, + 0, + )); + continue; + } + } + } + + // If not handling any auto-close operation, then just replace the selected + // text with the given input and move the selection to the end of the + // newly inserted text. + let anchor = snapshot.anchor_after(selection.end); + new_selections.push((selection.map(|_| anchor.clone()), 0)); + edits.push((selection.start..selection.end, text.clone())); } + + drop(snapshot); + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, Some(AutoindentMode::EachLine), cx); + }); + + let new_anchor_selections = new_selections.iter().map(|e| &e.0); + let new_selection_deltas = new_selections.iter().map(|e| e.1); + let snapshot = this.buffer.read(cx).read(cx); + let new_selections = resolve_multiple::(new_anchor_selections, &snapshot) + .zip(new_selection_deltas) + .map(|(selection, delta)| selection.map(|e| e + delta)) + .collect::>(); + + let mut i = 0; + for (position, delta, selection_id, pair) in new_autoclose_regions { + let position = position.to_offset(&snapshot) + delta; + let start = snapshot.anchor_before(position); + let end = snapshot.anchor_after(position); + while let Some(existing_state) = this.autoclose_regions.get(i) { + match existing_state.range.start.cmp(&start, &snapshot) { + Ordering::Less => i += 1, + Ordering::Greater => break, + Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) { + Ordering::Less => i += 1, + Ordering::Equal => break, + Ordering::Greater => break, + }, + } + } + this.autoclose_regions.insert( + i, + AutocloseRegion { + selection_id, + range: start..end, + pair, + }, + ); + } + + drop(snapshot); + this.change_selections(None, cx, |s| s.select(new_selections)); + this.trigger_completion_on_input(&text, cx); + }); } pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { @@ -2029,232 +2172,89 @@ impl Editor { } } - fn surround_with_bracket_pair(&mut self, text: &str, cx: &mut ViewContext) -> bool { - let snapshot = self.buffer.read(cx).snapshot(cx); - if let Some(pair) = snapshot - .language() - .and_then(|language| language.brackets().iter().find(|b| b.start == text)) - .cloned() - { - if self - .selections - .all::(cx) - .iter() - .any(|selection| selection.is_empty()) - { - return false; - } - - let mut selections = self.selections.disjoint_anchors().to_vec(); - for selection in &mut selections { - selection.end = selection.end.bias_left(&snapshot); - } - drop(snapshot); - - self.buffer.update(cx, |buffer, cx| { - let pair_start: Arc = pair.start.clone().into(); - let pair_end: Arc = pair.end.clone().into(); - buffer.edit( - selections.iter().flat_map(|s| { - [ - (s.start.clone()..s.start.clone(), pair_start.clone()), - (s.end.clone()..s.end.clone(), pair_end.clone()), - ] - }), - None, - cx, - ); - }); - - let snapshot = self.buffer.read(cx).read(cx); - for selection in &mut selections { - selection.end = selection.end.bias_right(&snapshot); - } - drop(snapshot); - - self.change_selections(None, cx, |s| s.select_anchors(selections)); - true - } else { - false - } - } - - fn autoclose_bracket_pairs(&mut self, cx: &mut ViewContext) { + /// If any empty selections is touching the start of its innermost containing autoclose + /// region, expand it to select the brackets. + fn select_autoclose_pair(&mut self, cx: &mut ViewContext) { let selections = self.selections.all::(cx); - let mut bracket_pair_state = None; - let mut new_selections = None; - self.buffer.update(cx, |buffer, cx| { - let mut snapshot = buffer.snapshot(cx); - let left_biased_selections = selections - .iter() - .map(|selection| selection.map(|p| snapshot.anchor_before(p))) - .collect::>(); - - let autoclose_pair = snapshot.language().and_then(|language| { - let first_selection_start = selections.first().unwrap().start; - let pair = language.brackets().iter().find(|pair| { - pair.close - && snapshot.contains_str_at( - first_selection_start.saturating_sub(pair.start.len()), - &pair.start, - ) - }); - pair.and_then(|pair| { - let should_autoclose = selections.iter().all(|selection| { - // Ensure all selections are parked at the end of a pair start. - if snapshot.contains_str_at( - selection.start.saturating_sub(pair.start.len()), - &pair.start, - ) { - snapshot - .chars_at(selection.start) - .next() - .map_or(true, |c| language.should_autoclose_before(c)) - } else { - false + let buffer = self.buffer.read(cx).read(cx); + let mut new_selections = Vec::new(); + for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) { + if let (Some(region), true) = (region, selection.is_empty()) { + let mut range = region.range.to_offset(&buffer); + if selection.start == range.start { + if range.start >= region.pair.start.len() { + range.start -= region.pair.start.len(); + if buffer.contains_str_at(range.start, ®ion.pair.start) { + if buffer.contains_str_at(range.end, ®ion.pair.end) { + range.end += region.pair.end.len(); + selection.start = range.start; + selection.end = range.end; + } } - }); - - if should_autoclose { - Some(pair.clone()) - } else { - None } - }) - }); - - if let Some(pair) = autoclose_pair { - let selection_ranges = selections - .iter() - .map(|selection| { - let start = selection.start.to_offset(&snapshot); - start..start - }) - .collect::>(); - - let pair_end: Arc = pair.end.clone().into(); - buffer.edit( - selection_ranges - .iter() - .map(|range| (range.clone(), pair_end.clone())), - None, - cx, - ); - snapshot = buffer.snapshot(cx); - - new_selections = Some( - resolve_multiple::(left_biased_selections.iter(), &snapshot) - .collect::>(), - ); - - if pair.end.len() == 1 { - let mut delta = 0; - bracket_pair_state = Some(BracketPairState { - ranges: selections - .iter() - .map(move |selection| { - let offset = selection.start + delta; - delta += 1; - snapshot.anchor_before(offset)..snapshot.anchor_after(offset) - }) - .collect(), - pair, - }); } } - }); + new_selections.push(selection); + } - if let Some(new_selections) = new_selections { - self.change_selections(None, cx, |s| { - s.select(new_selections); - }); - } - if let Some(bracket_pair_state) = bracket_pair_state { - self.autoclose_stack.push(bracket_pair_state); - } + drop(buffer); + self.change_selections(None, cx, |selections| selections.select(new_selections)); } - fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { - let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.selections.all::(cx); - let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() { - autoclose_pair - } else { - return false; - }; - if text != autoclose_pair.pair.end { - return false; - } + /// Iterate the given selections, and for each one, find the smallest surrounding + /// autoclose region. This uses the ordering of the selections and the autoclose + /// regions to avoid repeated comparisons. + fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>( + &'a self, + selections: impl IntoIterator>, + buffer: &'a MultiBufferSnapshot, + ) -> impl Iterator, Option<&'a AutocloseRegion>)> { + let mut i = 0; + let mut pair_states = self.autoclose_regions.as_slice(); + selections.into_iter().map(move |selection| { + let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); - debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); - - if old_selections - .iter() - .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer))) - .all(|(selection, autoclose_range)| { - let autoclose_range_end = autoclose_range.end.to_offset(&buffer); - selection.is_empty() && selection.start == autoclose_range_end - }) - { - let new_selections = old_selections - .into_iter() - .map(|selection| { - let cursor = selection.start + 1; - Selection { - id: selection.id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - self.autoclose_stack.pop(); - self.change_selections(Some(Autoscroll::Fit), cx, |s| { - s.select(new_selections); - }); - true - } else { - false - } - } - - fn select_autoclose_pair(&mut self, cx: &mut ViewContext) -> bool { - let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.selections.all::(cx); - let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() { - autoclose_pair - } else { - return false; - }; - - debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); - - let mut new_selections = Vec::new(); - for (selection, autoclose_range) in old_selections - .iter() - .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer))) - { - if selection.is_empty() - && autoclose_range.is_empty() - && selection.start == autoclose_range.start - { - new_selections.push(Selection { - id: selection.id, - start: selection.start - autoclose_pair.pair.start.len(), - end: selection.end + autoclose_pair.pair.end.len(), - reversed: true, - goal: selection.goal, - }); - } else { - return false; + let mut enclosing = None; + while let Some(pair_state) = pair_states.get(i) { + if pair_state.range.end.to_offset(buffer) < range.start { + pair_states = &pair_states[i + 1..]; + i = 0; + } else if pair_state.range.start.to_offset(buffer) > range.end { + break; + } else if pair_state.selection_id == selection.id { + enclosing = Some(pair_state); + i += 1; + } } - } - self.change_selections(Some(Autoscroll::Fit), cx, |selections| { - selections.select(new_selections) + (selection.clone(), enclosing) + }) + } + + /// Remove any autoclose regions that no longer contain their selection. + fn invalidate_autoclose_regions( + &mut self, + mut selections: &[Selection], + buffer: &MultiBufferSnapshot, + ) { + self.autoclose_regions.retain(|state| { + let mut i = 0; + while let Some(selection) = selections.get(i) { + if selection.end.cmp(&state.range.start, buffer).is_lt() { + selections = &selections[1..]; + continue; + } + if selection.start.cmp(&state.range.end, buffer).is_gt() { + break; + } + if selection.id == state.selection_id { + return true; + } else { + i += 1; + } + } + false }); - true } fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { @@ -2909,51 +2909,47 @@ impl Editor { pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { self.transact(cx, |this, cx| { - if !this.select_autoclose_pair(cx) { - let mut selections = this.selections.all::(cx); - if !this.selections.line_mode { - let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in &mut selections { - if selection.is_empty() { - let old_head = selection.head(); - let mut new_head = movement::left( - &display_map, - old_head.to_display_point(&display_map), - ) - .to_point(&display_map); - if let Some((buffer, line_buffer_range)) = display_map - .buffer_snapshot - .buffer_line_for_row(old_head.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_len = match indent_size.kind { - IndentKind::Space => { - cx.global::().tab_size(language_name.as_deref()) - } - IndentKind::Tab => NonZeroU32::new(1).unwrap(), - }; - if old_head.column <= indent_size.len && old_head.column > 0 { - let indent_len = indent_len.get(); - new_head = cmp::min( - new_head, - Point::new( - old_head.row, - ((old_head.column - 1) / indent_len) * indent_len, - ), - ); + this.select_autoclose_pair(cx); + let mut selections = this.selections.all::(cx); + if !this.selections.line_mode { + let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in &mut selections { + if selection.is_empty() { + let old_head = selection.head(); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .to_point(&display_map); + if let Some((buffer, line_buffer_range)) = display_map + .buffer_snapshot + .buffer_line_for_row(old_head.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_len = match indent_size.kind { + IndentKind::Space => { + cx.global::().tab_size(language_name.as_deref()) } + IndentKind::Tab => NonZeroU32::new(1).unwrap(), + }; + if old_head.column <= indent_size.len && old_head.column > 0 { + let indent_len = indent_len.get(); + new_head = cmp::min( + new_head, + Point::new( + old_head.row, + ((old_head.column - 1) / indent_len) * indent_len, + ), + ); } - - selection.set_head(new_head, SelectionGoal::None); } + + selection.set_head(new_head, SelectionGoal::None); } } - - this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections)); } + + this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections)); this.insert("", cx); }); } @@ -3957,17 +3953,16 @@ impl Editor { cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - if !this.select_autoclose_pair(cx) { - this.change_selections(Some(Autoscroll::Fit), cx, |s| { - let line_mode = s.line_mode; - s.move_with(|map, selection| { - if selection.is_empty() && !line_mode { - let cursor = movement::previous_word_start(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); + this.select_autoclose_pair(cx); + this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::previous_word_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } }); - } + }); this.insert("", cx); }); } @@ -3978,17 +3973,16 @@ impl Editor { cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { - if !this.select_autoclose_pair(cx) { - this.change_selections(Some(Autoscroll::Fit), cx, |s| { - let line_mode = s.line_mode; - s.move_with(|map, selection| { - if selection.is_empty() && !line_mode { - let cursor = movement::previous_subword_start(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); + this.select_autoclose_pair(cx); + this.change_selections(Some(Autoscroll::Fit), cx, |s| { + let line_mode = s.line_mode; + s.move_with(|map, selection| { + if selection.is_empty() && !line_mode { + let cursor = movement::previous_subword_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } }); - } + }); this.insert("", cx); }); } @@ -6495,12 +6489,6 @@ impl DerefMut for InvalidationStack { } } -impl InvalidationRegion for BracketPairState { - fn ranges(&self) -> &[Range] { - &self.ranges - } -} - impl InvalidationRegion for SnippetState { fn ranges(&self) -> &[Range] { &self.ranges[self.active_index] diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml index 0680717b2c..80b33b1243 100644 --- a/crates/zed/src/languages/html/config.toml +++ b/crates/zed/src/languages/html/config.toml @@ -1,6 +1,6 @@ name = "HTML" path_suffixes = ["html"] -autoclose_before = ">" +autoclose_before = ">})" brackets = [ { start = "<", end = ">", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true }, From 2da32af340980dc3eddefc924666774fba087710 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 28 Sep 2022 12:36:55 -0700 Subject: [PATCH 07/15] Update EditorTestContext usage to reflect new synchronous constructor --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 055b73d7dc..699b442a5d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9782,7 +9782,7 @@ mod tests { #[gpui::test] async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx).await; + let mut cx = EditorTestContext::new(cx); let html_language = Arc::new( Language::new( From 4f44375abd3d8a10ee2820db373d4992ab3df42b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 28 Sep 2022 13:38:54 -0700 Subject: [PATCH 08/15] Make Buffer::language_at fall back to Buffer::language For languages with no grammar (plain text), there will be no layers. --- crates/language/src/buffer.rs | 2 ++ crates/language/src/language.rs | 9 +++++++++ crates/language/src/syntax_map.rs | 1 + 3 files changed, 12 insertions(+) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 372f77cf20..4ff1b002b0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -648,6 +648,7 @@ impl Buffer { .layers_for_range(offset..offset, &self.text) .last() .map(|info| info.language.clone()) + .or_else(|| self.language.clone()) } pub fn parse_count(&self) -> usize { @@ -1841,6 +1842,7 @@ impl BufferSnapshot { .layers_for_range(offset..offset, &self.text) .last() .map(|info| info.language) + .or(self.language.as_ref()) } pub fn surrounding_word(&self, start: T) -> (Range, Option) { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 341f70bff9..b8d4ca309f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -26,6 +26,7 @@ use serde_json::Value; use std::{ any::Any, cell::RefCell, + fmt::Debug, mem, ops::Range, path::{Path, PathBuf}, @@ -866,6 +867,14 @@ impl Language { } } +impl Debug for Language { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Language") + .field("name", &self.config.name) + .finish() + } +} + impl Grammar { pub fn id(&self) -> usize { self.id diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index a7d9101d7b..8983406690 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -92,6 +92,7 @@ struct SyntaxLayer { language: Arc, } +#[derive(Debug)] pub struct SyntaxLayerInfo<'a> { pub depth: usize, pub node: Node<'a>, From c354b9b9596a41c40dfb4f573d5b66fe9c78e210 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 3 Oct 2022 13:24:37 -0700 Subject: [PATCH 09/15] Add assertions to test for autoclose with embedded languages --- crates/editor/src/editor.rs | 204 +++++++++++++++++++++++++++----- crates/language/src/language.rs | 2 +- 2 files changed, 173 insertions(+), 33 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 699b442a5d..769c03d6ff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6001,6 +6001,10 @@ impl Editor { } impl EditorSnapshot { + pub fn language_at(&self, position: T) -> Option<&Arc> { + self.display_snapshot.buffer_snapshot.language_at(position) + } + pub fn is_focused(&self) -> bool { self.is_focused } @@ -9788,13 +9792,24 @@ mod tests { Language::new( LanguageConfig { name: "HTML".into(), - brackets: vec![BracketPair { - start: "<".to_string(), - end: ">".to_string(), - close: true, - newline: true, - }], - autoclose_before: "})]".to_string(), + brackets: vec![ + BracketPair { + start: "<".into(), + end: ">".into(), + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + ..Default::default() + }, + ], + autoclose_before: "})]>".into(), ..Default::default() }, Some(tree_sitter_html::language()), @@ -9812,13 +9827,24 @@ mod tests { let javascript_language = Arc::new(Language::new( LanguageConfig { name: "JavaScript".into(), - brackets: vec![BracketPair { - start: "/*".to_string(), - end: "*/".to_string(), - close: true, - newline: true, - }], - autoclose_before: "})]".to_string(), + brackets: vec![ + BracketPair { + start: "/*".into(), + end: " */".into(), + ..Default::default() + }, + BracketPair { + start: "{".into(), + end: "}".into(), + ..Default::default() + }, + BracketPair { + start: "(".into(), + end: ")".into(), + ..Default::default() + }, + ], + autoclose_before: "})]>".into(), ..Default::default() }, Some(tree_sitter_javascript::language()), @@ -9839,31 +9865,145 @@ mod tests { - + ˇ "# .unindent(), ); - let cursors = cx.update_editor(|editor, cx| editor.selections.ranges::(cx)); - cx.update_buffer(|buffer, _| { - let snapshot = buffer.snapshot(); + // Precondition: different languages are active at different locations. + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let cursors = editor.selections.ranges::(cx); + let languages = cursors + .iter() + .map(|c| snapshot.language_at(c.start).unwrap().name()) + .collect::>(); assert_eq!( - snapshot - .language_at(cursors[0].start) - .unwrap() - .name() - .as_ref(), - "HTML" - ); - assert_eq!( - snapshot - .language_at(cursors[1].start) - .unwrap() - .name() - .as_ref(), - "JavaScript" + languages, + &["HTML".into(), "JavaScript".into(), "HTML".into()] ); }); + + // Angle brackets autoclose in HTML, but not JavaScript. + cx.update_editor(|editor, cx| { + editor.handle_input("<", cx); + editor.handle_input("a", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + + // Curly braces and parens autoclose in both HTML and JavaScript. + cx.update_editor(|editor, cx| { + editor.handle_input(" b=", cx); + editor.handle_input("{", cx); + editor.handle_input("c", cx); + editor.handle_input("(", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + + // Brackets that were already autoclosed are skipped. + cx.update_editor(|editor, cx| { + editor.handle_input(")", cx); + editor.handle_input("d", cx); + editor.handle_input("}", cx); + }); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| { + editor.handle_input(">", cx); + }); + cx.assert_editor_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Reset + cx.set_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| { + editor.handle_input("<", cx); + }); + cx.assert_editor_state( + &r#" + <ˇ> + + <ˇ> + "# + .unindent(), + ); + + // When backspacing, the closing angle brackets are removed. + cx.update_editor(|editor, cx| { + editor.backspace(&Backspace, cx); + }); + cx.assert_editor_state( + &r#" + ˇ + + ˇ + "# + .unindent(), + ); + + // Block comments autoclose in JavaScript, but not HTML. + cx.update_editor(|editor, cx| { + editor.handle_input("/", cx); + editor.handle_input("*", cx); + }); + cx.assert_editor_state( + &r#" + /*ˇ + + /*ˇ + "# + .unindent(), + ); } #[gpui::test] diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index b8d4ca309f..59da0909c6 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -271,7 +271,7 @@ pub struct FakeLspAdapter { pub disk_based_diagnostics_sources: Vec, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct BracketPair { pub start: String, pub end: String, From 218ba810133067a9fdd7c9230ebf5f9dbceb04e9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 3 Oct 2022 17:44:18 -0700 Subject: [PATCH 10/15] Fix autoclose error when cursor was at column 0 --- crates/editor/src/editor.rs | 263 ++++++++++++++---------------------- 1 file changed, 101 insertions(+), 162 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 769c03d6ff..93a47cf621 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -569,6 +569,7 @@ struct SelectNextState { done: bool, } +#[derive(Debug)] struct AutocloseRegion { selection_id: usize, range: Range, @@ -1883,19 +1884,20 @@ impl Editor { // If the inserted text is a suffix of an opening bracket and the // selection is preceded by the rest of the opening bracket, then // insert the closing bracket. - let should_autoclose = selection.start.column > (prefix_len as u32) - && snapshot.contains_str_at( - Point::new( - selection.start.row, - selection.start.column - (prefix_len as u32), - ), - &bracket_pair.start[..prefix_len], - ) - && snapshot - .chars_at(selection.start) - .next() - .map_or(true, |c| language.should_autoclose_before(c)); - if should_autoclose { + let following_text_allows_autoclose = snapshot + .chars_at(selection.start) + .next() + .map_or(true, |c| language.should_autoclose_before(c)); + let preceding_text_matches_prefix = prefix_len == 0 + || (selection.start.column >= (prefix_len as u32) + && snapshot.contains_str_at( + Point::new( + selection.start.row, + selection.start.column - (prefix_len as u32), + ), + &bracket_pair.start[..prefix_len], + )); + if following_text_allows_autoclose && preceding_text_matches_prefix { let anchor = snapshot.anchor_before(selection.end); new_selections .push((selection.map(|_| anchor.clone()), text.len())); @@ -2210,14 +2212,14 @@ impl Editor { buffer: &'a MultiBufferSnapshot, ) -> impl Iterator, Option<&'a AutocloseRegion>)> { let mut i = 0; - let mut pair_states = self.autoclose_regions.as_slice(); + let mut regions = self.autoclose_regions.as_slice(); selections.into_iter().map(move |selection| { let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); let mut enclosing = None; - while let Some(pair_state) = pair_states.get(i) { + while let Some(pair_state) = regions.get(i) { if pair_state.range.end.to_offset(buffer) < range.start { - pair_states = &pair_states[i + 1..]; + regions = ®ions[i + 1..]; i = 0; } else if pair_state.range.start.to_offset(buffer) > range.end { break; @@ -9594,7 +9596,8 @@ mod tests { #[gpui::test] async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + let mut cx = EditorTestContext::new(cx); + let language = Arc::new(Language::new( LanguageConfig { brackets: vec![ @@ -9623,165 +9626,101 @@ mod tests { Some(tree_sitter_rust::language()), )); - let text = r#" - a + let registry = Arc::new(LanguageRegistry::test()); + registry.add(language.clone()); + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(language), cx); + }); - / - - "# - .unindent(); - - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); - view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ]) - }); + cx.set_state( + &r#" + 🏀ˇ + εˇ + ❤️ˇ + "# + .unindent(), + ); + // autoclose multiple nested brackets at multiple cursors + cx.update_editor(|view, cx| { view.handle_input("{", cx); view.handle_input("{", cx); view.handle_input("{", cx); - assert_eq!( - view.text(cx), - " - {{{}}} - {{{}}} - / - - " - .unindent() - ); + }); + cx.assert_editor_state( + &" + 🏀{{{ˇ}}} + ε{{{ˇ}}} + ❤️{{{ˇ}}} + " + .unindent(), + ); + // skip over the auto-closed brackets when typing a closing bracket + cx.update_editor(|view, cx| { view.move_right(&MoveRight, cx); view.handle_input("}", cx); view.handle_input("}", cx); view.handle_input("}", cx); - assert_eq!( - view.text(cx), - " - {{{}}}} - {{{}}}} - / + }); + cx.assert_editor_state( + &" + 🏀{{{}}}}ˇ + ε{{{}}}}ˇ + ❤️{{{}}}}ˇ + " + .unindent(), + ); - " - .unindent() - ); - - view.undo(&Undo, cx); + // autoclose multi-character pairs + cx.set_state( + &" + ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| { view.handle_input("/", cx); view.handle_input("*", cx); - assert_eq!( - view.text(cx), - " - /* */ - /* */ - / - - " - .unindent() - ); - - view.undo(&Undo, cx); - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ]) - }); - view.handle_input("*", cx); - assert_eq!( - view.text(cx), - " - a - - /* - * - " - .unindent() - ); - - // Don't autoclose if the next character isn't whitespace and isn't - // listed in the language's "autoclose_before" section. - view.finalize_last_transaction(cx); - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) - }); - view.handle_input("{", cx); - assert_eq!( - view.text(cx), - " - {a - - /* - * - " - .unindent() - ); - - view.undo(&Undo, cx); - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1)]) - }); - view.handle_input("{", cx); - assert_eq!( - view.text(cx), - " - {a} - - /* - * - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)] - ); - - view.undo(&Undo, cx); - view.handle_input("[", cx); - assert_eq!( - view.text(cx), - " - [a] - - /* - * - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)] - ); - - view.undo(&Undo, cx); - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]) - }); - view.handle_input("[", cx); - assert_eq!( - view.text(cx), - " - a[ - - /* - * - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2)] - ); }); + cx.assert_editor_state( + &" + /*ˇ */ + /*ˇ */ + " + .unindent(), + ); + + // one cursor autocloses a multi-character pair, one cursor + // does not autoclose. + cx.set_state( + &" + /ˇ + ˇ + " + .unindent(), + ); + cx.update_editor(|view, cx| view.handle_input("*", cx)); + cx.assert_editor_state( + &" + /*ˇ */ + *ˇ + " + .unindent(), + ); + + // Don't autoclose if the next character isn't whitespace and isn't + // listed in the language's "autoclose_before" section. + cx.set_state("ˇa b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{ˇa b"); + + // Surround with brackets if text is selected + cx.set_state("«aˇ» b"); + cx.update_editor(|view, cx| view.handle_input("{", cx)); + cx.assert_editor_state("{«aˇ»} b"); } #[gpui::test] From d9fb8c90d8e3cec81d7915a5db37c00b5d5a6cdb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 4 Oct 2022 17:27:03 -0700 Subject: [PATCH 11/15] Start work on toggling block comments for HTML --- crates/editor/src/editor.rs | 308 ++++++++++++++++------ crates/language/src/language.rs | 17 +- crates/zed/src/languages/html/config.toml | 2 + 3 files changed, 240 insertions(+), 87 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 93a47cf621..b2420c1c44 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4487,105 +4487,184 @@ impl Editor { pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { self.transact(cx, |this, cx| { let mut selections = this.selections.all::(cx); - let mut all_selection_lines_are_comments = true; - let mut edit_ranges = Vec::new(); + let mut edits = Vec::new(); + let mut selection_edit_ranges = Vec::new(); let mut last_toggled_row = None; - this.buffer.update(cx, |buffer, cx| { - // TODO: Handle selections that cross excerpts - for selection in &mut selections { - // Get the line comment prefix. Split its trailing whitespace into a separate string, - // as that portion won't be used for detecting if a line is a comment. - let full_comment_prefix: Arc = if let Some(prefix) = buffer - .language_at(selection.start, cx) - .and_then(|l| l.line_comment_prefix().map(|p| p.into())) - { - prefix + let snapshot = this.buffer.read(cx).read(cx); + let empty_str: Arc = "".into(); + + fn comment_prefix_range( + snapshot: &MultiBufferSnapshot, + row: u32, + comment_prefix: &str, + comment_prefix_whitespace: &str, + ) -> Range { + 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() + .copied(); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_bytes + .by_ref() + .take(comment_prefix.len()) + .eq(comment_prefix.bytes()) + { + // Include any whitespace that matches the comment prefix. + let matching_whitespace_len = line_bytes + .zip(comment_prefix_whitespace.bytes()) + .take_while(|(a, b)| a == b) + .count() as u32; + let end = Point::new( + start.row, + start.column + comment_prefix.len() as u32 + matching_whitespace_len, + ); + start..end + } else { + start..start + } + } + + fn comment_suffix_range( + snapshot: &MultiBufferSnapshot, + row: u32, + comment_suffix: &str, + comment_suffix_has_leading_space: bool, + ) -> Range { + let end = Point::new(row, snapshot.line_len(row)); + let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32); + + let mut line_end_bytes = snapshot + .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end) + .flatten() + .copied(); + + let leading_space_len = if suffix_start_column > 0 + && line_end_bytes.next() == Some(b' ') + && comment_suffix_has_leading_space + { + 1 + } else { + 0 + }; + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if line_end_bytes.by_ref().eq(comment_suffix.bytes()) { + let start = Point::new(end.row, suffix_start_column - leading_space_len); + start..end + } else { + end..end + } + } + + // TODO: Handle selections that cross excerpts + for selection in &mut selections { + let language = if let Some(language) = snapshot.language_at(selection.start) { + language + } else { + continue; + }; + + let mut all_selection_lines_are_comments = true; + selection_edit_ranges.clear(); + + // If multiple selections contain a given row, avoid processing that + // row more than once. + let mut start_row = selection.start.row; + if last_toggled_row == Some(start_row) { + start_row += 1; + } + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + selection.end.row - 1 } else { - return; + selection.end.row }; + last_toggled_row = Some(end_row); + + // If the language has line comments, toggle those. + if let Some(full_comment_prefix) = language.line_comment_prefix() { + // Split the comment prefix's trailing whitespace into a separate string, + // as that portion won't be used for detecting if a line is a comment. let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - edit_ranges.clear(); - let snapshot = buffer.snapshot(cx); - - let end_row = - if selection.end.row > selection.start.row && selection.end.column == 0 { - selection.end.row - } else { - selection.end.row + 1 - }; - - for row in selection.start.row..end_row { - // If multiple selections contain a given row, avoid processing that - // row more than once. - if last_toggled_row == Some(row) { - continue; - } else { - last_toggled_row = Some(row); - } + for row in start_row..=end_row { if snapshot.is_line_blank(row) { continue; } - 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() - .copied(); - - // If this line currently begins with the line comment prefix, then record - // the range containing the prefix. - if all_selection_lines_are_comments - && line_bytes - .by_ref() - .take(comment_prefix.len()) - .eq(comment_prefix.bytes()) - { - // Include any whitespace that matches the comment prefix. - let matching_whitespace_len = line_bytes - .zip(comment_prefix_whitespace.bytes()) - .take_while(|(a, b)| a == b) - .count() - as u32; - let end = Point::new( - row, - start.column - + comment_prefix.len() as u32 - + matching_whitespace_len, - ); - edit_ranges.push(start..end); - } - // If this line does not begin with the line comment prefix, then record - // the position where the prefix should be inserted. - else { + let prefix_range = comment_prefix_range( + snapshot.deref(), + row, + comment_prefix, + comment_prefix_whitespace, + ); + if prefix_range.is_empty() { all_selection_lines_are_comments = false; - edit_ranges.push(start..start); } + selection_edit_ranges.push(prefix_range); } - if !edit_ranges.is_empty() { - if all_selection_lines_are_comments { - let empty_str: Arc = "".into(); - buffer.edit( - edit_ranges - .iter() - .cloned() - .map(|range| (range, empty_str.clone())), - None, - cx, - ); - } else { - let min_column = - edit_ranges.iter().map(|r| r.start.column).min().unwrap(); - let edits = edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - (position..position, full_comment_prefix.clone()) - }); - buffer.edit(edits, None, cx); - } + if all_selection_lines_are_comments { + edits.extend( + selection_edit_ranges + .iter() + .cloned() + .map(|range| (range, empty_str.clone())), + ); + } else { + let min_column = selection_edit_ranges + .iter() + .map(|r| r.start.column) + .min() + .unwrap_or(0); + edits.extend(selection_edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + (position..position, full_comment_prefix.clone()) + })); } + } else if let Some((full_comment_prefix, comment_suffix)) = + language.block_comment_delimiters() + { + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + + let prefix_range = comment_prefix_range( + snapshot.deref(), + start_row, + comment_prefix, + comment_prefix_whitespace, + ); + let suffix_range = comment_suffix_range( + snapshot.deref(), + end_row, + comment_suffix.trim_start_matches(' '), + comment_suffix.starts_with(' '), + ); + + if prefix_range.is_empty() || suffix_range.is_empty() { + edits.push(( + prefix_range.start..prefix_range.start, + full_comment_prefix.clone(), + )); + edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); + } else { + edits.push((prefix_range, empty_str.clone())); + edits.push((suffix_range, empty_str.clone())); + } + } else { + continue; } + } + + drop(snapshot); + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); }); let selections = this.selections.all::(cx); @@ -10777,7 +10856,7 @@ mod tests { cx.update(|cx| cx.set_global(Settings::test(cx))); let language = Arc::new(Language::new( LanguageConfig { - line_comment: Some("// ".to_string()), + line_comment: Some("// ".into()), ..Default::default() }, Some(tree_sitter_rust::language()), @@ -10855,6 +10934,67 @@ mod tests { }); } + #[gpui::test] + async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { + let mut cx = EditorTestContext::new(cx); + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + block_comment: Some(("".into())), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new(Language::new( + LanguageConfig { + name: "JavaScript".into(), + line_comment: Some("// ".into()), + ..Default::default() + }, + Some(tree_sitter_javascript::language()), + )); + + let registry = Arc::new(LanguageRegistry::test()); + registry.add(html_language.clone()); + registry.add(javascript_language.clone()); + + cx.update_buffer(|buffer, cx| { + buffer.set_language_registry(registry); + buffer.set_language(Some(html_language), cx); + }); + + cx.set_state( + &r#" +

A

ˇ +

B

ˇ +

C

ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.assert_editor_state( + &r#" + + + + "# + .unindent(), + ); + } + #[gpui::test] fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { cx.set_global(Settings::test(cx)); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 59da0909c6..c7c5def833 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -231,7 +231,10 @@ pub struct LanguageConfig { pub decrease_indent_pattern: Option, #[serde(default)] pub autoclose_before: String, - pub line_comment: Option, + #[serde(default)] + pub line_comment: Option>, + #[serde(default)] + pub block_comment: Option<(Arc, Arc)>, } impl Default for LanguageConfig { @@ -245,6 +248,7 @@ impl Default for LanguageConfig { decrease_indent_pattern: Default::default(), autoclose_before: Default::default(), line_comment: Default::default(), + block_comment: Default::default(), } } } @@ -768,8 +772,15 @@ impl Language { self.config.name.clone() } - pub fn line_comment_prefix(&self) -> Option<&str> { - self.config.line_comment.as_deref() + pub fn line_comment_prefix(&self) -> Option<&Arc> { + self.config.line_comment.as_ref() + } + + pub fn block_comment_delimiters(&self) -> Option<(&Arc, &Arc)> { + self.config + .block_comment + .as_ref() + .map(|(start, end)| (start, end)) } pub async fn disk_based_diagnostic_sources(&self) -> &[String] { diff --git a/crates/zed/src/languages/html/config.toml b/crates/zed/src/languages/html/config.toml index 80b33b1243..3e618da25e 100644 --- a/crates/zed/src/languages/html/config.toml +++ b/crates/zed/src/languages/html/config.toml @@ -8,3 +8,5 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "!--", end = " --", close = true, newline = false }, ] + +block_comment = [""] \ No newline at end of file From aa8680640863d6f4cfb4c82588b92e684af1af03 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Oct 2022 12:25:32 -0700 Subject: [PATCH 12/15] Finish generalizing ToggleComments to support block comments --- crates/editor/src/editor.rs | 102 ++++++++++++++++++++++++++++-- crates/language/src/buffer.rs | 1 + crates/language/src/syntax_map.rs | 47 ++++++++------ 3 files changed, 126 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b2420c1c44..f7c9f81c0c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4492,6 +4492,7 @@ impl Editor { let mut last_toggled_row = None; let snapshot = this.buffer.read(cx).read(cx); let empty_str: Arc = "".into(); + let mut suffixes_inserted = Vec::new(); fn comment_prefix_range( snapshot: &MultiBufferSnapshot, @@ -4569,7 +4570,6 @@ impl Editor { continue; }; - let mut all_selection_lines_are_comments = true; selection_edit_ranges.clear(); // If multiple selections contain a given row, avoid processing that @@ -4586,12 +4586,17 @@ impl Editor { }; last_toggled_row = Some(end_row); + if start_row > end_row { + continue; + } + // If the language has line comments, toggle those. if let Some(full_comment_prefix) = language.line_comment_prefix() { // Split the comment prefix's trailing whitespace into a separate string, // as that portion won't be used for detecting if a line is a comment. let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + let mut all_selection_lines_are_comments = true; for row in start_row..=end_row { if snapshot.is_line_blank(row) { @@ -4633,7 +4638,6 @@ impl Editor { { let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - let prefix_range = comment_prefix_range( snapshot.deref(), start_row, @@ -4653,6 +4657,7 @@ impl Editor { full_comment_prefix.clone(), )); edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); + suffixes_inserted.push((end_row, comment_suffix.len())); } else { edits.push((prefix_range, empty_str.clone())); edits.push((suffix_range, empty_str.clone())); @@ -4667,7 +4672,33 @@ impl Editor { buffer.edit(edits, None, cx); }); - let selections = this.selections.all::(cx); + // Adjust selections so that they end before any comment suffixes that + // were inserted. + let mut suffixes_inserted = suffixes_inserted.into_iter().peekable(); + let mut selections = this.selections.all::(cx); + let snapshot = this.buffer.read(cx).read(cx); + for selection in &mut selections { + while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() { + match row.cmp(&selection.end.row) { + Ordering::Less => { + suffixes_inserted.next(); + continue; + } + Ordering::Greater => break, + Ordering::Equal => { + if selection.end.column == snapshot.line_len(row) { + if selection.is_empty() { + selection.start.column -= suffix_len as u32; + } + selection.end.column -= suffix_len as u32; + } + break; + } + } + } + } + + drop(snapshot); this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections)); }); } @@ -10975,6 +11006,7 @@ mod tests { buffer.set_language(Some(html_language), cx); }); + // Toggle comments for empty selections cx.set_state( &r#"

A

ˇ @@ -10983,7 +11015,6 @@ mod tests { "# .unindent(), ); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); cx.assert_editor_state( &r#" @@ -10993,6 +11024,69 @@ mod tests { "# .unindent(), ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.assert_editor_state( + &r#" +

A

ˇ +

B

ˇ +

C

ˇ + "# + .unindent(), + ); + + // Toggle comments for mixture of empty and non-empty selections, where + // multiple selections occupy a given line. + cx.set_state( + &r#" +

+

ˇ»B

ˇ +

+

ˇ»D

ˇ + "# + .unindent(), + ); + + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.assert_editor_state( + &r#" + + + "# + .unindent(), + ); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.assert_editor_state( + &r#" +

+

ˇ»B

ˇ +

+

ˇ»D

ˇ + "# + .unindent(), + ); + + // Toggle comments when different languages are active for different + // selections. + cx.set_state( + &r#" + ˇ + "# + .unindent(), + ); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments, cx)); + cx.assert_editor_state( + &r#" + + // ˇvar x = new Y(); + + "# + .unindent(), + ); } #[gpui::test] diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ff1b002b0..b53a9e5573 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1840,6 +1840,7 @@ impl BufferSnapshot { let offset = position.to_offset(self); self.syntax .layers_for_range(offset..offset, &self.text) + .filter(|l| l.node.end_byte() > offset) .last() .map(|info| info.language) .or(self.language.as_ref()) diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 8983406690..64145e535b 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -525,19 +525,19 @@ impl SyntaxSnapshot { } #[cfg(test)] - pub fn layers(&self, buffer: &BufferSnapshot) -> Vec { - self.layers_for_range(0..buffer.len(), buffer) + pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec { + self.layers_for_range(0..buffer.len(), buffer).collect() } pub fn layers_for_range<'a, T: ToOffset>( - &self, + &'a self, range: Range, - buffer: &BufferSnapshot, - ) -> Vec { + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator { let start = buffer.anchor_before(range.start.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer)); - let mut cursor = self.layers.filter::<_, ()>(|summary| { + let mut cursor = self.layers.filter::<_, ()>(move |summary| { if summary.max_depth > summary.min_depth { true } else { @@ -547,21 +547,26 @@ impl SyntaxSnapshot { } }); - let mut result = Vec::new(); + // let mut result = Vec::new(); cursor.next(buffer); - while let Some(layer) = cursor.item() { - result.push(SyntaxLayerInfo { - language: &layer.language, - depth: layer.depth, - node: layer.tree.root_node_with_offset( - layer.range.start.to_offset(buffer), - layer.range.start.to_point(buffer).to_ts_point(), - ), - }); - cursor.next(buffer) - } + std::iter::from_fn(move || { + if let Some(layer) = cursor.item() { + let info = SyntaxLayerInfo { + language: &layer.language, + depth: layer.depth, + node: layer.tree.root_node_with_offset( + layer.range.start.to_offset(buffer), + layer.range.start.to_point(buffer).to_ts_point(), + ), + }; + cursor.next(buffer); + Some(info) + } else { + None + } + }) - result + // result } } @@ -1848,7 +1853,9 @@ mod tests { range: Range, expected_layers: &[&str], ) { - let layers = syntax_map.layers_for_range(range, &buffer); + let layers = syntax_map + .layers_for_range(range, &buffer) + .collect::>(); assert_eq!( layers.len(), expected_layers.len(), From 8b86781ad13d3e5eee604fb93c15cf943bee8bf5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Oct 2022 14:44:34 -0700 Subject: [PATCH 13/15] Remove last usages of MultiBufferSnapshot::language --- crates/editor/src/editor.rs | 6 ++++-- crates/editor/src/multi_buffer.rs | 7 ------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f7c9f81c0c..4e18d04889 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2021,7 +2021,7 @@ impl Editor { let end = selection.end; let mut insert_extra_newline = false; - if let Some(language) = buffer.language() { + if let Some(language) = buffer.language_at(start) { let leading_whitespace_len = buffer .reversed_chars_at(start) .take_while(|c| c.is_whitespace() && *c != '\n') @@ -2927,7 +2927,9 @@ impl Editor { { let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); - let language_name = buffer.language().map(|language| language.name()); + let language_name = buffer + .language_at(line_buffer_range.start) + .map(|language| language.name()); let indent_len = match indent_size.kind { IndentKind::Space => { cx.global::().tab_size(language_name.as_deref()) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 3b43f99ca0..cf9f29d73e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2501,13 +2501,6 @@ impl MultiBufferSnapshot { self.trailing_excerpt_update_count } - pub fn language(&self) -> Option<&Arc> { - self.excerpts - .iter() - .next() - .and_then(|excerpt| excerpt.buffer.language()) - } - pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { self.point_to_buffer_offset(point) .and_then(|(buffer, offset)| buffer.language_at(offset)) From 7fb5fe036a70e0018739ec1a251f8c1602c74724 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Oct 2022 17:07:35 -0700 Subject: [PATCH 14/15] Derive indent size from the language at the cursor when auto-indenting --- crates/editor/src/multi_buffer.rs | 16 ++++++++-- crates/language/src/buffer.rs | 52 ++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index cf9f29d73e..71cbefb78f 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1967,8 +1967,10 @@ impl MultiBufferSnapshot { let mut rows_for_excerpt = Vec::new(); let mut cursor = self.excerpts.cursor::(); - let mut rows = rows.into_iter().peekable(); + let mut prev_row = u32::MAX; + let mut prev_language_indent_size = IndentSize::default(); + while let Some(row) = rows.next() { cursor.seek(&Point::new(row, 0), Bias::Right, &()); let excerpt = match cursor.item() { @@ -1976,7 +1978,17 @@ impl MultiBufferSnapshot { _ => continue, }; - let single_indent_size = excerpt.buffer.single_indent_size(cx); + // Retrieve the language and indent size once for each disjoint region being indented. + let single_indent_size = if row.saturating_sub(1) == prev_row { + prev_language_indent_size + } else { + excerpt + .buffer + .language_indent_size_at(Point::new(row, 0), cx) + }; + prev_language_indent_size = single_indent_size; + prev_row = row; + let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row; let start_multibuffer_row = cursor.start().row; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b53a9e5573..9a1c292319 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -84,14 +84,15 @@ pub struct BufferSnapshot { parse_count: usize, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub struct IndentSize { pub len: u32, pub kind: IndentKind, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum IndentKind { + #[default] Space, Tab, } @@ -236,7 +237,6 @@ pub enum AutoindentMode { struct AutoindentRequest { before_edit: BufferSnapshot, entries: Vec, - indent_size: IndentSize, is_block_mode: bool, } @@ -249,6 +249,7 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, + indent_size: IndentSize, original_indent_column: Option, } @@ -794,10 +795,13 @@ impl Buffer { // buffer before this batch of edits. let mut row_ranges = Vec::new(); let mut old_to_new_rows = BTreeMap::new(); + let mut language_indent_sizes_by_new_row = Vec::new(); for entry in &request.entries { let position = entry.range.start; let new_row = position.to_point(&snapshot).row; let new_end_row = entry.range.end.to_point(&snapshot).row + 1; + language_indent_sizes_by_new_row.push((new_row, entry.indent_size)); + if !entry.first_line_is_new { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); @@ -811,6 +815,8 @@ impl Buffer { let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); + let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable(); + let mut language_indent_size = IndentSize::default(); for old_edited_range in old_edited_ranges { let suggestions = request .before_edit @@ -819,6 +825,17 @@ impl Buffer { .flatten(); for (old_row, suggestion) in old_edited_range.zip(suggestions) { if let Some(suggestion) = suggestion { + let new_row = *old_to_new_rows.get(&old_row).unwrap(); + + // Find the indent size based on the language for this row. + while let Some((row, size)) = language_indent_sizes.peek() { + if *row > new_row { + break; + } + language_indent_size = *size; + language_indent_sizes.next(); + } + let suggested_indent = old_to_new_rows .get(&suggestion.basis_row) .and_then(|from_row| old_suggestions.get(from_row).copied()) @@ -827,9 +844,8 @@ impl Buffer { .before_edit .indent_size_for_line(suggestion.basis_row) }) - .with_delta(suggestion.delta, request.indent_size); - old_suggestions - .insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent); + .with_delta(suggestion.delta, language_indent_size); + old_suggestions.insert(new_row, suggested_indent); } } yield_now().await; @@ -850,6 +866,8 @@ impl Buffer { // Compute new suggestions for each line, but only include them in the result // if they differ from the old suggestion for that line. + let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable(); + let mut language_indent_size = IndentSize::default(); for new_edited_row_range in new_edited_row_ranges { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -857,13 +875,22 @@ impl Buffer { .flatten(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { if let Some(suggestion) = suggestion { + // Find the indent size based on the language for this row. + while let Some((row, size)) = language_indent_sizes.peek() { + if *row > new_row { + break; + } + language_indent_size = *size; + language_indent_sizes.next(); + } + let suggested_indent = indent_sizes .get(&suggestion.basis_row) .copied() .unwrap_or_else(|| { snapshot.indent_size_for_line(suggestion.basis_row) }) - .with_delta(suggestion.delta, request.indent_size); + .with_delta(suggestion.delta, language_indent_size); if old_suggestions .get(&new_row) .map_or(true, |old_indentation| { @@ -1194,7 +1221,6 @@ impl Buffer { let edit_id = edit_operation.local_timestamp(); if let Some((before_edit, mode)) = autoindent_request { - let indent_size = before_edit.single_indent_size(cx); let (start_columns, is_block_mode) = match mode { AutoindentMode::Block { original_indent_columns: start_columns, @@ -1243,6 +1269,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, original_indent_column: start_column, + indent_size: before_edit.language_indent_size_at(range.start, cx), range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } @@ -1252,7 +1279,6 @@ impl Buffer { self.autoindent_requests.push(Arc::new(AutoindentRequest { before_edit, entries, - indent_size, is_block_mode, })); } @@ -1570,8 +1596,8 @@ impl BufferSnapshot { indent_size_for_line(self, row) } - pub fn single_indent_size(&self, cx: &AppContext) -> IndentSize { - let language_name = self.language().map(|language| language.name()); + pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { + let language_name = self.language_at(position).map(|language| language.name()); let settings = cx.global::(); if settings.hard_tabs(language_name.as_deref()) { IndentSize::tab() @@ -1832,10 +1858,6 @@ impl BufferSnapshot { } } - pub fn language(&self) -> Option<&Arc> { - self.language.as_ref() - } - pub fn language_at(&self, position: D) -> Option<&Arc> { let offset = position.to_offset(self); self.syntax From b7e115a6a1baacd08a31c6d52586e0a7de1d64aa Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 5 Oct 2022 17:58:10 -0700 Subject: [PATCH 15/15] Add a test for multi-language auto-indent --- Cargo.lock | 2 + crates/language/Cargo.toml | 2 + crates/language/src/tests.rs | 116 ++++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 79eae80258..f7a354ed28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2861,6 +2861,8 @@ dependencies = [ "text", "theme", "tree-sitter", + "tree-sitter-html", + "tree-sitter-javascript", "tree-sitter-json 0.19.0", "tree-sitter-python", "tree-sitter-rust", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 6e9f368e77..6d80eee779 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -63,6 +63,8 @@ util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" rand = "0.8.3" +tree-sitter-html = "*" +tree-sitter-javascript = "*" tree-sitter-json = "*" tree-sitter-rust = "*" tree-sitter-python = "*" diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 8f56f3287e..3cfddce71f 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -14,7 +14,7 @@ use std::{ }; use text::network::Network; use unindent::Unindent as _; -use util::post_inc; +use util::{post_inc, test::marked_text_ranges}; #[cfg(test)] #[ctor::ctor] @@ -1035,6 +1035,120 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) { }); } +#[gpui::test] +fn test_autoindent_with_injected_languages(cx: &mut MutableAppContext) { + cx.set_global({ + let mut settings = Settings::test(cx); + settings.language_overrides.extend([ + ( + "HTML".into(), + settings::EditorSettings { + tab_size: Some(2.try_into().unwrap()), + ..Default::default() + }, + ), + ( + "JavaScript".into(), + settings::EditorSettings { + tab_size: Some(8.try_into().unwrap()), + ..Default::default() + }, + ), + ]); + settings + }); + + let html_language = Arc::new( + Language::new( + LanguageConfig { + name: "HTML".into(), + ..Default::default() + }, + Some(tree_sitter_html::language()), + ) + .with_indents_query( + " + (element + (start_tag) @start + (end_tag)? @end) @indent + ", + ) + .unwrap() + .with_injection_query( + r#" + (script_element + (raw_text) @content + (#set! "language" "javascript")) + "#, + ) + .unwrap(), + ); + + let javascript_language = Arc::new( + Language::new( + LanguageConfig { + name: "JavaScript".into(), + ..Default::default() + }, + Some(tree_sitter_javascript::language()), + ) + .with_indents_query( + r#" + (object "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let language_registry = Arc::new(LanguageRegistry::test()); + language_registry.add(html_language.clone()); + language_registry.add(javascript_language.clone()); + + cx.add_model(|cx| { + let (text, ranges) = marked_text_ranges( + &" +
ˇ +
+ + ˇ + + " + .unindent(), + false, + ); + + let mut buffer = Buffer::new(0, text, cx); + buffer.set_language_registry(language_registry); + buffer.set_language(Some(html_language), cx); + buffer.edit( + ranges.into_iter().map(|range| (range, "\na")), + Some(AutoindentMode::EachLine), + cx, + ); + assert_eq!( + buffer.text(), + " +
+ a +
+ + + a + + " + .unindent() + ); + buffer + }); +} + #[gpui::test] fn test_serialization(cx: &mut gpui::MutableAppContext) { let mut now = Instant::now();