From fed6f708c0fa88a9824ef3ac61f018e6a17f42cc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Feb 2022 12:38:36 +0100 Subject: [PATCH 01/65] Start on project-wide find --- Cargo.lock | 118 ++++++++++++++++++++++++++++++++ crates/find/Cargo.toml | 1 + crates/find/src/find.rs | 2 + crates/find/src/project_find.rs | 46 +++++++++++++ crates/project/Cargo.toml | 1 + crates/project/src/project.rs | 109 ++++++++++++++++++++++++++++- 6 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 crates/find/src/project_find.rs diff --git a/Cargo.lock b/Cargo.lock index 63459cbe8ddf95b6a6ca7f41e4a609b2fc43ae8a..46fb54b5502ed8a87d92e1579ed9793d644300f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,7 +745,9 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" dependencies = [ + "lazy_static", "memchr", + "regex-automata", ] [[package]] @@ -776,6 +778,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytecount" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" + [[package]] name = "bytemuck" version = "1.5.1" @@ -1653,6 +1661,15 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + [[package]] name = "entities" version = "1.0.1" @@ -1782,6 +1799,7 @@ dependencies = [ "editor", "gpui", "postage", + "project", "regex", "smol", "theme", @@ -2248,6 +2266,90 @@ dependencies = [ "syn", ] +[[package]] +name = "grep" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51cb840c560b45a2ffd8abf00190382789d3f596663d5ffeb2e05931c20e8657" +dependencies = [ + "grep-cli", + "grep-matcher", + "grep-printer", + "grep-regex", + "grep-searcher", +] + +[[package]] +name = "grep-cli" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd110c34bb4460d0de5062413b773e385cbf8a85a63fc535590110a09e79e8a" +dependencies = [ + "atty", + "bstr", + "globset", + "lazy_static", + "log", + "regex", + "same-file", + "termcolor", + "winapi-util", +] + +[[package]] +name = "grep-matcher" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-printer" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c271a24daedf5675b61a275a1d0af06e03312ab7856d15433ae6cde044dc72" +dependencies = [ + "base64 0.13.0", + "bstr", + "grep-matcher", + "grep-searcher", + "serde", + "serde_json", + "termcolor", +] + +[[package]] +name = "grep-regex" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06" +dependencies = [ + "aho-corasick", + "bstr", + "grep-matcher", + "log", + "regex", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "grep-searcher" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d" +dependencies = [ + "bstr", + "bytecount", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memmap2 0.3.1", +] + [[package]] name = "group" version = "0.10.0" @@ -2911,6 +3013,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memmap2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.6.3" @@ -3563,6 +3674,7 @@ dependencies = [ "futures", "fuzzy", "gpui", + "grep", "ignore", "language", "lazy_static", @@ -3866,6 +3978,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.25" diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index acab695d12c17c3bd6501ac64adf9a4f2f718daf..39570334d6c3b378653246eaab3481e67cc1bc58 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -10,6 +10,7 @@ path = "src/find.rs" collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } +project = { path = "../project" } theme = { path = "../theme" } workspace = { path = "../workspace" } aho-corasick = "0.7" diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 76beed1f2ef21d15505c02fe92c53ed3f591738a..4be4216c370e319459aae3e68c5ae2abae808615 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,3 +1,5 @@ +mod project_find; + use aho_corasick::AhoCorasickBuilder; use anyhow::Result; use collections::HashMap; diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs new file mode 100644 index 0000000000000000000000000000000000000000..8bc2777a6fdb303d4785327d735d4136cf679874 --- /dev/null +++ b/crates/find/src/project_find.rs @@ -0,0 +1,46 @@ +use crate::SearchMode; +use editor::MultiBuffer; +use gpui::{Entity, ModelContext, ModelHandle, Task}; +use project::Project; + +struct ProjectFind { + last_search: SearchParams, + project: ModelHandle, + excerpts: ModelHandle, + pending_search: Task>, +} + +#[derive(Default)] +struct SearchParams { + query: String, + regex: bool, + whole_word: bool, + case_sensitive: bool, +} + +struct ProjectFindView { + model: ModelHandle, +} + +impl Entity for ProjectFind { + type Event = (); +} + +impl ProjectFind { + fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + let replica_id = project.read(cx).replica_id(); + Self { + project, + last_search: Default::default(), + excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), + pending_search: Task::ready(None), + } + } + + fn search(&mut self, params: SearchParams, cx: &mut ModelContext) { + self.pending_search = cx.spawn_weak(|this, cx| async move { + // + None + }); + } +} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index f72ba133c32df4135cbd4e0f68c6eac9e300252d..ec1669ec72621e1354defc4a3b5808cebb014a6b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -29,6 +29,7 @@ util = { path = "../util" } anyhow = "1.0.38" async-trait = "0.1" futures = "0.3" +grep = "0.2" ignore = "0.4" lazy_static = "1.4.0" libc = "0.2" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 236b02cd6c97ef0e940fdf88e4963b32806e8174..0c3ff7d7cf851f2ed0aaf08ea9d3f1588c7d2a25 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; -use futures::{future::Shared, Future, FutureExt}; +use futures::{future::Shared, Future, FutureExt, StreamExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, @@ -16,7 +16,7 @@ use gpui::{ use language::{ range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, - ToLspPosition, ToOffset, ToPointUtf16, Transaction, + Rope, ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -2042,6 +2042,111 @@ impl Project { ) } + pub fn search(&self, query: &str, cx: &mut ModelContext) { + if self.is_local() { + enum SearchItem { + Path(PathBuf), + Buffer((WeakModelHandle, Rope)), + } + + let (queue_tx, queue_rx) = smol::channel::bounded(1024); + + // Submit all worktree paths to the queue. + let snapshots = self + .strong_worktrees(cx) + .filter_map(|tree| { + let tree = tree.read(cx).as_local()?; + Some((tree.abs_path().clone(), tree.snapshot())) + }) + .collect::>(); + cx.background() + .spawn({ + let queue_tx = queue_tx.clone(); + async move { + for (snapshot_abs_path, snapshot) in snapshots { + for file in snapshot.files(false, 0) { + if queue_tx + .send(SearchItem::Path(snapshot_abs_path.join(&file.path))) + .await + .is_err() + { + return; + } + } + } + } + }) + .detach(); + + // Submit all the currently-open buffers that are dirty to the queue. + let buffers = self + .open_buffers + .values() + .filter_map(|buffer| { + if let OpenBuffer::Loaded(buffer) = buffer { + Some(buffer.clone()) + } else { + None + } + }) + .collect::>(); + cx.spawn_weak(|_, cx| async move { + for buffer in buffers.into_iter().filter_map(|buffer| buffer.upgrade(&cx)) { + let text = buffer.read_with(&cx, |buffer, _| { + if buffer.is_dirty() { + Some(buffer.as_rope().clone()) + } else { + None + } + }); + + if let Some(text) = text { + if queue_tx + .send(SearchItem::Buffer((buffer.downgrade(), text))) + .await + .is_err() + { + return; + } + } + } + }) + .detach(); + + let background = cx.background().clone(); + cx.background() + .spawn(async move { + let workers = background.num_cpus(); + background + .scoped(|scope| { + for _ in 0..workers { + let mut paths_rx = queue_rx.clone(); + scope.spawn(async move { + while let Some(item) = paths_rx.next().await { + match item { + SearchItem::Path(_) => todo!(), + SearchItem::Buffer(_) => todo!(), + } + } + }); + } + }) + .await; + }) + .detach(); + // let multiline = query.contains('\n'); + // let searcher = grep::searcher::SearcherBuilder::new() + // .multi_line(multiline) + // .build(); + // searcher.search_path( + // "hey".to_string(), + // "/hello/world", + // grep::searcher::sinks::Lossy(|row, mat| {}), + // ); + } else { + } + } + fn request_lsp( &self, buffer_handle: ModelHandle, From 119bfaa99f6875b9f0e1f1c40ce3234a3b9bc8d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Feb 2022 15:10:22 +0100 Subject: [PATCH 02/65] WIP --- crates/project/src/project.rs | 115 ++++++++++++++-------------------- 1 file changed, 47 insertions(+), 68 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0c3ff7d7cf851f2ed0aaf08ea9d3f1588c7d2a25..69422548706aa36bd7b498991fa94e77d8e75406 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -13,10 +13,11 @@ use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; +use grep::{matcher::Matcher, searcher::Searcher}; use language::{ range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, - Rope, ToLspPosition, ToOffset, ToPointUtf16, Transaction, + ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -2042,13 +2043,15 @@ impl Project { ) } - pub fn search(&self, query: &str, cx: &mut ModelContext) { + pub fn search( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Task, Vec>>> + where + T: Matcher, + { if self.is_local() { - enum SearchItem { - Path(PathBuf), - Buffer((WeakModelHandle, Rope)), - } - let (queue_tx, queue_rx) = smol::channel::bounded(1024); // Submit all worktree paths to the queue. @@ -2066,7 +2069,7 @@ impl Project { for (snapshot_abs_path, snapshot) in snapshots { for file in snapshot.files(false, 0) { if queue_tx - .send(SearchItem::Path(snapshot_abs_path.join(&file.path))) + .send((snapshot_abs_path.clone(), file.path.clone())) .await .is_err() { @@ -2078,73 +2081,49 @@ impl Project { }) .detach(); - // Submit all the currently-open buffers that are dirty to the queue. - let buffers = self - .open_buffers - .values() - .filter_map(|buffer| { - if let OpenBuffer::Loaded(buffer) = buffer { - Some(buffer.clone()) - } else { - None - } - }) - .collect::>(); - cx.spawn_weak(|_, cx| async move { - for buffer in buffers.into_iter().filter_map(|buffer| buffer.upgrade(&cx)) { - let text = buffer.read_with(&cx, |buffer, _| { - if buffer.is_dirty() { - Some(buffer.as_rope().clone()) - } else { - None - } - }); - - if let Some(text) = text { - if queue_tx - .send(SearchItem::Buffer((buffer.downgrade(), text))) - .await - .is_err() - { - return; - } - } - } - }) - .detach(); - - let background = cx.background().clone(); + let matcher = Arc::new(matcher); cx.background() - .spawn(async move { + .spawn({ + let background = cx.background().clone(); let workers = background.num_cpus(); - background - .scoped(|scope| { - for _ in 0..workers { - let mut paths_rx = queue_rx.clone(); - scope.spawn(async move { - while let Some(item) = paths_rx.next().await { - match item { - SearchItem::Path(_) => todo!(), - SearchItem::Buffer(_) => todo!(), + let searcher = searcher.clone(); + let matcher = matcher.clone(); + async move { + background + .scoped(|scope| { + for _ in 0..workers { + let mut paths_rx = queue_rx.clone(); + scope.spawn(async move { + let mut path = PathBuf::new(); + while let Some((snapshot_abs_path, file_path)) = + paths_rx.next().await + { + path.clear(); + path.push(snapshot_abs_path); + path.push(file_path); + let mut matched = false; + // searcher.search_path( + // matcher.as_ref(), + // &path, + // grep::searcher::sinks::Bytes(|_, _| { + // matched = true; + // Ok(false) + // }), + // ); + + if matched {} } - } - }); - } - }) - .await; + }); + } + }) + .await; + } }) .detach(); - // let multiline = query.contains('\n'); - // let searcher = grep::searcher::SearcherBuilder::new() - // .multi_line(multiline) - // .build(); - // searcher.search_path( - // "hey".to_string(), - // "/hello/world", - // grep::searcher::sinks::Lossy(|row, mat| {}), - // ); } else { } + + todo!() } fn request_lsp( From 26f7f4f5b24e7b4d0397c890cee7ff79755bb177 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Feb 2022 12:33:28 +0100 Subject: [PATCH 03/65] WIP: Remove ripgrep and start matching query for paths ourselves --- Cargo.lock | 119 +--------------------------------- crates/project/Cargo.toml | 3 +- crates/project/src/fs.rs | 11 +++- crates/project/src/project.rs | 95 ++++++++++++++++----------- 4 files changed, 72 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46fb54b5502ed8a87d92e1579ed9793d644300f3..e1bb0690f845983fc2b1c783aaa8b8bc8dc256b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,9 +745,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" dependencies = [ - "lazy_static", "memchr", - "regex-automata", ] [[package]] @@ -778,12 +776,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -[[package]] -name = "bytecount" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" - [[package]] name = "bytemuck" version = "1.5.1" @@ -1661,15 +1653,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "encoding_rs_io" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" -dependencies = [ - "encoding_rs", -] - [[package]] name = "entities" version = "1.0.1" @@ -2266,90 +2249,6 @@ dependencies = [ "syn", ] -[[package]] -name = "grep" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51cb840c560b45a2ffd8abf00190382789d3f596663d5ffeb2e05931c20e8657" -dependencies = [ - "grep-cli", - "grep-matcher", - "grep-printer", - "grep-regex", - "grep-searcher", -] - -[[package]] -name = "grep-cli" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd110c34bb4460d0de5062413b773e385cbf8a85a63fc535590110a09e79e8a" -dependencies = [ - "atty", - "bstr", - "globset", - "lazy_static", - "log", - "regex", - "same-file", - "termcolor", - "winapi-util", -] - -[[package]] -name = "grep-matcher" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc" -dependencies = [ - "memchr", -] - -[[package]] -name = "grep-printer" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c271a24daedf5675b61a275a1d0af06e03312ab7856d15433ae6cde044dc72" -dependencies = [ - "base64 0.13.0", - "bstr", - "grep-matcher", - "grep-searcher", - "serde", - "serde_json", - "termcolor", -] - -[[package]] -name = "grep-regex" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06" -dependencies = [ - "aho-corasick", - "bstr", - "grep-matcher", - "log", - "regex", - "regex-syntax", - "thread_local", -] - -[[package]] -name = "grep-searcher" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d" -dependencies = [ - "bstr", - "bytecount", - "encoding_rs", - "encoding_rs_io", - "grep-matcher", - "log", - "memmap2 0.3.1", -] - [[package]] name = "group" version = "0.10.0" @@ -3013,15 +2912,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memmap2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.6.3" @@ -3665,6 +3555,7 @@ dependencies = [ name = "project" version = "0.1.0" dependencies = [ + "aho-corasick", "anyhow", "async-trait", "client", @@ -3674,7 +3565,6 @@ dependencies = [ "futures", "fuzzy", "gpui", - "grep", "ignore", "language", "lazy_static", @@ -3684,6 +3574,7 @@ dependencies = [ "parking_lot", "postage", "rand 0.8.3", + "regex", "rpc", "serde", "serde_json", @@ -3978,12 +3869,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - [[package]] name = "regex-syntax" version = "0.6.25" diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index ec1669ec72621e1354defc4a3b5808cebb014a6b..dea5a10279a8f6e3dfe1c62d9526bbea0e797fac 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -26,10 +26,10 @@ lsp = { path = "../lsp" } rpc = { path = "../rpc" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } +aho-corasick = "0.7" anyhow = "1.0.38" async-trait = "0.1" futures = "0.3" -grep = "0.2" ignore = "0.4" lazy_static = "1.4.0" libc = "0.2" @@ -37,6 +37,7 @@ log = "0.4" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" +regex = "1.5" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } sha2 = "0.10" diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 7f89c29c8384e33d3817e180c42c288cdbda22a2..578be8cf82cf1a6e11648c6a62ba1c4f083bacf2 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -18,6 +18,7 @@ pub trait Fs: Send + Sync { async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; + async fn open_sync(&self, path: &Path) -> Result>; async fn load(&self, path: &Path) -> Result; async fn save(&self, path: &Path, text: &Rope) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; @@ -121,6 +122,10 @@ impl Fs for RealFs { } } + async fn open_sync(&self, path: &Path) -> Result> { + Ok(Box::new(std::fs::File::open(path)?)) + } + async fn load(&self, path: &Path) -> Result { let mut file = smol::fs::File::open(path).await?; let mut text = String::new(); @@ -203,7 +208,6 @@ impl Fs for RealFs { fn is_fake(&self) -> bool { false } - #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs { panic!("called `RealFs::as_fake`") @@ -535,6 +539,11 @@ impl Fs for FakeFs { Ok(()) } + async fn open_sync(&self, path: &Path) -> Result> { + let text = self.load(path).await?; + Ok(Box::new(io::Cursor::new(text))) + } + async fn load(&self, path: &Path) -> Result { let path = normalize_path(path); self.executor.simulate_random_delay().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 69422548706aa36bd7b498991fa94e77d8e75406..b77d8add93b54efdbee3f34ad59798b4513ef5d7 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3,6 +3,7 @@ mod ignore; mod lsp_command; pub mod worktree; +use aho_corasick::AhoCorasickBuilder; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; @@ -13,7 +14,6 @@ use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; -use grep::{matcher::Matcher, searcher::Searcher}; use language::{ range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, @@ -152,6 +152,10 @@ pub struct Symbol { pub signature: [u8; 32], } +pub enum SearchQuery { + Plain(String), +} + pub struct BufferRequestHandle(Rc>); #[derive(Default)] @@ -2043,16 +2047,13 @@ impl Project { ) } - pub fn search( + pub fn search( &self, query: SearchQuery, cx: &mut ModelContext, - ) -> Task, Vec>>> - where - T: Matcher, - { + ) -> Task, Vec>>> { if self.is_local() { - let (queue_tx, queue_rx) = smol::channel::bounded(1024); + let (paths_to_search_tx, paths_to_search_rx) = smol::channel::bounded(1024); // Submit all worktree paths to the queue. let snapshots = self @@ -2063,55 +2064,75 @@ impl Project { }) .collect::>(); cx.background() - .spawn({ - let queue_tx = queue_tx.clone(); - async move { - for (snapshot_abs_path, snapshot) in snapshots { - for file in snapshot.files(false, 0) { - if queue_tx - .send((snapshot_abs_path.clone(), file.path.clone())) - .await - .is_err() - { - return; - } + .spawn(async move { + for (snapshot_abs_path, snapshot) in snapshots { + for file in snapshot.files(false, 0) { + if paths_to_search_tx + .send((snapshot_abs_path.clone(), file.path.clone())) + .await + .is_err() + { + return; } } } }) .detach(); - let matcher = Arc::new(matcher); + let SearchQuery::Plain(query) = query; + let search = Arc::new( + AhoCorasickBuilder::new() + .auto_configure(&[&query]) + // .ascii_case_insensitive(!case_sensitive) + .build(&[&query]), + ); + let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024); cx.background() .spawn({ + let fs = self.fs.clone(); let background = cx.background().clone(); let workers = background.num_cpus(); - let searcher = searcher.clone(); - let matcher = matcher.clone(); + let search = search.clone(); async move { + let fs = &fs; + let search = &search; + let matching_paths_tx = &matching_paths_tx; background .scoped(|scope| { for _ in 0..workers { - let mut paths_rx = queue_rx.clone(); + let mut paths_to_search_rx = paths_to_search_rx.clone(); scope.spawn(async move { let mut path = PathBuf::new(); while let Some((snapshot_abs_path, file_path)) = - paths_rx.next().await + paths_to_search_rx.next().await { + if matching_paths_tx.is_closed() { + break; + } + path.clear(); - path.push(snapshot_abs_path); - path.push(file_path); - let mut matched = false; - // searcher.search_path( - // matcher.as_ref(), - // &path, - // grep::searcher::sinks::Bytes(|_, _| { - // matched = true; - // Ok(false) - // }), - // ); - - if matched {} + path.push(&snapshot_abs_path); + path.push(&file_path); + let matches = if let Some(file) = + fs.open_sync(&path).await.log_err() + { + search + .stream_find_iter(file) + .next() + .map_or(false, |mat| mat.is_ok()) + } else { + false + }; + + if matches { + if matching_paths_tx + .send((snapshot_abs_path, file_path)) + .await + .is_err() + { + break; + } + } } }); } From 6a323ce2ddf3d4db98c24c05390b16fd664426b1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Feb 2022 15:33:56 +0100 Subject: [PATCH 04/65] Implement a basic project-wide search using Aho-Corasick --- crates/project/src/project.rs | 154 ++++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 9 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b77d8add93b54efdbee3f34ad59798b4513ef5d7..02e202110c660ae138ad32795b49ac114f6b0e4d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2055,7 +2055,6 @@ impl Project { if self.is_local() { let (paths_to_search_tx, paths_to_search_rx) = smol::channel::bounded(1024); - // Submit all worktree paths to the queue. let snapshots = self .strong_worktrees(cx) .filter_map(|tree| { @@ -2068,7 +2067,7 @@ impl Project { for (snapshot_abs_path, snapshot) in snapshots { for file in snapshot.files(false, 0) { if paths_to_search_tx - .send((snapshot_abs_path.clone(), file.path.clone())) + .send((snapshot.id(), snapshot_abs_path.clone(), file.path.clone())) .await .is_err() { @@ -2086,12 +2085,12 @@ impl Project { // .ascii_case_insensitive(!case_sensitive) .build(&[&query]), ); - let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024); + let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); + let workers = cx.background().num_cpus(); cx.background() .spawn({ let fs = self.fs.clone(); let background = cx.background().clone(); - let workers = background.num_cpus(); let search = search.clone(); async move { let fs = &fs; @@ -2103,8 +2102,11 @@ impl Project { let mut paths_to_search_rx = paths_to_search_rx.clone(); scope.spawn(async move { let mut path = PathBuf::new(); - while let Some((snapshot_abs_path, file_path)) = - paths_to_search_rx.next().await + while let Some(( + worktree_id, + snapshot_abs_path, + file_path, + )) = paths_to_search_rx.next().await { if matching_paths_tx.is_closed() { break; @@ -2126,7 +2128,7 @@ impl Project { if matches { if matching_paths_tx - .send((snapshot_abs_path, file_path)) + .send((worktree_id, file_path)) .await .is_err() { @@ -2141,10 +2143,70 @@ impl Project { } }) .detach(); + + let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); + let buffers = self + .buffers_state + .borrow() + .open_buffers + .values() + .filter_map(|b| b.upgrade(cx)) + .collect::>(); + cx.spawn(|this, mut cx| async move { + for buffer in buffers { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx.send((buffer, snapshot)).await?; + } + + while let Some(project_path) = matching_paths_rx.next().await { + if let Some(buffer) = this + .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) + .await + .log_err() + { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx.send((buffer, snapshot)).await?; + } + } + + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + + let background = cx.background().clone(); + cx.background().spawn(async move { + let search = &search; + let mut matched_buffers = Vec::new(); + for _ in 0..workers { + matched_buffers.push(HashMap::default()); + } + background + .scoped(|scope| { + for worker_matched_buffers in matched_buffers.iter_mut() { + let mut buffers_rx = buffers_rx.clone(); + scope.spawn(async move { + while let Some((buffer, snapshot)) = buffers_rx.next().await { + for mat in search.stream_find_iter( + snapshot.as_rope().bytes_in_range(0..snapshot.len()), + ) { + let mat = mat.unwrap(); + let range = snapshot.anchor_before(mat.start()) + ..snapshot.anchor_after(mat.end()); + worker_matched_buffers + .entry(buffer.clone()) + .or_insert(Vec::new()) + .push(range); + } + } + }); + } + }) + .await; + matched_buffers.into_iter().flatten().collect() + }) } else { + todo!() } - - todo!() } fn request_lsp( @@ -4814,4 +4876,78 @@ mod tests { "const TWO: usize = one::THREE + one::THREE;" ); } + + #[gpui::test] + async fn test_search(mut cx: gpui::TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), &mut cx); + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(&cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + assert_eq!( + search(&project, SearchQuery::Plain("TWO".to_string()), &mut cx).await, + HashMap::from_iter([ + ("two.rs".to_string(), vec![6..9]), + ("three.rs".to_string(), vec![37..40]) + ]) + ); + + let buffer_4 = project + .update(&mut cx, |project, cx| { + project.open_buffer((worktree_id, "four.rs"), cx) + }) + .await + .unwrap(); + buffer_4.update(&mut cx, |buffer, cx| { + buffer.edit([20..28, 31..43], "two::TWO", cx); + }); + + assert_eq!( + search(&project, SearchQuery::Plain("TWO".to_string()), &mut cx).await, + HashMap::from_iter([ + ("two.rs".to_string(), vec![6..9]), + ("three.rs".to_string(), vec![37..40]), + ("four.rs".to_string(), vec![25..28, 36..39]) + ]) + ); + + async fn search( + project: &ModelHandle, + query: SearchQuery, + cx: &mut gpui::TestAppContext, + ) -> HashMap>> { + project + .update(cx, |project, cx| project.search(query, cx)) + .await + .into_iter() + .map(|(buffer, ranges)| { + buffer.read_with(cx, |buffer, _| { + let path = buffer.file().unwrap().path().to_string_lossy().to_string(); + let ranges = ranges + .into_iter() + .map(|range| range.to_offset(buffer)) + .collect::>(); + (path, ranges) + }) + }) + .collect() + } + } } From 76cc9b347e61387b570b8f5c5178ae33671f5f3f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Feb 2022 15:55:13 +0100 Subject: [PATCH 05/65] Extract a `search` module --- crates/project/src/project.rs | 38 +++++++++++++---------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 02e202110c660ae138ad32795b49ac114f6b0e4d..74890a2f51bf5bde87109eca0bfef31f3874d946 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,9 +1,9 @@ pub mod fs; mod ignore; mod lsp_command; +mod search; pub mod worktree; -use aho_corasick::AhoCorasickBuilder; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; @@ -23,6 +23,7 @@ use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; use postage::{broadcast, prelude::Stream, sink::Sink, watch}; use rand::prelude::*; +use search::SearchQuery; use sha2::{Digest, Sha256}; use smol::block_on; use std::{ @@ -152,10 +153,6 @@ pub struct Symbol { pub signature: [u8; 32], } -pub enum SearchQuery { - Plain(String), -} - pub struct BufferRequestHandle(Rc>); #[derive(Default)] @@ -2078,23 +2075,16 @@ impl Project { }) .detach(); - let SearchQuery::Plain(query) = query; - let search = Arc::new( - AhoCorasickBuilder::new() - .auto_configure(&[&query]) - // .ascii_case_insensitive(!case_sensitive) - .build(&[&query]), - ); let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); let workers = cx.background().num_cpus(); cx.background() .spawn({ let fs = self.fs.clone(); let background = cx.background().clone(); - let search = search.clone(); + let query = query.clone(); async move { let fs = &fs; - let search = &search; + let query = &query; let matching_paths_tx = &matching_paths_tx; background .scoped(|scope| { @@ -2118,10 +2108,10 @@ impl Project { let matches = if let Some(file) = fs.open_sync(&path).await.log_err() { - search - .stream_find_iter(file) + query + .search(file) .next() - .map_or(false, |mat| mat.is_ok()) + .map_or(false, |range| range.is_ok()) } else { false }; @@ -2175,7 +2165,7 @@ impl Project { let background = cx.background().clone(); cx.background().spawn(async move { - let search = &search; + let query = &query; let mut matched_buffers = Vec::new(); for _ in 0..workers { matched_buffers.push(HashMap::default()); @@ -2186,12 +2176,12 @@ impl Project { let mut buffers_rx = buffers_rx.clone(); scope.spawn(async move { while let Some((buffer, snapshot)) = buffers_rx.next().await { - for mat in search.stream_find_iter( + for range in query.search( snapshot.as_rope().bytes_in_range(0..snapshot.len()), ) { - let mat = mat.unwrap(); - let range = snapshot.anchor_before(mat.start()) - ..snapshot.anchor_after(mat.end()); + let range = range.unwrap(); + let range = snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end); worker_matched_buffers .entry(buffer.clone()) .or_insert(Vec::new()) @@ -4902,7 +4892,7 @@ mod tests { .await; assert_eq!( - search(&project, SearchQuery::Plain("TWO".to_string()), &mut cx).await, + search(&project, SearchQuery::text("TWO"), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]) @@ -4920,7 +4910,7 @@ mod tests { }); assert_eq!( - search(&project, SearchQuery::Plain("TWO".to_string()), &mut cx).await, + search(&project, SearchQuery::text("TWO"), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]), From e83d1fc9fcfc6c80fb14b55e38fe7c1a7a4b7235 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Feb 2022 16:33:31 +0100 Subject: [PATCH 06/65] Start on a regex implementation of `SearchQuery` --- crates/project/src/project.rs | 15 +++-- crates/project/src/search.rs | 108 ++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 crates/project/src/search.rs diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 74890a2f51bf5bde87109eca0bfef31f3874d946..aa9e47fcb747ea6c99113281550582c135e75723 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2108,10 +2108,7 @@ impl Project { let matches = if let Some(file) = fs.open_sync(&path).await.log_err() { - query - .search(file) - .next() - .map_or(false, |range| range.is_ok()) + query.is_contained_in_stream(file).unwrap_or(false) } else { false }; @@ -2176,10 +2173,12 @@ impl Project { let mut buffers_rx = buffers_rx.clone(); scope.spawn(async move { while let Some((buffer, snapshot)) = buffers_rx.next().await { - for range in query.search( - snapshot.as_rope().bytes_in_range(0..snapshot.len()), - ) { - let range = range.unwrap(); + for range in query + .search( + snapshot.as_rope().bytes_in_range(0..snapshot.len()), + ) + .unwrap() + { let range = snapshot.anchor_before(range.start) ..snapshot.anchor_after(range.end); worker_matched_buffers diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs new file mode 100644 index 0000000000000000000000000000000000000000..69be605c93d386edac75faad09e1b713ee120166 --- /dev/null +++ b/crates/project/src/search.rs @@ -0,0 +1,108 @@ +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; +use anyhow::Result; +use regex::{Regex, RegexBuilder}; +use std::{ + borrow::Cow, + io::{BufRead, BufReader, Read}, + ops::Range, + sync::Arc, +}; + +#[derive(Clone)] +pub enum SearchQuery { + Text { search: Arc> }, + Regex { multiline: bool, regex: Regex }, +} + +impl SearchQuery { + pub fn text(query: &str) -> Self { + let search = AhoCorasickBuilder::new() + .auto_configure(&[query]) + .build(&[query]); + Self::Text { + search: Arc::new(search), + } + } + + pub fn regex(query: &str, whole_word: bool, case_sensitive: bool) -> Result { + let mut query = Cow::Borrowed(query); + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = Cow::Owned(word_query); + } + + let multiline = query.contains("\n") || query.contains("\\n"); + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(multiline) + .build()?; + Ok(Self::Regex { multiline, regex }) + } + + pub fn is_contained_in_stream(&self, stream: T) -> Result { + match self { + SearchQuery::Text { search } => { + let mat = search.stream_find_iter(stream).next(); + match mat { + Some(Ok(_)) => Ok(true), + Some(Err(err)) => Err(err.into()), + None => Ok(false), + } + } + SearchQuery::Regex { multiline, regex } => { + let mut reader = BufReader::new(stream); + if *multiline { + let mut text = String::new(); + if let Err(err) = reader.read_to_string(&mut text) { + Err(err.into()) + } else { + Ok(regex.find(&text).is_some()) + } + } else { + for line in reader.lines() { + let line = line?; + if regex.find(&line).is_some() { + return Ok(true); + } + } + Ok(false) + } + } + } + } + + pub fn search<'a, T: 'a + Read>(&'a self, stream: T) -> Result>> { + let mut matches = Vec::new(); + match self { + SearchQuery::Text { search } => { + for mat in search.stream_find_iter(stream) { + let mat = mat?; + matches.push(mat.start()..mat.end()) + } + } + SearchQuery::Regex { multiline, regex } => { + let mut reader = BufReader::new(stream); + if *multiline { + let mut text = String::new(); + reader.read_to_string(&mut text)?; + matches.extend(regex.find_iter(&text).map(|mat| mat.start()..mat.end())); + } else { + let mut line_ix = 0; + for line in reader.lines() { + let line = line?; + matches.extend( + regex + .find_iter(&line) + .map(|mat| (line_ix + mat.start())..(line_ix + mat.end())), + ); + line_ix += line.len(); + } + } + } + } + Ok(matches) + } +} From 6d9b003634aef7c3011c64b6a2a586f5a77bd43a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Feb 2022 19:07:00 +0100 Subject: [PATCH 07/65] WIP: Start sketching in `ProjectFindView` Co-Authored-By: Nathan Sobo Co-Authored-By: Max Brunsfeld --- crates/find/src/buffer_find.rs | 946 +++++++++++++++++++++++++++++++ crates/find/src/find.rs | 952 +------------------------------- crates/find/src/project_find.rs | 124 ++++- crates/project/src/project.rs | 14 +- crates/project/src/search.rs | 2 +- 5 files changed, 1083 insertions(+), 955 deletions(-) create mode 100644 crates/find/src/buffer_find.rs diff --git a/crates/find/src/buffer_find.rs b/crates/find/src/buffer_find.rs new file mode 100644 index 0000000000000000000000000000000000000000..60349ad741b40202deb3c17e4af955e8ada2d659 --- /dev/null +++ b/crates/find/src/buffer_find.rs @@ -0,0 +1,946 @@ +use crate::SearchOption; +use aho_corasick::AhoCorasickBuilder; +use anyhow::Result; +use collections::HashMap; +use editor::{ + char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings, + MultiBufferSnapshot, +}; +use gpui::{ + action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use postage::watch; +use regex::RegexBuilder; +use smol::future::yield_now; +use std::{ + cmp::{self, Ordering}, + ops::Range, + sync::Arc, +}; +use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; + +action!(Deploy, bool); +action!(Dismiss); +action!(FocusEditor); +action!(ToggleMode, SearchOption); +action!(GoToMatch, Direction); + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), + Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), + Binding::new("escape", Dismiss, Some("FindBar")), + Binding::new("cmd-f", FocusEditor, Some("FindBar")), + Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), + Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), + Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), + Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), + ]); + cx.add_action(FindBar::deploy); + cx.add_action(FindBar::dismiss); + cx.add_action(FindBar::focus_editor); + cx.add_action(FindBar::toggle_mode); + cx.add_action(FindBar::go_to_match); + cx.add_action(FindBar::go_to_match_on_pane); +} + +struct FindBar { + settings: watch::Receiver, + query_editor: ViewHandle, + active_editor: Option>, + active_match_index: Option, + active_editor_subscription: Option, + editors_with_matches: HashMap, Vec>>, + pending_search: Option>, + case_sensitive_mode: bool, + whole_word_mode: bool, + regex_mode: bool, + query_contains_error: bool, + dismissed: bool, +} + +impl Entity for FindBar { + type Event = (); +} + +impl View for FindBar { + fn ui_name() -> &'static str { + "FindBar" + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let editor_container = if self.query_contains_error { + theme.find.invalid_editor + } else { + theme.find.editor.input.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.find.editor.max_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_mode_button("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_mode_button("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_mode_button("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.find.mode_button_group) + .aligned() + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + ) + .with_children(self.active_editor.as_ref().and_then(|editor| { + let matches = self.editors_with_matches.get(&editor.downgrade())?; + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, matches.len()) + } else { + "No matches".to_string() + }; + + Some( + Label::new(message, theme.find.match_index.text.clone()) + .contained() + .with_style(theme.find.match_index.container) + .aligned() + .boxed(), + ) + })) + .contained() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") + } +} + +impl Toolbar for FindBar { + fn active_item_changed( + &mut self, + item: Option>, + cx: &mut ViewContext, + ) -> bool { + self.active_editor_subscription.take(); + self.active_editor.take(); + self.pending_search.take(); + + if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { + self.active_editor_subscription = + Some(cx.subscribe(&editor, Self::on_active_editor_event)); + self.active_editor = Some(editor); + self.update_matches(false, cx); + true + } else { + false + } + } + + fn on_dismiss(&mut self, cx: &mut ViewContext) { + self.dismissed = true; + for (editor, _) in &self.editors_with_matches { + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } + } + } +} + +impl FindBar { + fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { + let query_editor = cx.add_view(|cx| { + Editor::auto_height( + 2, + { + let settings = settings.clone(); + Arc::new(move |_| { + let settings = settings.borrow(); + EditorSettings { + style: settings.theme.find.editor.input.as_editor(), + tab_size: settings.tab_size, + soft_wrap: editor::SoftWrap::None, + } + }) + }, + cx, + ) + }); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + + Self { + query_editor, + active_editor: None, + active_editor_subscription: None, + active_match_index: None, + editors_with_matches: Default::default(), + case_sensitive_mode: false, + whole_word_mode: false, + regex_mode: false, + settings, + pending_search: None, + query_contains_error: false, + dismissed: false, + } + } + + fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.read(cx).len(); + query_buffer.edit([0..len], query, cx); + }); + }); + } + + fn render_mode_button( + &self, + icon: &str, + mode: SearchOption, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + let is_active = self.is_mode_enabled(mode); + MouseEventHandler::new::(mode as usize, cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.mode_button, + (false, true) => &theme.hovered_mode_button, + (true, false) => &theme.active_mode_button, + (true, true) => &theme.active_hovered_mode_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn render_nav_button( + &self, + icon: &str, + direction: Direction, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + enum NavButton {} + MouseEventHandler::new::(direction as usize, cx, |state, _| { + let style = if state.hovered { + &theme.hovered_mode_button + } else { + &theme.mode_button + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { + let settings = workspace.settings(); + workspace.active_pane().update(cx, |pane, cx| { + pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); + + if let Some(find_bar) = pane + .active_toolbar() + .and_then(|toolbar| toolbar.downcast::()) + { + find_bar.update(cx, |find_bar, _| find_bar.dismissed = false); + let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); + let display_map = editor + .update(cx, |editor, cx| editor.snapshot(cx)) + .display_snapshot; + let selection = editor + .read(cx) + .newest_selection::(&display_map.buffer_snapshot); + + let mut text: String; + if selection.start == selection.end { + let point = selection.start.to_display_point(&display_map); + let range = editor::movement::surrounding_word(&display_map, point); + let range = range.start.to_offset(&display_map, Bias::Left) + ..range.end.to_offset(&display_map, Bias::Right); + text = display_map.buffer_snapshot.text_for_range(range).collect(); + if text.trim().is_empty() { + text = String::new(); + } + } else { + text = display_map + .buffer_snapshot + .text_for_range(selection.start..selection.end) + .collect(); + } + + if !text.is_empty() { + find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + } + + if *focus { + let query_editor = find_bar.read(cx).query_editor.clone(); + query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&editor::SelectAll, cx); + }); + cx.focus(&find_bar); + } + } + }); + } + + fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext) { + if pane.toolbar::().is_some() { + pane.dismiss_toolbar(cx); + } + } + + fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + if let Some(active_editor) = self.active_editor.as_ref() { + cx.focus(active_editor); + } + } + + fn is_mode_enabled(&self, mode: SearchOption) -> bool { + match mode { + SearchOption::WholeWord => self.whole_word_mode, + SearchOption::CaseSensitive => self.case_sensitive_mode, + SearchOption::Regex => self.regex_mode, + } + } + + fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { + let value = match mode { + SearchOption::WholeWord => &mut self.whole_word_mode, + SearchOption::CaseSensitive => &mut self.case_sensitive_mode, + SearchOption::Regex => &mut self.regex_mode, + }; + *value = !*value; + self.update_matches(true, cx); + cx.notify(); + } + + fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { + if let Some(mut index) = self.active_match_index { + if let Some(editor) = self.active_editor.as_ref() { + editor.update(cx, |editor, cx| { + let newest_selection = editor.newest_anchor_selection().clone(); + if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) { + let position = newest_selection.head(); + let buffer = editor.buffer().read(cx).read(cx); + if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { + if *direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { + if *direction == Direction::Next { + index = 0; + } + } else if *direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } else if *direction == Direction::Next { + if index == ranges.len() - 1 { + index = 0 + } else { + index += 1; + } + } + + let range_to_select = ranges[index].clone(); + drop(buffer); + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); + } + }); + } + } + } + + fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { + if let Some(find_bar) = pane.toolbar::() { + find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); + } + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => { + self.query_contains_error = false; + self.clear_matches(cx); + self.update_matches(true, cx); + cx.notify(); + } + _ => {} + } + } + + fn on_active_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => self.update_matches(false, cx), + editor::Event::SelectionsChanged => self.update_match_index(cx), + _ => {} + } + } + + fn clear_matches(&mut self, cx: &mut ViewContext) { + let mut active_editor_matches = None; + for (editor, ranges) in self.editors_with_matches.drain() { + if let Some(editor) = editor.upgrade(cx) { + if Some(&editor) == self.active_editor.as_ref() { + active_editor_matches = Some((editor.downgrade(), ranges)); + } else { + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } + } + } + self.editors_with_matches.extend(active_editor_matches); + } + + fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext) { + let query = self.query_editor.read(cx).text(cx); + self.pending_search.take(); + if let Some(editor) = self.active_editor.as_ref() { + if query.is_empty() { + self.active_match_index.take(); + editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); + } else { + let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); + let case_sensitive = self.case_sensitive_mode; + let whole_word = self.whole_word_mode; + let ranges = if self.regex_mode { + cx.background() + .spawn(regex_search(buffer, query, case_sensitive, whole_word)) + } else { + cx.background().spawn(async move { + Ok(search(buffer, query, case_sensitive, whole_word).await) + }) + }; + + let editor = editor.downgrade(); + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + match ranges.await { + Ok(ranges) => { + if let Some(editor) = editor.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.editors_with_matches + .insert(editor.downgrade(), ranges.clone()); + this.update_match_index(cx); + if !this.dismissed { + editor.update(cx, |editor, cx| { + let theme = &this.settings.borrow().theme.find; + + if select_closest_match { + if let Some(match_ix) = this.active_match_index { + editor.select_ranges( + [ranges[match_ix].clone()], + Some(Autoscroll::Fit), + cx, + ); + } + } + + editor.highlight_ranges::( + ranges, + theme.match_background, + cx, + ); + }); + } + }); + } + } + Err(_) => { + this.update(&mut cx, |this, cx| { + this.query_contains_error = true; + cx.notify(); + }); + } + } + })); + } + } + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + self.active_match_index = self.active_match_index(cx); + cx.notify(); + } + + fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { + let editor = self.active_editor.as_ref()?; + let ranges = self.editors_with_matches.get(&editor.downgrade())?; + let editor = editor.read(cx); + let position = editor.newest_anchor_selection().head(); + if ranges.is_empty() { + None + } else { + let buffer = editor.buffer().read(cx).read(cx); + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), + } + } + } +} + +const YIELD_INTERVAL: usize = 20000; + +async fn search( + buffer: MultiBufferSnapshot, + query: String, + case_sensitive: bool, + whole_word: bool, +) -> Vec> { + let mut ranges = Vec::new(); + + let search = AhoCorasickBuilder::new() + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); + for (ix, mat) in search + .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + + if whole_word { + let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } + + ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + } + + ranges +} + +async fn regex_search( + buffer: MultiBufferSnapshot, + mut query: String, + case_sensitive: bool, + whole_word: bool, +) -> Result>> { + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = word_query; + } + + let mut ranges = Vec::new(); + + if query.contains("\n") || query.contains("\\n") { + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(true) + .build()?; + for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); + } + } else { + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .build()?; + + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in buffer + .chunks(0..buffer.len(), false) + .map(|c| c.text) + .chain(["\n"]) + .enumerate() + { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end)); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } + } + } + + Ok(ranges) +} + +#[cfg(test)] +mod tests { + use super::*; + use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer}; + use gpui::{color::Color, TestAppContext}; + use std::sync::Arc; + use unindent::Unindent as _; + + #[gpui::test] + async fn test_find_simple(mut cx: TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); + theme.find.match_background = Color::red(); + let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); + + let buffer = cx.update(|cx| { + MultiBuffer::build_simple( + &r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + cx, + ) + }); + let editor = cx.add_view(Default::default(), |cx| { + Editor::new(buffer.clone(), Arc::new(EditorSettings::test), None, cx) + }); + + let find_bar = cx.add_view(Default::default(), |cx| { + let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx); + find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); + find_bar + }); + + // Search for a string that appears with different casing. + // By default, search is case-insensitive. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("us", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + Color::red(), + ), + ( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ), + ] + ); + }); + + // Switch to a case sensitive search. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchOption::CaseSensitive), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + )] + ); + }); + + // Search for a string that appears both as a whole word and + // within other words. By default, all results are found. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.set_query("or", cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), + Color::red(), + ), + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), + Color::red(), + ), + ( + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ( + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), + Color::red(), + ), + ] + ); + }); + + // Switch to a whole word search. + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.toggle_mode(&ToggleMode(SearchOption::WholeWord), cx); + }); + editor.next_notification(&cx).await; + editor.update(&mut cx, |editor, cx| { + assert_eq!( + editor.all_highlighted_ranges(cx), + &[ + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ] + ); + }); + + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + find_bar.update(&mut cx, |find_bar, cx| { + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the previous match selects + // the closest match to the left. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the next match selects the + // closest match to the right. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(1)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(1)); + }); + + // Park the cursor after the last match and ensure that going to the previous match selects + // the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + + // Park the cursor after the last match and ensure that going to the next match selects the + // first match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(2)); + find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(0)); + }); + + // Park the cursor before the first match and ensure that going to the previous match + // selects the last match. + editor.update(&mut cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); + }); + find_bar.update(&mut cx, |find_bar, cx| { + assert_eq!(find_bar.active_match_index, Some(0)); + find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + find_bar.read_with(&cx, |find_bar, _| { + assert_eq!(find_bar.active_match_index, Some(2)); + }); + } +} diff --git a/crates/find/src/find.rs b/crates/find/src/find.rs index 4be4216c370e319459aae3e68c5ae2abae808615..caf8b7a8436996904a8c4767af10c40d3b2b43bc 100644 --- a/crates/find/src/find.rs +++ b/crates/find/src/find.rs @@ -1,954 +1,16 @@ -mod project_find; - -use aho_corasick::AhoCorasickBuilder; -use anyhow::Result; -use collections::HashMap; -use editor::{ - char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, EditorSettings, - MultiBufferSnapshot, -}; -use gpui::{ - action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, - RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, -}; -use postage::watch; -use regex::RegexBuilder; -use smol::future::yield_now; -use std::{ - cmp::{self, Ordering}, - ops::Range, - sync::Arc, -}; -use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; +use gpui::MutableAppContext; -action!(Deploy, bool); -action!(Dismiss); -action!(FocusEditor); -action!(ToggleMode, SearchMode); -action!(GoToMatch, Direction); +mod buffer_find; +mod project_find; -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Direction { - Prev, - Next, +pub fn init(cx: &mut MutableAppContext) { + buffer_find::init(cx); + project_find::init(cx); } #[derive(Clone, Copy)] -pub enum SearchMode { +pub enum SearchOption { WholeWord, CaseSensitive, Regex, } - -pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), - Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), - Binding::new("escape", Dismiss, Some("FindBar")), - Binding::new("cmd-f", FocusEditor, Some("FindBar")), - Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), - Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), - Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), - Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), - ]); - cx.add_action(FindBar::deploy); - cx.add_action(FindBar::dismiss); - cx.add_action(FindBar::focus_editor); - cx.add_action(FindBar::toggle_mode); - cx.add_action(FindBar::go_to_match); - cx.add_action(FindBar::go_to_match_on_pane); -} - -struct FindBar { - settings: watch::Receiver, - query_editor: ViewHandle, - active_editor: Option>, - active_match_index: Option, - active_editor_subscription: Option, - editors_with_matches: HashMap, Vec>>, - pending_search: Option>, - case_sensitive_mode: bool, - whole_word_mode: bool, - regex_mode: bool, - query_contains_error: bool, - dismissed: bool, -} - -impl Entity for FindBar { - type Event = (); -} - -impl View for FindBar { - fn ui_name() -> &'static str { - "FindBar" - } - - fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &self.settings.borrow().theme; - let editor_container = if self.query_contains_error { - theme.find.invalid_editor - } else { - theme.find.editor.input.container - }; - Flex::row() - .with_child( - ChildView::new(&self.query_editor) - .contained() - .with_style(editor_container) - .aligned() - .constrained() - .with_max_width(theme.find.editor.max_width) - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_mode_button("Case", SearchMode::CaseSensitive, cx)) - .with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx)) - .with_child(self.render_mode_button("Regex", SearchMode::Regex, cx)) - .contained() - .with_style(theme.find.mode_button_group) - .aligned() - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_nav_button("<", Direction::Prev, cx)) - .with_child(self.render_nav_button(">", Direction::Next, cx)) - .aligned() - .boxed(), - ) - .with_children(self.active_editor.as_ref().and_then(|editor| { - let matches = self.editors_with_matches.get(&editor.downgrade())?; - let message = if let Some(match_ix) = self.active_match_index { - format!("{}/{}", match_ix + 1, matches.len()) - } else { - "No matches".to_string() - }; - - Some( - Label::new(message, theme.find.match_index.text.clone()) - .contained() - .with_style(theme.find.match_index.container) - .aligned() - .boxed(), - ) - })) - .contained() - .with_style(theme.find.container) - .constrained() - .with_height(theme.workspace.toolbar.height) - .named("find bar") - } -} - -impl Toolbar for FindBar { - fn active_item_changed( - &mut self, - item: Option>, - cx: &mut ViewContext, - ) -> bool { - self.active_editor_subscription.take(); - self.active_editor.take(); - self.pending_search.take(); - - if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { - self.active_editor_subscription = - Some(cx.subscribe(&editor, Self::on_active_editor_event)); - self.active_editor = Some(editor); - self.update_matches(false, cx); - true - } else { - false - } - } - - fn on_dismiss(&mut self, cx: &mut ViewContext) { - self.dismissed = true; - for (editor, _) in &self.editors_with_matches { - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); - } - } - } -} - -impl FindBar { - fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { - let query_editor = cx.add_view(|cx| { - Editor::auto_height( - 2, - { - let settings = settings.clone(); - Arc::new(move |_| { - let settings = settings.borrow(); - EditorSettings { - style: settings.theme.find.editor.input.as_editor(), - tab_size: settings.tab_size, - soft_wrap: editor::SoftWrap::None, - } - }) - }, - cx, - ) - }); - cx.subscribe(&query_editor, Self::on_query_editor_event) - .detach(); - - Self { - query_editor, - active_editor: None, - active_editor_subscription: None, - active_match_index: None, - editors_with_matches: Default::default(), - case_sensitive_mode: false, - whole_word_mode: false, - regex_mode: false, - settings, - pending_search: None, - query_contains_error: false, - dismissed: false, - } - } - - fn set_query(&mut self, query: &str, cx: &mut ViewContext) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.read(cx).len(); - query_buffer.edit([0..len], query, cx); - }); - }); - } - - fn render_mode_button( - &self, - icon: &str, - mode: SearchMode, - cx: &mut RenderContext, - ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; - let is_active = self.is_mode_enabled(mode); - MouseEventHandler::new::(mode as usize, cx, |state, _| { - let style = match (is_active, state.hovered) { - (false, false) => &theme.mode_button, - (false, true) => &theme.hovered_mode_button, - (true, false) => &theme.active_mode_button, - (true, true) => &theme.active_hovered_mode_button, - }; - Label::new(icon.to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) - .with_cursor_style(CursorStyle::PointingHand) - .boxed() - } - - fn render_nav_button( - &self, - icon: &str, - direction: Direction, - cx: &mut RenderContext, - ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; - enum NavButton {} - MouseEventHandler::new::(direction as usize, cx, |state, _| { - let style = if state.hovered { - &theme.hovered_mode_button - } else { - &theme.mode_button - }; - Label::new(icon.to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) - .with_cursor_style(CursorStyle::PointingHand) - .boxed() - } - - fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { - let settings = workspace.settings(); - workspace.active_pane().update(cx, |pane, cx| { - pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); - - if let Some(find_bar) = pane - .active_toolbar() - .and_then(|toolbar| toolbar.downcast::()) - { - find_bar.update(cx, |find_bar, _| find_bar.dismissed = false); - let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); - let display_map = editor - .update(cx, |editor, cx| editor.snapshot(cx)) - .display_snapshot; - let selection = editor - .read(cx) - .newest_selection::(&display_map.buffer_snapshot); - - let mut text: String; - if selection.start == selection.end { - let point = selection.start.to_display_point(&display_map); - let range = editor::movement::surrounding_word(&display_map, point); - let range = range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Right); - text = display_map.buffer_snapshot.text_for_range(range).collect(); - if text.trim().is_empty() { - text = String::new(); - } - } else { - text = display_map - .buffer_snapshot - .text_for_range(selection.start..selection.end) - .collect(); - } - - if !text.is_empty() { - find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); - } - - if *focus { - let query_editor = find_bar.read(cx).query_editor.clone(); - query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&editor::SelectAll, cx); - }); - cx.focus(&find_bar); - } - } - }); - } - - fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext) { - if pane.toolbar::().is_some() { - pane.dismiss_toolbar(cx); - } - } - - fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { - if let Some(active_editor) = self.active_editor.as_ref() { - cx.focus(active_editor); - } - } - - fn is_mode_enabled(&self, mode: SearchMode) -> bool { - match mode { - SearchMode::WholeWord => self.whole_word_mode, - SearchMode::CaseSensitive => self.case_sensitive_mode, - SearchMode::Regex => self.regex_mode, - } - } - - fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { - let value = match mode { - SearchMode::WholeWord => &mut self.whole_word_mode, - SearchMode::CaseSensitive => &mut self.case_sensitive_mode, - SearchMode::Regex => &mut self.regex_mode, - }; - *value = !*value; - self.update_matches(true, cx); - cx.notify(); - } - - fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { - if let Some(mut index) = self.active_match_index { - if let Some(editor) = self.active_editor.as_ref() { - editor.update(cx, |editor, cx| { - let newest_selection = editor.newest_anchor_selection().clone(); - if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) { - let position = newest_selection.head(); - let buffer = editor.buffer().read(cx).read(cx); - if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { - if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } - } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { - if *direction == Direction::Next { - index = 0; - } - } else if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } else if *direction == Direction::Next { - if index == ranges.len() - 1 { - index = 0 - } else { - index += 1; - } - } - - let range_to_select = ranges[index].clone(); - drop(buffer); - editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); - } - }); - } - } - } - - fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { - if let Some(find_bar) = pane.toolbar::() { - find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); - } - } - - fn on_query_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::Edited => { - self.query_contains_error = false; - self.clear_matches(cx); - self.update_matches(true, cx); - cx.notify(); - } - _ => {} - } - } - - fn on_active_editor_event( - &mut self, - _: ViewHandle, - event: &editor::Event, - cx: &mut ViewContext, - ) { - match event { - editor::Event::Edited => self.update_matches(false, cx), - editor::Event::SelectionsChanged => self.update_match_index(cx), - _ => {} - } - } - - fn clear_matches(&mut self, cx: &mut ViewContext) { - let mut active_editor_matches = None; - for (editor, ranges) in self.editors_with_matches.drain() { - if let Some(editor) = editor.upgrade(cx) { - if Some(&editor) == self.active_editor.as_ref() { - active_editor_matches = Some((editor.downgrade(), ranges)); - } else { - editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); - } - } - } - self.editors_with_matches.extend(active_editor_matches); - } - - fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext) { - let query = self.query_editor.read(cx).text(cx); - self.pending_search.take(); - if let Some(editor) = self.active_editor.as_ref() { - if query.is_empty() { - self.active_match_index.take(); - editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); - } else { - let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); - let case_sensitive = self.case_sensitive_mode; - let whole_word = self.whole_word_mode; - let ranges = if self.regex_mode { - cx.background() - .spawn(regex_search(buffer, query, case_sensitive, whole_word)) - } else { - cx.background().spawn(async move { - Ok(search(buffer, query, case_sensitive, whole_word).await) - }) - }; - - let editor = editor.downgrade(); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - match ranges.await { - Ok(ranges) => { - if let Some(editor) = editor.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.editors_with_matches - .insert(editor.downgrade(), ranges.clone()); - this.update_match_index(cx); - if !this.dismissed { - editor.update(cx, |editor, cx| { - let theme = &this.settings.borrow().theme.find; - - if select_closest_match { - if let Some(match_ix) = this.active_match_index { - editor.select_ranges( - [ranges[match_ix].clone()], - Some(Autoscroll::Fit), - cx, - ); - } - } - - editor.highlight_ranges::( - ranges, - theme.match_background, - cx, - ); - }); - } - }); - } - } - Err(_) => { - this.update(&mut cx, |this, cx| { - this.query_contains_error = true; - cx.notify(); - }); - } - } - })); - } - } - } - - fn update_match_index(&mut self, cx: &mut ViewContext) { - self.active_match_index = self.active_match_index(cx); - cx.notify(); - } - - fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { - let editor = self.active_editor.as_ref()?; - let ranges = self.editors_with_matches.get(&editor.downgrade())?; - let editor = editor.read(cx); - let position = editor.newest_anchor_selection().head(); - if ranges.is_empty() { - None - } else { - let buffer = editor.buffer().read(cx).read(cx); - match ranges.binary_search_by(|probe| { - if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { - Ordering::Less - } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { - Ordering::Greater - } else { - Ordering::Equal - } - }) { - Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), - } - } - } -} - -const YIELD_INTERVAL: usize = 20000; - -async fn search( - buffer: MultiBufferSnapshot, - query: String, - case_sensitive: bool, - whole_word: bool, -) -> Vec> { - let mut ranges = Vec::new(); - - let search = AhoCorasickBuilder::new() - .auto_configure(&[&query]) - .ascii_case_insensitive(!case_sensitive) - .build(&[&query]); - for (ix, mat) in search - .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) - .enumerate() - { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - let mat = mat.unwrap(); - - if whole_word { - let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap()); - let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { - continue; - } - } - - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); - } - - ranges -} - -async fn regex_search( - buffer: MultiBufferSnapshot, - mut query: String, - case_sensitive: bool, - whole_word: bool, -) -> Result>> { - if whole_word { - let mut word_query = String::new(); - word_query.push_str("\\b"); - word_query.push_str(&query); - word_query.push_str("\\b"); - query = word_query; - } - - let mut ranges = Vec::new(); - - if query.contains("\n") || query.contains("\\n") { - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .multi_line(true) - .build()?; - for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); - } - } else { - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .build()?; - - let mut line = String::new(); - let mut line_offset = 0; - for (chunk_ix, chunk) in buffer - .chunks(0..buffer.len(), false) - .map(|c| c.text) - .chain(["\n"]) - .enumerate() - { - if (chunk_ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - for mat in regex.find_iter(&line) { - let start = line_offset + mat.start(); - let end = line_offset + mat.end(); - ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end)); - } - - line_offset += line.len() + 1; - line.clear(); - } - line.push_str(text); - } - } - } - - Ok(ranges) -} - -#[cfg(test)] -mod tests { - use super::*; - use editor::{DisplayPoint, Editor, EditorSettings, MultiBuffer}; - use gpui::{color::Color, TestAppContext}; - use std::sync::Arc; - use unindent::Unindent as _; - - #[gpui::test] - async fn test_find_simple(mut cx: TestAppContext) { - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); - theme.find.match_background = Color::red(); - let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); - - let buffer = cx.update(|cx| { - MultiBuffer::build_simple( - &r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - cx, - ) - }); - let editor = cx.add_view(Default::default(), |cx| { - Editor::new(buffer.clone(), Arc::new(EditorSettings::test), None, cx) - }); - - let find_bar = cx.add_view(Default::default(), |cx| { - let mut find_bar = FindBar::new(watch::channel_with(settings).1, cx); - find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); - find_bar - }); - - // Search for a string that appears with different casing. - // By default, search is case-insensitive. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("us", cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[ - ( - DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), - Color::red(), - ), - ( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - ), - ] - ); - }); - - // Switch to a case sensitive search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - )] - ); - }); - - // Search for a string that appears both as a whole word and - // within other words. By default, all results are found. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("or", cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[ - ( - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), - Color::red(), - ), - ( - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), - ), - ( - DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), - Color::red(), - ), - ( - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), - Color::red(), - ), - ( - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), - ), - ( - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), - ), - ( - DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), - Color::red(), - ), - ] - ); - }); - - // Switch to a whole word search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx); - }); - editor.next_notification(&cx).await; - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.all_highlighted_ranges(cx), - &[ - ( - DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), - ), - ( - DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), - ), - ( - DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), - ), - ] - ); - }); - - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); - }); - - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the previous match selects - // the closest match to the left. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the next match selects the - // closest match to the right. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); - }); - - // Park the cursor after the last match and ensure that going to the previous match selects - // the last match. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - - // Park the cursor after the last match and ensure that going to the next match selects the - // first match. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); - }); - - // Park the cursor before the first match and ensure that going to the previous match - // selects the last match. - editor.update(&mut cx, |editor, cx| { - editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); - }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); - assert_eq!( - editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), - [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] - ); - }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); - }); - } -} diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 8bc2777a6fdb303d4785327d735d4136cf679874..fee56502ce0126ec542ad4b630c3bdc598866f7b 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,7 +1,24 @@ -use crate::SearchMode; -use editor::MultiBuffer; -use gpui::{Entity, ModelContext, ModelHandle, Task}; +use anyhow::Result; +use editor::{Editor, MultiBuffer}; +use gpui::{ + action, elements::*, keymap::Binding, ElementBox, Entity, Handle, ModelContext, ModelHandle, + MutableAppContext, Task, View, ViewContext, ViewHandle, +}; use project::Project; +use std::{borrow::Borrow, sync::Arc}; +use workspace::Workspace; + +action!(Deploy); +action!(Search); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-shift-f", Deploy, None), + Binding::new("enter", Search, Some("ProjectFindView")), + ]); + cx.add_action(ProjectFindView::deploy); + cx.add_async_action(ProjectFindView::search); +} struct ProjectFind { last_search: SearchParams, @@ -20,6 +37,8 @@ struct SearchParams { struct ProjectFindView { model: ModelHandle, + query_editor: ViewHandle, + results_editor: ViewHandle, } impl Entity for ProjectFind { @@ -44,3 +63,102 @@ impl ProjectFind { }); } } + +impl workspace::Item for ProjectFind { + type View = ProjectFindView; + + fn build_view( + model: ModelHandle, + workspace: &workspace::Workspace, + nav_history: workspace::ItemNavHistory, + cx: &mut gpui::ViewContext, + ) -> Self::View { + let settings = workspace.settings(); + let excerpts = model.read(cx).excerpts.clone(); + let build_settings = editor::settings_builder(excerpts.downgrade(), workspace.settings()); + ProjectFindView { + model, + query_editor: cx.add_view(|cx| Editor::single_line(build_settings.clone(), cx)), + results_editor: cx.add_view(|cx| { + Editor::for_buffer( + excerpts, + build_settings, + Some(workspace.project().clone()), + cx, + ) + }), + } + } + + fn project_path(&self) -> Option { + None + } +} + +impl Entity for ProjectFindView { + type Event = (); +} + +impl View for ProjectFindView { + fn ui_name() -> &'static str { + "ProjectFindView" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + Flex::column() + .with_child(ChildView::new(&self.query_editor).boxed()) + .with_child(ChildView::new(&self.results_editor).boxed()) + .boxed() + } +} + +impl workspace::ItemView for ProjectFindView { + fn item_id(&self, cx: &gpui::AppContext) -> usize { + self.model.id() + } + + fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + Label::new("Project Find".to_string(), style.label.clone()).boxed() + } + + fn project_path(&self, cx: &gpui::AppContext) -> Option { + None + } + + fn can_save(&self, _: &gpui::AppContext) -> bool { + true + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.save(project, cx)) + } + + fn can_save_as(&self, cx: &gpui::AppContext) -> bool { + false + } + + fn save_as( + &mut self, + project: ModelHandle, + abs_path: std::path::PathBuf, + cx: &mut ViewContext, + ) -> Task> { + unreachable!("save_as should not have been called") + } +} + +impl ProjectFindView { + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); + workspace.open_item(model, cx); + } + + fn search(&mut self, _: &Search, cx: &mut ViewContext) -> Option>> { + todo!() + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index aa9e47fcb747ea6c99113281550582c135e75723..a78b6356b86d5d690ace9e6097ffe6d4b9b4846a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2108,7 +2108,7 @@ impl Project { let matches = if let Some(file) = fs.open_sync(&path).await.log_err() { - query.is_contained_in_stream(file).unwrap_or(false) + query.detect(file).unwrap_or(false) } else { false }; @@ -2132,7 +2132,7 @@ impl Project { .detach(); let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); - let buffers = self + let open_buffers = self .buffers_state .borrow() .open_buffers @@ -2140,9 +2140,9 @@ impl Project { .filter_map(|b| b.upgrade(cx)) .collect::>(); cx.spawn(|this, mut cx| async move { - for buffer in buffers { + for buffer in &open_buffers { let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; + buffers_tx.send((buffer.clone(), snapshot)).await?; } while let Some(project_path) = matching_paths_rx.next().await { @@ -2151,8 +2151,10 @@ impl Project { .await .log_err() { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; + if !open_buffers.contains(&buffer) { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx.send((buffer, snapshot)).await?; + } } } diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 69be605c93d386edac75faad09e1b713ee120166..548a4c71dcfa56b5aba514a6578b2aeb8425d142 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -42,7 +42,7 @@ impl SearchQuery { Ok(Self::Regex { multiline, regex }) } - pub fn is_contained_in_stream(&self, stream: T) -> Result { + pub fn detect(&self, stream: T) -> Result { match self { SearchQuery::Text { search } => { let mat = search.stream_find_iter(stream).next(); From 0bf944e03845d8aa433d8ce82e6364511ce094af Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 10:27:45 +0100 Subject: [PATCH 08/65] Use `Project::search` in `ProjectFind` and show search results --- Cargo.lock | 4 +- crates/editor/src/editor.rs | 4 +- crates/editor/src/multi_buffer.rs | 114 +++++------ crates/find/Cargo.toml | 4 +- crates/find/src/buffer_find.rs | 295 ++++++++++------------------ crates/find/src/project_find.rs | 233 ++++++++++++++++++---- crates/language/src/buffer.rs | 20 ++ crates/project/src/project.rs | 13 +- crates/project/src/search.rs | 103 +++++++--- crates/text/src/rope.rs | 6 + crates/theme/src/theme.rs | 10 +- crates/zed/assets/themes/_base.toml | 16 +- 12 files changed, 485 insertions(+), 337 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc41bbe90099ce96e7631db08000f7d6106a8b09..2dd0f7c036ba787193c70918c89e044a9b9e34c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1776,15 +1776,13 @@ dependencies = [ name = "find" version = "0.1.0" dependencies = [ - "aho-corasick", "anyhow", "collections", "editor", "gpui", + "language", "postage", "project", - "regex", - "smol", "theme", "unindent", "workspace", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 87b3814589165bec4d3fd4129c0172cbab5dcc8d..dcbbe76872ffa9871bb80742a0a308570e91f130 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -30,14 +30,14 @@ use gpui::{ }; use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; +pub use language::{char_kind, CharKind}; use language::{ AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ - char_kind, Anchor, AnchorRangeExt, CharKind, ExcerptId, MultiBuffer, MultiBufferSnapshot, - ToOffset, ToPoint, + Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use ordered_float::OrderedFloat; use postage::watch; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4fc4488af9124e040ed8017e07d5fc6ca862f8df..f447350ec564f5cfd38ccac6a85bb8132e44208d 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,8 +7,9 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline, - OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId, + char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, + Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, + TransactionId, }; use std::{ cell::{Ref, RefCell}, @@ -50,14 +51,6 @@ struct History { group_interval: Duration, } -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] -pub enum CharKind { - Newline, - Punctuation, - Whitespace, - Word, -} - #[derive(Clone)] struct Transaction { id: TransactionId, @@ -102,6 +95,7 @@ pub struct MultiBufferSnapshot { } pub struct ExcerptBoundary { + pub id: ExcerptId, pub row: u32, pub buffer: BufferSnapshot, pub range: Range, @@ -773,6 +767,21 @@ impl MultiBuffer { ids } + pub fn clear(&mut self, cx: &mut ModelContext) { + self.buffers.borrow_mut().clear(); + let mut snapshot = self.snapshot.borrow_mut(); + let prev_len = snapshot.len(); + snapshot.excerpts = Default::default(); + snapshot.trailing_excerpt_update_count += 1; + snapshot.is_dirty = false; + snapshot.has_conflict = false; + self.subscriptions.publish_mut([Edit { + old: 0..prev_len, + new: 0..0, + }]); + cx.notify(); + } + pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle) -> Vec { self.buffers .borrow() @@ -1342,9 +1351,12 @@ impl MultiBufferSnapshot { (start..end, word_kind) } - fn as_singleton(&self) -> Option<&Excerpt> { + pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> { if self.singleton { - self.excerpts.iter().next() + self.excerpts + .iter() + .next() + .map(|e| (&e.id, e.buffer_id, &e.buffer)) } else { None } @@ -1359,8 +1371,8 @@ impl MultiBufferSnapshot { } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_offset(offset, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_offset(offset, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1378,8 +1390,8 @@ impl MultiBufferSnapshot { } pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_point(point, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point(point, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1397,8 +1409,8 @@ impl MultiBufferSnapshot { } pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_point_utf16(point, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point_utf16(point, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1466,8 +1478,8 @@ impl MultiBufferSnapshot { } pub fn offset_to_point(&self, offset: usize) -> Point { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.offset_to_point(offset); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point(offset); } let mut cursor = self.excerpts.cursor::<(usize, Point)>(); @@ -1487,8 +1499,8 @@ impl MultiBufferSnapshot { } pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.offset_to_point_utf16(offset); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point_utf16(offset); } let mut cursor = self.excerpts.cursor::<(usize, PointUtf16)>(); @@ -1508,8 +1520,8 @@ impl MultiBufferSnapshot { } pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_to_point_utf16(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_point_utf16(point); } let mut cursor = self.excerpts.cursor::<(Point, PointUtf16)>(); @@ -1529,8 +1541,8 @@ impl MultiBufferSnapshot { } pub fn point_to_offset(&self, point: Point) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_to_offset(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_offset(point); } let mut cursor = self.excerpts.cursor::<(Point, usize)>(); @@ -1550,8 +1562,8 @@ impl MultiBufferSnapshot { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_utf16_to_offset(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_utf16_to_offset(point); } let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(); @@ -1711,9 +1723,8 @@ impl MultiBufferSnapshot { D: TextDimension + Ord + Sub, I: 'a + IntoIterator, { - if let Some(excerpt) = self.as_singleton() { - return excerpt - .buffer + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer .summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor)) .collect(); } @@ -1878,11 +1889,11 @@ impl MultiBufferSnapshot { pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { let offset = position.to_offset(self); - if let Some(excerpt) = self.as_singleton() { + if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { return Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor: excerpt.buffer.anchor_at(offset, bias), + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: buffer.anchor_at(offset, bias), }; } @@ -1989,6 +2000,7 @@ impl MultiBufferSnapshot { let excerpt = cursor.item()?; let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id; let boundary = ExcerptBoundary { + id: excerpt.id.clone(), row: cursor.start().1.row, buffer: excerpt.buffer.clone(), range: excerpt.range.clone(), @@ -2090,7 +2102,7 @@ impl MultiBufferSnapshot { { self.as_singleton() .into_iter() - .flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id)) + .flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id)) } pub fn diagnostics_in_range<'a, T, O>( @@ -2101,11 +2113,11 @@ impl MultiBufferSnapshot { T: 'a + ToOffset, O: 'a + text::FromAnchor, { - self.as_singleton().into_iter().flat_map(move |excerpt| { - excerpt - .buffer - .diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) - }) + self.as_singleton() + .into_iter() + .flat_map(move |(_, _, buffer)| { + buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) + }) } pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { @@ -2147,16 +2159,16 @@ impl MultiBufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - let excerpt = self.as_singleton()?; - let outline = excerpt.buffer.outline(theme)?; + let (excerpt_id, _, buffer) = self.as_singleton()?; + let outline = buffer.outline(theme)?; Some(Outline::new( outline .items .into_iter() .map(|item| OutlineItem { depth: item.depth, - range: self.anchor_in_excerpt(excerpt.id.clone(), item.range.start) - ..self.anchor_in_excerpt(excerpt.id.clone(), item.range.end), + range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) + ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, @@ -2764,18 +2776,6 @@ impl ToPointUtf16 for PointUtf16 { } } -pub fn char_kind(c: char) -> CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { - CharKind::Whitespace - } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 39570334d6c3b378653246eaab3481e67cc1bc58..1c0781511672976ac02d0d54446e6f214f0f4b9d 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -10,14 +10,12 @@ path = "src/find.rs" collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } +language = { path = "../language" } project = { path = "../project" } theme = { path = "../theme" } workspace = { path = "../workspace" } -aho-corasick = "0.7" anyhow = "1.0" postage = { version = "0.4.1", features = ["futures-traits"] } -regex = "1.5" -smol = { version = "1.2" } [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/find/src/buffer_find.rs b/crates/find/src/buffer_find.rs index 94ca1d935778f70e838786f1298f439df88a2f25..41aa96fa688eaeb982a2a3944109da18d5feb38c 100644 --- a/crates/find/src/buffer_find.rs +++ b/crates/find/src/buffer_find.rs @@ -1,17 +1,13 @@ use crate::SearchOption; -use aho_corasick::AhoCorasickBuilder; -use anyhow::Result; use collections::HashMap; -use editor::{ - char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, MultiBufferSnapshot, -}; +use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::AnchorRangeExt; use postage::watch; -use regex::RegexBuilder; -use smol::future::yield_now; +use project::search::SearchQuery; use std::{ cmp::{self, Ordering}, ops::Range, @@ -21,7 +17,7 @@ use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); action!(Dismiss); action!(FocusEditor); -action!(ToggleMode, SearchOption); +action!(ToggleSearchOption, SearchOption); action!(GoToMatch, Direction); #[derive(Clone, Copy, PartialEq, Eq)] @@ -44,7 +40,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(FindBar::deploy); cx.add_action(FindBar::dismiss); cx.add_action(FindBar::focus_editor); - cx.add_action(FindBar::toggle_mode); + cx.add_action(FindBar::toggle_search_option); cx.add_action(FindBar::go_to_match); cx.add_action(FindBar::go_to_match_on_pane); } @@ -57,9 +53,9 @@ struct FindBar { active_editor_subscription: Option, editors_with_matches: HashMap, Vec>>, pending_search: Option>, - case_sensitive_mode: bool, - whole_word_mode: bool, - regex_mode: bool, + case_sensitive: bool, + whole_word: bool, + regex: bool, query_contains_error: bool, dismissed: bool, } @@ -96,11 +92,11 @@ impl View for FindBar { ) .with_child( Flex::row() - .with_child(self.render_mode_button("Case", SearchOption::CaseSensitive, cx)) - .with_child(self.render_mode_button("Word", SearchOption::WholeWord, cx)) - .with_child(self.render_mode_button("Regex", SearchOption::Regex, cx)) + .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) .contained() - .with_style(theme.find.mode_button_group) + .with_style(theme.find.option_button_group) .aligned() .boxed(), ) @@ -185,9 +181,9 @@ impl FindBar { active_editor_subscription: None, active_match_index: None, editors_with_matches: Default::default(), - case_sensitive_mode: false, - whole_word_mode: false, - regex_mode: false, + case_sensitive: false, + whole_word: false, + regex: false, settings, pending_search: None, query_contains_error: false, @@ -204,27 +200,27 @@ impl FindBar { }); } - fn render_mode_button( + fn render_search_option( &self, icon: &str, - mode: SearchOption, + search_option: SearchOption, cx: &mut RenderContext, ) -> ElementBox { let theme = &self.settings.borrow().theme.find; - let is_active = self.is_mode_enabled(mode); - MouseEventHandler::new::(mode as usize, cx, |state, _| { + let is_active = self.is_search_option_enabled(search_option); + MouseEventHandler::new::(search_option as usize, cx, |state, _| { let style = match (is_active, state.hovered) { - (false, false) => &theme.mode_button, - (false, true) => &theme.hovered_mode_button, - (true, false) => &theme.active_mode_button, - (true, true) => &theme.active_hovered_mode_button, + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, }; Label::new(icon.to_string(), style.text.clone()) .contained() .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -239,9 +235,9 @@ impl FindBar { enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, _| { let style = if state.hovered { - &theme.hovered_mode_button + &theme.hovered_option_button } else { - &theme.mode_button + &theme.option_button }; Label::new(icon.to_string(), style.text.clone()) .contained() @@ -315,19 +311,23 @@ impl FindBar { } } - fn is_mode_enabled(&self, mode: SearchOption) -> bool { - match mode { - SearchOption::WholeWord => self.whole_word_mode, - SearchOption::CaseSensitive => self.case_sensitive_mode, - SearchOption::Regex => self.regex_mode, + fn is_search_option_enabled(&self, search_option: SearchOption) -> bool { + match search_option { + SearchOption::WholeWord => self.whole_word, + SearchOption::CaseSensitive => self.case_sensitive, + SearchOption::Regex => self.regex, } } - fn toggle_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { - let value = match mode { - SearchOption::WholeWord => &mut self.whole_word_mode, - SearchOption::CaseSensitive => &mut self.case_sensitive_mode, - SearchOption::Regex => &mut self.regex_mode, + fn toggle_search_option( + &mut self, + ToggleSearchOption(search_option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + let value = match search_option { + SearchOption::WholeWord => &mut self.whole_word, + SearchOption::CaseSensitive => &mut self.case_sensitive, + SearchOption::Regex => &mut self.regex, }; *value = !*value; self.update_matches(true, cx); @@ -436,56 +436,81 @@ impl FindBar { editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); } else { let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); - let case_sensitive = self.case_sensitive_mode; - let whole_word = self.whole_word_mode; - let ranges = if self.regex_mode { - cx.background() - .spawn(regex_search(buffer, query, case_sensitive, whole_word)) + let query = if self.regex { + match SearchQuery::regex(query, self.whole_word, self.case_sensitive) { + Ok(query) => query, + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return; + } + } } else { - cx.background().spawn(async move { - Ok(search(buffer, query, case_sensitive, whole_word).await) - }) + SearchQuery::text(query, self.whole_word, self.case_sensitive) }; + let ranges = cx.background().spawn(async move { + let mut ranges = Vec::new(); + if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { + ranges.extend( + query + .search(excerpt_buffer.as_rope()) + .await + .into_iter() + .map(|range| { + buffer.anchor_after(range.start) + ..buffer.anchor_before(range.end) + }), + ); + } else { + for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { + let excerpt_range = excerpt.range.to_offset(&excerpt.buffer); + let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone()); + ranges.extend(query.search(&rope).await.into_iter().map(|range| { + let start = excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(excerpt.id.clone(), start) + ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + })); + } + } + ranges + }); + let editor = editor.downgrade(); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - match ranges.await { - Ok(ranges) => { - if let Some(editor) = editor.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.editors_with_matches - .insert(editor.downgrade(), ranges.clone()); - this.update_match_index(cx); - if !this.dismissed { - editor.update(cx, |editor, cx| { - let theme = &this.settings.borrow().theme.find; - - if select_closest_match { - if let Some(match_ix) = this.active_match_index { - editor.select_ranges( - [ranges[match_ix].clone()], - Some(Autoscroll::Fit), - cx, - ); - } - } - - editor.highlight_ranges::( - ranges, - theme.match_background, + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let ranges = ranges.await; + if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) { + this.update(&mut cx, |this, cx| { + this.editors_with_matches + .insert(editor.downgrade(), ranges.clone()); + this.update_match_index(cx); + if !this.dismissed { + editor.update(cx, |editor, cx| { + let theme = &this.settings.borrow().theme.find; + + if select_closest_match { + if let Some(match_ix) = this.active_match_index { + editor.select_ranges( + [ranges[match_ix].clone()], + Some(Autoscroll::Fit), cx, ); - }); + } } + + editor.highlight_ranges::( + ranges, + theme.match_background, + cx, + ); }); } - } - Err(_) => { - this.update(&mut cx, |this, cx| { - this.query_contains_error = true; - cx.notify(); - }); - } + }); } })); } @@ -521,110 +546,6 @@ impl FindBar { } } -const YIELD_INTERVAL: usize = 20000; - -async fn search( - buffer: MultiBufferSnapshot, - query: String, - case_sensitive: bool, - whole_word: bool, -) -> Vec> { - let mut ranges = Vec::new(); - - let search = AhoCorasickBuilder::new() - .auto_configure(&[&query]) - .ascii_case_insensitive(!case_sensitive) - .build(&[&query]); - for (ix, mat) in search - .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) - .enumerate() - { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - let mat = mat.unwrap(); - - if whole_word { - let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap()); - let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { - continue; - } - } - - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); - } - - ranges -} - -async fn regex_search( - buffer: MultiBufferSnapshot, - mut query: String, - case_sensitive: bool, - whole_word: bool, -) -> Result>> { - if whole_word { - let mut word_query = String::new(); - word_query.push_str("\\b"); - word_query.push_str(&query); - word_query.push_str("\\b"); - query = word_query; - } - - let mut ranges = Vec::new(); - - if query.contains("\n") || query.contains("\\n") { - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .multi_line(true) - .build()?; - for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); - } - } else { - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .build()?; - - let mut line = String::new(); - let mut line_offset = 0; - for (chunk_ix, chunk) in buffer - .chunks(0..buffer.len(), false) - .map(|c| c.text) - .chain(["\n"]) - .enumerate() - { - if (chunk_ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - for mat in regex.find_iter(&line) { - let start = line_offset + mat.start(); - let end = line_offset + mat.end(); - ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end)); - } - - line_offset += line.len() + 1; - line.clear(); - } - line.push_str(text); - } - } - } - - Ok(ranges) -} - #[cfg(test)] mod tests { use super::*; @@ -687,7 +608,7 @@ mod tests { // Switch to a case sensitive search. find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchOption::CaseSensitive), cx); + find_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -744,7 +665,7 @@ mod tests { // Switch to a whole word search. find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchOption::WholeWord), cx); + find_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index f43aa3bc0784d2ccdd4fa28f7d94016bb6d7dbc5..5c84c233c563407f289d4d5b6271ddd42a7f002f 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,43 +1,45 @@ -use anyhow::Result; -use editor::{Editor, MultiBuffer}; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer}; use gpui::{ - action, elements::*, keymap::Binding, ElementBox, Entity, ModelContext, ModelHandle, - MutableAppContext, Task, View, ViewContext, ViewHandle, + action, elements::*, keymap::Binding, platform::CursorStyle, ElementBox, Entity, ModelContext, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, }; -use project::Project; -use workspace::Workspace; +use postage::watch; +use project::{search::SearchQuery, Project}; +use std::{any::TypeId, ops::Range}; +use workspace::{Settings, Workspace}; + +use crate::SearchOption; action!(Deploy); action!(Search); +action!(ToggleSearchOption, SearchOption); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ - Binding::new("cmd-shift-f", Deploy, None), + Binding::new("cmd-shift-F", Deploy, None), Binding::new("enter", Search, Some("ProjectFindView")), ]); cx.add_action(ProjectFindView::deploy); - cx.add_async_action(ProjectFindView::search); + cx.add_action(ProjectFindView::search); + cx.add_action(ProjectFindView::toggle_search_option); } struct ProjectFind { - last_search: SearchParams, project: ModelHandle, excerpts: ModelHandle, pending_search: Task>, -} - -#[derive(Default)] -struct SearchParams { - query: String, - regex: bool, - whole_word: bool, - case_sensitive: bool, + highlighted_ranges: Vec>, } struct ProjectFindView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, + case_sensitive: bool, + whole_word: bool, + regex: bool, + query_contains_error: bool, + settings: watch::Receiver, } impl Entity for ProjectFind { @@ -49,15 +51,39 @@ impl ProjectFind { let replica_id = project.read(cx).replica_id(); Self { project, - last_search: Default::default(), excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), pending_search: Task::ready(None), + highlighted_ranges: Default::default(), } } - fn search(&mut self, params: SearchParams, cx: &mut ModelContext) { - self.pending_search = cx.spawn_weak(|this, cx| async move { - // + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + let search = self + .project + .update(cx, |project, cx| project.search(query, cx)); + self.pending_search = cx.spawn_weak(|this, mut cx| async move { + let matches = search.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.highlighted_ranges.clear(); + let mut matches = matches.into_iter().collect::>(); + matches + .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + for (buffer, buffer_matches) in matches { + let ranges_to_highlight = excerpts.push_excerpts_with_context_lines( + buffer, + buffer_matches.clone(), + 1, + cx, + ); + this.highlighted_ranges.extend(ranges_to_highlight); + } + }); + cx.notify(); + }); + } None }); } @@ -74,6 +100,8 @@ impl workspace::Item for ProjectFind { ) -> Self::View { let settings = workspace.settings(); let excerpts = model.read(cx).excerpts.clone(); + cx.observe(&model, ProjectFindView::on_model_changed) + .detach(); ProjectFindView { model, query_editor: cx.add_view(|cx| { @@ -84,13 +112,20 @@ impl workspace::Item for ProjectFind { ) }), results_editor: cx.add_view(|cx| { - Editor::for_buffer( + let mut editor = Editor::for_buffer( excerpts, Some(workspace.project().clone()), settings.clone(), cx, - ) + ); + editor.set_nav_history(Some(nav_history)); + editor }), + case_sensitive: false, + whole_word: false, + regex: false, + query_contains_error: false, + settings, } } @@ -108,24 +143,43 @@ impl View for ProjectFindView { "ProjectFindView" } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { Flex::column() - .with_child(ChildView::new(&self.query_editor).boxed()) - .with_child(ChildView::new(&self.results_editor).boxed()) + .with_child(self.render_query_editor(cx)) + .with_child( + ChildView::new(&self.results_editor) + .flexible(1., true) + .boxed(), + ) .boxed() } } impl workspace::ItemView for ProjectFindView { - fn item_id(&self, cx: &gpui::AppContext) -> usize { + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &gpui::AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.into()) + } else if type_id == TypeId::of::() { + Some((&self.results_editor).into()) + } else { + None + } + } + + fn item_id(&self, _: &gpui::AppContext) -> usize { self.model.id() } - fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + fn tab_content(&self, style: &theme::Tab, _: &gpui::AppContext) -> ElementBox { Label::new("Project Find".to_string(), style.label.clone()).boxed() } - fn project_path(&self, cx: &gpui::AppContext) -> Option { + fn project_path(&self, _: &gpui::AppContext) -> Option { None } @@ -142,15 +196,15 @@ impl workspace::ItemView for ProjectFindView { .update(cx, |editor, cx| editor.save(project, cx)) } - fn can_save_as(&self, cx: &gpui::AppContext) -> bool { + fn can_save_as(&self, _: &gpui::AppContext) -> bool { false } fn save_as( &mut self, - project: ModelHandle, - abs_path: std::path::PathBuf, - cx: &mut ViewContext, + _: ModelHandle, + _: std::path::PathBuf, + _: &mut ViewContext, ) -> Task> { unreachable!("save_as should not have been called") } @@ -162,7 +216,116 @@ impl ProjectFindView { workspace.open_item(model, cx); } - fn search(&mut self, _: &Search, cx: &mut ViewContext) -> Option>> { - todo!() + fn search(&mut self, _: &Search, cx: &mut ViewContext) { + let text = self.query_editor.read(cx).text(cx); + let query = if self.regex { + match SearchQuery::regex(text, self.case_sensitive, self.whole_word) { + Ok(query) => query, + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return; + } + } + } else { + SearchQuery::text(text, self.case_sensitive, self.whole_word) + }; + + self.model.update(cx, |model, cx| model.search(query, cx)); + } + + fn toggle_search_option( + &mut self, + ToggleSearchOption(option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + let value = match option { + SearchOption::WholeWord => &mut self.whole_word, + SearchOption::CaseSensitive => &mut self.case_sensitive, + SearchOption::Regex => &mut self.regex, + }; + *value = !*value; + self.search(&Search, cx); + cx.notify(); + } + + fn on_model_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + let theme = &self.settings.borrow().theme.find; + self.results_editor.update(cx, |editor, cx| { + let model = self.model.read(cx); + editor.highlight_ranges::( + model.highlighted_ranges.clone(), + theme.match_background, + cx, + ); + editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + }); + } + + fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let editor_container = if self.query_contains_error { + theme.find.invalid_editor + } else { + theme.find.editor.input.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.find.editor.max_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.find.option_button_group) + .aligned() + .boxed(), + ) + .contained() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") + } + + fn render_option_button( + &self, + icon: &str, + option: SearchOption, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + let is_active = self.is_option_enabled(option); + MouseEventHandler::new::(option as usize, cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn is_option_enabled(&self, option: SearchOption) -> bool { + match option { + SearchOption::WholeWord => self.whole_word, + SearchOption::CaseSensitive => self.case_sensitive, + SearchOption::Regex => self.regex, + } } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 336ad737c36da5298e635cd15fd5a045f5e7a129..3d26da982718f1372e0b17eac0b90bb77fb148c1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -365,6 +365,14 @@ pub(crate) struct DiagnosticEndpoint { severity: DiagnosticSeverity, } +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] +pub enum CharKind { + Newline, + Punctuation, + Whitespace, + Word, +} + impl Buffer { pub fn new>>( replica_id: ReplicaId, @@ -2659,3 +2667,15 @@ pub fn contiguous_ranges( } }) } + +pub fn char_kind(c: char) -> CharKind { + if c == '\n' { + CharKind::Newline + } else if c.is_whitespace() { + CharKind::Whitespace + } else if c.is_alphanumeric() || c == '_' { + CharKind::Word + } else { + CharKind::Punctuation + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a78b6356b86d5d690ace9e6097ffe6d4b9b4846a..0be7c643e61c277ebd3446c6a80b3944a95137cf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,7 +1,7 @@ pub mod fs; mod ignore; mod lsp_command; -mod search; +pub mod search; pub mod worktree; use anyhow::{anyhow, Context, Result}; @@ -2175,12 +2175,7 @@ impl Project { let mut buffers_rx = buffers_rx.clone(); scope.spawn(async move { while let Some((buffer, snapshot)) = buffers_rx.next().await { - for range in query - .search( - snapshot.as_rope().bytes_in_range(0..snapshot.len()), - ) - .unwrap() - { + for range in query.search(snapshot.as_rope()).await { let range = snapshot.anchor_before(range.start) ..snapshot.anchor_after(range.end); worker_matched_buffers @@ -4893,7 +4888,7 @@ mod tests { .await; assert_eq!( - search(&project, SearchQuery::text("TWO"), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, false), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]) @@ -4911,7 +4906,7 @@ mod tests { }); assert_eq!( - search(&project, SearchQuery::text("TWO"), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, false), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]), diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 548a4c71dcfa56b5aba514a6578b2aeb8425d142..688374b8bb1d6c1b3b3e6aea1072466a7e8301f8 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,8 +1,9 @@ use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; use anyhow::Result; +use language::{char_kind, Rope}; use regex::{Regex, RegexBuilder}; +use smol::future::yield_now; use std::{ - borrow::Cow, io::{BufRead, BufReader, Read}, ops::Range, sync::Arc, @@ -10,28 +11,39 @@ use std::{ #[derive(Clone)] pub enum SearchQuery { - Text { search: Arc> }, - Regex { multiline: bool, regex: Regex }, + Text { + search: Arc>, + query: String, + whole_word: bool, + }, + Regex { + multiline: bool, + regex: Regex, + }, } impl SearchQuery { - pub fn text(query: &str) -> Self { + pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { + let query = query.to_string(); let search = AhoCorasickBuilder::new() - .auto_configure(&[query]) - .build(&[query]); + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); Self::Text { search: Arc::new(search), + query, + whole_word, } } - pub fn regex(query: &str, whole_word: bool, case_sensitive: bool) -> Result { - let mut query = Cow::Borrowed(query); + pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { + let mut query = query.to_string(); if whole_word { let mut word_query = String::new(); word_query.push_str("\\b"); word_query.push_str(&query); word_query.push_str("\\b"); - query = Cow::Owned(word_query); + query = word_query } let multiline = query.contains("\n") || query.contains("\\n"); @@ -44,7 +56,7 @@ impl SearchQuery { pub fn detect(&self, stream: T) -> Result { match self { - SearchQuery::Text { search } => { + SearchQuery::Text { search, .. } => { let mat = search.stream_find_iter(stream).next(); match mat { Some(Ok(_)) => Ok(true), @@ -74,35 +86,70 @@ impl SearchQuery { } } - pub fn search<'a, T: 'a + Read>(&'a self, stream: T) -> Result>> { + pub async fn search(&self, rope: &Rope) -> Vec> { + const YIELD_INTERVAL: usize = 20000; + let mut matches = Vec::new(); match self { - SearchQuery::Text { search } => { - for mat in search.stream_find_iter(stream) { - let mat = mat?; + SearchQuery::Text { + search, whole_word, .. + } => { + for (ix, mat) in search + .stream_find_iter(rope.bytes_in_range(0..rope.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + if *whole_word { + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } matches.push(mat.start()..mat.end()) } } SearchQuery::Regex { multiline, regex } => { - let mut reader = BufReader::new(stream); if *multiline { - let mut text = String::new(); - reader.read_to_string(&mut text)?; - matches.extend(regex.find_iter(&text).map(|mat| mat.start()..mat.end())); + let text = rope.to_string(); + for (ix, mat) in regex.find_iter(&text).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + matches.push(mat.start()..mat.end()); + } } else { - let mut line_ix = 0; - for line in reader.lines() { - let line = line?; - matches.extend( - regex - .find_iter(&line) - .map(|mat| (line_ix + mat.start())..(line_ix + mat.end())), - ); - line_ix += line.len(); + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + matches.push(start..end); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } } } } } - Ok(matches) + matches } } diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index cd474cc4da1b33991659f20d690702395c33c711..3d1cb28eb18bc0605a64cc40aa66b4618a2dcb2b 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -48,6 +48,12 @@ impl Rope { *self = new_rope; } + pub fn slice(&self, range: Range) -> Rope { + let mut cursor = self.cursor(0); + cursor.seek_forward(range.start); + cursor.slice(range.end) + } + pub fn push(&mut self, text: &str) { let mut new_chunks = SmallVec::<[_; 16]>::new(); let mut new_chunk = ArrayString::new(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 34640211f67b58e2f6bb6c3cae720582ef13ac35..483d3992762bd62368e0fe9bceb76e2472d1a2f1 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -100,11 +100,11 @@ pub struct Find { pub container: ContainerStyle, pub editor: FindEditor, pub invalid_editor: ContainerStyle, - pub mode_button_group: ContainerStyle, - pub mode_button: ContainedText, - pub active_mode_button: ContainedText, - pub hovered_mode_button: ContainedText, - pub active_hovered_mode_button: ContainedText, + pub option_button_group: ContainerStyle, + pub option_button: ContainedText, + pub active_option_button: ContainedText, + pub hovered_option_button: ContainedText, + pub active_hovered_option_button: ContainedText, pub match_background: Color, pub match_index: ContainedText, } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index de1344239a06bdd5e7aa2d95c331a9e11547b743..479b5773667c7f0117c78edd79245182ae0db21f 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -352,7 +352,7 @@ tab_summary_spacing = 10 match_background = "$state.highlighted_line" background = "$surface.1" -[find.mode_button] +[find.option_button] extends = "$text.1" padding = { left = 6, right = 6, top = 1, bottom = 1 } corner_radius = 6 @@ -361,19 +361,19 @@ border = { width = 1, color = "$border.0" } margin.left = 1 margin.right = 1 -[find.mode_button_group] +[find.option_button_group] padding = { left = 2, right = 2 } -[find.active_mode_button] -extends = "$find.mode_button" +[find.active_option_button] +extends = "$find.option_button" background = "$surface.2" -[find.hovered_mode_button] -extends = "$find.mode_button" +[find.hovered_option_button] +extends = "$find.option_button" background = "$surface.2" -[find.active_hovered_mode_button] -extends = "$find.mode_button" +[find.active_hovered_option_button] +extends = "$find.option_button" background = "$surface.2" [find.match_index] From 7aacb637623a8e8d3c8cab1f56c37d440e8a907e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 10:48:22 +0100 Subject: [PATCH 09/65] Respect field editor background, color and selection styling --- crates/editor/src/editor.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dcbbe76872ffa9871bb80742a0a308570e91f130..faf91143befc64295a50a4acb691c98659d5746b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5463,9 +5463,14 @@ fn build_style( get_field_editor_theme: Option, cx: &AppContext, ) -> EditorStyle { - let theme = settings.theme.editor.clone(); + let mut theme = settings.theme.editor.clone(); if let Some(get_field_editor_theme) = get_field_editor_theme { let field_editor_theme = get_field_editor_theme(&settings.theme); + if let Some(background) = field_editor_theme.container.background_color { + theme.background = background; + } + theme.text_color = field_editor_theme.text.color; + theme.selection = field_editor_theme.selection; EditorStyle { text: field_editor_theme.text, placeholder_text: field_editor_theme.placeholder_text, From a0772108733278ebf4c00c320adbe269550cabef Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 10:58:32 +0100 Subject: [PATCH 10/65] Focus query editor when deploying project-find --- crates/find/src/project_find.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 5c84c233c563407f289d4d5b6271ddd42a7f002f..bd541b731d705eaec6bb8f986069467b89384bea 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -153,6 +153,10 @@ impl View for ProjectFindView { ) .boxed() } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_editor); + } } impl workspace::ItemView for ProjectFindView { From 6a0cca7178b303e7d50237d31818bc933ca7cda4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 10:58:45 +0100 Subject: [PATCH 11/65] Add a fast path for when the search query is empty --- crates/project/src/search.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 688374b8bb1d6c1b3b3e6aea1072466a7e8301f8..dd2699238dcfc4866f8be3bb2d60494e84776bdc 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -55,6 +55,10 @@ impl SearchQuery { } pub fn detect(&self, stream: T) -> Result { + if self.as_str().is_empty() { + return Ok(false); + } + match self { SearchQuery::Text { search, .. } => { let mat = search.stream_find_iter(stream).next(); @@ -89,6 +93,10 @@ impl SearchQuery { pub async fn search(&self, rope: &Rope) -> Vec> { const YIELD_INTERVAL: usize = 20000; + if self.as_str().is_empty() { + return Default::default(); + } + let mut matches = Vec::new(); match self { SearchQuery::Text { @@ -152,4 +160,11 @@ impl SearchQuery { } matches } + + fn as_str(&self) -> &str { + match self { + SearchQuery::Text { query, .. } => query.as_str(), + SearchQuery::Regex { regex, .. } => regex.as_str(), + } + } } From 561123d6de106cd02014c39934bd3c074c923a8e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 11:49:33 +0100 Subject: [PATCH 12/65] Avoid extra `smol::channel` when iterating through snapshot paths --- crates/project/src/project.rs | 125 ++++++++++++++++++--------------- crates/project/src/worktree.rs | 8 +-- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0be7c643e61c277ebd3446c6a80b3944a95137cf..827c816fea49e9c256a502241bc3f73e5fd52023 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,6 +28,7 @@ use sha2::{Digest, Sha256}; use smol::block_on; use std::{ cell::RefCell, + cmp, convert::TryInto, hash::Hash, mem, @@ -2050,33 +2051,18 @@ impl Project { cx: &mut ModelContext, ) -> Task, Vec>>> { if self.is_local() { - let (paths_to_search_tx, paths_to_search_rx) = smol::channel::bounded(1024); - let snapshots = self .strong_worktrees(cx) .filter_map(|tree| { let tree = tree.read(cx).as_local()?; - Some((tree.abs_path().clone(), tree.snapshot())) + Some(tree.snapshot()) }) .collect::>(); - cx.background() - .spawn(async move { - for (snapshot_abs_path, snapshot) in snapshots { - for file in snapshot.files(false, 0) { - if paths_to_search_tx - .send((snapshot.id(), snapshot_abs_path.clone(), file.path.clone())) - .await - .is_err() - { - return; - } - } - } - }) - .detach(); + let background = cx.background().clone(); + let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + let workers = background.num_cpus().min(path_count); let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); - let workers = cx.background().num_cpus(); cx.background() .spawn({ let fs = self.fs.clone(); @@ -2086,41 +2072,64 @@ impl Project { let fs = &fs; let query = &query; let matching_paths_tx = &matching_paths_tx; + let paths_per_worker = (path_count + workers - 1) / workers; + let snapshots = &snapshots; background .scoped(|scope| { - for _ in 0..workers { - let mut paths_to_search_rx = paths_to_search_rx.clone(); + for worker_ix in 0..workers { + let worker_start_ix = worker_ix * paths_per_worker; + let worker_end_ix = worker_start_ix + paths_per_worker; scope.spawn(async move { - let mut path = PathBuf::new(); - while let Some(( - worktree_id, - snapshot_abs_path, - file_path, - )) = paths_to_search_rx.next().await - { - if matching_paths_tx.is_closed() { + let mut snapshot_start_ix = 0; + let mut abs_path = PathBuf::new(); + for snapshot in snapshots { + let snapshot_end_ix = + snapshot_start_ix + snapshot.visible_file_count(); + if worker_end_ix <= snapshot_start_ix { break; - } - - path.clear(); - path.push(&snapshot_abs_path); - path.push(&file_path); - let matches = if let Some(file) = - fs.open_sync(&path).await.log_err() - { - query.detect(file).unwrap_or(false) + } else if worker_start_ix > snapshot_end_ix { + snapshot_start_ix = snapshot_end_ix; + continue; } else { - false - }; - - if matches { - if matching_paths_tx - .send((worktree_id, file_path)) - .await - .is_err() + let start_in_snapshot = worker_start_ix + .saturating_sub(snapshot_start_ix); + let end_in_snapshot = + cmp::min(worker_end_ix, snapshot_end_ix) + - snapshot_start_ix; + + for entry in snapshot + .files(false, start_in_snapshot) + .take(end_in_snapshot - start_in_snapshot) { - break; + if matching_paths_tx.is_closed() { + break; + } + + abs_path.clear(); + abs_path.push(&snapshot.abs_path()); + abs_path.push(&entry.path); + let matches = if let Some(file) = + fs.open_sync(&abs_path).await.log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + }; + + if matches { + let project_path = + (snapshot.id(), entry.path.clone()); + if matching_paths_tx + .send(project_path) + .await + .is_err() + { + break; + } + } } + + snapshot_start_ix = snapshot_end_ix; } } }); @@ -2175,14 +2184,16 @@ impl Project { let mut buffers_rx = buffers_rx.clone(); scope.spawn(async move { while let Some((buffer, snapshot)) = buffers_rx.next().await { - for range in query.search(snapshot.as_rope()).await { - let range = snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end); - worker_matched_buffers - .entry(buffer.clone()) - .or_insert(Vec::new()) - .push(range); - } + let buffer_matches = query + .search(snapshot.as_rope()) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect(); + worker_matched_buffers.insert(buffer.clone(), buffer_matches); } }); } @@ -4888,7 +4899,7 @@ mod tests { .await; assert_eq!( - search(&project, SearchQuery::text("TWO", false, false), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, true), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]) @@ -4906,7 +4917,7 @@ mod tests { }); assert_eq!( - search(&project, SearchQuery::text("TWO", false, false), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, true), &mut cx).await, HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]), diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 795ca23c8f28cd732858b52c82d1ef2ac9615ac0..6455af3e53864caf647df14a3510424b71a7d96c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -554,10 +554,6 @@ impl LocalWorktree { Ok((tree, scan_states_tx)) } - pub fn abs_path(&self) -> &Arc { - &self.abs_path - } - pub fn contains_abs_path(&self, path: &Path) -> bool { path.starts_with(&self.abs_path) } @@ -1017,6 +1013,10 @@ impl Snapshot { } impl LocalSnapshot { + pub fn abs_path(&self) -> &Arc { + &self.abs_path + } + #[cfg(test)] pub(crate) fn to_proto( &self, From 5be93044f6cb68dac2674b37409fea2bbd83af42 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 12:17:25 +0100 Subject: [PATCH 13/65] Focus results editor when project find matches are updated --- crates/find/src/project_find.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index bd541b731d705eaec6bb8f986069467b89384bea..0743e2cce0af45d4e8327daab3ddeb3f722f7ae1 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -264,6 +264,7 @@ impl ProjectFindView { ); editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); }); + cx.focus(&self.results_editor); } fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { From 2147db9b41b26b9d7d274e6aead46a2cec24b952 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 12:29:50 +0100 Subject: [PATCH 14/65] Open searched buffers in parallel --- crates/project/src/project.rs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 827c816fea49e9c256a502241bc3f73e5fd52023..fdee1d2db7a2cad7253d1c6cfe6859c4dca460a3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2148,23 +2148,36 @@ impl Project { .values() .filter_map(|b| b.upgrade(cx)) .collect::>(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|this, cx| async move { for buffer in &open_buffers { let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); buffers_tx.send((buffer.clone(), snapshot)).await?; } + let open_buffers = Rc::new(RefCell::new(open_buffers)); while let Some(project_path) = matching_paths_rx.next().await { - if let Some(buffer) = this - .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) - .await - .log_err() - { - if !open_buffers.contains(&buffer) { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; - } + if buffers_tx.is_closed() { + break; } + + let this = this.clone(); + let open_buffers = open_buffers.clone(); + let buffers_tx = buffers_tx.clone(); + cx.spawn(|mut cx| async move { + if let Some(buffer) = this + .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) + .await + .log_err() + { + if open_buffers.borrow_mut().insert(buffer.clone()) { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx.send((buffer, snapshot)).await?; + } + } + + Ok::<_, anyhow::Error>(()) + }) + .detach(); } Ok::<_, anyhow::Error>(()) From 51c645f6b4037b9b53a5600ac7cd93d54f7bb679 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 05:04:45 -0700 Subject: [PATCH 15/65] Toggle focus between query editor and results on cmd-shift-F --- crates/find/src/project_find.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 0743e2cce0af45d4e8327daab3ddeb3f722f7ae1..ff34015d95b8142339e7aed67769f5483689ad74 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -13,15 +13,18 @@ use crate::SearchOption; action!(Deploy); action!(Search); action!(ToggleSearchOption, SearchOption); +action!(ToggleFocus); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ - Binding::new("cmd-shift-F", Deploy, None), + Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), + Binding::new("cmd-shift-F", Deploy, Some("Workspace")), Binding::new("enter", Search, Some("ProjectFindView")), ]); cx.add_action(ProjectFindView::deploy); cx.add_action(ProjectFindView::search); cx.add_action(ProjectFindView::toggle_search_option); + cx.add_action(ProjectFindView::toggle_focus); } struct ProjectFind { @@ -253,6 +256,14 @@ impl ProjectFindView { cx.notify(); } + fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { + if self.query_editor.is_focused(cx) { + cx.focus(&self.results_editor); + } else { + cx.focus(&self.query_editor); + } + } + fn on_model_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { let theme = &self.settings.borrow().theme.find; self.results_editor.update(cx, |editor, cx| { From f649074d36e5647a1e444998bb03a44a0a043fa1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 15:27:34 +0100 Subject: [PATCH 16/65] Refine project find's UX Co-Authored-By: Nathan Sobo --- crates/editor/src/multi_buffer.rs | 6 +++ crates/find/src/project_find.rs | 60 +++++++++++++++++++---------- crates/sum_tree/src/cursor.rs | 28 ++++++++------ crates/sum_tree/src/sum_tree.rs | 8 ++++ crates/theme/src/theme.rs | 1 + crates/zed/assets/themes/_base.toml | 1 + 6 files changed, 73 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index f447350ec564f5cfd38ccac6a85bb8132e44208d..0868a8799fc41aa08cb1bf080d0a6558e4a8e52b 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1175,6 +1175,12 @@ impl MultiBuffer { let mut buffers = Vec::new(); for _ in 0..mutation_count { + if rng.gen_bool(0.05) { + log::info!("Clearing multi-buffer"); + self.clear(cx); + continue; + } + let excerpt_ids = self .buffers .borrow() diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index ff34015d95b8142339e7aed67769f5483689ad74..99f5105a388f029da0433b1d9675df3d2326bc4b 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -30,7 +30,7 @@ pub fn init(cx: &mut MutableAppContext) { struct ProjectFind { project: ModelHandle, excerpts: ModelHandle, - pending_search: Task>, + pending_search: Option>>, highlighted_ranges: Vec>, } @@ -55,7 +55,7 @@ impl ProjectFind { Self { project, excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), - pending_search: Task::ready(None), + pending_search: None, highlighted_ranges: Default::default(), } } @@ -64,7 +64,8 @@ impl ProjectFind { let search = self .project .update(cx, |project, cx| project.search(query, cx)); - self.pending_search = cx.spawn_weak(|this, mut cx| async move { + self.highlighted_ranges.clear(); + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { let matches = search.await; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { @@ -84,11 +85,13 @@ impl ProjectFind { this.highlighted_ranges.extend(ranges_to_highlight); } }); + this.pending_search.take(); cx.notify(); }); } None - }); + })); + cx.notify(); } } @@ -147,13 +150,31 @@ impl View for ProjectFindView { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let model = &self.model.read(cx); + let results = if model.highlighted_ranges.is_empty() { + let theme = &self.settings.borrow().theme; + let text = if self.query_editor.read(cx).text(cx).is_empty() { + "" + } else if model.pending_search.is_some() { + "Searching..." + } else { + "No results" + }; + Label::new(text.to_string(), theme.find.results_status.clone()) + .aligned() + .contained() + .with_background_color(theme.editor.background) + .flexible(1., true) + .boxed() + } else { + ChildView::new(&self.results_editor) + .flexible(1., true) + .boxed() + }; + Flex::column() .with_child(self.render_query_editor(cx)) - .with_child( - ChildView::new(&self.results_editor) - .flexible(1., true) - .boxed(), - ) + .with_child(results) .boxed() } @@ -265,17 +286,16 @@ impl ProjectFindView { } fn on_model_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { - let theme = &self.settings.borrow().theme.find; - self.results_editor.update(cx, |editor, cx| { - let model = self.model.read(cx); - editor.highlight_ranges::( - model.highlighted_ranges.clone(), - theme.match_background, - cx, - ); - editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); - }); - cx.focus(&self.results_editor); + let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); + if !highlighted_ranges.is_empty() { + let theme = &self.settings.borrow().theme.find; + self.results_editor.update(cx, |editor, cx| { + editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); + editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + }); + cx.focus(&self.results_editor); + } + cx.notify(); } fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index cbb6f7f6f5270931d5cba49e754fd79e5f0defe2..fab2aa5251979da3d20a1e00caa7d0df17d75159 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -34,13 +34,13 @@ where stack: ArrayVec::new(), position: D::default(), did_seek: false, - at_end: false, + at_end: tree.is_empty(), } } fn reset(&mut self) { self.did_seek = false; - self.at_end = false; + self.at_end = self.tree.is_empty(); self.stack.truncate(0); self.position = D::default(); } @@ -139,7 +139,7 @@ where if self.at_end { self.position = D::default(); self.descend_to_last_item(self.tree, cx); - self.at_end = false; + self.at_end = self.tree.is_empty(); } else { while let Some(entry) = self.stack.pop() { if entry.index > 0 { @@ -195,13 +195,15 @@ where { let mut descend = false; - if self.stack.is_empty() && !self.at_end { - self.stack.push(StackEntry { - tree: self.tree, - index: 0, - position: D::default(), - }); - descend = true; + if self.stack.is_empty() { + if !self.at_end { + self.stack.push(StackEntry { + tree: self.tree, + index: 0, + position: D::default(), + }); + descend = true; + } self.did_seek = true; } @@ -279,6 +281,10 @@ where cx: &::Context, ) { self.did_seek = true; + if subtree.is_empty() { + return; + } + loop { match subtree.0.as_ref() { Node::Internal { @@ -298,7 +304,7 @@ where subtree = child_trees.last().unwrap(); } Node::Leaf { item_summaries, .. } => { - let last_index = item_summaries.len().saturating_sub(1); + let last_index = item_summaries.len() - 1; for item_summary in &item_summaries[0..last_index] { self.position.add_summary(item_summary, cx); } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index ea21672b10745445c6de403523c6fb3e4fdcb993..c372ffc6b07d532610cf9fc062c6434022c20fe3 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -821,6 +821,14 @@ mod tests { assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start().sum, 0); + cursor.prev(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.start().sum, 0); + cursor.next(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.start().sum, 0); // Single-element tree let mut tree = SumTree::::new(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 483d3992762bd62368e0fe9bceb76e2472d1a2f1..babd9f6e2001690c9342f1f4c268ddc7464cb503 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -107,6 +107,7 @@ pub struct Find { pub active_hovered_option_button: ContainedText, pub match_background: Color, pub match_index: ContainedText, + pub results_status: TextStyle, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 479b5773667c7f0117c78edd79245182ae0db21f..f7fd023f23a1d8e8fcc3f45e7e2f70115d972ce7 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -351,6 +351,7 @@ tab_summary_spacing = 10 [find] match_background = "$state.highlighted_line" background = "$surface.1" +results_status = { extends = "$text.0", size = 18 } [find.option_button] extends = "$text.1" From ff0fa0e0bdfb1e37fbba673a52155f682f8ec56c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 15:36:16 +0100 Subject: [PATCH 17/65] Gracefully handle passing an empty set of ranges to `push_excerpts` --- crates/editor/src/multi_buffer.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 0868a8799fc41aa08cb1bf080d0a6558e4a8e52b..57c7b380676068769bdaebbe0ecd1199f04eeb06 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -687,6 +687,11 @@ impl MultiBuffer { O: text::ToOffset, { assert_eq!(self.history.transaction_depth, 0); + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_none() { + return Default::default(); + } + self.sync(cx); let buffer_id = buffer.id(); @@ -715,7 +720,7 @@ impl MultiBuffer { let edit_start = new_excerpts.summary().text.bytes; new_excerpts.update_last( |excerpt| { - excerpt.has_trailing_newline = true; + excerpt.has_trailing_newline = ranges.peek().is_some(); prev_id = excerpt.id.clone(); }, &(), @@ -727,7 +732,6 @@ impl MultiBuffer { } let mut ids = Vec::new(); - let mut ranges = ranges.into_iter().peekable(); while let Some(range) = ranges.next() { let id = ExcerptId::between(&prev_id, &next_id); if let Err(ix) = buffer_state.excerpts.binary_search(&id) { @@ -1210,16 +1214,26 @@ impl MultiBuffer { }; let buffer = buffer_handle.read(cx); - let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); - let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let buffer_text = buffer.text(); + let ranges = (0..rng.gen_range(0..5)) + .map(|_| { + let end_ix = + buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); + let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + start_ix..end_ix + }) + .collect::>(); log::info!( - "Inserting excerpt from buffer {} and range {:?}: {:?}", + "Inserting excerpts from buffer {} and ranges {:?}: {:?}", buffer_handle.id(), - start_ix..end_ix, - &buffer.text()[start_ix..end_ix] + ranges, + ranges + .iter() + .map(|range| &buffer_text[range.clone()]) + .collect::>() ); - let excerpt_id = self.push_excerpts(buffer_handle.clone(), [start_ix..end_ix], cx); + let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); log::info!("Inserted with id: {:?}", excerpt_id); } else { let remove_count = rng.gen_range(1..=excerpt_ids.len()); From 2611b5449ffa50da71458698869840bcb6914a56 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 15:36:43 +0100 Subject: [PATCH 18/65] Always `sync` before clearing or removing excerpts from `MultiBuffer` We don't have any test that proves this is needed but seems good nonetheless. --- crates/editor/src/multi_buffer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 57c7b380676068769bdaebbe0ecd1199f04eeb06..42f4d72f52c6e14335b9bf6e260055a9a6325373 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -772,6 +772,7 @@ impl MultiBuffer { } pub fn clear(&mut self, cx: &mut ModelContext) { + self.sync(cx); self.buffers.borrow_mut().clear(); let mut snapshot = self.snapshot.borrow_mut(); let prev_len = snapshot.len(); @@ -853,6 +854,7 @@ impl MultiBuffer { excerpt_ids: impl IntoIterator, cx: &mut ModelContext, ) { + self.sync(cx); let mut buffers = self.buffers.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut(); let mut new_excerpts = SumTree::new(); From 29e035a70d4b1daa407bef933657c8beb4341c4f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 15:40:19 +0100 Subject: [PATCH 19/65] Don't report a buffer when it doesn't contain any matches --- crates/project/src/project.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fdee1d2db7a2cad7253d1c6cfe6859c4dca460a3..641d89fa29515800b05f1f2cd1314292124727f6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2205,8 +2205,11 @@ impl Project { snapshot.anchor_before(range.start) ..snapshot.anchor_after(range.end) }) - .collect(); - worker_matched_buffers.insert(buffer.clone(), buffer_matches); + .collect::>(); + if !buffer_matches.is_empty() { + worker_matched_buffers + .insert(buffer.clone(), buffer_matches); + } } }); } From 88bfe5acb05a17aab797ef7a45218ea2455b7af6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 16:20:02 +0100 Subject: [PATCH 20/65] Allow splitting project find and maintain the searches in sync --- crates/find/src/project_find.rs | 71 ++++++++++++++++++++++++++++----- crates/project/src/search.rs | 56 +++++++++++++++++++++----- 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 99f5105a388f029da0433b1d9675df3d2326bc4b..37cda8a7cb08f5d25a114e3cc5ccc0c592abf4ac 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -30,6 +30,7 @@ pub fn init(cx: &mut MutableAppContext) { struct ProjectFind { project: ModelHandle, excerpts: ModelHandle, + query: Option, pending_search: Option>>, highlighted_ranges: Vec>, } @@ -55,7 +56,8 @@ impl ProjectFind { Self { project, excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), - pending_search: None, + query: Default::default(), + pending_search: Default::default(), highlighted_ranges: Default::default(), } } @@ -63,7 +65,8 @@ impl ProjectFind { fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { let search = self .project - .update(cx, |project, cx| project.search(query, cx)); + .update(cx, |project, cx| project.search(query.clone(), cx)); + self.query = Some(query.clone()); self.highlighted_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { let matches = search.await; @@ -106,7 +109,7 @@ impl workspace::Item for ProjectFind { ) -> Self::View { let settings = workspace.settings(); let excerpts = model.read(cx).excerpts.clone(); - cx.observe(&model, ProjectFindView::on_model_changed) + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) .detach(); ProjectFindView { model, @@ -236,6 +239,36 @@ impl workspace::ItemView for ProjectFindView { ) -> Task> { unreachable!("save_as should not have been called") } + + fn clone_on_split(&self, cx: &mut ViewContext) -> Option + where + Self: Sized, + { + let query_editor = cx.add_view(|cx| { + Editor::single_line( + self.settings.clone(), + Some(|theme| theme.find.editor.input.clone()), + cx, + ) + }); + let results_editor = self.results_editor.update(cx, |editor, cx| { + cx.add_view(|cx| editor.clone_on_split(cx).unwrap()) + }); + cx.observe(&self.model, |this, _, cx| this.model_changed(true, cx)) + .detach(); + let mut view = Self { + model: self.model.clone(), + query_editor, + results_editor, + case_sensitive: self.case_sensitive, + whole_word: self.whole_word, + regex: self.regex, + query_contains_error: self.query_contains_error, + settings: self.settings.clone(), + }; + view.model_changed(false, cx); + Some(view) + } } impl ProjectFindView { @@ -247,7 +280,7 @@ impl ProjectFindView { fn search(&mut self, _: &Search, cx: &mut ViewContext) { let text = self.query_editor.read(cx).text(cx); let query = if self.regex { - match SearchQuery::regex(text, self.case_sensitive, self.whole_word) { + match SearchQuery::regex(text, self.whole_word, self.case_sensitive) { Ok(query) => query, Err(_) => { self.query_contains_error = true; @@ -256,7 +289,7 @@ impl ProjectFindView { } } } else { - SearchQuery::text(text, self.case_sensitive, self.whole_word) + SearchQuery::text(text, self.whole_word, self.case_sensitive) }; self.model.update(cx, |model, cx| model.search(query, cx)); @@ -285,16 +318,36 @@ impl ProjectFindView { } } - fn on_model_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { - let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); + fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { + let model = self.model.read(cx); + let highlighted_ranges = model.highlighted_ranges.clone(); + if let Some(query) = model.query.clone() { + self.case_sensitive = query.case_sensitive(); + self.whole_word = query.whole_word(); + self.regex = query.is_regex(); + self.query_editor.update(cx, |query_editor, cx| { + if query_editor.text(cx) != query.as_str() { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.read(cx).len(); + query_buffer.edit([0..len], query.as_str(), cx); + }); + } + }); + } + if !highlighted_ranges.is_empty() { let theme = &self.settings.borrow().theme.find; self.results_editor.update(cx, |editor, cx| { editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); - editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + if reset_selections { + editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + } }); - cx.focus(&self.results_editor); + if self.query_editor.is_focused(cx) { + cx.focus(&self.results_editor); + } } + cx.notify(); } diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index dd2699238dcfc4866f8be3bb2d60494e84776bdc..4f89c68aa23e1969ddc33f021366eb9dce772c4b 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -13,12 +13,16 @@ use std::{ pub enum SearchQuery { Text { search: Arc>, - query: String, + query: Arc, whole_word: bool, + case_sensitive: bool, }, Regex { - multiline: bool, regex: Regex, + query: Arc, + multiline: bool, + whole_word: bool, + case_sensitive: bool, }, } @@ -31,13 +35,15 @@ impl SearchQuery { .build(&[&query]); Self::Text { search: Arc::new(search), - query, + query: Arc::from(query), whole_word, + case_sensitive, } } pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { let mut query = query.to_string(); + let initial_query = Arc::from(query.as_str()); if whole_word { let mut word_query = String::new(); word_query.push_str("\\b"); @@ -51,7 +57,13 @@ impl SearchQuery { .case_insensitive(!case_sensitive) .multi_line(multiline) .build()?; - Ok(Self::Regex { multiline, regex }) + Ok(Self::Regex { + regex, + query: initial_query, + multiline, + whole_word, + case_sensitive, + }) } pub fn detect(&self, stream: T) -> Result { @@ -60,7 +72,7 @@ impl SearchQuery { } match self { - SearchQuery::Text { search, .. } => { + Self::Text { search, .. } => { let mat = search.stream_find_iter(stream).next(); match mat { Some(Ok(_)) => Ok(true), @@ -68,7 +80,9 @@ impl SearchQuery { None => Ok(false), } } - SearchQuery::Regex { multiline, regex } => { + Self::Regex { + regex, multiline, .. + } => { let mut reader = BufReader::new(stream); if *multiline { let mut text = String::new(); @@ -99,7 +113,7 @@ impl SearchQuery { let mut matches = Vec::new(); match self { - SearchQuery::Text { + Self::Text { search, whole_word, .. } => { for (ix, mat) in search @@ -123,7 +137,9 @@ impl SearchQuery { matches.push(mat.start()..mat.end()) } } - SearchQuery::Regex { multiline, regex } => { + Self::Regex { + regex, multiline, .. + } => { if *multiline { let text = rope.to_string(); for (ix, mat) in regex.find_iter(&text).enumerate() { @@ -161,10 +177,28 @@ impl SearchQuery { matches } - fn as_str(&self) -> &str { + pub fn as_str(&self) -> &str { + match self { + Self::Text { query, .. } => query.as_ref(), + Self::Regex { query, .. } => query.as_ref(), + } + } + + pub fn whole_word(&self) -> bool { + match self { + Self::Text { whole_word, .. } => *whole_word, + Self::Regex { whole_word, .. } => *whole_word, + } + } + + pub fn case_sensitive(&self) -> bool { match self { - SearchQuery::Text { query, .. } => query.as_str(), - SearchQuery::Regex { regex, .. } => regex.as_str(), + Self::Text { case_sensitive, .. } => *case_sensitive, + Self::Regex { case_sensitive, .. } => *case_sensitive, } } + + pub fn is_regex(&self) -> bool { + matches!(self, Self::Regex { .. }) + } } From b506db7c9385ce6b0ed3f15399fee92fde4026a2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 17:22:30 +0100 Subject: [PATCH 21/65] Use the new split pane's navigation history when cloning an item --- crates/diagnostics/src/diagnostics.rs | 15 ++++----- crates/editor/src/editor.rs | 7 ++-- crates/editor/src/items.rs | 8 +++-- crates/find/src/project_find.rs | 46 +++++++++++++++++++-------- crates/workspace/src/pane.rs | 4 +++ crates/workspace/src/workspace.rs | 21 +++++++++--- 6 files changed, 67 insertions(+), 34 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 198a2be493e0b038fc47fe797c54c19ac583aaf8..6445c8b971be017fa658baf5be47c835fad42a39 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -598,7 +598,11 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged) } - fn clone_on_split(&self, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option where Self: Sized, { @@ -608,13 +612,8 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { self.settings.clone(), cx, ); - diagnostics.editor.update(cx, |editor, cx| { - let nav_history = self - .editor - .read(cx) - .nav_history() - .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle())); - editor.set_nav_history(nav_history); + diagnostics.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); }); Some(diagnostics) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index faf91143befc64295a50a4acb691c98659d5746b..a5d2edc6121a078583238c2efcab123ce173ee3e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -834,7 +834,7 @@ impl Editor { Self::new(EditorMode::Full, buffer, project, settings, None, cx) } - pub fn clone(&self, cx: &mut ViewContext) -> Self { + pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { let mut clone = Self::new( self.mode, self.buffer.clone(), @@ -845,10 +845,7 @@ impl Editor { ); clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); - clone.nav_history = self - .nav_history - .as_ref() - .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle())); + clone.nav_history = Some(nav_history); clone } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1d89bd567fae24a05dd2b13ec66934cf444f3262..e7583d428010e617145e8b7624b201df0d0b2748 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -194,11 +194,15 @@ impl ItemView for Editor { }) } - fn clone_on_split(&self, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option where Self: Sized, { - Some(self.clone(cx)) + Some(self.clone(nav_history, cx)) } fn deactivated(&mut self, cx: &mut ViewContext) { diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 37cda8a7cb08f5d25a114e3cc5ccc0c592abf4ac..24db15c664fcb8c5aba886a6c18769ebd324ca2a 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,14 +1,18 @@ +use crate::SearchOption; use editor::{Anchor, Autoscroll, Editor, MultiBuffer}; use gpui::{ - action, elements::*, keymap::Binding, platform::CursorStyle, ElementBox, Entity, ModelContext, - ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, + ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, }; use postage::watch; use project::{search::SearchQuery, Project}; -use std::{any::TypeId, ops::Range}; -use workspace::{Settings, Workspace}; - -use crate::SearchOption; +use std::{ + any::{Any, TypeId}, + ops::Range, + path::PathBuf, +}; +use workspace::{Item, ItemNavHistory, ItemView, Settings, Workspace}; action!(Deploy); action!(Search); @@ -98,13 +102,13 @@ impl ProjectFind { } } -impl workspace::Item for ProjectFind { +impl Item for ProjectFind { type View = ProjectFindView; fn build_view( model: ModelHandle, - workspace: &workspace::Workspace, - nav_history: workspace::ItemNavHistory, + workspace: &Workspace, + nav_history: ItemNavHistory, cx: &mut gpui::ViewContext, ) -> Self::View { let settings = workspace.settings(); @@ -186,7 +190,7 @@ impl View for ProjectFindView { } } -impl workspace::ItemView for ProjectFindView { +impl ItemView for ProjectFindView { fn act_as_type( &self, type_id: TypeId, @@ -202,6 +206,11 @@ impl workspace::ItemView for ProjectFindView { } } + fn deactivated(&mut self, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.deactivated(cx)); + } + fn item_id(&self, _: &gpui::AppContext) -> usize { self.model.id() } @@ -234,13 +243,17 @@ impl workspace::ItemView for ProjectFindView { fn save_as( &mut self, _: ModelHandle, - _: std::path::PathBuf, + _: PathBuf, _: &mut ViewContext, ) -> Task> { unreachable!("save_as should not have been called") } - fn clone_on_split(&self, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option where Self: Sized, { @@ -251,8 +264,8 @@ impl workspace::ItemView for ProjectFindView { cx, ) }); - let results_editor = self.results_editor.update(cx, |editor, cx| { - cx.add_view(|cx| editor.clone_on_split(cx).unwrap()) + let results_editor = self.results_editor.update(cx, |results_editor, cx| { + cx.add_view(|cx| results_editor.clone(nav_history, cx)) }); cx.observe(&self.model, |this, _, cx| this.model_changed(true, cx)) .detach(); @@ -269,6 +282,11 @@ impl workspace::ItemView for ProjectFindView { view.model_changed(false, cx); Some(view) } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.navigate(data, cx)); + } } impl ProjectFindView { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c1f34c94c7bd37e030e0ee67d500bb92ad7ebf1e..dbf677e9a346365c041473d44fc7715e93113b6c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -149,6 +149,10 @@ impl Pane { } } + pub(crate) fn nav_history(&self) -> &Rc> { + &self.nav_history + } + pub fn activate(&self, cx: &mut ViewContext) { cx.emit(Event::Activate); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c7368ddbe96da439aea8443536e1dc64ff095d68..7783f360d1532c42dada73588a7590559a5a13c0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -156,7 +156,7 @@ pub trait ItemView: View { fn item_id(&self, cx: &AppContext) -> usize; fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn clone_on_split(&self, _: &mut ViewContext) -> Option + fn clone_on_split(&self, _: ItemNavHistory, _: &mut ViewContext) -> Option where Self: Sized, { @@ -229,7 +229,11 @@ pub trait ItemViewHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; - fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; + fn clone_on_split( + &self, + nav_history: Rc>, + cx: &mut MutableAppContext, + ) -> Option>; fn added_to_pane(&mut self, cx: &mut ViewContext); fn deactivated(&self, cx: &mut MutableAppContext); fn navigate(&self, data: Box, cx: &mut MutableAppContext); @@ -373,9 +377,15 @@ impl ItemViewHandle for ViewHandle { Box::new(self.clone()) } - fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option> { + fn clone_on_split( + &self, + nav_history: Rc>, + cx: &mut MutableAppContext, + ) -> Option> { self.update(cx, |item, cx| { - cx.add_option_view(|cx| item.clone_on_split(cx)) + cx.add_option_view(|cx| { + item.clone_on_split(ItemNavHistory::new(nav_history, &cx.handle()), cx) + }) }) .map(|handle| Box::new(handle) as Box) } @@ -1047,7 +1057,8 @@ impl Workspace { let new_pane = self.add_pane(cx); self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { - if let Some(clone) = item.clone_on_split(cx.as_mut()) { + let nav_history = new_pane.read(cx).nav_history().clone(); + if let Some(clone) = item.clone_on_split(nav_history, cx.as_mut()) { new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } From 1e04411066209618ca4bce5081403c2f01901ac3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 17:23:03 +0100 Subject: [PATCH 22/65] Don't focus query editor if there are matches on tab switch --- crates/find/src/project_find.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 24db15c664fcb8c5aba886a6c18769ebd324ca2a..7bd9ee278411d3ef0c1f0db69b16aed6a2e716c5 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -186,7 +186,11 @@ impl View for ProjectFindView { } fn on_focus(&mut self, cx: &mut ViewContext) { - cx.focus(&self.query_editor); + if self.model.read(cx).highlighted_ranges.is_empty() { + cx.focus(&self.query_editor); + } else { + cx.focus(&self.results_editor); + } } } @@ -227,6 +231,14 @@ impl ItemView for ProjectFindView { true } + fn is_dirty(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).has_conflict(cx) + } + fn save( &mut self, project: ModelHandle, From 7123407f42d0f0feff22103110366193bf347ce7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 18:10:48 +0100 Subject: [PATCH 23/65] Don't share query editor state after project find has been split Co-Authored-By: Nathan Sobo --- crates/editor/src/multi_buffer.rs | 32 ++++++++++++++++ crates/find/src/project_find.rs | 64 ++++++++++++++++++------------- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 42f4d72f52c6e14335b9bf6e260055a9a6325373..c5d38bed6a68a5ebc8a42eb223831b5e76528d4b 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -43,6 +43,7 @@ pub struct MultiBuffer { title: Option, } +#[derive(Clone)] struct History { next_transaction_id: TransactionId, undo_stack: Vec, @@ -168,6 +169,37 @@ impl MultiBuffer { } } + pub fn clone(&self, new_cx: &mut ModelContext) -> Self { + let mut buffers = HashMap::default(); + for (buffer_id, buffer_state) in self.buffers.borrow().iter() { + buffers.insert( + *buffer_id, + BufferState { + buffer: buffer_state.buffer.clone(), + last_version: buffer_state.last_version.clone(), + last_parse_count: buffer_state.last_parse_count, + last_selections_update_count: buffer_state.last_selections_update_count, + last_diagnostics_update_count: buffer_state.last_diagnostics_update_count, + last_file_update_count: buffer_state.last_file_update_count, + excerpts: buffer_state.excerpts.clone(), + _subscriptions: [ + new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), + new_cx.subscribe(&buffer_state.buffer, Self::on_buffer_event), + ], + }, + ); + } + Self { + snapshot: RefCell::new(self.snapshot.borrow().clone()), + buffers: RefCell::new(buffers), + subscriptions: Default::default(), + singleton: self.singleton, + replica_id: self.replica_id, + history: self.history.clone(), + title: self.title.clone(), + } + } + pub fn with_title(mut self, title: String) -> Self { self.title = Some(title); self diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 7bd9ee278411d3ef0c1f0db69b16aed6a2e716c5..8c9c31ac12f98a906e325172be5a3187602dec6c 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -34,7 +34,6 @@ pub fn init(cx: &mut MutableAppContext) { struct ProjectFind { project: ModelHandle, excerpts: ModelHandle, - query: Option, pending_search: Option>>, highlighted_ranges: Vec>, } @@ -60,17 +59,26 @@ impl ProjectFind { Self { project, excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), - query: Default::default(), pending_search: Default::default(), highlighted_ranges: Default::default(), } } + fn clone(&self, new_cx: &mut ModelContext) -> Self { + Self { + project: self.project.clone(), + excerpts: self + .excerpts + .update(new_cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + pending_search: Default::default(), + highlighted_ranges: self.highlighted_ranges.clone(), + } + } + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { let search = self .project .update(cx, |project, cx| project.search(query.clone(), cx)); - self.query = Some(query.clone()); self.highlighted_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { let matches = search.await; @@ -270,19 +278,38 @@ impl ItemView for ProjectFindView { Self: Sized, { let query_editor = cx.add_view(|cx| { - Editor::single_line( + let query = self.query_editor.read(cx).text(cx); + let editor = Editor::single_line( self.settings.clone(), Some(|theme| theme.find.editor.input.clone()), cx, - ) + ); + editor + .buffer() + .update(cx, |buffer, cx| buffer.edit([0..0], query, cx)); + editor }); - let results_editor = self.results_editor.update(cx, |results_editor, cx| { - cx.add_view(|cx| results_editor.clone(nav_history, cx)) - }); - cx.observe(&self.model, |this, _, cx| this.model_changed(true, cx)) + let model = self + .model + .update(cx, |model, cx| cx.add_model(|cx| model.clone(cx))); + + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) .detach(); + let results_editor = cx.add_view(|cx| { + let model = model.read(cx); + let excerpts = model.excerpts.clone(); + let project = model.project.clone(); + let scroll_position = self + .results_editor + .update(cx, |editor, cx| editor.scroll_position(cx)); + + let mut editor = Editor::for_buffer(excerpts, Some(project), self.settings.clone(), cx); + editor.set_nav_history(Some(nav_history)); + editor.set_scroll_position(scroll_position, cx); + editor + }); let mut view = Self { - model: self.model.clone(), + model, query_editor, results_editor, case_sensitive: self.case_sensitive, @@ -349,22 +376,7 @@ impl ProjectFindView { } fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { - let model = self.model.read(cx); - let highlighted_ranges = model.highlighted_ranges.clone(); - if let Some(query) = model.query.clone() { - self.case_sensitive = query.case_sensitive(); - self.whole_word = query.whole_word(); - self.regex = query.is_regex(); - self.query_editor.update(cx, |query_editor, cx| { - if query_editor.text(cx) != query.as_str() { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.read(cx).len(); - query_buffer.edit([0..len], query.as_str(), cx); - }); - } - }); - } - + let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); if !highlighted_ranges.is_empty() { let theme = &self.settings.borrow().theme.find; self.results_editor.update(cx, |editor, cx| { From e278c423d3b53dd1ddd3d19c03c04ae4835eda2c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 18:30:04 +0100 Subject: [PATCH 24/65] Don't assume that cloning on split will reuse the same underlying model Co-Authored-By: Max Brunsfeld --- crates/diagnostics/src/diagnostics.rs | 6 +++--- crates/editor/src/items.rs | 6 +++--- crates/find/src/project_find.rs | 6 +++--- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 9 +++++---- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 6445c8b971be017fa658baf5be47c835fad42a39..e668ce326f126bff33d47360de3c8ad4e74dabb5 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -28,7 +28,7 @@ use std::{ sync::Arc, }; use util::TryFutureExt; -use workspace::{ItemNavHistory, ItemViewHandle as _, Workspace}; +use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Workspace}; action!(Deploy); action!(OpenExcerpts); @@ -536,8 +536,8 @@ impl workspace::Item for ProjectDiagnostics { } impl workspace::ItemView for ProjectDiagnosticsEditor { - fn item_id(&self, _: &AppContext) -> usize { - self.model.id() + fn item(&self, _: &AppContext) -> Box { + Box::new(self.model.clone()) } fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index e7583d428010e617145e8b7624b201df0d0b2748..dedd712867d650bde881733ad05cce76bb46234b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -158,11 +158,11 @@ impl WeakItemHandle for WeakMultiBufferItemHandle { } impl ItemView for Editor { - fn item_id(&self, cx: &AppContext) -> usize { + fn item(&self, cx: &AppContext) -> Box { if let Some(buffer) = self.buffer.read(cx).as_singleton() { - buffer.id() + Box::new(BufferItemHandle(buffer)) } else { - self.buffer.id() + Box::new(MultiBufferItemHandle(self.buffer.clone())) } } diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 8c9c31ac12f98a906e325172be5a3187602dec6c..55d25b9dbbe14a0e054ac89d146139ec0345dd07 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -12,7 +12,7 @@ use std::{ ops::Range, path::PathBuf, }; -use workspace::{Item, ItemNavHistory, ItemView, Settings, Workspace}; +use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; action!(Deploy); action!(Search); @@ -223,8 +223,8 @@ impl ItemView for ProjectFindView { .update(cx, |editor, cx| editor.deactivated(cx)); } - fn item_id(&self, _: &gpui::AppContext) -> usize { - self.model.id() + fn item(&self, _: &gpui::AppContext) -> Box { + Box::new(self.model.clone()) } fn tab_content(&self, style: &theme::Tab, _: &gpui::AppContext) -> ElementBox { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index dbf677e9a346365c041473d44fc7715e93113b6c..beafd0eb8c7de83e529763264fec129aa8cd5894 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -283,7 +283,7 @@ impl Pane { item_view.added_to_pane(cx); let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len()); self.item_views - .insert(item_idx, (item_view.item_id(cx), item_view)); + .insert(item_idx, (item_view.item(cx).id(), item_view)); self.activate_item(item_idx, cx); cx.notify(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7783f360d1532c42dada73588a7590559a5a13c0..bff2d2f94616f7120a83dc8d8b061b675c7dc161 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -153,7 +153,7 @@ pub trait Item: Entity + Sized { pub trait ItemView: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} - fn item_id(&self, cx: &AppContext) -> usize; + fn item(&self, cx: &AppContext) -> Box; fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn clone_on_split(&self, _: ItemNavHistory, _: &mut ViewContext) -> Option @@ -225,7 +225,7 @@ pub trait WeakItemHandle { } pub trait ItemViewHandle: 'static { - fn item_id(&self, cx: &AppContext) -> usize; + fn item(&self, cx: &AppContext) -> Box; fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; @@ -361,8 +361,8 @@ impl dyn ItemViewHandle { } impl ItemViewHandle for ViewHandle { - fn item_id(&self, cx: &AppContext) -> usize { - self.read(cx).item_id(cx) + fn item(&self, cx: &AppContext) -> Box { + self.read(cx).item(cx) } fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { @@ -1059,6 +1059,7 @@ impl Workspace { if let Some(item) = pane.read(cx).active_item() { let nav_history = new_pane.read(cx).nav_history().clone(); if let Some(clone) = item.clone_on_split(nav_history, cx.as_mut()) { + self.items.insert(clone.item(cx).downgrade()); new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } From 368301fcecbc000e362ad2785ccfb01bf94d4175 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Feb 2022 18:30:28 +0100 Subject: [PATCH 25/65] Reuse a previous project find whenever possible Co-Authored-By: Max Brunsfeld --- crates/find/src/project_find.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 55d25b9dbbe14a0e054ac89d146139ec0345dd07..196cde33a14e7a1670d4e8475dbea1805f673e73 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -330,8 +330,12 @@ impl ItemView for ProjectFindView { impl ProjectFindView { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); - workspace.open_item(model, cx); + if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_item(&existing, cx); + } else { + let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); + workspace.open_item(model, cx); + } } fn search(&mut self, _: &Search, cx: &mut ViewContext) { From 9a97588f7940f34eb1abbbda0433b9af074686c6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Feb 2022 10:04:57 -0800 Subject: [PATCH 26/65] Eliminate RwLock around LanguageServer's outbound message channel We observed a deadlock when quitting zed. The main thread was attempting to acquire a write lock to this outbound message sender. We weren't able to understand exactly how this occurred, but we removed the use of a lock there, so this shouldn't happen anymore. Co-Authored-By: Antonio Scandurra --- crates/lsp/src/lsp.rs | 67 +++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index c563c233118fb106299c7d57606abf78e8d3e8ac..2e2efdb28c0da5462184f11851ebeb71537c91c1 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -33,7 +33,7 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, - outbound_tx: RwLock>>>, + outbound_tx: channel::Sender>, capabilities: watch::Receiver>, notification_handlers: Arc>>, response_handlers: Arc>>, @@ -213,7 +213,7 @@ impl LanguageServer { response_handlers, capabilities: capabilities_rx, next_id: Default::default(), - outbound_tx: RwLock::new(Some(outbound_tx)), + outbound_tx, executor: executor.clone(), io_tasks: Mutex::new(Some((input_task, output_task))), initialized: initialized_rx, @@ -296,37 +296,41 @@ impl LanguageServer { let request = Self::request_internal::( &this.next_id, &this.response_handlers, - this.outbound_tx.read().as_ref(), + &this.outbound_tx, params, ); let response = request.await?; Self::notify_internal::( - this.outbound_tx.read().as_ref(), + &this.outbound_tx, InitializedParams {}, )?; Ok(response.capabilities) } - pub fn shutdown(&self) -> Option>> { + pub fn shutdown(&self) -> Option>> { if let Some(tasks) = self.io_tasks.lock().take() { let response_handlers = self.response_handlers.clone(); - let outbound_tx = self.outbound_tx.write().take(); let next_id = AtomicUsize::new(self.next_id.load(SeqCst)); + let outbound_tx = self.outbound_tx.clone(); let mut output_done = self.output_done_rx.lock().take().unwrap(); - Some(async move { - Self::request_internal::( - &next_id, - &response_handlers, - outbound_tx.as_ref(), - (), - ) - .await?; - Self::notify_internal::(outbound_tx.as_ref(), ())?; - drop(outbound_tx); - output_done.recv().await; - drop(tasks); - Ok(()) - }) + let shutdown_request = Self::request_internal::( + &next_id, + &response_handlers, + &outbound_tx, + (), + ); + let exit = Self::notify_internal::(&outbound_tx, ()); + outbound_tx.close(); + Some( + async move { + shutdown_request.await?; + exit?; + output_done.recv().await; + drop(tasks); + Ok(()) + } + .log_err(), + ) } else { None } @@ -375,7 +379,7 @@ impl LanguageServer { Self::request_internal::( &this.next_id, &this.response_handlers, - this.outbound_tx.read().as_ref(), + &this.outbound_tx, params, ) .await @@ -385,7 +389,7 @@ impl LanguageServer { fn request_internal( next_id: &AtomicUsize, response_handlers: &Mutex>, - outbound_tx: Option<&channel::Sender>>, + outbound_tx: &channel::Sender>, params: T::Params, ) -> impl 'static + Future> where @@ -415,16 +419,8 @@ impl LanguageServer { ); let send = outbound_tx - .as_ref() - .ok_or_else(|| { - anyhow!("tried to send a request to a language server that has been shut down") - }) - .and_then(|outbound_tx| { - outbound_tx - .try_send(message) - .context("failed to write to language server's stdin")?; - Ok(()) - }); + .try_send(message) + .context("failed to write to language server's stdin"); async move { send?; rx.recv().await.unwrap() @@ -438,13 +434,13 @@ impl LanguageServer { let this = self.clone(); async move { this.initialized.clone().recv().await; - Self::notify_internal::(this.outbound_tx.read().as_ref(), params)?; + Self::notify_internal::(&this.outbound_tx, params)?; Ok(()) } } fn notify_internal( - outbound_tx: Option<&channel::Sender>>, + outbound_tx: &channel::Sender>, params: T::Params, ) -> Result<()> { let message = serde_json::to_vec(&Notification { @@ -453,9 +449,6 @@ impl LanguageServer { params, }) .unwrap(); - let outbound_tx = outbound_tx - .as_ref() - .ok_or_else(|| anyhow!("tried to notify a language server that has been shut down"))?; outbound_tx.try_send(message)?; Ok(()) } From 8dce91be235d2ec50331c06f8d9ed917986f3b25 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Feb 2022 10:39:44 -0800 Subject: [PATCH 27/65] Upgrade time crates to silence warning on Rust 1.59 --- Cargo.lock | 35 ++++++++++++++++++++++------------- crates/zed/Cargo.toml | 1 - 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dd0f7c036ba787193c70918c89e044a9b9e34c0..89400ed4eb2f92f636bdae50e0a87f6e3c0898a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,7 +867,7 @@ dependencies = [ "gpui", "postage", "theme", - "time 0.3.2", + "time 0.3.7", "util", "workspace", ] @@ -988,7 +988,7 @@ dependencies = [ "sum_tree", "surf", "thiserror", - "time 0.3.2", + "time 0.3.7", "tiny_http", "util", ] @@ -1134,7 +1134,7 @@ dependencies = [ "percent-encoding", "rand 0.8.3", "sha2 0.9.5", - "time 0.2.25", + "time 0.2.27", "version_check", ] @@ -2231,7 +2231,7 @@ dependencies = [ "smallvec", "smol", "sum_tree", - "time 0.3.2", + "time 0.3.7", "tiny-skia", "tree-sitter", "usvg", @@ -3088,6 +3088,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "oauth2" version = "4.1.0" @@ -4679,7 +4688,7 @@ dependencies = [ "sqlx-rt 0.5.5", "stringprep", "thiserror", - "time 0.2.25", + "time 0.2.27", "url", "uuid", "webpki", @@ -5127,9 +5136,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" dependencies = [ "const_fn", "libc", @@ -5142,11 +5151,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.2" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ "libc", + "num_threads", ] [[package]] @@ -5161,9 +5171,9 @@ dependencies = [ [[package]] name = "time-macros-impl" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -5891,7 +5901,6 @@ dependencies = [ "theme", "theme_selector", "thiserror", - "time 0.3.2", "tiny_http", "toml", "tree-sitter", @@ -5943,7 +5952,7 @@ dependencies = [ "surf", "tide", "tide-compress", - "time 0.2.25", + "time 0.2.27", "toml", "util", "zed", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9dea5ad1b6ba99fe7bfc44e2f37c810477f6982a..ac25887aebe6b327b566f085504e34c5e748c4a7 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -88,7 +88,6 @@ smol = "1.2.5" surf = "2.2" tempdir = { version = "0.3.7", optional = true } thiserror = "1.0.29" -time = "0.3" tiny_http = "0.8" toml = "0.5" tree-sitter = "0.20.4" From 1278f5484f11d6e543acaba3c8845eb4ad7d3fc0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Feb 2022 12:38:31 -0800 Subject: [PATCH 28/65] Add project search RPC messages --- crates/rpc/proto/zed.proto | 47 +++++++++++++++++++++++++------------- crates/rpc/src/proto.rs | 4 ++++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ebdb39942ddfa4968ba5ffcaa4acf8bd8a794140..18e32560268bd3e56087d2ce687dd68ec712bfb7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -21,6 +21,7 @@ message Envelope { LeaveProject leave_project = 15; AddProjectCollaborator add_project_collaborator = 16; RemoveProjectCollaborator remove_project_collaborator = 17; + GetDefinition get_definition = 18; GetDefinitionResponse get_definition_response = 19; GetReferences get_references = 20; @@ -61,22 +62,24 @@ message Envelope { PrepareRenameResponse prepare_rename_response = 54; PerformRename perform_rename = 55; PerformRenameResponse perform_rename_response = 56; - - GetChannels get_channels = 57; - GetChannelsResponse get_channels_response = 58; - JoinChannel join_channel = 59; - JoinChannelResponse join_channel_response = 60; - LeaveChannel leave_channel = 61; - SendChannelMessage send_channel_message = 62; - SendChannelMessageResponse send_channel_message_response = 63; - ChannelMessageSent channel_message_sent = 64; - GetChannelMessages get_channel_messages = 65; - GetChannelMessagesResponse get_channel_messages_response = 66; - - UpdateContacts update_contacts = 67; - - GetUsers get_users = 68; - GetUsersResponse get_users_response = 69; + SearchProject search_project = 57; + SearchProjectResponse search_project_response = 58; + + GetChannels get_channels = 59; + GetChannelsResponse get_channels_response = 60; + JoinChannel join_channel = 61; + JoinChannelResponse join_channel_response = 62; + LeaveChannel leave_channel = 63; + SendChannelMessage send_channel_message = 64; + SendChannelMessageResponse send_channel_message_response = 65; + ChannelMessageSent channel_message_sent = 66; + GetChannelMessages get_channel_messages = 67; + GetChannelMessagesResponse get_channel_messages_response = 68; + + UpdateContacts update_contacts = 69; + + GetUsers get_users = 70; + GetUsersResponse get_users_response = 71; } } @@ -366,6 +369,18 @@ message PerformRenameResponse { ProjectTransaction transaction = 2; } +message SearchProject { + uint64 project_id = 1; + string query = 2; + bool regex = 3; + bool whole_word = 4; + bool case_sensitive = 5; +} + +message SearchProjectResponse { + repeated Location locations = 1; +} + message CodeAction { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 5ec46eb353a59652eff919b9f8fce9fdb11aebb3..c5da067f17dd1403631371237cdfd4afb9c6e132 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -190,6 +190,8 @@ messages!( (RegisterWorktree, Foreground), (RemoveProjectCollaborator, Foreground), (SaveBuffer, Foreground), + (SearchProject, Background), + (SearchProjectResponse, Background), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), @@ -230,6 +232,7 @@ request_messages!( (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (SaveBuffer, BufferSaved), + (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), (ShareProject, Ack), (Test, Test), @@ -262,6 +265,7 @@ entity_messages!( PrepareRename, RemoveProjectCollaborator, SaveBuffer, + SearchProject, UnregisterWorktree, UnshareProject, UpdateBuffer, From e822c6a64e43f1b447cc6a8721d038acecf66c3c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 15:09:47 -0700 Subject: [PATCH 29/65] Handle project-wide search on guests Co-Authored-By: Max Brunsfeld --- Cargo.lock | 1 + crates/find/Cargo.toml | 1 + crates/find/src/project_find.rs | 3 +- crates/project/src/project.rs | 84 ++++++- crates/project/src/search.rs | 431 +++++++++++++++++--------------- crates/server/src/rpc.rs | 129 +++++++++- 6 files changed, 434 insertions(+), 215 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89400ed4eb2f92f636bdae50e0a87f6e3c0898a4..3862053f043861ca73f4137ceddf807d6fe43f71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1785,6 +1785,7 @@ dependencies = [ "project", "theme", "unindent", + "util", "workspace", ] diff --git a/crates/find/Cargo.toml b/crates/find/Cargo.toml index 1c0781511672976ac02d0d54446e6f214f0f4b9d..5e644e40c44b69d60beae613129f31fcbac36b7f 100644 --- a/crates/find/Cargo.toml +++ b/crates/find/Cargo.toml @@ -13,6 +13,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } project = { path = "../project" } theme = { path = "../theme" } +util = { path = "../util" } workspace = { path = "../workspace" } anyhow = "1.0" postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 196cde33a14e7a1670d4e8475dbea1805f673e73..f98a12ec4ce0e4bd64b90e7e1c6ba008d61656ea 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -12,6 +12,7 @@ use std::{ ops::Range, path::PathBuf, }; +use util::ResultExt as _; use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; action!(Deploy); @@ -81,7 +82,7 @@ impl ProjectFind { .update(cx, |project, cx| project.search(query.clone(), cx)); self.highlighted_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let matches = search.await; + let matches = search.await.log_err()?; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { this.highlighted_ranges.clear(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 641d89fa29515800b05f1f2cd1314292124727f6..30c75443f12f115090701ace1eda7f906dffd929 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -15,6 +15,7 @@ use gpui::{ UpgradeModelHandle, WeakModelHandle, }; use language::{ + proto::{deserialize_anchor, serialize_anchor}, range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, @@ -226,6 +227,7 @@ impl Project { client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_search_project); client.add_entity_request_handler(Self::handle_get_project_symbols); client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); client.add_entity_request_handler(Self::handle_open_buffer); @@ -2049,7 +2051,7 @@ impl Project { &self, query: SearchQuery, cx: &mut ModelContext, - ) -> Task, Vec>>> { + ) -> Task, Vec>>>> { if self.is_local() { let snapshots = self .strong_worktrees(cx) @@ -2215,10 +2217,38 @@ impl Project { } }) .await; - matched_buffers.into_iter().flatten().collect() + Ok(matched_buffers.into_iter().flatten().collect()) + }) + } else if let Some(project_id) = self.remote_id() { + let request = self.client.request(query.to_proto(project_id)); + let request_handle = self.start_buffer_request(cx); + cx.spawn(|this, mut cx| async move { + let response = request.await?; + let mut result = HashMap::default(); + for location in response.locations { + let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + let target_buffer = this + .update(&mut cx, |this, cx| { + this.deserialize_buffer(buffer, request_handle.clone(), cx) + }) + .await?; + let start = location + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = location + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + result + .entry(target_buffer) + .or_insert(Vec::new()) + .push(start..end) + } + Ok(result) }) } else { - todo!() + Task::ready(Ok(Default::default())) } } @@ -3012,6 +3042,36 @@ impl Project { }) } + async fn handle_search_project( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let query = SearchQuery::from_proto(envelope.payload)?; + let result = this + .update(&mut cx, |this, cx| this.search(query, cx)) + .await?; + + this.update(&mut cx, |this, cx| { + let mut locations = Vec::new(); + for (buffer, ranges) in result { + for range in ranges { + let start = serialize_anchor(&range.start); + let end = serialize_anchor(&range.end); + let buffer = this.serialize_buffer_for_peer(&buffer, peer_id, cx); + locations.push(proto::Location { + buffer: Some(buffer), + start: Some(start), + end: Some(end), + }); + } + } + Ok(proto::SearchProjectResponse { locations }) + }) + } + async fn handle_open_buffer_for_symbol( this: ModelHandle, envelope: TypedEnvelope, @@ -4915,7 +4975,9 @@ mod tests { .await; assert_eq!( - search(&project, SearchQuery::text("TWO", false, true), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, true), &mut cx) + .await + .unwrap(), HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]) @@ -4933,7 +4995,9 @@ mod tests { }); assert_eq!( - search(&project, SearchQuery::text("TWO", false, true), &mut cx).await, + search(&project, SearchQuery::text("TWO", false, true), &mut cx) + .await + .unwrap(), HashMap::from_iter([ ("two.rs".to_string(), vec![6..9]), ("three.rs".to_string(), vec![37..40]), @@ -4945,10 +5009,12 @@ mod tests { project: &ModelHandle, query: SearchQuery, cx: &mut gpui::TestAppContext, - ) -> HashMap>> { - project + ) -> Result>>> { + let results = project .update(cx, |project, cx| project.search(query, cx)) - .await + .await?; + + Ok(results .into_iter() .map(|(buffer, ranges)| { buffer.read_with(cx, |buffer, _| { @@ -4960,7 +5026,7 @@ mod tests { (path, ranges) }) }) - .collect() + .collect()) } } } diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 4f89c68aa23e1969ddc33f021366eb9dce772c4b..a82460dfa3be954266325b5a1f9113fe53d359f7 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,204 +1,227 @@ -use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; -use anyhow::Result; -use language::{char_kind, Rope}; -use regex::{Regex, RegexBuilder}; -use smol::future::yield_now; -use std::{ - io::{BufRead, BufReader, Read}, - ops::Range, - sync::Arc, -}; - -#[derive(Clone)] -pub enum SearchQuery { - Text { - search: Arc>, - query: Arc, - whole_word: bool, - case_sensitive: bool, - }, - Regex { - regex: Regex, - query: Arc, - multiline: bool, - whole_word: bool, - case_sensitive: bool, - }, -} - -impl SearchQuery { - pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { - let query = query.to_string(); - let search = AhoCorasickBuilder::new() - .auto_configure(&[&query]) - .ascii_case_insensitive(!case_sensitive) - .build(&[&query]); - Self::Text { - search: Arc::new(search), - query: Arc::from(query), - whole_word, - case_sensitive, - } - } - - pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { - let mut query = query.to_string(); - let initial_query = Arc::from(query.as_str()); - if whole_word { - let mut word_query = String::new(); - word_query.push_str("\\b"); - word_query.push_str(&query); - word_query.push_str("\\b"); - query = word_query - } - - let multiline = query.contains("\n") || query.contains("\\n"); - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .multi_line(multiline) - .build()?; - Ok(Self::Regex { - regex, - query: initial_query, - multiline, - whole_word, - case_sensitive, - }) - } - - pub fn detect(&self, stream: T) -> Result { - if self.as_str().is_empty() { - return Ok(false); - } - - match self { - Self::Text { search, .. } => { - let mat = search.stream_find_iter(stream).next(); - match mat { - Some(Ok(_)) => Ok(true), - Some(Err(err)) => Err(err.into()), - None => Ok(false), - } - } - Self::Regex { - regex, multiline, .. - } => { - let mut reader = BufReader::new(stream); - if *multiline { - let mut text = String::new(); - if let Err(err) = reader.read_to_string(&mut text) { - Err(err.into()) - } else { - Ok(regex.find(&text).is_some()) - } - } else { - for line in reader.lines() { - let line = line?; - if regex.find(&line).is_some() { - return Ok(true); - } - } - Ok(false) - } - } - } - } - - pub async fn search(&self, rope: &Rope) -> Vec> { - const YIELD_INTERVAL: usize = 20000; - - if self.as_str().is_empty() { - return Default::default(); - } - - let mut matches = Vec::new(); - match self { - Self::Text { - search, whole_word, .. - } => { - for (ix, mat) in search - .stream_find_iter(rope.bytes_in_range(0..rope.len())) - .enumerate() - { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - let mat = mat.unwrap(); - if *whole_word { - let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); - let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = rope.chars_at(mat.end()).next().map(char_kind); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { - continue; - } - } - matches.push(mat.start()..mat.end()) - } - } - Self::Regex { - regex, multiline, .. - } => { - if *multiline { - let text = rope.to_string(); - for (ix, mat) in regex.find_iter(&text).enumerate() { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - matches.push(mat.start()..mat.end()); - } - } else { - let mut line = String::new(); - let mut line_offset = 0; - for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { - if (chunk_ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - for mat in regex.find_iter(&line) { - let start = line_offset + mat.start(); - let end = line_offset + mat.end(); - matches.push(start..end); - } - - line_offset += line.len() + 1; - line.clear(); - } - line.push_str(text); - } - } - } - } - } - matches - } - - pub fn as_str(&self) -> &str { - match self { - Self::Text { query, .. } => query.as_ref(), - Self::Regex { query, .. } => query.as_ref(), - } - } - - pub fn whole_word(&self) -> bool { - match self { - Self::Text { whole_word, .. } => *whole_word, - Self::Regex { whole_word, .. } => *whole_word, - } - } - - pub fn case_sensitive(&self) -> bool { - match self { - Self::Text { case_sensitive, .. } => *case_sensitive, - Self::Regex { case_sensitive, .. } => *case_sensitive, - } - } - - pub fn is_regex(&self) -> bool { - matches!(self, Self::Regex { .. }) - } -} +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; +use anyhow::Result; +use client::proto; +use language::{char_kind, Rope}; +use regex::{Regex, RegexBuilder}; +use smol::future::yield_now; +use std::{ + io::{BufRead, BufReader, Read}, + ops::Range, + sync::Arc, +}; + +#[derive(Clone)] +pub enum SearchQuery { + Text { + search: Arc>, + query: Arc, + whole_word: bool, + case_sensitive: bool, + }, + Regex { + regex: Regex, + query: Arc, + multiline: bool, + whole_word: bool, + case_sensitive: bool, + }, +} + +impl SearchQuery { + pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { + let query = query.to_string(); + let search = AhoCorasickBuilder::new() + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); + Self::Text { + search: Arc::new(search), + query: Arc::from(query), + whole_word, + case_sensitive, + } + } + + pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { + let mut query = query.to_string(); + let initial_query = Arc::from(query.as_str()); + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = word_query + } + + let multiline = query.contains("\n") || query.contains("\\n"); + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(multiline) + .build()?; + Ok(Self::Regex { + regex, + query: initial_query, + multiline, + whole_word, + case_sensitive, + }) + } + + pub fn from_proto(message: proto::SearchProject) -> Result { + if message.regex { + Self::regex(message.query, message.whole_word, message.case_sensitive) + } else { + Ok(Self::text( + message.query, + message.whole_word, + message.case_sensitive, + )) + } + } + + pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { + proto::SearchProject { + project_id, + query: self.as_str().to_string(), + regex: self.is_regex(), + whole_word: self.whole_word(), + case_sensitive: self.case_sensitive(), + } + } + + pub fn detect(&self, stream: T) -> Result { + if self.as_str().is_empty() { + return Ok(false); + } + + match self { + Self::Text { search, .. } => { + let mat = search.stream_find_iter(stream).next(); + match mat { + Some(Ok(_)) => Ok(true), + Some(Err(err)) => Err(err.into()), + None => Ok(false), + } + } + Self::Regex { + regex, multiline, .. + } => { + let mut reader = BufReader::new(stream); + if *multiline { + let mut text = String::new(); + if let Err(err) = reader.read_to_string(&mut text) { + Err(err.into()) + } else { + Ok(regex.find(&text).is_some()) + } + } else { + for line in reader.lines() { + let line = line?; + if regex.find(&line).is_some() { + return Ok(true); + } + } + Ok(false) + } + } + } + } + + pub async fn search(&self, rope: &Rope) -> Vec> { + const YIELD_INTERVAL: usize = 20000; + + if self.as_str().is_empty() { + return Default::default(); + } + + let mut matches = Vec::new(); + match self { + Self::Text { + search, whole_word, .. + } => { + for (ix, mat) in search + .stream_find_iter(rope.bytes_in_range(0..rope.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + if *whole_word { + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } + matches.push(mat.start()..mat.end()) + } + } + Self::Regex { + regex, multiline, .. + } => { + if *multiline { + let text = rope.to_string(); + for (ix, mat) in regex.find_iter(&text).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + matches.push(mat.start()..mat.end()); + } + } else { + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + matches.push(start..end); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } + } + } + } + } + matches + } + + pub fn as_str(&self) -> &str { + match self { + Self::Text { query, .. } => query.as_ref(), + Self::Regex { query, .. } => query.as_ref(), + } + } + + pub fn whole_word(&self) -> bool { + match self { + Self::Text { whole_word, .. } => *whole_word, + Self::Regex { whole_word, .. } => *whole_word, + } + } + + pub fn case_sensitive(&self) -> bool { + match self { + Self::Text { case_sensitive, .. } => *case_sensitive, + Self::Regex { case_sensitive, .. } => *case_sensitive, + } + } + + pub fn is_regex(&self) -> bool { + matches!(self, Self::Regex { .. }) + } +} diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 35617c048f4a4ce80962b96339ac14207ea8e9c6..c7762bd7599c671635a674fdf1256c8439779528 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -79,6 +79,7 @@ impl Server { .add_message_handler(Server::disk_based_diagnostics_updated) .add_request_handler(Server::get_definition) .add_request_handler(Server::get_references) + .add_request_handler(Server::search_project) .add_request_handler(Server::get_document_highlights) .add_request_handler(Server::get_project_symbols) .add_request_handler(Server::open_buffer_for_symbol) @@ -570,6 +571,20 @@ impl Server { .await?) } + async fn search_project( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + let host_connection_id = self + .state() + .read_project(request.payload.project_id, request.sender_id)? + .host_connection_id; + Ok(self + .peer + .forward_request(request.sender_id, host_connection_id, request.payload) + .await?) + } + async fn get_document_highlights( self: Arc, request: TypedEnvelope, @@ -1186,7 +1201,7 @@ mod tests { LanguageConfig, LanguageRegistry, LanguageServerConfig, Point, ToLspPosition, }, lsp, - project::{DiagnosticSummary, Project, ProjectPath}, + project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath}, workspace::{Settings, Workspace, WorkspaceParams}, }; @@ -2843,6 +2858,118 @@ mod tests { }); } + #[gpui::test(iterations = 10)] + async fn test_project_search(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = FakeFs::new(cx_a.background()); + fs.insert_tree( + "/root-1", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a": "hello world", + "b": "goodnight moon", + "c": "a world of goo", + "d": "world champion of clown world", + }), + ) + .await; + fs.insert_tree( + "/root-2", + json!({ + "e": "disney world is fun", + }), + ) + .await; + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + + let (worktree_1, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/root-1", false, cx) + }) + .await + .unwrap(); + worktree_1 + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_2, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/root-2", false, cx) + }) + .await + .unwrap(); + worktree_2 + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + + eprintln!("sharing"); + + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let results = project_b + .update(&mut cx_b, |project, cx| { + project.search(SearchQuery::text("world", false, false), cx) + }) + .await + .unwrap(); + + let mut ranges_by_path = results + .into_iter() + .map(|(buffer, ranges)| { + buffer.read_with(&cx_b, |buffer, cx| { + let path = buffer.file().unwrap().full_path(cx); + let offset_ranges = ranges + .into_iter() + .map(|range| range.to_offset(buffer)) + .collect::>(); + (path, offset_ranges) + }) + }) + .collect::>(); + ranges_by_path.sort_by_key(|(path, _)| path.clone()); + + assert_eq!( + ranges_by_path, + &[ + (PathBuf::from("root-1/a"), vec![6..11]), + (PathBuf::from("root-1/c"), vec![2..7]), + (PathBuf::from("root-1/d"), vec![0..5, 24..29]), + (PathBuf::from("root-2/e"), vec![7..12]), + ] + ); + } + #[gpui::test(iterations = 10)] async fn test_document_highlights(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); From 92f411f01efa5a0820814d3066f74d021b5d22cc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 15:20:42 -0700 Subject: [PATCH 30/65] Extract generic forward_project_request function on server All these methods did the same thing with different message types. Co-Authored-By: Max Brunsfeld --- crates/server/src/rpc.rs | 225 +++++---------------------------------- 1 file changed, 24 insertions(+), 201 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index c7762bd7599c671635a674fdf1256c8439779528..7887cb763e0b31307f6572a09a0782d0f8e75542 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -12,7 +12,7 @@ use collections::{HashMap, HashSet}; use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use rpc::{ - proto::{self, AnyTypedEnvelope, EnvelopedMessage, RequestMessage}, + proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, Connection, ConnectionId, Peer, TypedEnvelope, }; use sha1::{Digest as _, Sha1}; @@ -77,26 +77,28 @@ impl Server { .add_message_handler(Server::update_diagnostic_summary) .add_message_handler(Server::disk_based_diagnostics_updating) .add_message_handler(Server::disk_based_diagnostics_updated) - .add_request_handler(Server::get_definition) - .add_request_handler(Server::get_references) - .add_request_handler(Server::search_project) - .add_request_handler(Server::get_document_highlights) - .add_request_handler(Server::get_project_symbols) - .add_request_handler(Server::open_buffer_for_symbol) - .add_request_handler(Server::open_buffer) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler( + Server::forward_project_request::, + ) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_message_handler(Server::close_buffer) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) .add_message_handler(Server::buffer_saved) .add_request_handler(Server::save_buffer) - .add_request_handler(Server::format_buffers) - .add_request_handler(Server::get_completions) - .add_request_handler(Server::apply_additional_edits_for_completion) - .add_request_handler(Server::get_code_actions) - .add_request_handler(Server::apply_code_action) - .add_request_handler(Server::prepare_rename) - .add_request_handler(Server::perform_rename) .add_request_handler(Server::get_channels) .add_request_handler(Server::get_users) .add_request_handler(Server::join_channel) @@ -543,97 +545,16 @@ impl Server { Ok(()) } - async fn get_definition( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn get_references( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn search_project( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn get_document_highlights( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn get_project_symbols( + async fn forward_project_request( self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn open_buffer_for_symbol( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn open_buffer( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { + request: TypedEnvelope, + ) -> tide::Result + where + T: EntityMessage + RequestMessage, + { let host_connection_id = self .state() - .read_project(request.payload.project_id, request.sender_id)? + .read_project(request.payload.remote_entity_id(), request.sender_id)? .host_connection_id; Ok(self .peer @@ -680,104 +601,6 @@ impl Server { Ok(response) } - async fn format_buffers( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn get_completions( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn apply_additional_edits_for_completion( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn get_code_actions( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn apply_code_action( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn prepare_rename( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn perform_rename( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - async fn update_buffer( self: Arc, request: TypedEnvelope, From d5cc3fea3d59c3b35f1113bec4cb2657d64cfc2a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 16:13:53 -0700 Subject: [PATCH 31/65] Implement Debug for keymap::MatchResult Helpful when debugging issues with keystroke dispatch. Co-Authored-By: Max Brunsfeld --- crates/gpui/src/keymap.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 848cb8fe393344710612fe5f382507b196162e06..05fbd5b74b4d4e4c2a0b0595a75d4859b7fa0106 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -76,6 +76,19 @@ pub enum MatchResult { Action(Box), } +impl Debug for MatchResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MatchResult::None => f.debug_struct("MatchResult::None").finish(), + MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(), + MatchResult::Action(action) => f + .debug_tuple("MatchResult::Action") + .field(&action.name()) + .finish(), + } + } +} + impl Matcher { pub fn new(keymap: Keymap) -> Self { Self { From ed6c8b183689b48fad7dfecc732552cd10671690 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 16:14:16 -0700 Subject: [PATCH 32/65] Allow actions to be propagated from nested ViewContexts Co-Authored-By: Max Brunsfeld --- crates/gpui/src/app.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 90c25efe5bb838e63194c155365c6d1747b81ac7..dec73ab6c2a6396895c81bcb2edbba94deff2326 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -710,7 +710,7 @@ impl ReadViewWith for TestAppContext { } type ActionCallback = - dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize) -> bool; + dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize); type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; @@ -741,6 +741,7 @@ pub struct MutableAppContext { pending_flushes: usize, flushing_effects: bool, next_cursor_style_handle_id: Arc, + halt_action_dispatch: bool, } impl MutableAppContext { @@ -785,6 +786,7 @@ impl MutableAppContext { pending_flushes: 0, flushing_effects: false, next_cursor_style_handle_id: Default::default(), + halt_action_dispatch: false, } } @@ -876,7 +878,6 @@ impl MutableAppContext { action, &mut cx, ); - cx.halt_action_dispatch }, ); @@ -1167,7 +1168,7 @@ impl MutableAppContext { action: &dyn AnyAction, ) -> bool { self.update(|this| { - let mut halted_dispatch = false; + this.halt_action_dispatch = false; for view_id in path.iter().rev() { if let Some(mut view) = this.cx.views.remove(&(window_id, *view_id)) { let type_id = view.as_any().type_id(); @@ -1178,10 +1179,9 @@ impl MutableAppContext { .and_then(|h| h.remove_entry(&action.id())) { for handler in handlers.iter_mut().rev() { - let halt_dispatch = - handler(view.as_mut(), action, this, window_id, *view_id); - if halt_dispatch { - halted_dispatch = true; + this.halt_action_dispatch = true; + handler(view.as_mut(), action, this, window_id, *view_id); + if this.halt_action_dispatch { break; } } @@ -1193,16 +1193,16 @@ impl MutableAppContext { this.cx.views.insert((window_id, *view_id), view); - if halted_dispatch { + if this.halt_action_dispatch { break; } } } - if !halted_dispatch { - halted_dispatch = this.dispatch_global_action_any(action); + if !this.halt_action_dispatch { + this.dispatch_global_action_any(action); } - halted_dispatch + this.halt_action_dispatch }) } @@ -2344,7 +2344,6 @@ pub struct ViewContext<'a, T: ?Sized> { window_id: usize, view_id: usize, view_type: PhantomData, - halt_action_dispatch: bool, } impl<'a, T: View> ViewContext<'a, T> { @@ -2354,7 +2353,6 @@ impl<'a, T: View> ViewContext<'a, T> { window_id, view_id, view_type: PhantomData, - halt_action_dispatch: true, } } @@ -2529,7 +2527,7 @@ impl<'a, T: View> ViewContext<'a, T> { } pub fn propagate_action(&mut self) { - self.halt_action_dispatch = false; + self.app.halt_action_dispatch = false; } pub fn spawn(&self, f: F) -> Task @@ -4337,7 +4335,10 @@ mod tests { let actions_clone = actions.clone(); cx.add_action(move |view: &mut ViewA, _: &Action, cx| { if view.id != 1 { - cx.propagate_action(); + cx.add_view(|cx| { + cx.propagate_action(); // Still works on a nested ViewContext + ViewB { id: 5 } + }); } actions_clone.borrow_mut().push(format!("{} b", view.id)); }); From dea40c5d1a194493ba0287a3082318277a06e057 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 16:14:45 -0700 Subject: [PATCH 33/65] Don't show buffer search UI on ProjectSearchView Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 17680 +++++++++++++++--------------- crates/find/src/buffer_find.rs | 17 +- crates/find/src/project_find.rs | 3 + 3 files changed, 8858 insertions(+), 8842 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a5d2edc6121a078583238c2efcab123ce173ee3e..21f1d9a4ad7bdc4087198fa8ffcf8dce5bc03127 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1,8835 +1,8845 @@ -pub mod display_map; -mod element; -pub mod items; -pub mod movement; -mod multi_buffer; - -#[cfg(test)] -mod test; - -use aho_corasick::AhoCorasick; -use anyhow::Result; -use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; -pub use display_map::DisplayPoint; -use display_map::*; -pub use element::*; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - action, - color::Color, - elements::*, - executor, - fonts::{self, HighlightStyle, TextStyle}, - geometry::vector::{vec2f, Vector2F}, - keymap::Binding, - platform::CursorStyle, - text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, - ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, - WeakViewHandle, -}; -use items::{BufferItemHandle, MultiBufferItemHandle}; -use itertools::Itertools as _; -pub use language::{char_kind, CharKind}; -use language::{ - AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, -}; -use multi_buffer::MultiBufferChunks; -pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, -}; -use ordered_float::OrderedFloat; -use postage::watch; -use project::{Project, ProjectTransaction}; -use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; -use smol::Timer; -use snippet::Snippet; -use std::{ - any::TypeId, - cmp::{self, Ordering, Reverse}, - iter::{self, FromIterator}, - mem, - ops::{Deref, DerefMut, Range, RangeInclusive, Sub}, - sync::Arc, - time::{Duration, Instant}, -}; -pub use sum_tree::Bias; -use text::rope::TextDimension; -use theme::DiagnosticStyle; -use util::{post_inc, ResultExt, TryFutureExt}; -use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace}; - -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const MAX_LINE_LEN: usize = 1024; -const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; - -action!(Cancel); -action!(Backspace); -action!(Delete); -action!(Input, String); -action!(Newline); -action!(Tab); -action!(Outdent); -action!(DeleteLine); -action!(DeleteToPreviousWordBoundary); -action!(DeleteToNextWordBoundary); -action!(DeleteToBeginningOfLine); -action!(DeleteToEndOfLine); -action!(CutToEndOfLine); -action!(DuplicateLine); -action!(MoveLineUp); -action!(MoveLineDown); -action!(Cut); -action!(Copy); -action!(Paste); -action!(Undo); -action!(Redo); -action!(MoveUp); -action!(MoveDown); -action!(MoveLeft); -action!(MoveRight); -action!(MoveToPreviousWordBoundary); -action!(MoveToNextWordBoundary); -action!(MoveToBeginningOfLine); -action!(MoveToEndOfLine); -action!(MoveToBeginning); -action!(MoveToEnd); -action!(SelectUp); -action!(SelectDown); -action!(SelectLeft); -action!(SelectRight); -action!(SelectToPreviousWordBoundary); -action!(SelectToNextWordBoundary); -action!(SelectToBeginningOfLine, bool); -action!(SelectToEndOfLine, bool); -action!(SelectToBeginning); -action!(SelectToEnd); -action!(SelectAll); -action!(SelectLine); -action!(SplitSelectionIntoLines); -action!(AddSelectionAbove); -action!(AddSelectionBelow); -action!(SelectNext, bool); -action!(ToggleComments); -action!(SelectLargerSyntaxNode); -action!(SelectSmallerSyntaxNode); -action!(MoveToEnclosingBracket); -action!(ShowNextDiagnostic); -action!(GoToDefinition); -action!(FindAllReferences); -action!(Rename); -action!(ConfirmRename); -action!(PageUp); -action!(PageDown); -action!(Fold); -action!(Unfold); -action!(FoldSelectedRanges); -action!(Scroll, Vector2F); -action!(Select, SelectPhase); -action!(ShowCompletions); -action!(ToggleCodeActions, bool); -action!(ConfirmCompletion, Option); -action!(ConfirmCodeAction, Option); - -pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { - path_openers.push(Box::new(items::BufferOpener)); - cx.add_bindings(vec![ - Binding::new("escape", Cancel, Some("Editor")), - Binding::new("backspace", Backspace, Some("Editor")), - Binding::new("ctrl-h", Backspace, Some("Editor")), - Binding::new("delete", Delete, Some("Editor")), - Binding::new("ctrl-d", Delete, Some("Editor")), - Binding::new("enter", Newline, Some("Editor && mode == full")), - Binding::new( - "alt-enter", - Input("\n".into()), - Some("Editor && mode == auto_height"), - ), - Binding::new( - "enter", - ConfirmCompletion(None), - Some("Editor && showing_completions"), - ), - Binding::new( - "enter", - ConfirmCodeAction(None), - Some("Editor && showing_code_actions"), - ), - Binding::new("enter", ConfirmRename, Some("Editor && renaming")), - Binding::new("tab", Tab, Some("Editor")), - Binding::new( - "tab", - ConfirmCompletion(None), - Some("Editor && showing_completions"), - ), - Binding::new("shift-tab", Outdent, Some("Editor")), - Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), - Binding::new( - "alt-backspace", - DeleteToPreviousWordBoundary, - Some("Editor"), - ), - Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")), - Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")), - Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")), - Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")), - Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")), - Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")), - Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")), - Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")), - Binding::new("cmd-x", Cut, Some("Editor")), - Binding::new("cmd-c", Copy, Some("Editor")), - Binding::new("cmd-v", Paste, Some("Editor")), - Binding::new("cmd-z", Undo, Some("Editor")), - Binding::new("cmd-shift-Z", Redo, Some("Editor")), - Binding::new("up", MoveUp, Some("Editor")), - Binding::new("down", MoveDown, Some("Editor")), - Binding::new("left", MoveLeft, Some("Editor")), - Binding::new("right", MoveRight, Some("Editor")), - Binding::new("ctrl-p", MoveUp, Some("Editor")), - Binding::new("ctrl-n", MoveDown, Some("Editor")), - Binding::new("ctrl-b", MoveLeft, Some("Editor")), - Binding::new("ctrl-f", MoveRight, Some("Editor")), - Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")), - Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")), - Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")), - Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")), - Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")), - Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")), - Binding::new("cmd-up", MoveToBeginning, Some("Editor")), - Binding::new("cmd-down", MoveToEnd, Some("Editor")), - Binding::new("shift-up", SelectUp, Some("Editor")), - Binding::new("ctrl-shift-P", SelectUp, Some("Editor")), - Binding::new("shift-down", SelectDown, Some("Editor")), - Binding::new("ctrl-shift-N", SelectDown, Some("Editor")), - Binding::new("shift-left", SelectLeft, Some("Editor")), - Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")), - Binding::new("shift-right", SelectRight, Some("Editor")), - Binding::new("ctrl-shift-F", SelectRight, Some("Editor")), - Binding::new( - "alt-shift-left", - SelectToPreviousWordBoundary, - Some("Editor"), - ), - Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")), - Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")), - Binding::new( - "cmd-shift-left", - SelectToBeginningOfLine(true), - Some("Editor"), - ), - Binding::new( - "ctrl-shift-A", - SelectToBeginningOfLine(true), - Some("Editor"), - ), - Binding::new("cmd-shift-right", SelectToEndOfLine(true), Some("Editor")), - Binding::new("ctrl-shift-E", SelectToEndOfLine(true), Some("Editor")), - Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")), - Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")), - Binding::new("cmd-a", SelectAll, Some("Editor")), - Binding::new("cmd-l", SelectLine, Some("Editor")), - Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")), - Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")), - Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")), - Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")), - Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")), - Binding::new("cmd-d", SelectNext(false), Some("Editor")), - Binding::new("cmd-k cmd-d", SelectNext(true), Some("Editor")), - Binding::new("cmd-/", ToggleComments, Some("Editor")), - Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")), - Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), - Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), - Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), - Binding::new("f8", ShowNextDiagnostic, Some("Editor")), - Binding::new("f2", Rename, Some("Editor")), - Binding::new("f12", GoToDefinition, Some("Editor")), - Binding::new("alt-shift-f12", FindAllReferences, Some("Editor")), - Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")), - Binding::new("pageup", PageUp, Some("Editor")), - Binding::new("pagedown", PageDown, Some("Editor")), - Binding::new("alt-cmd-[", Fold, Some("Editor")), - Binding::new("alt-cmd-]", Unfold, Some("Editor")), - Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")), - Binding::new("ctrl-space", ShowCompletions, Some("Editor")), - Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")), - ]); - - cx.add_action(Editor::open_new); - cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); - cx.add_action(Editor::select); - cx.add_action(Editor::cancel); - cx.add_action(Editor::handle_input); - cx.add_action(Editor::newline); - cx.add_action(Editor::backspace); - cx.add_action(Editor::delete); - cx.add_action(Editor::tab); - cx.add_action(Editor::outdent); - cx.add_action(Editor::delete_line); - cx.add_action(Editor::delete_to_previous_word_boundary); - cx.add_action(Editor::delete_to_next_word_boundary); - cx.add_action(Editor::delete_to_beginning_of_line); - cx.add_action(Editor::delete_to_end_of_line); - cx.add_action(Editor::cut_to_end_of_line); - cx.add_action(Editor::duplicate_line); - cx.add_action(Editor::move_line_up); - cx.add_action(Editor::move_line_down); - cx.add_action(Editor::cut); - cx.add_action(Editor::copy); - cx.add_action(Editor::paste); - cx.add_action(Editor::undo); - cx.add_action(Editor::redo); - cx.add_action(Editor::move_up); - cx.add_action(Editor::move_down); - cx.add_action(Editor::move_left); - cx.add_action(Editor::move_right); - cx.add_action(Editor::move_to_previous_word_boundary); - cx.add_action(Editor::move_to_next_word_boundary); - cx.add_action(Editor::move_to_beginning_of_line); - cx.add_action(Editor::move_to_end_of_line); - cx.add_action(Editor::move_to_beginning); - cx.add_action(Editor::move_to_end); - cx.add_action(Editor::select_up); - cx.add_action(Editor::select_down); - cx.add_action(Editor::select_left); - cx.add_action(Editor::select_right); - cx.add_action(Editor::select_to_previous_word_boundary); - cx.add_action(Editor::select_to_next_word_boundary); - cx.add_action(Editor::select_to_beginning_of_line); - cx.add_action(Editor::select_to_end_of_line); - cx.add_action(Editor::select_to_beginning); - cx.add_action(Editor::select_to_end); - cx.add_action(Editor::select_all); - cx.add_action(Editor::select_line); - cx.add_action(Editor::split_selection_into_lines); - cx.add_action(Editor::add_selection_above); - cx.add_action(Editor::add_selection_below); - cx.add_action(Editor::select_next); - cx.add_action(Editor::toggle_comments); - cx.add_action(Editor::select_larger_syntax_node); - cx.add_action(Editor::select_smaller_syntax_node); - cx.add_action(Editor::move_to_enclosing_bracket); - cx.add_action(Editor::show_next_diagnostic); - cx.add_action(Editor::go_to_definition); - cx.add_action(Editor::page_up); - cx.add_action(Editor::page_down); - cx.add_action(Editor::fold); - cx.add_action(Editor::unfold); - cx.add_action(Editor::fold_selected_ranges); - cx.add_action(Editor::show_completions); - cx.add_action(Editor::toggle_code_actions); - cx.add_async_action(Editor::confirm_completion); - cx.add_async_action(Editor::confirm_code_action); - cx.add_async_action(Editor::rename); - cx.add_async_action(Editor::confirm_rename); - cx.add_async_action(Editor::find_all_references); -} - -trait SelectionExt { - fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; - fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; - fn display_range(&self, map: &DisplaySnapshot) -> Range; - fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) - -> Range; -} - -trait InvalidationRegion { - fn ranges(&self) -> &[Range]; -} - -#[derive(Clone, Debug)] -pub enum SelectPhase { - Begin { - position: DisplayPoint, - add: bool, - click_count: usize, - }, - BeginColumnar { - position: DisplayPoint, - overshoot: u32, - }, - Extend { - position: DisplayPoint, - click_count: usize, - }, - Update { - position: DisplayPoint, - overshoot: u32, - scroll_position: Vector2F, - }, - End, -} - -#[derive(Clone, Debug)] -pub enum SelectMode { - Character, - Word(Range), - Line(Range), - All, -} - -#[derive(PartialEq, Eq)] -pub enum Autoscroll { - Fit, - Center, - Newest, -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum EditorMode { - SingleLine, - AutoHeight { max_lines: usize }, - Full, -} - -#[derive(Clone)] -pub enum SoftWrap { - None, - EditorWidth, - Column(u32), -} - -#[derive(Clone)] -pub struct EditorStyle { - pub text: TextStyle, - pub placeholder_text: Option, - pub theme: theme::Editor, -} - -type CompletionId = usize; - -pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor; - -pub struct Editor { - handle: WeakViewHandle, - buffer: ModelHandle, - display_map: ModelHandle, - next_selection_id: usize, - selections: Arc<[Selection]>, - pending_selection: Option, - columnar_selection_tail: Option, - add_selections_state: Option, - select_next_state: Option, - selection_history: - HashMap]>, Option]>>)>, - autoclose_stack: InvalidationStack, - snippet_stack: InvalidationStack, - select_larger_syntax_node_stack: Vec]>>, - active_diagnostics: Option, - scroll_position: Vector2F, - scroll_top_anchor: Option, - autoscroll_request: Option, - settings: watch::Receiver, - soft_wrap_mode_override: Option, - get_field_editor_theme: Option, - project: Option>, - focused: bool, - show_local_cursors: bool, - blink_epoch: usize, - blinking_paused: bool, - mode: EditorMode, - vertical_scroll_margin: f32, - placeholder_text: Option>, - highlighted_rows: Option>, - highlighted_ranges: BTreeMap>)>, - nav_history: Option, - context_menu: Option, - completion_tasks: Vec<(CompletionId, Task>)>, - next_completion_id: CompletionId, - available_code_actions: Option<(ModelHandle, Arc<[CodeAction]>)>, - code_actions_task: Option>, - document_highlights_task: Option>, - pending_rename: Option, -} - -pub struct EditorSnapshot { - pub mode: EditorMode, - pub display_snapshot: DisplaySnapshot, - pub placeholder_text: Option>, - is_focused: bool, - scroll_position: Vector2F, - scroll_top_anchor: Option, -} - -#[derive(Clone)] -pub struct PendingSelection { - selection: Selection, - mode: SelectMode, -} - -struct AddSelectionsState { - above: bool, - stack: Vec, -} - -struct SelectNextState { - query: AhoCorasick, - wordwise: bool, - done: bool, -} - -struct BracketPairState { - ranges: Vec>, - pair: BracketPair, -} - -struct SnippetState { - ranges: Vec>>, - active_index: usize, -} - -pub struct RenameState { - pub range: Range, - pub old_name: String, - pub editor: ViewHandle, - block_id: BlockId, -} - -struct InvalidationStack(Vec); - -enum ContextMenu { - Completions(CompletionsMenu), - CodeActions(CodeActionsMenu), -} - -impl ContextMenu { - fn select_prev(&mut self, cx: &mut ViewContext) -> bool { - if self.visible() { - match self { - ContextMenu::Completions(menu) => menu.select_prev(cx), - ContextMenu::CodeActions(menu) => menu.select_prev(cx), - } - true - } else { - false - } - } - - fn select_next(&mut self, cx: &mut ViewContext) -> bool { - if self.visible() { - match self { - ContextMenu::Completions(menu) => menu.select_next(cx), - ContextMenu::CodeActions(menu) => menu.select_next(cx), - } - true - } else { - false - } - } - - fn visible(&self) -> bool { - match self { - ContextMenu::Completions(menu) => menu.visible(), - ContextMenu::CodeActions(menu) => menu.visible(), - } - } - - fn render( - &self, - cursor_position: DisplayPoint, - style: EditorStyle, - cx: &AppContext, - ) -> (DisplayPoint, ElementBox) { - match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), - ContextMenu::CodeActions(menu) => menu.render(cursor_position, style), - } - } -} - -struct CompletionsMenu { - id: CompletionId, - initial_position: Anchor, - buffer: ModelHandle, - completions: Arc<[Completion]>, - match_candidates: Vec, - matches: Arc<[StringMatch]>, - selected_item: usize, - list: UniformListState, -} - -impl CompletionsMenu { - fn select_prev(&mut self, cx: &mut ViewContext) { - if self.selected_item > 0 { - self.selected_item -= 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - } - cx.notify(); - } - - fn select_next(&mut self, cx: &mut ViewContext) { - if self.selected_item + 1 < self.matches.len() { - self.selected_item += 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - } - cx.notify(); - } - - fn visible(&self) -> bool { - !self.matches.is_empty() - } - - fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox { - enum CompletionTag {} - - let completions = self.completions.clone(); - let matches = self.matches.clone(); - let selected_item = self.selected_item; - let container_style = style.autocomplete.container; - UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| { - let start_ix = range.start; - for (ix, mat) in matches[range].iter().enumerate() { - let completion = &completions[mat.candidate_id]; - let item_ix = start_ix + ix; - items.push( - MouseEventHandler::new::( - mat.candidate_id, - cx, - |state, _| { - let item_style = if item_ix == selected_item { - style.autocomplete.selected_item - } else if state.hovered { - style.autocomplete.hovered_item - } else { - style.autocomplete.item - }; - - Text::new(completion.label.text.clone(), style.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - style.text.color.into(), - styled_runs_for_code_label( - &completion.label, - style.text.color, - &style.syntax, - ), - &mat.positions, - )) - .contained() - .with_style(item_style) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { - cx.dispatch_action(ConfirmCompletion(Some(item_ix))); - }) - .boxed(), - ); - } - }) - .with_width_from_item( - self.matches - .iter() - .enumerate() - .max_by_key(|(_, mat)| { - self.completions[mat.candidate_id] - .label - .text - .chars() - .count() - }) - .map(|(ix, _)| ix), - ) - .contained() - .with_style(container_style) - .boxed() - } - - pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { - let mut matches = if let Some(query) = query { - fuzzy::match_strings( - &self.match_candidates, - query, - false, - 100, - &Default::default(), - executor, - ) - .await - } else { - self.match_candidates - .iter() - .enumerate() - .map(|(candidate_id, candidate)| StringMatch { - candidate_id, - score: Default::default(), - positions: Default::default(), - string: candidate.string.clone(), - }) - .collect() - }; - matches.sort_unstable_by_key(|mat| { - ( - Reverse(OrderedFloat(mat.score)), - self.completions[mat.candidate_id].sort_key(), - ) - }); - - for mat in &mut matches { - let filter_start = self.completions[mat.candidate_id].label.filter_range.start; - for position in &mut mat.positions { - *position += filter_start; - } - } - - self.matches = matches.into(); - } -} - -#[derive(Clone)] -struct CodeActionsMenu { - actions: Arc<[CodeAction]>, - buffer: ModelHandle, - selected_item: usize, - list: UniformListState, - deployed_from_indicator: bool, -} - -impl CodeActionsMenu { - fn select_prev(&mut self, cx: &mut ViewContext) { - if self.selected_item > 0 { - self.selected_item -= 1; - cx.notify() - } - } - - fn select_next(&mut self, cx: &mut ViewContext) { - if self.selected_item + 1 < self.actions.len() { - self.selected_item += 1; - cx.notify() - } - } - - fn visible(&self) -> bool { - !self.actions.is_empty() - } - - fn render( - &self, - mut cursor_position: DisplayPoint, - style: EditorStyle, - ) -> (DisplayPoint, ElementBox) { - enum ActionTag {} - - let container_style = style.autocomplete.container; - let actions = self.actions.clone(); - let selected_item = self.selected_item; - let element = - UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| { - let start_ix = range.start; - for (ix, action) in actions[range].iter().enumerate() { - let item_ix = start_ix + ix; - items.push( - MouseEventHandler::new::(item_ix, cx, |state, _| { - let item_style = if item_ix == selected_item { - style.autocomplete.selected_item - } else if state.hovered { - style.autocomplete.hovered_item - } else { - style.autocomplete.item - }; - - Text::new(action.lsp_action.title.clone(), style.text.clone()) - .with_soft_wrap(false) - .contained() - .with_style(item_style) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { - cx.dispatch_action(ConfirmCodeAction(Some(item_ix))); - }) - .boxed(), - ); - } - }) - .with_width_from_item( - self.actions - .iter() - .enumerate() - .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) - .map(|(ix, _)| ix), - ) - .contained() - .with_style(container_style) - .boxed(); - - if self.deployed_from_indicator { - *cursor_position.column_mut() = 0; - } - - (cursor_position, element) - } -} - -#[derive(Debug)] -struct ActiveDiagnosticGroup { - primary_range: Range, - primary_message: String, - blocks: HashMap, - is_valid: bool, -} - -#[derive(Serialize, Deserialize)] -struct ClipboardSelection { - len: usize, - is_entire_line: bool, -} - -pub struct NavigationData { - anchor: Anchor, - offset: usize, -} - -impl Editor { - pub fn single_line( - settings: watch::Receiver, - field_editor_style: Option, - cx: &mut ViewContext, - ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine, - buffer, - None, - settings, - field_editor_style, - cx, - ) - } - - pub fn auto_height( - max_lines: usize, - settings: watch::Receiver, - field_editor_style: Option, - cx: &mut ViewContext, - ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::AutoHeight { max_lines }, - buffer, - None, - settings, - field_editor_style, - cx, - ) - } - - pub fn for_buffer( - buffer: ModelHandle, - project: Option>, - settings: watch::Receiver, - cx: &mut ViewContext, - ) -> Self { - Self::new(EditorMode::Full, buffer, project, settings, None, cx) - } - - pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { - let mut clone = Self::new( - self.mode, - self.buffer.clone(), - self.project.clone(), - self.settings.clone(), - self.get_field_editor_theme, - cx, - ); - clone.scroll_position = self.scroll_position; - clone.scroll_top_anchor = self.scroll_top_anchor.clone(); - clone.nav_history = Some(nav_history); - clone - } - - fn new( - mode: EditorMode, - buffer: ModelHandle, - project: Option>, - settings: watch::Receiver, - get_field_editor_theme: Option, - cx: &mut ViewContext, - ) -> Self { - let display_map = cx.add_model(|cx| { - let settings = settings.borrow(); - let style = build_style(&*settings, get_field_editor_theme, cx); - DisplayMap::new( - buffer.clone(), - settings.tab_size, - style.text.font_id, - style.text.font_size, - None, - 2, - 1, - cx, - ) - }); - cx.observe(&buffer, Self::on_buffer_changed).detach(); - cx.subscribe(&buffer, Self::on_buffer_event).detach(); - cx.observe(&display_map, Self::on_display_map_changed) - .detach(); - - let mut this = Self { - handle: cx.weak_handle(), - buffer, - display_map, - selections: Arc::from([]), - pending_selection: Some(PendingSelection { - selection: Selection { - id: 0, - start: Anchor::min(), - end: Anchor::min(), - reversed: false, - goal: SelectionGoal::None, - }, - mode: SelectMode::Character, - }), - columnar_selection_tail: None, - next_selection_id: 1, - add_selections_state: None, - select_next_state: None, - selection_history: Default::default(), - autoclose_stack: Default::default(), - snippet_stack: Default::default(), - select_larger_syntax_node_stack: Vec::new(), - active_diagnostics: None, - settings, - soft_wrap_mode_override: None, - get_field_editor_theme, - project, - scroll_position: Vector2F::zero(), - scroll_top_anchor: None, - autoscroll_request: None, - focused: false, - show_local_cursors: false, - blink_epoch: 0, - blinking_paused: false, - mode, - vertical_scroll_margin: 3.0, - placeholder_text: None, - highlighted_rows: None, - highlighted_ranges: Default::default(), - nav_history: None, - context_menu: None, - completion_tasks: Default::default(), - next_completion_id: 0, - available_code_actions: Default::default(), - code_actions_task: Default::default(), - document_highlights_task: Default::default(), - pending_rename: Default::default(), - }; - this.end_selection(cx); - this - } - - pub fn open_new( - workspace: &mut Workspace, - _: &workspace::OpenNew, - cx: &mut ViewContext, - ) { - let buffer = cx - .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx)); - workspace.open_item(BufferItemHandle(buffer), cx); - } - - pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { - self.buffer.read(cx).replica_id() - } - - pub fn buffer(&self) -> &ModelHandle { - &self.buffer - } - - pub fn title(&self, cx: &AppContext) -> String { - self.buffer().read(cx).title(cx) - } - - pub fn snapshot(&mut self, cx: &mut MutableAppContext) -> EditorSnapshot { - EditorSnapshot { - mode: self.mode, - display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), - scroll_position: self.scroll_position, - scroll_top_anchor: self.scroll_top_anchor.clone(), - placeholder_text: self.placeholder_text.clone(), - is_focused: self - .handle - .upgrade(cx) - .map_or(false, |handle| handle.is_focused(cx)), - } - } - - pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { - self.buffer.read(cx).language(cx) - } - - fn style(&self, cx: &AppContext) -> EditorStyle { - build_style(&*self.settings.borrow(), self.get_field_editor_theme, cx) - } - - pub fn set_placeholder_text( - &mut self, - placeholder_text: impl Into>, - cx: &mut ViewContext, - ) { - self.placeholder_text = Some(placeholder_text.into()); - cx.notify(); - } - - pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext) { - self.vertical_scroll_margin = margin_rows as f32; - cx.notify(); - } - - pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { - let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if scroll_position.y() == 0. { - self.scroll_top_anchor = None; - self.scroll_position = scroll_position; - } else { - let scroll_top_buffer_offset = - DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); - let anchor = map - .buffer_snapshot - .anchor_at(scroll_top_buffer_offset, Bias::Right); - self.scroll_position = vec2f( - scroll_position.x(), - scroll_position.y() - anchor.to_display_point(&map).row() as f32, - ); - self.scroll_top_anchor = Some(anchor); - } - - cx.notify(); - } - - pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) - } - - pub fn clamp_scroll_left(&mut self, max: f32) -> bool { - if max < self.scroll_position.x() { - self.scroll_position.set_x(max); - true - } else { - false - } - } - - pub fn autoscroll_vertically( - &mut self, - viewport_height: f32, - line_height: f32, - cx: &mut ViewContext, - ) -> bool { - let visible_lines = viewport_height / line_height; - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut scroll_position = - compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor); - let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { - (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) - } else { - display_map.max_point().row().saturating_sub(1) as f32 - }; - if scroll_position.y() > max_scroll_top { - scroll_position.set_y(max_scroll_top); - self.set_scroll_position(scroll_position, cx); - } - - let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() { - autoscroll - } else { - return false; - }; - - let first_cursor_top; - let last_cursor_bottom; - if let Some(highlighted_rows) = &self.highlighted_rows { - first_cursor_top = highlighted_rows.start as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else if autoscroll == Autoscroll::Newest { - let newest_selection = self.newest_selection::(&display_map.buffer_snapshot); - first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else { - let selections = self.local_selections::(cx); - first_cursor_top = selections - .first() - .unwrap() - .head() - .to_display_point(&display_map) - .row() as f32; - last_cursor_bottom = selections - .last() - .unwrap() - .head() - .to_display_point(&display_map) - .row() as f32 - + 1.0; - } - - let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { - 0. - } else { - ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() - }; - if margin < 0.0 { - return false; - } - - match autoscroll { - Autoscroll::Fit | Autoscroll::Newest => { - let margin = margin.min(self.vertical_scroll_margin); - let target_top = (first_cursor_top - margin).max(0.0); - let target_bottom = last_cursor_bottom + margin; - let start_row = scroll_position.y(); - let end_row = start_row + visible_lines; - - if target_top < start_row { - scroll_position.set_y(target_top); - self.set_scroll_position(scroll_position, cx); - } else if target_bottom >= end_row { - scroll_position.set_y(target_bottom - visible_lines); - self.set_scroll_position(scroll_position, cx); - } - } - Autoscroll::Center => { - scroll_position.set_y((first_cursor_top - margin).max(0.0)); - self.set_scroll_position(scroll_position, cx); - } - } - - true - } - - pub fn autoscroll_horizontally( - &mut self, - start_row: u32, - viewport_width: f32, - scroll_width: f32, - max_glyph_width: f32, - layouts: &[text_layout::Line], - cx: &mut ViewContext, - ) -> bool { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.local_selections::(cx); - - let mut target_left; - let mut target_right; - - if self.highlighted_rows.is_some() { - target_left = 0.0_f32; - target_right = 0.0_f32; - } else { - target_left = std::f32::INFINITY; - target_right = 0.0_f32; - for selection in selections { - let head = selection.head().to_display_point(&display_map); - if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { - let start_column = head.column().saturating_sub(3); - let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); - target_left = target_left.min( - layouts[(head.row() - start_row) as usize] - .x_for_index(start_column as usize), - ); - target_right = target_right.max( - layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) - + max_glyph_width, - ); - } - } - } - - target_right = target_right.min(scroll_width); - - if target_right - target_left > viewport_width { - return false; - } - - let scroll_left = self.scroll_position.x() * max_glyph_width; - let scroll_right = scroll_left + viewport_width; - - if target_left < scroll_left { - self.scroll_position.set_x(target_left / max_glyph_width); - true - } else if target_right > scroll_right { - self.scroll_position - .set_x((target_right - viewport_width) / max_glyph_width); - true - } else { - false - } - } - - fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { - self.hide_context_menu(cx); - - match phase { - SelectPhase::Begin { - position, - add, - click_count, - } => self.begin_selection(*position, *add, *click_count, cx), - SelectPhase::BeginColumnar { - position, - overshoot, - } => self.begin_columnar_selection(*position, *overshoot, cx), - SelectPhase::Extend { - position, - click_count, - } => self.extend_selection(*position, *click_count, cx), - SelectPhase::Update { - position, - overshoot, - scroll_position, - } => self.update_selection(*position, *overshoot, *scroll_position, cx), - SelectPhase::End => self.end_selection(cx), - } - } - - fn extend_selection( - &mut self, - position: DisplayPoint, - click_count: usize, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self - .newest_selection::(&display_map.buffer_snapshot) - .tail(); - self.begin_selection(position, false, click_count, cx); - - let position = position.to_offset(&display_map, Bias::Left); - let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); - let mut pending = self.pending_selection.clone().unwrap(); - - if position >= tail { - pending.selection.start = tail_anchor.clone(); - } else { - pending.selection.end = tail_anchor.clone(); - pending.selection.reversed = true; - } - - match &mut pending.mode { - SelectMode::Word(range) | SelectMode::Line(range) => { - *range = tail_anchor.clone()..tail_anchor - } - _ => {} - } - - self.set_selections(self.selections.clone(), Some(pending), cx); - } - - fn begin_selection( - &mut self, - position: DisplayPoint, - add: bool, - click_count: usize, - cx: &mut ViewContext, - ) { - if !self.focused { - cx.focus_self(); - cx.emit(Event::Activate); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let newest_selection = self.newest_anchor_selection().clone(); - - let start; - let end; - let mode; - match click_count { - 1 => { - start = buffer.anchor_before(position.to_point(&display_map)); - end = start.clone(); - mode = SelectMode::Character; - } - 2 => { - let range = movement::surrounding_word(&display_map, position); - start = buffer.anchor_before(range.start.to_point(&display_map)); - end = buffer.anchor_before(range.end.to_point(&display_map)); - mode = SelectMode::Word(start.clone()..end.clone()); - } - 3 => { - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - start = buffer.anchor_before(line_start); - end = buffer.anchor_before(next_line_start); - mode = SelectMode::Line(start.clone()..end.clone()); - } - _ => { - start = buffer.anchor_before(0); - end = buffer.anchor_before(buffer.len()); - mode = SelectMode::All; - } - } - - self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); - - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start, - end, - reversed: false, - goal: SelectionGoal::None, - }; - - let mut selections; - if add { - selections = self.selections.clone(); - // Remove the newest selection if it was added due to a previous mouse up - // within this multi-click. - if click_count > 1 { - selections = self - .selections - .iter() - .filter(|selection| selection.id != newest_selection.id) - .cloned() - .collect(); - } - } else { - selections = Arc::from([]); - } - self.set_selections(selections, Some(PendingSelection { selection, mode }), cx); - - cx.notify(); - } - - fn begin_columnar_selection( - &mut self, - position: DisplayPoint, - overshoot: u32, - cx: &mut ViewContext, - ) { - if !self.focused { - cx.focus_self(); - cx.emit(Event::Activate); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self - .newest_selection::(&display_map.buffer_snapshot) - .tail(); - self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); - - self.select_columns( - tail.to_display_point(&display_map), - position, - overshoot, - &display_map, - cx, - ); - } - - fn update_selection( - &mut self, - position: DisplayPoint, - overshoot: u32, - scroll_position: Vector2F, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if let Some(tail) = self.columnar_selection_tail.as_ref() { - let tail = tail.to_display_point(&display_map); - self.select_columns(tail, position, overshoot, &display_map, cx); - } else if let Some(mut pending) = self.pending_selection.clone() { - let buffer = self.buffer.read(cx).snapshot(cx); - let head; - let tail; - match &pending.mode { - SelectMode::Character => { - head = position.to_point(&display_map); - tail = pending.selection.tail().to_point(&buffer); - } - SelectMode::Word(original_range) => { - let original_display_range = original_range.start.to_display_point(&display_map) - ..original_range.end.to_display_point(&display_map); - let original_buffer_range = original_display_range.start.to_point(&display_map) - ..original_display_range.end.to_point(&display_map); - if movement::is_inside_word(&display_map, position) - || original_display_range.contains(&position) - { - let word_range = movement::surrounding_word(&display_map, position); - if word_range.start < original_display_range.start { - head = word_range.start.to_point(&display_map); - } else { - head = word_range.end.to_point(&display_map); - } - } else { - head = position.to_point(&display_map); - } - - if head <= original_buffer_range.start { - tail = original_buffer_range.end; - } else { - tail = original_buffer_range.start; - } - } - SelectMode::Line(original_range) => { - let original_range = original_range.to_point(&display_map.buffer_snapshot); - - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - - if line_start < original_range.start { - head = line_start - } else { - head = next_line_start - } - - if head <= original_range.start { - tail = original_range.end; - } else { - tail = original_range.start; - } - } - SelectMode::All => { - return; - } - }; - - if head < tail { - pending.selection.start = buffer.anchor_before(head); - pending.selection.end = buffer.anchor_before(tail); - pending.selection.reversed = true; - } else { - pending.selection.start = buffer.anchor_before(tail); - pending.selection.end = buffer.anchor_before(head); - pending.selection.reversed = false; - } - self.set_selections(self.selections.clone(), Some(pending), cx); - } else { - log::error!("update_selection dispatched with no pending selection"); - return; - } - - self.set_scroll_position(scroll_position, cx); - cx.notify(); - } - - fn end_selection(&mut self, cx: &mut ViewContext) { - self.columnar_selection_tail.take(); - if self.pending_selection.is_some() { - let selections = self.local_selections::(cx); - self.update_selections(selections, None, cx); - } - } - - fn select_columns( - &mut self, - tail: DisplayPoint, - head: DisplayPoint, - overshoot: u32, - display_map: &DisplaySnapshot, - cx: &mut ViewContext, - ) { - let start_row = cmp::min(tail.row(), head.row()); - let end_row = cmp::max(tail.row(), head.row()); - let start_column = cmp::min(tail.column(), head.column() + overshoot); - let end_column = cmp::max(tail.column(), head.column() + overshoot); - let reversed = start_column < tail.column(); - - let selections = (start_row..=end_row) - .filter_map(|row| { - if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { - let start = display_map - .clip_point(DisplayPoint::new(row, start_column), Bias::Left) - .to_point(&display_map); - let end = display_map - .clip_point(DisplayPoint::new(row, end_column), Bias::Right) - .to_point(&display_map); - Some(Selection { - id: post_inc(&mut self.next_selection_id), - start, - end, - reversed, - goal: SelectionGoal::None, - }) - } else { - None - } - }) - .collect::>(); - - self.update_selections(selections, None, cx); - cx.notify(); - } - - pub fn is_selecting(&self) -> bool { - self.pending_selection.is_some() || self.columnar_selection_tail.is_some() - } - - pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if self.take_rename(cx).is_some() { - return; - } - - if self.hide_context_menu(cx).is_some() { - return; - } - - if self.snippet_stack.pop().is_some() { - return; - } - - if self.mode != EditorMode::Full { - cx.propagate_action(); - return; - } - - if self.active_diagnostics.is_some() { - self.dismiss_diagnostics(cx); - } else if let Some(pending) = self.pending_selection.clone() { - let mut selections = self.selections.clone(); - if selections.is_empty() { - selections = Arc::from([pending.selection]); - } - self.set_selections(selections, None, cx); - self.request_autoscroll(Autoscroll::Fit, cx); - } else { - let buffer = self.buffer.read(cx).snapshot(cx); - let mut oldest_selection = self.oldest_selection::(&buffer); - if self.selection_count() == 1 { - if oldest_selection.is_empty() { - cx.propagate_action(); - return; - } - - oldest_selection.start = oldest_selection.head().clone(); - oldest_selection.end = oldest_selection.head().clone(); - } - self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx); - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn selected_ranges>( - &self, - cx: &mut MutableAppContext, - ) -> Vec> { - self.local_selections::(cx) - .iter() - .map(|s| { - if s.reversed { - s.end.clone()..s.start.clone() - } else { - s.start.clone()..s.end.clone() - } - }) - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec> { - let display_map = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); - self.selections - .iter() - .chain( - self.pending_selection - .as_ref() - .map(|pending| &pending.selection), - ) - .map(|s| { - if s.reversed { - s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) - } else { - s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map) - } - }) - .collect() - } - - pub fn select_ranges( - &mut self, - ranges: I, - autoscroll: Option, - cx: &mut ViewContext, - ) where - I: IntoIterator>, - T: ToOffset, - { - let buffer = self.buffer.read(cx).snapshot(cx); - let selections = ranges - .into_iter() - .map(|range| { - let mut start = range.start.to_offset(&buffer); - let mut end = range.end.to_offset(&buffer); - let reversed = if start > end { - mem::swap(&mut start, &mut end); - true - } else { - false - }; - Selection { - id: post_inc(&mut self.next_selection_id), - start, - end, - reversed, - goal: SelectionGoal::None, - } - }) - .collect::>(); - self.update_selections(selections, autoscroll, cx); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext) - where - T: IntoIterator>, - { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = ranges - .into_iter() - .map(|range| { - let mut start = range.start; - let mut end = range.end; - let reversed = if start > end { - mem::swap(&mut start, &mut end); - true - } else { - false - }; - Selection { - id: post_inc(&mut self.next_selection_id), - start: start.to_point(&display_map), - end: end.to_point(&display_map), - reversed, - goal: SelectionGoal::None, - } - }) - .collect(); - self.update_selections(selections, None, cx); - } - - pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { - let text = action.0.as_ref(); - if !self.skip_autoclose_end(text, cx) { - self.start_transaction(cx); - self.insert(text, cx); - self.autoclose_pairs(cx); - self.end_transaction(cx); - self.trigger_completion_on_input(text, cx); - } - } - - pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut old_selections = SmallVec::<[_; 32]>::new(); - { - let selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).snapshot(cx); - for selection in selections.iter() { - let start_point = selection.start.to_point(&buffer); - let indent = buffer - .indent_column_for_line(start_point.row) - .min(start_point.column); - let start = selection.start; - let end = selection.end; - - let mut insert_extra_newline = false; - if let Some(language) = buffer.language() { - let leading_whitespace_len = buffer - .reversed_chars_at(start) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - let trailing_whitespace_len = buffer - .chars_at(end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - insert_extra_newline = language.brackets().iter().any(|pair| { - let pair_start = pair.start.trim_end(); - let pair_end = pair.end.trim_start(); - - pair.newline - && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) - && buffer.contains_str_at( - (start - leading_whitespace_len).saturating_sub(pair_start.len()), - pair_start, - ) - }); - } - - old_selections.push(( - selection.id, - buffer.anchor_after(end), - start..end, - indent, - insert_extra_newline, - )); - } - } - - self.buffer.update(cx, |buffer, cx| { - let mut delta = 0_isize; - let mut pending_edit: Option = None; - for (_, _, range, indent, insert_extra_newline) in &old_selections { - if pending_edit.as_ref().map_or(false, |pending| { - pending.indent != *indent - || pending.insert_extra_newline != *insert_extra_newline - }) { - let pending = pending_edit.take().unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); - } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); - delta += pending.delta; - } - - let start = (range.start as isize + delta) as usize; - let end = (range.end as isize + delta) as usize; - let mut text_len = *indent as usize + 1; - if *insert_extra_newline { - text_len *= 2; - } - - let pending = pending_edit.get_or_insert_with(Default::default); - pending.delta += text_len as isize - (end - start) as isize; - pending.indent = *indent; - pending.insert_extra_newline = *insert_extra_newline; - pending.ranges.push(start..end); - } - - let pending = pending_edit.unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); - } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); - - let buffer = buffer.read(cx); - self.selections = self - .selections - .iter() - .cloned() - .zip(old_selections) - .map( - |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { - let mut cursor = end_anchor.to_point(&buffer); - if insert_extra_newline { - cursor.row -= 1; - cursor.column = buffer.line_len(cursor.row); - } - let anchor = buffer.anchor_after(cursor); - new_selection.start = anchor.clone(); - new_selection.end = anchor; - new_selection - }, - ) - .collect(); - }); - - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); - - #[derive(Default)] - struct PendingEdit { - indent: u32, - insert_extra_newline: bool, - delta: isize, - ranges: SmallVec<[Range; 32]>, - } - } - - pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { - self.start_transaction(cx); - - let old_selections = self.local_selections::(cx); - let selection_anchors = self.buffer.update(cx, |buffer, cx| { - let anchors = { - let snapshot = buffer.read(cx); - old_selections - .iter() - .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) - .collect::>() - }; - let edit_ranges = old_selections.iter().map(|s| s.start..s.end); - buffer.edit_with_autoindent(edit_ranges, text, cx); - anchors - }); - - let selections = { - let snapshot = self.buffer.read(cx).read(cx); - selection_anchors - .into_iter() - .map(|(id, goal, position)| { - let position = position.to_offset(&snapshot); - Selection { - id, - start: position, - end: position, - goal, - reversed: false, - } - }) - .collect() - }; - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { - let selection = self.newest_anchor_selection(); - if self - .buffer - .read(cx) - .is_completion_trigger(selection.head(), text, cx) - { - self.show_completions(&ShowCompletions, cx); - } else { - self.hide_context_menu(cx); - } - } - - fn autoclose_pairs(&mut self, cx: &mut ViewContext) { - let selections = self.local_selections::(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 { - id: selection.id, - start: snapshot.anchor_before(selection.start), - end: snapshot.anchor_before(selection.end), - reversed: selection.reversed, - goal: selection.goal, - }) - .collect::>(); - - let autoclose_pair = snapshot.language().and_then(|language| { - let first_selection_start = selections.first().unwrap().start; - let pair = language.brackets().iter().find(|pair| { - snapshot.contains_str_at( - first_selection_start.saturating_sub(pair.start.len()), - &pair.start, - ) - }); - pair.and_then(|pair| { - let should_autoclose = selections[1..].iter().all(|selection| { - snapshot.contains_str_at( - selection.start.saturating_sub(pair.start.len()), - &pair.start, - ) - }); - - 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::>(); - - buffer.edit(selection_ranges, &pair.end, cx); - snapshot = buffer.snapshot(cx); - - new_selections = Some( - self.resolve_selections::(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, - }); - } - } - }); - - if let Some(new_selections) = new_selections { - self.update_selections(new_selections, None, cx); - } - if let Some(bracket_pair_state) = bracket_pair_state { - self.autoclose_stack.push(bracket_pair_state); - } - } - - fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { - let old_selections = self.local_selections::(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; - } - - debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); - - let buffer = self.buffer.read(cx).snapshot(cx); - 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.update_selections(new_selections, Some(Autoscroll::Fit), cx); - true - } else { - false - } - } - - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { - let offset = position.to_offset(buffer); - let (word_range, kind) = buffer.surrounding_word(offset); - if offset > word_range.start && kind == Some(CharKind::Word) { - Some( - buffer - .text_for_range(word_range.start..offset) - .collect::(), - ) - } else { - None - } - } - - fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { - if self.pending_rename.is_some() { - return; - } - - let project = if let Some(project) = self.project.clone() { - project - } else { - return; - }; - - let position = self.newest_anchor_selection().head(); - let (buffer, buffer_position) = if let Some(output) = self - .buffer - .read(cx) - .text_anchor_for_position(position.clone(), cx) - { - output - } else { - return; - }; - - let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); - let completions = project.update(cx, |project, cx| { - project.completions(&buffer, buffer_position.clone(), cx) - }); - - let id = post_inc(&mut self.next_completion_id); - let task = cx.spawn_weak(|this, mut cx| { - async move { - let completions = completions.await?; - if completions.is_empty() { - return Ok(()); - } - - let mut menu = CompletionsMenu { - id, - initial_position: position, - match_candidates: completions - .iter() - .enumerate() - .map(|(id, completion)| { - StringMatchCandidate::new( - id, - completion.label.text[completion.label.filter_range.clone()].into(), - ) - }) - .collect(), - buffer, - completions: completions.into(), - matches: Vec::new().into(), - selected_item: 0, - list: Default::default(), - }; - - menu.filter(query.as_deref(), cx.background()).await; - - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - match this.context_menu.as_ref() { - None => {} - Some(ContextMenu::Completions(prev_menu)) => { - if prev_menu.id > menu.id { - return; - } - } - _ => return, - } - - this.completion_tasks.retain(|(id, _)| *id > menu.id); - if this.focused { - this.show_context_menu(ContextMenu::Completions(menu), cx); - } - - cx.notify(); - }); - } - Ok::<_, anyhow::Error>(()) - } - .log_err() - }); - self.completion_tasks.push((id, task)); - } - - pub fn confirm_completion( - &mut self, - ConfirmCompletion(completion_ix): &ConfirmCompletion, - cx: &mut ViewContext, - ) -> Option>> { - use language::ToOffset as _; - - let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { - menu - } else { - return None; - }; - - let mat = completions_menu - .matches - .get(completion_ix.unwrap_or(completions_menu.selected_item))?; - let buffer_handle = completions_menu.buffer; - let completion = completions_menu.completions.get(mat.candidate_id)?; - - let snippet; - let text; - if completion.is_snippet() { - snippet = Some(Snippet::parse(&completion.new_text).log_err()?); - text = snippet.as_ref().unwrap().text.clone(); - } else { - snippet = None; - text = completion.new_text.clone(); - }; - let buffer = buffer_handle.read(cx); - let old_range = completion.old_range.to_offset(&buffer); - let old_text = buffer.text_for_range(old_range.clone()).collect::(); - - let selections = self.local_selections::(cx); - let newest_selection = self.newest_anchor_selection(); - if newest_selection.start.buffer_id != Some(buffer_handle.id()) { - return None; - } - - let lookbehind = newest_selection - .start - .text_anchor - .to_offset(buffer) - .saturating_sub(old_range.start); - let lookahead = old_range - .end - .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); - let mut common_prefix_len = old_text - .bytes() - .zip(text.bytes()) - .take_while(|(a, b)| a == b) - .count(); - - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut ranges = Vec::new(); - for selection in &selections { - if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { - let start = selection.start.saturating_sub(lookbehind); - let end = selection.end + lookahead; - ranges.push(start + common_prefix_len..end); - } else { - common_prefix_len = 0; - ranges.clear(); - ranges.extend(selections.iter().map(|s| { - if s.id == newest_selection.id { - old_range.clone() - } else { - s.start..s.end - } - })); - break; - } - } - let text = &text[common_prefix_len..]; - - self.start_transaction(cx); - if let Some(mut snippet) = snippet { - snippet.text = text.to_string(); - for tabstop in snippet.tabstops.iter_mut().flatten() { - tabstop.start -= common_prefix_len as isize; - tabstop.end -= common_prefix_len as isize; - } - - self.insert_snippet(&ranges, snippet, cx).log_err(); - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(ranges, text, cx); - }); - } - self.end_transaction(cx); - - let project = self.project.clone()?; - let apply_edits = project.update(cx, |project, cx| { - project.apply_additional_edits_for_completion( - buffer_handle, - completion.clone(), - true, - cx, - ) - }); - Some(cx.foreground().spawn(async move { - apply_edits.await?; - Ok(()) - })) - } - - pub fn toggle_code_actions( - &mut self, - &ToggleCodeActions(deployed_from_indicator): &ToggleCodeActions, - cx: &mut ViewContext, - ) { - if matches!( - self.context_menu.as_ref(), - Some(ContextMenu::CodeActions(_)) - ) { - self.context_menu.take(); - cx.notify(); - return; - } - - let mut task = self.code_actions_task.take(); - cx.spawn_weak(|this, mut cx| async move { - while let Some(prev_task) = task { - prev_task.await; - task = this - .upgrade(&cx) - .and_then(|this| this.update(&mut cx, |this, _| this.code_actions_task.take())); - } - - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if this.focused { - if let Some((buffer, actions)) = this.available_code_actions.clone() { - this.show_context_menu( - ContextMenu::CodeActions(CodeActionsMenu { - buffer, - actions, - selected_item: Default::default(), - list: Default::default(), - deployed_from_indicator, - }), - cx, - ); - } - } - }) - } - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - } - - pub fn confirm_code_action( - workspace: &mut Workspace, - ConfirmCodeAction(action_ix): &ConfirmCodeAction, - cx: &mut ViewContext, - ) -> Option>> { - let editor = workspace.active_item(cx)?.act_as::(cx)?; - let actions_menu = if let ContextMenu::CodeActions(menu) = - editor.update(cx, |editor, cx| editor.hide_context_menu(cx))? - { - menu - } else { - return None; - }; - let action_ix = action_ix.unwrap_or(actions_menu.selected_item); - let action = actions_menu.actions.get(action_ix)?.clone(); - let title = action.lsp_action.title.clone(); - let buffer = actions_menu.buffer; - - let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { - project.apply_code_action(buffer, action, true, cx) - }); - Some(cx.spawn(|workspace, cx| async move { - let project_transaction = apply_code_actions.await?; - Self::open_project_transaction(editor, workspace, project_transaction, title, cx).await - })) - } - - async fn open_project_transaction( - this: ViewHandle, - workspace: ViewHandle, - transaction: ProjectTransaction, - title: String, - mut cx: AsyncAppContext, - ) -> Result<()> { - let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); - - // If the code action's edits are all contained within this editor, then - // avoid opening a new editor to display them. - let mut entries = transaction.0.iter(); - if let Some((buffer, transaction)) = entries.next() { - if entries.next().is_none() { - let excerpt = this.read_with(&cx, |editor, cx| { - editor - .buffer() - .read(cx) - .excerpt_containing(editor.newest_anchor_selection().head(), cx) - }); - if let Some((excerpted_buffer, excerpt_range)) = excerpt { - if excerpted_buffer == *buffer { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - let excerpt_range = excerpt_range.to_offset(&snapshot); - if snapshot - .edited_ranges_for_transaction(transaction) - .all(|range| { - excerpt_range.start <= range.start && excerpt_range.end >= range.end - }) - { - return Ok(()); - } - } - } - } - } - - let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - for (buffer, transaction) in &transaction.0 { - let snapshot = buffer.read(cx).snapshot(); - ranges_to_highlight.extend( - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - snapshot - .edited_ranges_for_transaction::(transaction) - .collect(), - 1, - cx, - ), - ); - } - multibuffer.push_transaction(&transaction.0); - multibuffer - }); - - workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_ranges::(ranges_to_highlight, color, cx); - }); - } - }); - - Ok(()) - } - - fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { - let project = self.project.as_ref()?; - let buffer = self.buffer.read(cx); - let newest_selection = self.newest_anchor_selection().clone(); - let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; - let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?; - if start_buffer != end_buffer { - return None; - } - - let actions = project.update(cx, |project, cx| { - project.code_actions(&start_buffer, start..end, cx) - }); - self.code_actions_task = Some(cx.spawn_weak(|this, mut cx| async move { - let actions = actions.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.available_code_actions = actions.log_err().and_then(|actions| { - if actions.is_empty() { - None - } else { - Some((start_buffer, actions.into())) - } - }); - cx.notify(); - }) - } - })); - None - } - - fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { - let project = self.project.as_ref()?; - let buffer = self.buffer.read(cx); - let newest_selection = self.newest_anchor_selection().clone(); - let cursor_position = newest_selection.head(); - let (cursor_buffer, cursor_buffer_position) = - buffer.text_anchor_for_position(cursor_position.clone(), cx)?; - let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; - if cursor_buffer != tail_buffer { - return None; - } - - let highlights = project.update(cx, |project, cx| { - project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - }); - - enum DocumentHighlightRead {} - enum DocumentHighlightWrite {} - - self.document_highlights_task = Some(cx.spawn_weak(|this, mut cx| async move { - let highlights = highlights.log_err().await; - if let Some((this, highlights)) = this.upgrade(&cx).zip(highlights) { - this.update(&mut cx, |this, cx| { - let buffer_id = cursor_position.buffer_id; - let excerpt_id = cursor_position.excerpt_id.clone(); - let style = this.style(cx); - let read_background = style.document_highlight_read_background; - let write_background = style.document_highlight_write_background; - let buffer = this.buffer.read(cx); - if !buffer - .text_anchor_for_position(cursor_position, cx) - .map_or(false, |(buffer, _)| buffer == cursor_buffer) - { - return; - } - - let mut write_ranges = Vec::new(); - let mut read_ranges = Vec::new(); - for highlight in highlights { - let range = Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.start, - }..Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.end, - }; - if highlight.kind == lsp::DocumentHighlightKind::WRITE { - write_ranges.push(range); - } else { - read_ranges.push(range); - } - } - - this.highlight_ranges::( - read_ranges, - read_background, - cx, - ); - this.highlight_ranges::( - write_ranges, - write_background, - cx, - ); - cx.notify(); - }); - } - })); - None - } - - pub fn render_code_actions_indicator( - &self, - style: &EditorStyle, - cx: &mut ViewContext, - ) -> Option { - if self.available_code_actions.is_some() { - enum Tag {} - Some( - MouseEventHandler::new::(0, cx, |_, _| { - Svg::new("icons/zap.svg") - .with_color(style.code_actions_indicator) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_padding(Padding::uniform(3.)) - .on_mouse_down(|cx| { - cx.dispatch_action(ToggleCodeActions(true)); - }) - .boxed(), - ) - } else { - None - } - } - - pub fn context_menu_visible(&self) -> bool { - self.context_menu - .as_ref() - .map_or(false, |menu| menu.visible()) - } - - pub fn render_context_menu( - &self, - cursor_position: DisplayPoint, - style: EditorStyle, - cx: &AppContext, - ) -> Option<(DisplayPoint, ElementBox)> { - self.context_menu - .as_ref() - .map(|menu| menu.render(cursor_position, style, cx)) - } - - fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { - if !matches!(menu, ContextMenu::Completions(_)) { - self.completion_tasks.clear(); - } - self.context_menu = Some(menu); - cx.notify(); - } - - fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { - cx.notify(); - self.completion_tasks.clear(); - self.context_menu.take() - } - - pub fn insert_snippet( - &mut self, - insertion_ranges: &[Range], - snippet: Snippet, - cx: &mut ViewContext, - ) -> Result<()> { - let tabstops = self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(insertion_ranges.iter().cloned(), &snippet.text, cx); - - let snapshot = &*buffer.read(cx); - let snippet = &snippet; - snippet - .tabstops - .iter() - .map(|tabstop| { - let mut tabstop_ranges = tabstop - .iter() - .flat_map(|tabstop_range| { - let mut delta = 0 as isize; - insertion_ranges.iter().map(move |insertion_range| { - let insertion_start = insertion_range.start as isize + delta; - delta += - snippet.text.len() as isize - insertion_range.len() as isize; - - let start = snapshot.anchor_before( - (insertion_start + tabstop_range.start) as usize, - ); - let end = snapshot - .anchor_after((insertion_start + tabstop_range.end) as usize); - start..end - }) - }) - .collect::>(); - tabstop_ranges - .sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot).unwrap()); - tabstop_ranges - }) - .collect::>() - }); - - if let Some(tabstop) = tabstops.first() { - self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx); - self.snippet_stack.push(SnippetState { - active_index: 0, - ranges: tabstops, - }); - } - - Ok(()) - } - - pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { - self.move_to_snippet_tabstop(Bias::Right, cx) - } - - pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) { - self.move_to_snippet_tabstop(Bias::Left, cx); - } - - pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { - let buffer = self.buffer.read(cx).snapshot(cx); - - if let Some(snippet) = self.snippet_stack.last_mut() { - match bias { - Bias::Left => { - if snippet.active_index > 0 { - snippet.active_index -= 1; - } else { - return false; - } - } - Bias::Right => { - if snippet.active_index + 1 < snippet.ranges.len() { - snippet.active_index += 1; - } else { - return false; - } - } - } - if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { - let new_selections = current_ranges - .iter() - .map(|new_range| { - let new_range = new_range.to_offset(&buffer); - Selection { - id: post_inc(&mut self.next_selection_id), - start: new_range.start, - end: new_range.end, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - - // Remove the snippet state when moving to the last tabstop. - if snippet.active_index + 1 == snippet.ranges.len() { - self.snippet_stack.pop(); - } - - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - return true; - } - self.snippet_stack.pop(); - } - - false - } - - pub fn clear(&mut self, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_all(&SelectAll, cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert(&"", cx); - self.end_transaction(cx); - } - - pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if self.move_to_next_snippet_tabstop(cx) { - return; - } - - self.start_transaction(cx); - let tab_size = self.settings.borrow().tab_size; - let mut selections = self.local_selections::(cx); - let mut last_indent = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - if selection.is_empty() { - let char_column = buffer - .read(cx) - .text_for_range(Point::new(selection.start.row, 0)..selection.start) - .flat_map(str::chars) - .count(); - let chars_to_next_tab_stop = tab_size - (char_column % tab_size); - buffer.edit( - [selection.start..selection.start], - " ".repeat(chars_to_next_tab_stop), - cx, - ); - selection.start.column += chars_to_next_tab_stop as u32; - selection.end = selection.start; - } else { - let mut start_row = selection.start.row; - let mut end_row = selection.end.row + 1; - - // If a selection ends at the beginning of a line, don't indent - // that last line. - if selection.end.column == 0 { - end_row -= 1; - } - - // Avoid re-indenting a row that has already been indented by a - // previous selection, but still update this selection's column - // to reflect that indentation. - if let Some((last_indent_row, last_indent_len)) = last_indent { - if last_indent_row == selection.start.row { - selection.start.column += last_indent_len; - start_row += 1; - } - if last_indent_row == selection.end.row { - selection.end.column += last_indent_len; - } - } - - for row in start_row..end_row { - let indent_column = buffer.read(cx).indent_column_for_line(row) as usize; - let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); - let row_start = Point::new(row, 0); - buffer.edit( - [row_start..row_start], - " ".repeat(columns_to_next_tab_stop), - cx, - ); - - // Update this selection's endpoints to reflect the indentation. - if row == selection.start.row { - selection.start.column += columns_to_next_tab_stop as u32; - } - if row == selection.end.row { - selection.end.column += columns_to_next_tab_stop as u32; - } - - last_indent = Some((row, columns_to_next_tab_stop as u32)); - } - } - } - }); - - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { - if !self.snippet_stack.is_empty() { - self.move_to_prev_snippet_tabstop(cx); - return; - } - - self.start_transaction(cx); - let tab_size = self.settings.borrow().tab_size; - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut deletion_ranges = Vec::new(); - let mut last_outdent = None; - { - let buffer = self.buffer.read(cx).read(cx); - for selection in &selections { - let mut rows = selection.spanned_rows(false, &display_map); - - // Avoid re-outdenting a row that has already been outdented by a - // previous selection. - if let Some(last_row) = last_outdent { - if last_row == rows.start { - rows.start += 1; - } - } - - for row in rows { - let column = buffer.indent_column_for_line(row) as usize; - if column > 0 { - let mut deletion_len = (column % tab_size) as u32; - if deletion_len == 0 { - deletion_len = tab_size as u32; - } - deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len)); - last_outdent = Some(row); - } - } - } - } - self.buffer.update(cx, |buffer, cx| { - buffer.edit(deletion_ranges, "", cx); - }); - - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); - } - - pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { - self.start_transaction(cx); - - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut new_cursors = Vec::new(); - let mut edit_ranges = Vec::new(); - let mut selections = selections.iter().peekable(); - while let Some(selection) = selections.next() { - let mut rows = selection.spanned_rows(false, &display_map); - let goal_display_column = selection.head().to_display_point(&display_map).column(); - - // Accumulate contiguous regions of rows that we want to delete. - while let Some(next_selection) = selections.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start <= rows.end { - rows.end = next_rows.end; - selections.next().unwrap(); - } else { - break; - } - } - - let mut edit_start = Point::new(rows.start, 0).to_offset(&buffer); - let edit_end; - let cursor_buffer_row; - if buffer.max_point().row >= rows.end { - // If there's a line after the range, delete the \n from the end of the row range - // and position the cursor on the next line. - edit_end = Point::new(rows.end, 0).to_offset(&buffer); - cursor_buffer_row = rows.end; - } else { - // If there isn't a line after the range, delete the \n from the line before the - // start of the row range and position the cursor there. - edit_start = edit_start.saturating_sub(1); - edit_end = buffer.len(); - cursor_buffer_row = rows.start.saturating_sub(1); - } - - let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map); - *cursor.column_mut() = - cmp::min(goal_display_column, display_map.line_len(cursor.row())); - - new_cursors.push(( - selection.id, - buffer.anchor_after(cursor.to_point(&display_map)), - )); - edit_ranges.push(edit_start..edit_end); - } - - let buffer = self.buffer.update(cx, |buffer, cx| { - buffer.edit(edit_ranges, "", cx); - buffer.snapshot(cx) - }); - let new_selections = new_cursors - .into_iter() - .map(|(id, cursor)| { - let cursor = cursor.to_point(&buffer); - Selection { - id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { - self.start_transaction(cx); - - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - - let mut edits = Vec::new(); - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - // Avoid duplicating the same lines twice. - let mut rows = selection.spanned_rows(false, &display_map); - - while let Some(next_selection) = selections_iter.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start <= rows.end - 1 { - rows.end = next_rows.end; - selections_iter.next().unwrap(); - } else { - break; - } - } - - // Copy the text from the selected row region and splice it at the start of the region. - let start = Point::new(rows.start, 0); - let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1)); - let text = buffer - .text_for_range(start..end) - .chain(Some("\n")) - .collect::(); - edits.push((start, text, rows.len() as u32)); - } - - self.buffer.update(cx, |buffer, cx| { - for (point, text, _) in edits.into_iter().rev() { - buffer.edit(Some(point..point), text, cx); - } - }); - - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); - } - - pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_ranges = Vec::new(); - - let selections = self.local_selections::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - contiguous_row_selections.push(selection.clone()); - let start_row = selection.start.row; - let mut end_row = if selection.end.column > 0 || selection.is_empty() { - display_map.next_line_boundary(selection.end).0.row + 1 - } else { - selection.end.row - }; - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row { - end_row = if next_selection.end.column > 0 || next_selection.is_empty() { - display_map.next_line_boundary(next_selection.end).0.row + 1 - } else { - next_selection.end.row - }; - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } - - // Move the text spanned by the row range to be before the line preceding the row range - if start_row > 0 { - let range_to_move = Point::new(start_row - 1, buffer.line_len(start_row - 1)) - ..Point::new(end_row - 1, buffer.line_len(end_row - 1)); - let insertion_point = display_map - .prev_line_boundary(Point::new(start_row - 1, 0)) - .0; - - // Don't move lines across excerpts - if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(insertion_point), - Bound::Included(range_to_move.end), - )) - .next() - .is_none() - { - let text = buffer - .text_for_range(range_to_move.clone()) - .flat_map(|s| s.chars()) - .skip(1) - .chain(['\n']) - .collect::(); - - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor.clone()..insertion_anchor, text)); - - let row_delta = range_to_move.start.row - insertion_point.row + 1; - - // Move selections up - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row -= row_delta; - selection.end.row -= row_delta; - selection - }, - )); - - // Move folds up - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.start.to_point(&buffer); - let mut end = fold.end.to_point(&buffer); - start.row -= row_delta; - end.row -= row_delta; - refold_ranges.push(start..end); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.extend(contiguous_row_selections.drain(..)); - } - - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } - }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_ranges = Vec::new(); - - let selections = self.local_selections::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - contiguous_row_selections.push(selection.clone()); - let start_row = selection.start.row; - let mut end_row = if selection.end.column > 0 || selection.is_empty() { - display_map.next_line_boundary(selection.end).0.row + 1 - } else { - selection.end.row - }; - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row { - end_row = if next_selection.end.column > 0 || next_selection.is_empty() { - display_map.next_line_boundary(next_selection.end).0.row + 1 - } else { - next_selection.end.row - }; - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } - - // Move the text spanned by the row range to be after the last line of the row range - if end_row <= buffer.max_point().row { - let range_to_move = Point::new(start_row, 0)..Point::new(end_row, 0); - let insertion_point = display_map.next_line_boundary(Point::new(end_row, 0)).0; - - // Don't move lines across excerpt boundaries - if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(range_to_move.start), - Bound::Included(insertion_point), - )) - .next() - .is_none() - { - let mut text = String::from("\n"); - text.extend(buffer.text_for_range(range_to_move.clone())); - text.pop(); // Drop trailing newline - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor.clone()..insertion_anchor, text)); - - let row_delta = insertion_point.row - range_to_move.end.row + 1; - - // Move selections down - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row += row_delta; - selection.end.row += row_delta; - selection - }, - )); - - // Move folds down - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.start.to_point(&buffer); - let mut end = fold.end.to_point(&buffer); - start.row += row_delta; - end.row += row_delta; - refold_ranges.push(start..end); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.extend(contiguous_row_selections.drain(..)); - } - - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } - }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut text = String::new(); - let mut selections = self.local_selections::(cx); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let buffer = self.buffer.read(cx).read(cx); - let max_point = buffer.max_point(); - for selection in &mut selections { - let is_entire_line = selection.is_empty(); - if is_entire_line { - selection.start = Point::new(selection.start.row, 0); - selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); - selection.goal = SelectionGoal::None; - } - let mut len = 0; - for chunk in buffer.text_for_range(selection.start..selection.end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - }); - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); - } - - pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - let selections = self.local_selections::(cx); - let mut text = String::new(); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let buffer = self.buffer.read(cx).read(cx); - let max_point = buffer.max_point(); - for selection in selections.iter() { - let mut start = selection.start; - let mut end = selection.end; - let is_entire_line = selection.is_empty(); - if is_entire_line { - start = Point::new(start.row, 0); - end = cmp::min(max_point, Point::new(start.row + 1, 0)); - } - let mut len = 0; - for chunk in buffer.text_for_range(start..end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - }); - } - } - - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); - } - - pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.as_mut().read_from_clipboard() { - let clipboard_text = item.text(); - if let Some(mut clipboard_selections) = item.metadata::>() { - let mut selections = self.local_selections::(cx); - let all_selections_were_entire_line = - clipboard_selections.iter().all(|s| s.is_entire_line); - if clipboard_selections.len() != selections.len() { - clipboard_selections.clear(); - } - - let mut delta = 0_isize; - let mut start_offset = 0; - for (i, selection) in selections.iter_mut().enumerate() { - let to_insert; - let entire_line; - if let Some(clipboard_selection) = clipboard_selections.get(i) { - let end_offset = start_offset + clipboard_selection.len; - to_insert = &clipboard_text[start_offset..end_offset]; - entire_line = clipboard_selection.is_entire_line; - start_offset = end_offset - } else { - to_insert = clipboard_text.as_str(); - entire_line = all_selections_were_entire_line; - } - - selection.start = (selection.start as isize + delta) as usize; - selection.end = (selection.end as isize + delta) as usize; - - self.buffer.update(cx, |buffer, cx| { - // If the corresponding selection was empty when this slice of the - // clipboard text was written, then the entire line containing the - // selection was copied. If this selection is also currently empty, - // then paste the line before the current line of the buffer. - let range = if selection.is_empty() && entire_line { - let column = selection.start.to_point(&buffer.read(cx)).column as usize; - let line_start = selection.start - column; - line_start..line_start - } else { - selection.start..selection.end - }; - - delta += to_insert.len() as isize - range.len() as isize; - buffer.edit([range], to_insert, cx); - selection.start += to_insert.len(); - selection.end = selection.start; - }); - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } else { - self.insert(clipboard_text, cx); - } - } - } - - pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { - if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { - if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); - } - self.request_autoscroll(Autoscroll::Fit, cx); - } - } - - pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { - if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { - if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); - } - self.request_autoscroll(Autoscroll::Fit, cx); - } - } - - pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.end = selection.start.clone(); - } else { - let cursor = movement::left(&display_map, start) - .unwrap() - .to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.start = selection.end.clone(); - } else { - let cursor = movement::right(&display_map, end) - .unwrap() - .to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - if self.take_rename(cx).is_some() { - return; - } - - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_prev(cx) { - return; - } - } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { - selection.goal = SelectionGoal::None; - } - - let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap(); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { - self.take_rename(cx); - - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_next(cx) { - return; - } - } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { - selection.goal = SelectionGoal::None; - } - - let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap(); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_to_previous_word_boundary( - &mut self, - _: &MoveToPreviousWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_previous_word_boundary( - &mut self, - _: &SelectToPreviousWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_previous_word_boundary( - &mut self, - _: &DeleteToPreviousWordBoundary, - cx: &mut ViewContext, - ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = - movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn move_to_next_word_boundary( - &mut self, - _: &MoveToNextWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_next_word_boundary( - &mut self, - _: &SelectToNextWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_next_word_boundary( - &mut self, - _: &DeleteToNextWordBoundary, - cx: &mut ViewContext, - ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = - movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn move_to_beginning_of_line( - &mut self, - _: &MoveToBeginningOfLine, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, true); - let cursor = new_head.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_beginning_of_line( - &mut self, - SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_beginning_of_line( - &mut self, - _: &DeleteToBeginningOfLine, - cx: &mut ViewContext, - ) { - self.start_transaction(cx); - self.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); - self.backspace(&Backspace, cx); - self.end_transaction(cx); - } - - pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - { - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, true); - let anchor = new_head.to_point(&display_map); - selection.start = anchor.clone(); - selection.end = anchor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_end_of_line( - &mut self, - SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.delete(&Delete, cx); - self.end_transaction(cx); - } - - pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.cut(&Cut, cx); - self.end_transaction(cx); - } - - pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start: 0, - end: 0, - reversed: false, - goal: SelectionGoal::None, - }; - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { - let mut selection = self.local_selections::(cx).last().unwrap().clone(); - selection.set_head(Point::zero()); - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let cursor = self.buffer.read(cx).read(cx).len(); - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - }; - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn set_nav_history(&mut self, nav_history: Option) { - self.nav_history = nav_history; - } - - pub fn nav_history(&self) -> Option<&ItemNavHistory> { - self.nav_history.as_ref() - } - - fn push_to_nav_history( - &self, - position: Anchor, - new_position: Option, - cx: &mut ViewContext, - ) { - if let Some(nav_history) = &self.nav_history { - let buffer = self.buffer.read(cx).read(cx); - let offset = position.to_offset(&buffer); - let point = position.to_point(&buffer); - drop(buffer); - - if let Some(new_position) = new_position { - let row_delta = (new_position.row as i64 - point.row as i64).abs(); - if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { - return; - } - } - - nav_history.push(Some(NavigationData { - anchor: position, - offset, - })); - } - } - - pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { - let mut selection = self.local_selections::(cx).first().unwrap().clone(); - selection.set_head(self.buffer.read(cx).read(cx).len()); - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start: 0, - end: self.buffer.read(cx).read(cx).len(), - reversed: false, - goal: SelectionGoal::None, - }; - self.update_selections(vec![selection], None, cx); - } - - pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - let max_point = display_map.buffer_snapshot.max_point(); - for selection in &mut selections { - let rows = selection.spanned_rows(true, &display_map); - selection.start = Point::new(rows.start, 0); - selection.end = cmp::min(max_point, Point::new(rows.end, 0)); - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn split_selection_into_lines( - &mut self, - _: &SplitSelectionIntoLines, - cx: &mut ViewContext, - ) { - let mut to_unfold = Vec::new(); - let mut new_selections = Vec::new(); - { - let selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).read(cx); - for selection in selections { - for row in selection.start.row..selection.end.row { - let cursor = Point::new(row, buffer.line_len(row)); - new_selections.push(Selection { - id: post_inc(&mut self.next_selection_id), - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - }); - } - new_selections.push(Selection { - id: selection.id, - start: selection.end, - end: selection.end, - reversed: false, - goal: SelectionGoal::None, - }); - to_unfold.push(selection.start..selection.end); - } - } - self.unfold_ranges(to_unfold, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - } - - pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext) { - self.add_selection(true, cx); - } - - pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext) { - self.add_selection(false, cx); - } - - fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - let mut state = self.add_selections_state.take().unwrap_or_else(|| { - let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); - let range = oldest_selection.display_range(&display_map).sorted(); - let columns = cmp::min(range.start.column(), range.end.column()) - ..cmp::max(range.start.column(), range.end.column()); - - selections.clear(); - let mut stack = Vec::new(); - for row in range.start.row()..=range.end.row() { - if let Some(selection) = self.build_columnar_selection( - &display_map, - row, - &columns, - oldest_selection.reversed, - ) { - stack.push(selection.id); - selections.push(selection); - } - } - - if above { - stack.reverse(); - } - - AddSelectionsState { above, stack } - }); - - let last_added_selection = *state.stack.last().unwrap(); - let mut new_selections = Vec::new(); - if above == state.above { - let end_row = if above { - 0 - } else { - display_map.max_point().row() - }; - - 'outer: for selection in selections { - if selection.id == last_added_selection { - let range = selection.display_range(&display_map).sorted(); - debug_assert_eq!(range.start.row(), range.end.row()); - let mut row = range.start.row(); - let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal - { - start..end - } else { - cmp::min(range.start.column(), range.end.column()) - ..cmp::max(range.start.column(), range.end.column()) - }; - - while row != end_row { - if above { - row -= 1; - } else { - row += 1; - } - - if let Some(new_selection) = self.build_columnar_selection( - &display_map, - row, - &columns, - selection.reversed, - ) { - state.stack.push(new_selection.id); - if above { - new_selections.push(new_selection); - new_selections.push(selection); - } else { - new_selections.push(selection); - new_selections.push(new_selection); - } - - continue 'outer; - } - } - } - - new_selections.push(selection); - } - } else { - new_selections = selections; - new_selections.retain(|s| s.id != last_added_selection); - state.stack.pop(); - } - - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - if state.stack.len() > 1 { - self.add_selections_state = Some(state); - } - } - - pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) { - let replace_newest = action.0; - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let mut selections = self.local_selections::(cx); - if let Some(mut select_next_state) = self.select_next_state.take() { - let query = &select_next_state.query; - if !select_next_state.done { - let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); - let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); - let mut next_selected_range = None; - - let bytes_after_last_selection = - buffer.bytes_in_range(last_selection.end..buffer.len()); - let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); - let query_matches = query - .stream_find_iter(bytes_after_last_selection) - .map(|result| (last_selection.end, result)) - .chain( - query - .stream_find_iter(bytes_before_first_selection) - .map(|result| (0, result)), - ); - for (start_offset, query_match) in query_matches { - let query_match = query_match.unwrap(); // can only fail due to I/O - let offset_range = - start_offset + query_match.start()..start_offset + query_match.end(); - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); - - if !select_next_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) - { - next_selected_range = Some(offset_range); - break; - } - } - - if let Some(next_selected_range) = next_selected_range { - if replace_newest { - if let Some(newest_id) = - selections.iter().max_by_key(|s| s.id).map(|s| s.id) - { - selections.retain(|s| s.id != newest_id); - } - } - selections.push(Selection { - id: post_inc(&mut self.next_selection_id), - start: next_selected_range.start, - end: next_selected_range.end, - reversed: false, - goal: SelectionGoal::None, - }); - self.update_selections(selections, Some(Autoscroll::Newest), cx); - } else { - select_next_state.done = true; - } - } - - self.select_next_state = Some(select_next_state); - } else if selections.len() == 1 { - let selection = selections.last_mut().unwrap(); - if selection.start == selection.end { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let select_state = SelectNextState { - query: AhoCorasick::new_auto_configured(&[query]), - wordwise: true, - done: false, - }; - self.update_selections(selections, Some(Autoscroll::Newest), cx); - self.select_next_state = Some(select_state); - } else { - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - self.select_next_state = Some(SelectNextState { - query: AhoCorasick::new_auto_configured(&[query]), - wordwise: false, - done: false, - }); - self.select_next(action, cx); - } - } - } - - pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { - // 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 = - if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) { - prefix.to_string() - } else { - return; - }; - let comment_prefix = full_comment_prefix.trim_end_matches(' '); - let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - - self.start_transaction(cx); - let mut selections = self.local_selections::(cx); - let mut all_selection_lines_are_comments = true; - let mut edit_ranges = Vec::new(); - let mut last_toggled_row = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - 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); - } - - if snapshot.is_line_blank(row) { - continue; - } - - let start = Point::new(row, snapshot.indent_column_for_line(row)); - 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 { - all_selection_lines_are_comments = false; - edit_ranges.push(start..start); - } - } - - if !edit_ranges.is_empty() { - if all_selection_lines_are_comments { - buffer.edit(edit_ranges.iter().cloned(), "", cx); - } else { - let min_column = edit_ranges.iter().map(|r| r.start.column).min().unwrap(); - let edit_ranges = edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - position..position - }); - buffer.edit(edit_ranges, &full_comment_prefix, cx); - } - } - } - }); - - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); - } - - pub fn select_larger_syntax_node( - &mut self, - _: &SelectLargerSyntaxNode, - cx: &mut ViewContext, - ) { - let old_selections = self.local_selections::(cx).into_boxed_slice(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); - let mut selected_larger_node = false; - let new_selections = old_selections - .iter() - .map(|selection| { - let old_range = selection.start..selection.end; - let mut new_range = old_range.clone(); - while let Some(containing_range) = - buffer.range_for_syntax_ancestor(new_range.clone()) - { - new_range = containing_range; - if !display_map.intersects_fold(new_range.start) - && !display_map.intersects_fold(new_range.end) - { - break; - } - } - - selected_larger_node |= new_range != old_range; - Selection { - id: selection.id, - start: new_range.start, - end: new_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - } - }) - .collect::>(); - - if selected_larger_node { - stack.push(old_selections); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - } - self.select_larger_syntax_node_stack = stack; - } - - pub fn select_smaller_syntax_node( - &mut self, - _: &SelectSmallerSyntaxNode, - cx: &mut ViewContext, - ) { - let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); - if let Some(selections) = stack.pop() { - self.update_selections(selections.to_vec(), Some(Autoscroll::Fit), cx); - } - self.select_larger_syntax_node_stack = stack; - } - - pub fn move_to_enclosing_bracket( - &mut self, - _: &MoveToEnclosingBracket, - cx: &mut ViewContext, - ) { - let mut selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).snapshot(cx); - for selection in &mut selections { - if let Some((open_range, close_range)) = - buffer.enclosing_bracket_ranges(selection.start..selection.end) - { - let close_range = close_range.to_inclusive(); - let destination = if close_range.contains(&selection.start) - && close_range.contains(&selection.end) - { - open_range.end - } else { - *close_range.start() - }; - selection.start = destination; - selection.end = destination; - } - } - - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext) { - let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self.newest_selection::(&buffer); - let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { - active_diagnostics - .primary_range - .to_offset(&buffer) - .to_inclusive() - }); - let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() { - if active_primary_range.contains(&selection.head()) { - *active_primary_range.end() - } else { - selection.head() - } - } else { - selection.head() - }; - - loop { - let next_group = buffer - .diagnostics_in_range::<_, usize>(search_start..buffer.len()) - .find_map(|entry| { - if entry.diagnostic.is_primary - && !entry.range.is_empty() - && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) - { - Some((entry.range, entry.diagnostic.group_id)) - } else { - None - } - }); - - if let Some((primary_range, group_id)) = next_group { - self.activate_diagnostics(group_id, cx); - self.update_selections( - vec![Selection { - id: selection.id, - start: primary_range.start, - end: primary_range.start, - reversed: false, - goal: SelectionGoal::None, - }], - Some(Autoscroll::Center), - cx, - ); - break; - } else if search_start == 0 { - break; - } else { - // Cycle around to the start of the buffer, potentially moving back to the start of - // the currently active diagnostic. - search_start = 0; - active_primary_range.take(); - } - } - } - - pub fn go_to_definition( - workspace: &mut Workspace, - _: &GoToDefinition, - cx: &mut ViewContext, - ) { - let active_item = workspace.active_item(cx); - let editor_handle = if let Some(editor) = active_item - .as_ref() - .and_then(|item| item.act_as::(cx)) - { - editor - } else { - return; - }; - - let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); - let (buffer, head) = - if let Some(text_anchor) = editor.buffer.read(cx).text_anchor_for_position(head, cx) { - text_anchor - } else { - return; - }; - - let definitions = workspace - .project() - .update(cx, |project, cx| project.definition(&buffer, head, cx)); - cx.spawn(|workspace, mut cx| async move { - let definitions = definitions.await?; - workspace.update(&mut cx, |workspace, cx| { - for definition in definitions { - let range = definition.range.to_offset(definition.buffer.read(cx)); - let target_editor_handle = workspace - .open_item(BufferItemHandle(definition.buffer), cx) - .downcast::() - .unwrap(); - - target_editor_handle.update(cx, |target_editor, cx| { - // When selecting a definition in a different buffer, disable the nav history - // to avoid creating a history entry at the previous cursor location. - let disabled_history = if editor_handle == target_editor_handle { - None - } else { - target_editor.nav_history.take() - }; - target_editor.select_ranges([range], Some(Autoscroll::Center), cx); - if disabled_history.is_some() { - target_editor.nav_history = disabled_history; - } - }); - } - }); - - Ok::<(), anyhow::Error>(()) - }) - .detach_and_log_err(cx); - } - - pub fn find_all_references( - workspace: &mut Workspace, - _: &FindAllReferences, - cx: &mut ViewContext, - ) -> Option>> { - let active_item = workspace.active_item(cx)?; - let editor_handle = active_item.act_as::(cx)?; - - let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); - let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; - let replica_id = editor.replica_id(cx); - - let references = workspace - .project() - .update(cx, |project, cx| project.references(&buffer, head, cx)); - Some(cx.spawn(|workspace, mut cx| async move { - let mut locations = references.await?; - if locations.is_empty() { - return Ok(()); - } - - locations.sort_by_key(|location| location.buffer.id()); - let mut locations = locations.into_iter().peekable(); - let mut ranges_to_highlight = Vec::new(); - - let excerpt_buffer = cx.add_model(|cx| { - let mut symbol_name = None; - let mut multibuffer = MultiBuffer::new(replica_id); - while let Some(location) = locations.next() { - let buffer = location.buffer.read(cx); - let mut ranges_for_buffer = Vec::new(); - let range = location.range.to_offset(buffer); - ranges_for_buffer.push(range.clone()); - if symbol_name.is_none() { - symbol_name = Some(buffer.text_for_range(range).collect::()); - } - - while let Some(next_location) = locations.peek() { - if next_location.buffer == location.buffer { - ranges_for_buffer.push(next_location.range.to_offset(buffer)); - locations.next(); - } else { - break; - } - } - - ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); - ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines( - location.buffer.clone(), - ranges_for_buffer, - 1, - cx, - )); - } - multibuffer.with_title(format!("References to `{}`", symbol_name.unwrap())) - }); - - workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_ranges::(ranges_to_highlight, color, cx); - }); - } - }); - - Ok(()) - })) - } - - pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { - use language::ToOffset as _; - - let project = self.project.clone()?; - let selection = self.newest_anchor_selection().clone(); - let (cursor_buffer, cursor_buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.head(), cx)?; - let (tail_buffer, tail_buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.tail(), cx)?; - if tail_buffer != cursor_buffer { - return None; - } - - let snapshot = cursor_buffer.read(cx).snapshot(); - let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); - let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot); - let prepare_rename = project.update(cx, |project, cx| { - project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) - }); - - Some(cx.spawn(|this, mut cx| async move { - if let Some(rename_range) = prepare_rename.await? { - let rename_buffer_range = rename_range.to_offset(&snapshot); - let cursor_offset_in_rename_range = - cursor_buffer_offset.saturating_sub(rename_buffer_range.start); - let tail_offset_in_rename_range = - tail_buffer_offset.saturating_sub(rename_buffer_range.start); - - this.update(&mut cx, |this, cx| { - this.take_rename(cx); - let style = this.style(cx); - let buffer = this.buffer.read(cx).read(cx); - let cursor_offset = selection.head().to_offset(&buffer); - let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); - let rename_end = rename_start + rename_buffer_range.len(); - let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); - let old_name = buffer - .text_for_range(rename_start..rename_end) - .collect::(); - drop(buffer); - - // Position the selection in the rename editor so that it matches the current selection. - let rename_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line(this.settings.clone(), None, cx); - editor - .buffer - .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx)); - editor.select_ranges( - [tail_offset_in_rename_range..cursor_offset_in_rename_range], - None, - cx, - ); - editor.highlight_ranges::( - vec![Anchor::min()..Anchor::max()], - style.diff_background_inserted, - cx, - ); - editor - }); - this.highlight_ranges::( - vec![range.clone()], - style.diff_background_deleted, - cx, - ); - this.update_selections( - vec![Selection { - id: selection.id, - start: rename_end, - end: rename_end, - reversed: false, - goal: SelectionGoal::None, - }], - None, - cx, - ); - cx.focus(&rename_editor); - let block_id = this.insert_blocks( - [BlockProperties { - position: range.start.clone(), - height: 1, - render: Arc::new({ - let editor = rename_editor.clone(); - move |cx: &BlockContext| { - ChildView::new(editor.clone()) - .contained() - .with_padding_left(cx.anchor_x) - .boxed() - } - }), - disposition: BlockDisposition::Below, - }], - cx, - )[0]; - this.pending_rename = Some(RenameState { - range, - old_name, - editor: rename_editor, - block_id, - }); - }); - } - - Ok(()) - })) - } - - pub fn confirm_rename( - workspace: &mut Workspace, - _: &ConfirmRename, - cx: &mut ViewContext, - ) -> Option>> { - let editor = workspace.active_item(cx)?.act_as::(cx)?; - - let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { - let rename = editor.take_rename(cx)?; - let buffer = editor.buffer.read(cx); - let (start_buffer, start) = - buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; - let (end_buffer, end) = - buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; - if start_buffer == end_buffer { - let new_name = rename.editor.read(cx).text(cx); - Some((start_buffer, start..end, rename.old_name, new_name)) - } else { - None - } - })?; - - let rename = workspace.project().clone().update(cx, |project, cx| { - project.perform_rename( - buffer.clone(), - range.start.clone(), - new_name.clone(), - true, - cx, - ) - }); - - Some(cx.spawn(|workspace, cx| async move { - let project_transaction = rename.await?; - Self::open_project_transaction( - editor, - workspace, - project_transaction, - format!("Rename: {} → {}", old_name, new_name), - cx, - ) - .await - })) - } - - fn take_rename(&mut self, cx: &mut ViewContext) -> Option { - let rename = self.pending_rename.take()?; - self.remove_blocks([rename.block_id].into_iter().collect(), cx); - self.clear_highlighted_ranges::(cx); - - let editor = rename.editor.read(cx); - let buffer = editor.buffer.read(cx).snapshot(cx); - let selection = editor.newest_selection::(&buffer); - - // Update the selection to match the position of the selection inside - // the rename editor. - let snapshot = self.buffer.read(cx).snapshot(cx); - let rename_range = rename.range.to_offset(&snapshot); - let start = snapshot - .clip_offset(rename_range.start + selection.start, Bias::Left) - .min(rename_range.end); - let end = snapshot - .clip_offset(rename_range.start + selection.end, Bias::Left) - .min(rename_range.end); - self.update_selections( - vec![Selection { - id: self.newest_anchor_selection().id, - start, - end, - reversed: selection.reversed, - goal: SelectionGoal::None, - }], - None, - cx, - ); - - Some(rename) - } - - fn invalidate_rename_range( - &mut self, - buffer: &MultiBufferSnapshot, - cx: &mut ViewContext, - ) { - if let Some(rename) = self.pending_rename.as_ref() { - if self.selections.len() == 1 { - let head = self.selections[0].head().to_offset(buffer); - let range = rename.range.to_offset(buffer).to_inclusive(); - if range.contains(&head) { - return; - } - } - let rename = self.pending_rename.take().unwrap(); - self.remove_blocks([rename.block_id].into_iter().collect(), cx); - self.clear_highlighted_ranges::(cx); - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn pending_rename(&self) -> Option<&RenameState> { - self.pending_rename.as_ref() - } - - fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { - if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { - let buffer = self.buffer.read(cx).snapshot(cx); - let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); - let is_valid = buffer - .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) - .any(|entry| { - entry.diagnostic.is_primary - && !entry.range.is_empty() - && entry.range.start == primary_range_start - && entry.diagnostic.message == active_diagnostics.primary_message - }); - - if is_valid != active_diagnostics.is_valid { - active_diagnostics.is_valid = is_valid; - let mut new_styles = HashMap::default(); - for (block_id, diagnostic) in &active_diagnostics.blocks { - new_styles.insert( - *block_id, - diagnostic_block_renderer( - diagnostic.clone(), - is_valid, - self.settings.clone(), - ), - ); - } - self.display_map - .update(cx, |display_map, _| display_map.replace_blocks(new_styles)); - } - } - } - - fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { - self.dismiss_diagnostics(cx); - self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut primary_range = None; - let mut primary_message = None; - let mut group_end = Point::zero(); - let diagnostic_group = buffer - .diagnostic_group::(group_id) - .map(|entry| { - if entry.range.end > group_end { - group_end = entry.range.end; - } - if entry.diagnostic.is_primary { - primary_range = Some(entry.range.clone()); - primary_message = Some(entry.diagnostic.message.clone()); - } - entry - }) - .collect::>(); - let primary_range = primary_range.unwrap(); - let primary_message = primary_message.unwrap(); - let primary_range = - buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end); - - let blocks = display_map - .insert_blocks( - diagnostic_group.iter().map(|entry| { - let diagnostic = entry.diagnostic.clone(); - let message_height = diagnostic.message.lines().count() as u8; - BlockProperties { - position: buffer.anchor_after(entry.range.start), - height: message_height, - render: diagnostic_block_renderer( - diagnostic, - true, - self.settings.clone(), - ), - disposition: BlockDisposition::Below, - } - }), - cx, - ) - .into_iter() - .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic)) - .collect(); - - Some(ActiveDiagnosticGroup { - primary_range, - primary_message, - blocks, - is_valid: true, - }) - }); - } - - fn dismiss_diagnostics(&mut self, cx: &mut ViewContext) { - if let Some(active_diagnostic_group) = self.active_diagnostics.take() { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); - }); - cx.notify(); - } - } - - fn build_columnar_selection( - &mut self, - display_map: &DisplaySnapshot, - row: u32, - columns: &Range, - reversed: bool, - ) -> Option> { - let is_empty = columns.start == columns.end; - let line_len = display_map.line_len(row); - if columns.start < line_len || (is_empty && columns.start == line_len) { - let start = DisplayPoint::new(row, columns.start); - let end = DisplayPoint::new(row, cmp::min(columns.end, line_len)); - Some(Selection { - id: post_inc(&mut self.next_selection_id), - start: start.to_point(display_map), - end: end.to_point(display_map), - reversed, - goal: SelectionGoal::ColumnRange { - start: columns.start, - end: columns.end, - }, - }) - } else { - None - } - } - - pub fn local_selections_in_range( - &self, - range: Range, - display_map: &DisplaySnapshot, - ) -> Vec> { - let buffer = &display_map.buffer_snapshot; - - let start_ix = match self - .selections - .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer).unwrap()) - { - Ok(ix) | Err(ix) => ix, - }; - let end_ix = match self - .selections - .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer).unwrap()) - { - Ok(ix) => ix + 1, - Err(ix) => ix, - }; - - fn point_selection( - selection: &Selection, - buffer: &MultiBufferSnapshot, - ) -> Selection { - let start = selection.start.to_point(&buffer); - let end = selection.end.to_point(&buffer); - Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - } - } - - self.selections[start_ix..end_ix] - .iter() - .chain( - self.pending_selection - .as_ref() - .map(|pending| &pending.selection), - ) - .map(|s| point_selection(s, &buffer)) - .collect() - } - - pub fn local_selections<'a, D>(&self, cx: &'a AppContext) -> Vec> - where - D: 'a + TextDimension + Ord + Sub, - { - let buffer = self.buffer.read(cx).snapshot(cx); - let mut selections = self - .resolve_selections::(self.selections.iter(), &buffer) - .peekable(); - - let mut pending_selection = self.pending_selection::(&buffer); - - iter::from_fn(move || { - if let Some(pending) = pending_selection.as_mut() { - while let Some(next_selection) = selections.peek() { - if pending.start <= next_selection.end && pending.end >= next_selection.start { - let next_selection = selections.next().unwrap(); - if next_selection.start < pending.start { - pending.start = next_selection.start; - } - if next_selection.end > pending.end { - pending.end = next_selection.end; - } - } else if next_selection.end < pending.start { - return selections.next(); - } else { - break; - } - } - - pending_selection.take() - } else { - selections.next() - } - }) - .collect() - } - - fn resolve_selections<'a, D, I>( - &self, - selections: I, - snapshot: &MultiBufferSnapshot, - ) -> impl 'a + Iterator> - where - D: TextDimension + Ord + Sub, - I: 'a + IntoIterator>, - { - let (to_summarize, selections) = selections.into_iter().tee(); - let mut summaries = snapshot - .summaries_for_anchors::(to_summarize.flat_map(|s| [&s.start, &s.end])) - .into_iter(); - selections.map(move |s| Selection { - id: s.id, - start: summaries.next().unwrap(), - end: summaries.next().unwrap(), - reversed: s.reversed, - goal: s.goal, - }) - } - - fn pending_selection>( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Option> { - self.pending_selection - .as_ref() - .map(|pending| self.resolve_selection(&pending.selection, &snapshot)) - } - - fn resolve_selection>( - &self, - selection: &Selection, - buffer: &MultiBufferSnapshot, - ) -> Selection { - Selection { - id: selection.id, - start: selection.start.summary::(&buffer), - end: selection.end.summary::(&buffer), - reversed: selection.reversed, - goal: selection.goal, - } - } - - fn selection_count<'a>(&self) -> usize { - let mut count = self.selections.len(); - if self.pending_selection.is_some() { - count += 1; - } - count - } - - pub fn oldest_selection>( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Selection { - self.selections - .iter() - .min_by_key(|s| s.id) - .map(|selection| self.resolve_selection(selection, snapshot)) - .or_else(|| self.pending_selection(snapshot)) - .unwrap() - } - - pub fn newest_selection>( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Selection { - self.resolve_selection(self.newest_anchor_selection(), snapshot) - } - - pub fn newest_anchor_selection(&self) -> &Selection { - self.pending_selection - .as_ref() - .map(|s| &s.selection) - .or_else(|| self.selections.iter().max_by_key(|s| s.id)) - .unwrap() - } - - pub fn update_selections( - &mut self, - mut selections: Vec>, - autoscroll: Option, - cx: &mut ViewContext, - ) where - T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, - { - let buffer = self.buffer.read(cx).snapshot(cx); - selections.sort_unstable_by_key(|s| s.start); - - // Merge overlapping selections. - let mut i = 1; - while i < selections.len() { - if selections[i - 1].end >= selections[i].start { - let removed = selections.remove(i); - if removed.start < selections[i - 1].start { - selections[i - 1].start = removed.start; - } - if removed.end > selections[i - 1].end { - selections[i - 1].end = removed.end; - } - } else { - i += 1; - } - } - - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - - self.set_selections( - Arc::from_iter(selections.into_iter().map(|selection| { - let end_bias = if selection.end > selection.start { - Bias::Left - } else { - Bias::Right - }; - Selection { - id: selection.id, - start: buffer.anchor_after(selection.start), - end: buffer.anchor_at(selection.end, end_bias), - reversed: selection.reversed, - goal: selection.goal, - } - })), - None, - cx, - ); - } - - /// Compute new ranges for any selections that were located in excerpts that have - /// since been removed. - /// - /// Returns a `HashMap` indicating which selections whose former head position - /// was no longer present. The keys of the map are selection ids. The values are - /// the id of the new excerpt where the head of the selection has been moved. - pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { - let snapshot = self.buffer.read(cx).read(cx); - let anchors_with_status = snapshot.refresh_anchors( - self.selections - .iter() - .flat_map(|selection| [&selection.start, &selection.end]), - ); - let offsets = - snapshot.summaries_for_anchors::(anchors_with_status.iter().map(|a| &a.1)); - let offsets = offsets.chunks(2); - let statuses = anchors_with_status - .chunks(2) - .map(|a| (a[0].0 / 2, a[0].2, a[1].2)); - - let mut selections_with_lost_position = HashMap::default(); - let new_selections = offsets - .zip(statuses) - .map(|(offsets, (selection_ix, kept_start, kept_end))| { - let selection = &self.selections[selection_ix]; - let kept_head = if selection.reversed { - kept_start - } else { - kept_end - }; - if !kept_head { - selections_with_lost_position - .insert(selection.id, selection.head().excerpt_id.clone()); - } - - Selection { - id: selection.id, - start: offsets[0], - end: offsets[1], - reversed: selection.reversed, - goal: selection.goal, - } - }) - .collect(); - drop(snapshot); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - selections_with_lost_position - } - - fn set_selections( - &mut self, - selections: Arc<[Selection]>, - pending_selection: Option, - cx: &mut ViewContext, - ) { - let old_cursor_position = self.newest_anchor_selection().head(); - - self.selections = selections; - self.pending_selection = pending_selection; - if self.focused { - self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections(&self.selections, cx) - }); - } - - let display_map = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - self.add_selections_state = None; - self.select_next_state = None; - self.select_larger_syntax_node_stack.clear(); - self.autoclose_stack.invalidate(&self.selections, &buffer); - self.snippet_stack.invalidate(&self.selections, &buffer); - self.invalidate_rename_range(&buffer, cx); - - let new_cursor_position = self.newest_anchor_selection().head(); - - self.push_to_nav_history( - old_cursor_position.clone(), - Some(new_cursor_position.to_point(&buffer)), - cx, - ); - - let completion_menu = match self.context_menu.as_mut() { - Some(ContextMenu::Completions(menu)) => Some(menu), - _ => { - self.context_menu.take(); - None - } - }; - - if let Some(completion_menu) = completion_menu { - let cursor_position = new_cursor_position.to_offset(&buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position.clone()); - if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) - { - let query = Self::completion_query(&buffer, cursor_position); - cx.background() - .block(completion_menu.filter(query.as_deref(), cx.background().clone())); - self.show_completions(&ShowCompletions, cx); - } else { - self.hide_context_menu(cx); - } - } - - if old_cursor_position.to_display_point(&display_map).row() - != new_cursor_position.to_display_point(&display_map).row() - { - self.available_code_actions.take(); - } - self.refresh_code_actions(cx); - self.refresh_document_highlights(cx); - - self.pause_cursor_blinking(cx); - cx.emit(Event::SelectionsChanged); - } - - pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { - self.autoscroll_request = Some(autoscroll); - cx.notify(); - } - - fn start_transaction(&mut self, cx: &mut ViewContext) { - self.start_transaction_at(Instant::now(), cx); - } - - fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { - self.end_selection(cx); - if let Some(tx_id) = self - .buffer - .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) - { - self.selection_history - .insert(tx_id, (self.selections.clone(), None)); - } - } - - fn end_transaction(&mut self, cx: &mut ViewContext) { - self.end_transaction_at(Instant::now(), cx); - } - - fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { - if let Some(tx_id) = self - .buffer - .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { - *end_selections = Some(self.selections.clone()); - } else { - log::error!("unexpectedly ended a transaction that wasn't started by this editor"); - } - } - } - - pub fn page_up(&mut self, _: &PageUp, _: &mut ViewContext) { - log::info!("Editor::page_up"); - } - - pub fn page_down(&mut self, _: &PageDown, _: &mut ViewContext) { - log::info!("Editor::page_down"); - } - - pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { - let mut fold_ranges = Vec::new(); - - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in selections { - let range = selection.display_range(&display_map).sorted(); - let buffer_start_row = range.start.to_point(&display_map).row; - - for row in (0..=range.end.row()).rev() { - if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) { - let fold_range = self.foldable_range_for_line(&display_map, row); - if fold_range.end.row >= buffer_start_row { - fold_ranges.push(fold_range); - if row <= range.start.row() { - break; - } - } - } - } - } - - self.fold_ranges(fold_ranges, cx); - } - - pub fn unfold(&mut self, _: &Unfold, cx: &mut ViewContext) { - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let ranges = selections - .iter() - .map(|s| { - let range = s.display_range(&display_map).sorted(); - let mut start = range.start.to_point(&display_map); - let mut end = range.end.to_point(&display_map); - start.column = 0; - end.column = buffer.line_len(end.row); - start..end - }) - .collect::>(); - self.unfold_ranges(ranges, cx); - } - - fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool { - let max_point = display_map.max_point(); - if display_row >= max_point.row() { - false - } else { - let (start_indent, is_blank) = display_map.line_indent(display_row); - if is_blank { - false - } else { - for display_row in display_row + 1..=max_point.row() { - let (indent, is_blank) = display_map.line_indent(display_row); - if !is_blank { - return indent > start_indent; - } - } - false - } - } - } - - fn foldable_range_for_line( - &self, - display_map: &DisplaySnapshot, - start_row: u32, - ) -> Range { - let max_point = display_map.max_point(); - - let (start_indent, _) = display_map.line_indent(start_row); - let start = DisplayPoint::new(start_row, display_map.line_len(start_row)); - let mut end = None; - for row in start_row + 1..=max_point.row() { - let (indent, is_blank) = display_map.line_indent(row); - if !is_blank && indent <= start_indent { - end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1))); - break; - } - } - - let end = end.unwrap_or(max_point); - return start.to_point(display_map)..end.to_point(display_map); - } - - pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { - let selections = self.local_selections::(cx); - let ranges = selections.into_iter().map(|s| s.start..s.end); - self.fold_ranges(ranges, cx); - } - - fn fold_ranges( - &mut self, - ranges: impl IntoIterator>, - cx: &mut ViewContext, - ) { - let mut ranges = ranges.into_iter().peekable(); - if ranges.peek().is_some() { - self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); - self.request_autoscroll(Autoscroll::Fit, cx); - cx.notify(); - } - } - - fn unfold_ranges(&mut self, ranges: Vec>, cx: &mut ViewContext) { - if !ranges.is_empty() { - self.display_map - .update(cx, |map, cx| map.unfold(ranges, cx)); - self.request_autoscroll(Autoscroll::Fit, cx); - cx.notify(); - } - } - - pub fn insert_blocks( - &mut self, - blocks: impl IntoIterator>, - cx: &mut ViewContext, - ) -> Vec { - let blocks = self - .display_map - .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); - self.request_autoscroll(Autoscroll::Fit, cx); - blocks - } - - pub fn replace_blocks( - &mut self, - blocks: HashMap, - cx: &mut ViewContext, - ) { - self.display_map - .update(cx, |display_map, _| display_map.replace_blocks(blocks)); - self.request_autoscroll(Autoscroll::Fit, cx); - } - - pub fn remove_blocks(&mut self, block_ids: HashSet, cx: &mut ViewContext) { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(block_ids, cx) - }); - } - - pub fn longest_row(&self, cx: &mut MutableAppContext) -> u32 { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .longest_row() - } - - pub fn max_point(&self, cx: &mut MutableAppContext) -> DisplayPoint { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .max_point() - } - - pub fn text(&self, cx: &AppContext) -> String { - self.buffer.read(cx).read(cx).text() - } - - pub fn display_text(&self, cx: &mut MutableAppContext) -> String { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .text() - } - - pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { - let language = self.language(cx); - let settings = self.settings.borrow(); - let mode = self - .soft_wrap_mode_override - .unwrap_or_else(|| settings.soft_wrap(language)); - match mode { - settings::SoftWrap::None => SoftWrap::None, - settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length(language)) - } - } - } - - pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext) { - self.soft_wrap_mode_override = Some(mode); - cx.notify(); - } - - pub fn set_wrap_width(&self, width: Option, cx: &mut MutableAppContext) -> bool { - self.display_map - .update(cx, |map, cx| map.set_wrap_width(width, cx)) - } - - pub fn set_highlighted_rows(&mut self, rows: Option>) { - self.highlighted_rows = rows; - } - - pub fn highlighted_rows(&self) -> Option> { - self.highlighted_rows.clone() - } - - pub fn highlight_ranges( - &mut self, - ranges: Vec>, - color: Color, - cx: &mut ViewContext, - ) { - self.highlighted_ranges - .insert(TypeId::of::(), (color, ranges)); - cx.notify(); - } - - pub fn clear_highlighted_ranges( - &mut self, - cx: &mut ViewContext, - ) -> Option<(Color, Vec>)> { - cx.notify(); - self.highlighted_ranges.remove(&TypeId::of::()) - } - - #[cfg(feature = "test-support")] - pub fn all_highlighted_ranges( - &mut self, - cx: &mut ViewContext, - ) -> Vec<(Range, Color)> { - let snapshot = self.snapshot(cx); - let buffer = &snapshot.buffer_snapshot; - let start = buffer.anchor_before(0); - let end = buffer.anchor_after(buffer.len()); - self.highlighted_ranges_in_range(start..end, &snapshot) - } - - pub fn highlighted_ranges_for_type(&self) -> Option<(Color, &[Range])> { - self.highlighted_ranges - .get(&TypeId::of::()) - .map(|(color, ranges)| (*color, ranges.as_slice())) - } - - pub fn highlighted_ranges_in_range( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - ) -> Vec<(Range, Color)> { - let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - for (color, ranges) in self.highlighted_ranges.values() { - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { - break; - } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, *color)) - } - } - results - } - - fn next_blink_epoch(&mut self) -> usize { - self.blink_epoch += 1; - self.blink_epoch - } - - fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { - if !self.focused { - return; - } - - self.show_local_cursors = true; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| { - let this = this.downgrade(); - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) - } - } - }) - .detach(); - } - - fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch { - self.blinking_paused = false; - self.blink_cursors(epoch, cx); - } - } - - fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch && self.focused && !self.blinking_paused { - self.show_local_cursors = !self.show_local_cursors; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| { - let this = this.downgrade(); - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); - } - } - }) - .detach(); - } - } - - pub fn show_local_cursors(&self) -> bool { - self.show_local_cursors - } - - fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { - cx.notify(); - } - - fn on_buffer_event( - &mut self, - _: ModelHandle, - event: &language::Event, - cx: &mut ViewContext, - ) { - match event { - language::Event::Edited => { - self.refresh_active_diagnostics(cx); - self.refresh_code_actions(cx); - cx.emit(Event::Edited); - } - language::Event::Dirtied => cx.emit(Event::Dirtied), - language::Event::Saved => cx.emit(Event::Saved), - language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), - language::Event::Reloaded => cx.emit(Event::TitleChanged), - language::Event::Closed => cx.emit(Event::Closed), - language::Event::DiagnosticsUpdated => { - self.refresh_active_diagnostics(cx); - } - _ => {} - } - } - - fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { - cx.notify(); - } -} - -impl EditorSnapshot { - pub fn is_focused(&self) -> bool { - self.is_focused - } - - pub fn placeholder_text(&self) -> Option<&Arc> { - self.placeholder_text.as_ref() - } - - pub fn scroll_position(&self) -> Vector2F { - compute_scroll_position( - &self.display_snapshot, - self.scroll_position, - &self.scroll_top_anchor, - ) - } -} - -impl Deref for EditorSnapshot { - type Target = DisplaySnapshot; - - fn deref(&self) -> &Self::Target { - &self.display_snapshot - } -} - -fn compute_scroll_position( - snapshot: &DisplaySnapshot, - mut scroll_position: Vector2F, - scroll_top_anchor: &Option, -) -> Vector2F { - if let Some(anchor) = scroll_top_anchor { - let scroll_top = anchor.to_display_point(snapshot).row() as f32; - scroll_position.set_y(scroll_top + scroll_position.y()); - } else { - scroll_position.set_y(0.); - } - scroll_position -} - -#[derive(Copy, Clone)] -pub enum Event { - Activate, - Edited, - Blurred, - Dirtied, - Saved, - TitleChanged, - SelectionsChanged, - Closed, -} - -impl Entity for Editor { - type Event = Event; -} - -impl View for Editor { - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let style = self.style(cx); - self.display_map.update(cx, |map, cx| { - map.set_font(style.text.font_id, style.text.font_size, cx) - }); - EditorElement::new(self.handle.clone(), style.clone()).boxed() - } - - fn ui_name() -> &'static str { - "Editor" - } - - fn on_focus(&mut self, cx: &mut ViewContext) { - self.focused = true; - self.blink_cursors(self.blink_epoch, cx); - self.buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx); - buffer.set_active_selections(&self.selections, cx) - }); - } - - fn on_blur(&mut self, cx: &mut ViewContext) { - self.focused = false; - self.show_local_cursors = false; - self.buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - self.hide_context_menu(cx); - cx.emit(Event::Blurred); - cx.notify(); - } - - fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { - let mut cx = Self::default_keymap_context(); - let mode = match self.mode { - EditorMode::SingleLine => "single_line", - EditorMode::AutoHeight { .. } => "auto_height", - EditorMode::Full => "full", - }; - cx.map.insert("mode".into(), mode.into()); - if self.pending_rename.is_some() { - cx.set.insert("renaming".into()); - } - match self.context_menu.as_ref() { - Some(ContextMenu::Completions(_)) => { - cx.set.insert("showing_completions".into()); - } - Some(ContextMenu::CodeActions(_)) => { - cx.set.insert("showing_code_actions".into()); - } - None => {} - } - cx - } -} - -fn build_style( - settings: &Settings, - get_field_editor_theme: Option, - cx: &AppContext, -) -> EditorStyle { - let mut theme = settings.theme.editor.clone(); - if let Some(get_field_editor_theme) = get_field_editor_theme { - let field_editor_theme = get_field_editor_theme(&settings.theme); - if let Some(background) = field_editor_theme.container.background_color { - theme.background = background; - } - theme.text_color = field_editor_theme.text.color; - theme.selection = field_editor_theme.selection; - EditorStyle { - text: field_editor_theme.text, - placeholder_text: field_editor_theme.placeholder_text, - theme, - } - } else { - let font_cache = cx.font_cache(); - let font_family_id = settings.buffer_font_family; - let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); - let font_properties = Default::default(); - let font_id = font_cache - .select_font(font_family_id, &font_properties) - .unwrap(); - let font_size = settings.buffer_font_size; - EditorStyle { - text: TextStyle { - color: settings.theme.editor.text_color, - font_family_name, - font_family_id, - font_id, - font_size, - font_properties, - underline: None, - }, - placeholder_text: None, - theme, - } - } -} - -impl SelectionExt for Selection { - fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range { - let start = self.start.to_point(buffer); - let end = self.end.to_point(buffer); - if self.reversed { - end..start - } else { - start..end - } - } - - fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range { - let start = self.start.to_offset(buffer); - let end = self.end.to_offset(buffer); - if self.reversed { - end..start - } else { - start..end - } - } - - fn display_range(&self, map: &DisplaySnapshot) -> Range { - let start = self - .start - .to_point(&map.buffer_snapshot) - .to_display_point(map); - let end = self - .end - .to_point(&map.buffer_snapshot) - .to_display_point(map); - if self.reversed { - end..start - } else { - start..end - } - } - - fn spanned_rows( - &self, - include_end_if_at_line_start: bool, - map: &DisplaySnapshot, - ) -> Range { - let start = self.start.to_point(&map.buffer_snapshot); - let mut end = self.end.to_point(&map.buffer_snapshot); - if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { - end.row -= 1; - } - - let buffer_start = map.prev_line_boundary(start).0; - let buffer_end = map.next_line_boundary(end).0; - buffer_start.row..buffer_end.row + 1 - } -} - -impl InvalidationStack { - fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) - where - S: Clone + ToOffset, - { - while let Some(region) = self.last() { - let all_selections_inside_invalidation_ranges = - if selections.len() == region.ranges().len() { - selections - .iter() - .zip(region.ranges().iter().map(|r| r.to_offset(&buffer))) - .all(|(selection, invalidation_range)| { - let head = selection.head().to_offset(&buffer); - invalidation_range.start <= head && invalidation_range.end >= head - }) - } else { - false - }; - - if all_selections_inside_invalidation_ranges { - break; - } else { - self.pop(); - } - } - } -} - -impl Default for InvalidationStack { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Deref for InvalidationStack { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for InvalidationStack { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl InvalidationRegion for BracketPairState { - fn ranges(&self) -> &[Range] { - &self.ranges - } -} - -impl InvalidationRegion for SnippetState { - fn ranges(&self) -> &[Range] { - &self.ranges[self.active_index] - } -} - -impl Deref for EditorStyle { - type Target = theme::Editor; - - fn deref(&self) -> &Self::Target { - &self.theme - } -} - -pub fn diagnostic_block_renderer( - diagnostic: Diagnostic, - is_valid: bool, - settings: watch::Receiver, -) -> RenderBlock { - let mut highlighted_lines = Vec::new(); - for line in diagnostic.message.lines() { - highlighted_lines.push(highlight_diagnostic_message(line)); - } - - Arc::new(move |cx: &BlockContext| { - let settings = settings.borrow(); - let theme = &settings.theme.editor; - let style = diagnostic_style(diagnostic.severity, is_valid, theme); - let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); - Flex::column() - .with_children(highlighted_lines.iter().map(|(line, highlights)| { - Label::new( - line.clone(), - style.message.clone().with_font_size(font_size), - ) - .with_highlights(highlights.clone()) - .contained() - .with_margin_left(cx.anchor_x) - .boxed() - })) - .aligned() - .left() - .boxed() - }) -} - -pub fn highlight_diagnostic_message(message: &str) -> (String, Vec) { - let mut message_without_backticks = String::new(); - let mut prev_offset = 0; - let mut inside_block = false; - let mut highlights = Vec::new(); - for (match_ix, (offset, _)) in message - .match_indices('`') - .chain([(message.len(), "")]) - .enumerate() - { - message_without_backticks.push_str(&message[prev_offset..offset]); - if inside_block { - highlights.extend(prev_offset - match_ix..offset - match_ix); - } - - inside_block = !inside_block; - prev_offset = offset + 1; - } - - (message_without_backticks, highlights) -} - -pub fn diagnostic_style( - severity: DiagnosticSeverity, - valid: bool, - theme: &theme::Editor, -) -> DiagnosticStyle { - match (severity, valid) { - (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(), - (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(), - (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(), - (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(), - (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(), - (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(), - (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(), - (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(), - _ => theme.invalid_hint_diagnostic.clone(), - } -} - -pub fn combine_syntax_and_fuzzy_match_highlights( - text: &str, - default_style: HighlightStyle, - syntax_ranges: impl Iterator, HighlightStyle)>, - match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut result = Vec::new(); - let mut match_indices = match_indices.iter().copied().peekable(); - - for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) - { - syntax_highlight.font_properties.weight(Default::default()); - - // Add highlights for any fuzzy match characters before the next - // syntax highlight range. - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.start { - break; - } - match_indices.next(); - let end_index = char_ix_after(match_index, text); - let mut match_style = default_style; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - } - - if range.start == usize::MAX { - break; - } - - // Add highlights for any fuzzy match characters within the - // syntax highlight range. - let mut offset = range.start; - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.end { - break; - } - - match_indices.next(); - if match_index > offset { - result.push((offset..match_index, syntax_highlight)); - } - - let mut end_index = char_ix_after(match_index, text); - while let Some(&next_match_index) = match_indices.peek() { - if next_match_index == end_index && next_match_index < range.end { - end_index = char_ix_after(next_match_index, text); - match_indices.next(); - } else { - break; - } - } - - let mut match_style = syntax_highlight; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - offset = end_index; - } - - if offset < range.end { - result.push((offset..range.end, syntax_highlight)); - } - } - - fn char_ix_after(ix: usize, text: &str) -> usize { - ix + text[ix..].chars().next().unwrap().len_utf8() - } - - result -} - -pub fn styled_runs_for_code_label<'a>( - label: &'a CodeLabel, - default_color: Color, - syntax_theme: &'a theme::SyntaxTheme, -) -> impl 'a + Iterator, HighlightStyle)> { - const MUTED_OPACITY: usize = 165; - - let mut muted_default_style = HighlightStyle { - color: default_color, - ..Default::default() - }; - muted_default_style.color.a = ((default_color.a as usize * MUTED_OPACITY) / 255) as u8; - - let mut prev_end = label.filter_range.end; - label - .runs - .iter() - .enumerate() - .flat_map(move |(ix, (range, highlight_id))| { - let style = if let Some(style) = highlight_id.style(syntax_theme) { - style - } else { - return Default::default(); - }; - let mut muted_style = style.clone(); - muted_style.color.a = ((style.color.a as usize * MUTED_OPACITY) / 255) as u8; - - let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); - if range.start >= label.filter_range.end { - if range.start > prev_end { - runs.push((prev_end..range.start, muted_default_style)); - } - runs.push((range.clone(), muted_style)); - } else if range.end <= label.filter_range.end { - runs.push((range.clone(), style)); - } else { - runs.push((range.start..label.filter_range.end, style)); - runs.push((label.filter_range.end..range.end, muted_style)); - } - prev_end = cmp::max(prev_end, range.end); - - if ix + 1 == label.runs.len() && label.text.len() > prev_end { - runs.push((prev_end..label.text.len(), muted_default_style)); - } - - runs - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use language::LanguageConfig; - use lsp::FakeLanguageServer; - use project::{FakeFs, ProjectPath}; - use smol::stream::StreamExt; - use std::{cell::RefCell, rc::Rc, time::Instant}; - use text::Point; - use unindent::Unindent; - use util::test::sample_text; - - #[gpui::test] - fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { - let mut now = Instant::now(); - let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); - let group_interval = buffer.read(cx).transaction_group_interval(); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let settings = Settings::test(cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - editor.update(cx, |editor, cx| { - editor.start_transaction_at(now, cx); - editor.select_ranges([2..4], None, cx); - editor.insert("cd", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cd56"); - assert_eq!(editor.selected_ranges(cx), vec![4..4]); - - editor.start_transaction_at(now, cx); - editor.select_ranges([4..5], None, cx); - editor.insert("e", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selected_ranges(cx), vec![5..5]); - - now += group_interval + Duration::from_millis(1); - editor.select_ranges([2..2], None, cx); - - // Simulate an edit in another editor - buffer.update(cx, |buffer, cx| { - buffer.start_transaction_at(now, cx); - buffer.edit([0..1], "a", cx); - buffer.edit([1..1], "b", cx); - buffer.end_transaction_at(now, cx); - }); - - assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selected_ranges(cx), vec![3..3]); - - // Last transaction happened past the group interval in a different editor. - // Undo it individually and don't restore selections. - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selected_ranges(cx), vec![2..2]); - - // First two transactions happened within the group interval in this editor. - // Undo them together and restore selections. - editor.undo(&Undo, cx); - editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. - assert_eq!(editor.text(cx), "123456"); - assert_eq!(editor.selected_ranges(cx), vec![0..0]); - - // Redo the first two transactions together. - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selected_ranges(cx), vec![5..5]); - - // Redo the last transaction on its own. - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selected_ranges(cx), vec![6..6]); - - // Test empty transactions. - editor.start_transaction_at(now, cx); - editor.end_transaction_at(now, cx); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "12cde6"); - }); - } - - #[gpui::test] - fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let settings = Settings::test(cx); - let (_, editor) = - cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - editor.update(cx, |view, cx| { - view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [ - DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) - ] - ); - - editor.update(cx, |view, cx| { - view.end_selection(cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] - ); - } - - #[gpui::test] - fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let settings = Settings::test(cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - }); - - view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - }); - } - - #[gpui::test] - fn test_navigation_history(cx: &mut gpui::MutableAppContext) { - cx.add_window(Default::default(), |cx| { - use workspace::ItemView; - let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), settings, cx); - editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle())); - - // Move the cursor a small distance. - // Nothing is added to the navigation history. - editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - editor.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); - assert!(nav_history.borrow_mut().pop_backward().is_none()); - - // Move the cursor a large distance. - // The history can jump back to the previous position. - editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); - let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] - ); - - // Move the cursor a small distance via the mouse. - // Nothing is added to the navigation history. - editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); - editor.end_selection(cx); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - assert!(nav_history.borrow_mut().pop_backward().is_none()); - - // Move the cursor a large distance via the mouse. - // The history can jump back to the previous position. - editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); - editor.end_selection(cx); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] - ); - let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - - editor - }); - } - - #[gpui::test] - fn test_cancel(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let settings = Settings::test(cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - view.end_selection(cx); - - view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx); - view.end_selection(cx); - assert_eq!( - view.selected_display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), - ] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] - ); - }); - } - - #[gpui::test] - fn test_fold(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple( - &" - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() { - 2 - } - - fn c() { - 3 - } - } - " - .unindent(), - cx, - ); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], cx); - view.fold(&Fold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() {… - } - - fn c() {… - } - } - " - .unindent(), - ); - - view.fold(&Fold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo {… - } - " - .unindent(), - ); - - view.unfold(&Unfold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() {… - } - - fn c() {… - } - } - " - .unindent(), - ); - - view.unfold(&Unfold, cx); - assert_eq!(view.display_text(cx), buffer.read(cx).read(cx).text()); - }); - } - - #[gpui::test] - fn test_move_cursor(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - vec![ - Point::new(1, 0)..Point::new(1, 0), - Point::new(1, 1)..Point::new(1, 1), - ], - "\t", - cx, - ); - }); - - view.update(cx, |view, cx| { - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_to_end(&MoveToEnd, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] - ); - - view.move_to_beginning(&MoveToBeginning, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], cx); - view.select_to_beginning(&SelectToBeginning, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] - ); - - view.select_to_end(&SelectToEnd, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] - ); - }); - } - - #[gpui::test] - fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - assert_eq!('ⓐ'.len_utf8(), 3); - assert_eq!('α'.len_utf8(), 2); - - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 6)..Point::new(0, 12), - Point::new(1, 2)..Point::new(1, 4), - Point::new(2, 4)..Point::new(2, 8), - ], - cx, - ); - assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ…".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "ab…".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "ab".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "a".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "α".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβ…".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβ…ε".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "ab…e".len())] - ); - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ…ⓔ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ…".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - }); - } - - #[gpui::test] - fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - view.update(cx, |view, cx| { - view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx); - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "abcd".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβγ".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(3, "abcd".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(3, "abcd".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβγ".len())] - ); - }); - } - - #[gpui::test] - fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("abc\n def", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ], - cx, - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_end_of_line(&MoveToEndOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - }); - - // Moving to the end of line again is a no-op. - view.update(cx, |view, cx| { - view.move_to_end_of_line(&MoveToEndOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_left(&MoveLeft, cx); - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_end_of_line(&SelectToEndOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.delete_to_end_of_line(&DeleteToEndOfLine, cx); - assert_eq!(view.display_text(cx), "ab\n de"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ] - ); - }); - - view.update(cx, |view, cx| { - view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); - assert_eq!(view.display_text(cx), "\n"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - } - - #[gpui::test] - fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), - ], - cx, - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_right(&MoveRight, cx); - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] - ); - }); - } - - #[gpui::test] - fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.), cx); - assert_eq!( - view.display_text(cx), - "use one::{\n two::three::\n four::five\n};" - ); - - view.select_display_ranges(&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], cx); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] - ); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] - ); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] - ); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] - ); - - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] - ); - - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] - ); - }); - } - - #[gpui::test] - fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("one two three four", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the preceding word fragment is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // characters selected - they are deleted - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), - ], - cx, - ); - view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); - }); - - assert_eq!(buffer.read(cx).read(cx).text(), "e two te four"); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the following word fragment is deleted - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - // characters selected - they are deleted - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), - ], - cx, - ); - view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); - }); - - assert_eq!(buffer.read(cx).read(cx).text(), "e t te our"); - } - - #[gpui::test] - fn test_newline(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), - ], - cx, - ); - - view.newline(&Newline, cx); - assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); - }); - } - - #[gpui::test] - fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple( - " - a - b( - X - ) - c( - X - ) - " - .unindent() - .as_str(), - cx, - ); - - let settings = Settings::test(&cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - let mut editor = build_editor(buffer.clone(), settings, cx); - editor.select_ranges( - [ - Point::new(2, 4)..Point::new(2, 5), - Point::new(5, 4)..Point::new(5, 5), - ], - None, - cx, - ); - editor - }); - - // Edit the buffer directly, deleting ranges surrounding the editor's selections - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - Point::new(1, 2)..Point::new(3, 0), - Point::new(4, 2)..Point::new(6, 0), - ], - "", - cx, - ); - assert_eq!( - buffer.read(cx).text(), - " - a - b() - c() - " - .unindent() - ); - }); - - editor.update(cx, |editor, cx| { - assert_eq!( - editor.selected_ranges(cx), - &[ - Point::new(1, 2)..Point::new(1, 2), - Point::new(2, 2)..Point::new(2, 2), - ], - ); - - editor.newline(&Newline, cx); - assert_eq!( - editor.text(cx), - " - a - b( - ) - c( - ) - " - .unindent() - ); - - // The selections are moved after the inserted newlines - assert_eq!( - editor.selected_ranges(cx), - &[ - Point::new(2, 0)..Point::new(2, 0), - Point::new(4, 0)..Point::new(4, 0), - ], - ); - }); - } - - #[gpui::test] - fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - - let settings = Settings::test(&cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - let mut editor = build_editor(buffer.clone(), settings, cx); - editor.select_ranges([3..4, 11..12, 19..20], None, cx); - editor - }); - - // Edit the buffer directly, deleting ranges surrounding the editor's selections - buffer.update(cx, |buffer, cx| { - buffer.edit([2..5, 10..13, 18..21], "", cx); - assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); - }); - - editor.update(cx, |editor, cx| { - assert_eq!(editor.selected_ranges(cx), &[2..2, 7..7, 12..12],); - - editor.insert("Z", cx); - assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); - - // The selections are moved after the inserted characters - assert_eq!(editor.selected_ranges(cx), &[3..3, 9..9, 15..15],); - }); - } - - #[gpui::test] - fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple(" one two\nthree\n four", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - // two selections on the same line - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9), - ], - cx, - ); - - // indent from mid-tabstop to full tabstop - view.tab(&Tab, cx); - assert_eq!(view.text(cx), " one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11), - ] - ); - - // outdent from 1 tabstop to 0 tabstops - view.outdent(&Outdent, cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - ] - ); - - // select across line ending - view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); - - // indent and outdent affect only the preceding line - view.tab(&Tab, cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] - ); - view.outdent(&Outdent, cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)] - ); - - // Ensure that indenting/outdenting works when the cursor is at column 0. - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.tab(&Tab, cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.outdent(&Outdent, cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - }); - } - - #[gpui::test] - fn test_backspace(cx: &mut gpui::MutableAppContext) { - let buffer = - MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the preceding character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.backspace(&Backspace, cx); - }); - - assert_eq!( - buffer.read(cx).read(cx).text(), - "oe two three\nfou five six\nseven ten\n" - ); - } - - #[gpui::test] - fn test_delete(cx: &mut gpui::MutableAppContext) { - let buffer = - MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the following character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.delete(&Delete, cx); - }); - - assert_eq!( - buffer.read(cx).read(cx).text(), - "on two three\nfou five six\nseven ten\n" - ); - } - - #[gpui::test] - fn test_delete_line(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi"); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) - ] - ); - }); - - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi\n"); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] - ); - }); - } - - #[gpui::test] - fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), - ] - ); - }); - - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), - ], - cx, - ); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), - DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[gpui::test] - fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - cx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), - ], - cx, - ); - assert_eq!( - view.display_text(cx), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" - ); - - view.move_line_up(&MoveLineUp, cx); - assert_eq!( - view.display_text(cx), - "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); - assert_eq!( - view.display_text(cx), - "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); - assert_eq!( - view.display_text(cx), - "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_up(&MoveLineUp, cx); - assert_eq!( - view.display_text(cx), - "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] - ); - }); - } - - #[gpui::test] - fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - let snapshot = buffer.read(cx).snapshot(cx); - let (_, editor) = - cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - editor.update(cx, |editor, cx| { - editor.insert_blocks( - [BlockProperties { - position: snapshot.anchor_after(Point::new(2, 0)), - disposition: BlockDisposition::Below, - height: 1, - render: Arc::new(|_| Empty::new().boxed()), - }], - cx, - ); - editor.select_ranges([Point::new(2, 0)..Point::new(2, 0)], None, cx); - editor.move_line_down(&MoveLineDown, cx); - }); - } - - #[gpui::test] - fn test_clipboard(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); - let settings = Settings::test(&cx); - let view = cx - .add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }) - .1; - - // Cut with three selections. Clipboard text is divided into three slices. - view.update(cx, |view, cx| { - view.select_ranges(vec![0..7, 11..17, 22..27], None, cx); - view.cut(&Cut, cx); - assert_eq!(view.display_text(cx), "two four six "); - }); - - // Paste with three cursors. Each cursor pastes one slice of the clipboard text. - view.update(cx, |view, cx| { - view.select_ranges(vec![4..4, 9..9, 13..13], None, cx); - view.paste(&Paste, cx); - assert_eq!(view.display_text(cx), "two one✅ four three six five "); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(0, 22)..DisplayPoint::new(0, 22), - DisplayPoint::new(0, 31)..DisplayPoint::new(0, 31) - ] - ); - }); - - // Paste again but with only two cursors. Since the number of cursors doesn't - // match the number of slices in the clipboard, the entire clipboard text - // is pasted at each cursor. - view.update(cx, |view, cx| { - view.select_ranges(vec![0..0, 31..31], None, cx); - view.handle_input(&Input("( ".into()), cx); - view.paste(&Paste, cx); - view.handle_input(&Input(") ".into()), cx); - assert_eq!( - view.display_text(cx), - "( one✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - }); - - view.update(cx, |view, cx| { - view.select_ranges(vec![0..0], None, cx); - view.handle_input(&Input("123\n4567\n89\n".into()), cx); - assert_eq!( - view.display_text(cx), - "123\n4567\n89\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - }); - - // Cut with three selections, one of which is full-line. - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), - ], - cx, - ); - view.cut(&Cut, cx); - assert_eq!( - view.display_text(cx), - "13\n9\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - }); - - // Paste with three selections, noticing how the copied selection that was full-line - // gets inserted before the second cursor. - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), - ], - cx, - ); - view.paste(&Paste, cx); - assert_eq!( - view.display_text(cx), - "123\n4567\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), - ] - ); - }); - - // Copy with a single cursor only, which writes the whole line into the clipboard. - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], cx); - view.copy(&Copy, cx); - }); - - // Paste with three selections, noticing how the copied full-line selection is inserted - // before the empty selections but replaces the selection that is non-empty. - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - ], - cx, - ); - view.paste(&Paste, cx); - assert_eq!( - view.display_text(cx), - "123\n123\n123\n67\n123\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[gpui::test] - fn test_select_all(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_all(&SelectAll, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] - ); - }); - } - - #[gpui::test] - fn test_select_line(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ], - cx, - ); - view.select_line(&SelectLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] - ); - }); - } - - #[gpui::test] - fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - cx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), - ], - cx, - ); - assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i"); - }); - - view.update(cx, |view, cx| { - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); - assert_eq!( - view.display_text(cx), - "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i" - ); - assert_eq!( - view.selected_display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)], cx); - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); - assert_eq!( - view.display_text(cx), - "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" - ); - assert_eq!( - view.selected_display_ranges(cx), - [ - DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), - DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), - DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), - DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), - DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) - ] - ); - }); - } - - #[gpui::test] - fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx); - }); - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], cx); - }); - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], cx); - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], cx); - }); - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - }); - } - - #[gpui::test] - async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - )); - - let text = r#" - use mod1::mod2::{mod3, mod4}; - - fn fn_1(param1: bool, param2: &str) { - let var1 = "text"; - } - "# - .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, settings, cx)); - view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(&mut cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ], - cx, - ); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] - ); - - // Trying to expand the selected syntax node one more time has no effect. - view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] - ); - - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ] - ); - - // Trying to shrink the selected syntax node one more time has no effect. - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ] - ); - - // Ensure that we keep expanding the selection if the larger selection starts or ends within - // a fold. - view.update(&mut cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 21)..Point::new(0, 24), - Point::new(3, 20)..Point::new(3, 22), - ], - cx, - ); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), - ] - ); - } - - #[gpui::test] - async fn test_autoindent_selections(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new( - Language::new( - LanguageConfig { - brackets: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: false, - newline: true, - }, - BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: false, - newline: true, - }, - ], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_indents_query( - r#" - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap(), - ); - - let text = "fn a() {}"; - - 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 (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); - editor - .condition(&cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) - .await; - - editor.update(&mut cx, |editor, cx| { - editor.select_ranges([5..5, 8..8, 9..9], None, cx); - editor.newline(&Newline, cx); - assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); - assert_eq!( - editor.selected_ranges(cx), - &[ - Point::new(1, 4)..Point::new(1, 4), - Point::new(3, 4)..Point::new(3, 4), - Point::new(5, 0)..Point::new(5, 0) - ] - ); - }); - } - - #[gpui::test] - async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig { - brackets: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/*".to_string(), - end: " */".to_string(), - close: true, - newline: true, - }, - ], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = r#" - a - - / - - "# - .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, settings, cx)); - view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(&mut cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ], - cx, - ); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - assert_eq!( - view.text(cx), - " - {{{}}} - {{{}}} - / - - " - .unindent() - ); - - view.move_right(&MoveRight, cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); - assert_eq!( - view.text(cx), - " - {{{}}}} - {{{}}}} - / - - " - .unindent() - ); - - view.undo(&Undo, cx); - view.handle_input(&Input("/".to_string()), cx); - view.handle_input(&Input("*".to_string()), cx); - assert_eq!( - view.text(cx), - " - /* */ - /* */ - / - - " - .unindent() - ); - - view.undo(&Undo, cx); - view.select_display_ranges( - &[ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.handle_input(&Input("*".to_string()), cx); - assert_eq!( - view.text(cx), - " - a - - /* - * - " - .unindent() - ); - }); - } - - #[gpui::test] - async fn test_snippets(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - - let text = " - a. b - a. b - a. b - " - .unindent(); - let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); - - editor.update(&mut cx, |editor, cx| { - let buffer = &editor.snapshot(cx).buffer_snapshot; - let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); - let insertion_ranges = [ - Point::new(0, 2).to_offset(buffer)..Point::new(0, 2).to_offset(buffer), - Point::new(1, 2).to_offset(buffer)..Point::new(1, 2).to_offset(buffer), - Point::new(2, 2).to_offset(buffer)..Point::new(2, 2).to_offset(buffer), - ]; - - editor - .insert_snippet(&insertion_ranges, snippet, cx) - .unwrap(); - assert_eq!( - editor.text(cx), - " - a.f(one, two, three) b - a.f(one, two, three) b - a.f(one, two, three) b - " - .unindent() - ); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 4)..Point::new(0, 7), - Point::new(0, 14)..Point::new(0, 19), - Point::new(1, 4)..Point::new(1, 7), - Point::new(1, 14)..Point::new(1, 19), - Point::new(2, 4)..Point::new(2, 7), - Point::new(2, 14)..Point::new(2, 19), - ] - ); - - // Can't move earlier than the first tab stop - editor.move_to_prev_snippet_tabstop(cx); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 4)..Point::new(0, 7), - Point::new(0, 14)..Point::new(0, 19), - Point::new(1, 4)..Point::new(1, 7), - Point::new(1, 14)..Point::new(1, 19), - Point::new(2, 4)..Point::new(2, 7), - Point::new(2, 14)..Point::new(2, 19), - ] - ); - - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 9)..Point::new(0, 12), - Point::new(1, 9)..Point::new(1, 12), - Point::new(2, 9)..Point::new(2, 12) - ] - ); - - editor.move_to_prev_snippet_tabstop(cx); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 4)..Point::new(0, 7), - Point::new(0, 14)..Point::new(0, 19), - Point::new(1, 4)..Point::new(1, 7), - Point::new(1, 14)..Point::new(1, 19), - Point::new(2, 4)..Point::new(2, 7), - Point::new(2, 14)..Point::new(2, 19), - ] - ); - - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 20)..Point::new(0, 20), - Point::new(1, 20)..Point::new(1, 20), - Point::new(2, 20)..Point::new(2, 20) - ] - ); - - // As soon as the last tab stop is reached, snippet state is gone - editor.move_to_prev_snippet_tabstop(cx); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 20)..Point::new(0, 20), - Point::new(1, 20)..Point::new(1, 20), - Point::new(2, 20)..Point::new(2, 20) - ] - ); - }); - } - - #[gpui::test] - async fn test_completion(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let (language_server, mut fake) = cx.update(|cx| { - lsp::LanguageServer::fake_with_capabilities( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - }); - - let text = " - one - two - three - " - .unindent(); - - let fs = FakeFs::new(cx.background().clone()); - fs.insert_file("/file", text).await; - - let project = Project::test(fs, &mut cx); - - let (worktree, relative_path) = project - .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree("/file", false, cx) - }) - .await - .unwrap(); - let project_path = ProjectPath { - worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), - path: relative_path.into(), - }; - let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) - .await - .unwrap(); - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language_server(Some(language_server), cx); - }); - - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - buffer.next_notification(&cx).await; - - let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); - - editor.update(&mut cx, |editor, cx| { - editor.project = Some(project); - editor.select_ranges([Point::new(0, 3)..Point::new(0, 3)], None, cx); - editor.handle_input(&Input(".".to_string()), cx); - }); - - handle_completion_request( - &mut fake, - "/file", - Point::new(0, 4), - vec![ - (Point::new(0, 4)..Point::new(0, 4), "first_completion"), - (Point::new(0, 4)..Point::new(0, 4), "second_completion"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - let apply_additional_edits = editor.update(&mut cx, |editor, cx| { - editor.move_down(&MoveDown, cx); - let apply_additional_edits = editor - .confirm_completion(&ConfirmCompletion(None), cx) - .unwrap(); - assert_eq!( - editor.text(cx), - " - one.second_completion - two - three - " - .unindent() - ); - apply_additional_edits - }); - - handle_resolve_completion_request( - &mut fake, - Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")), - ) - .await; - apply_additional_edits.await.unwrap(); - assert_eq!( - editor.read_with(&cx, |editor, cx| editor.text(cx)), - " - one.second_completion - two - three - additional edit - " - .unindent() - ); - - editor.update(&mut cx, |editor, cx| { - editor.select_ranges( - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 5)..Point::new(2, 5), - ], - None, - cx, - ); - - editor.handle_input(&Input(" ".to_string()), cx); - assert!(editor.context_menu.is_none()); - editor.handle_input(&Input("s".to_string()), cx); - assert!(editor.context_menu.is_none()); - }); - - handle_completion_request( - &mut fake, - "/file", - Point::new(2, 7), - vec![ - (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"), - (Point::new(2, 6)..Point::new(2, 7), "fifth_completion"), - (Point::new(2, 6)..Point::new(2, 7), "sixth_completion"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - editor.update(&mut cx, |editor, cx| { - editor.handle_input(&Input("i".to_string()), cx); - }); - - handle_completion_request( - &mut fake, - "/file", - Point::new(2, 8), - vec![ - (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"), - (Point::new(2, 6)..Point::new(2, 8), "fifth_completion"), - (Point::new(2, 6)..Point::new(2, 8), "sixth_completion"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - let apply_additional_edits = editor.update(&mut cx, |editor, cx| { - let apply_additional_edits = editor - .confirm_completion(&ConfirmCompletion(None), cx) - .unwrap(); - assert_eq!( - editor.text(cx), - " - one.second_completion - two sixth_completion - three sixth_completion - additional edit - " - .unindent() - ); - apply_additional_edits - }); - handle_resolve_completion_request(&mut fake, None).await; - apply_additional_edits.await.unwrap(); - - async fn handle_completion_request( - fake: &mut FakeLanguageServer, - path: &'static str, - position: Point, - completions: Vec<(Range, &'static str)>, - ) { - fake.handle_request::(move |params, _| { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path).unwrap() - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(position.row, position.column) - ); - Some(lsp::CompletionResponse::Array( - completions - .iter() - .map(|(range, new_text)| lsp::CompletionItem { - label: new_text.to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.start.row, range.start.column), - ), - new_text: new_text.to_string(), - })), - ..Default::default() - }) - .collect(), - )) - }) - .next() - .await; - } - - async fn handle_resolve_completion_request( - fake: &mut FakeLanguageServer, - edit: Option<(Range, &'static str)>, - ) { - fake.handle_request::(move |_, _| { - lsp::CompletionItem { - additional_text_edits: edit.clone().map(|(range, new_text)| { - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.end.row, range.end.column), - ), - new_text.to_string(), - )] - }), - ..Default::default() - } - }) - .next() - .await; - } - } - - #[gpui::test] - async fn test_toggle_comment(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("// ".to_string()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = " - fn a() { - //b(); - // c(); - // d(); - } - " - .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, settings, cx)); - - view.update(&mut cx, |editor, cx| { - // If multiple selections intersect a line, the line is only - // toggled once. - editor.select_display_ranges( - &[ - DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3), - DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6), - ], - cx, - ); - editor.toggle_comments(&ToggleComments, cx); - assert_eq!( - editor.text(cx), - " - fn a() { - b(); - c(); - d(); - } - " - .unindent() - ); - - // The comment prefix is inserted at the same column for every line - // in a selection. - editor.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)], cx); - editor.toggle_comments(&ToggleComments, cx); - assert_eq!( - editor.text(cx), - " - fn a() { - // b(); - // c(); - // d(); - } - " - .unindent() - ); - - // If a selection ends at the beginning of a line, that line is not toggled. - editor.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)], cx); - editor.toggle_comments(&ToggleComments, cx); - assert_eq!( - editor.text(cx), - " - fn a() { - // b(); - c(); - // d(); - } - " - .unindent() - ); - }); - } - - #[gpui::test] - fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer.clone(), - [ - Point::new(0, 0)..Point::new(0, 4), - Point::new(1, 0)..Point::new(1, 4), - ], - cx, - ); - multibuffer - }); - - assert_eq!(multibuffer.read(cx).read(cx).text(), "aaaa\nbbbb"); - - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(multibuffer, settings, cx) - }); - view.update(cx, |view, cx| { - assert_eq!(view.text(cx), "aaaa\nbbbb"); - view.select_ranges( - [ - Point::new(0, 0)..Point::new(0, 0), - Point::new(1, 0)..Point::new(1, 0), - ], - None, - cx, - ); - - view.handle_input(&Input("X".to_string()), cx); - assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); - assert_eq!( - view.selected_ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - ] - ) - }); - } - - #[gpui::test] - fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer, - [ - Point::new(0, 0)..Point::new(1, 4), - Point::new(1, 0)..Point::new(2, 4), - ], - cx, - ); - multibuffer - }); - - assert_eq!( - multibuffer.read(cx).read(cx).text(), - "aaaa\nbbbb\nbbbb\ncccc" - ); - - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(multibuffer, settings, cx) - }); - view.update(cx, |view, cx| { - view.select_ranges( - [ - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 3)..Point::new(2, 3), - ], - None, - cx, - ); - - view.handle_input(&Input("X".to_string()), cx); - assert_eq!(view.text(cx), "aaaa\nbXbbXb\nbXbbXb\ncccc"); - assert_eq!( - view.selected_ranges(cx), - [ - Point::new(1, 2)..Point::new(1, 2), - Point::new(2, 5)..Point::new(2, 5), - ] - ); - - view.newline(&Newline, cx); - assert_eq!(view.text(cx), "aaaa\nbX\nbbX\nb\nbX\nbbX\nb\ncccc"); - assert_eq!( - view.selected_ranges(cx), - [ - Point::new(2, 0)..Point::new(2, 0), - Point::new(6, 0)..Point::new(6, 0), - ] - ); - }); - } - - #[gpui::test] - fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); - let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - excerpt1_id = multibuffer - .push_excerpts( - buffer.clone(), - [ - Point::new(0, 0)..Point::new(1, 4), - Point::new(1, 0)..Point::new(2, 4), - ], - cx, - ) - .into_iter() - .next(); - multibuffer - }); - assert_eq!( - multibuffer.read(cx).read(cx).text(), - "aaaa\nbbbb\nbbbb\ncccc" - ); - let (_, editor) = cx.add_window(Default::default(), |cx| { - let mut editor = build_editor(multibuffer.clone(), settings, cx); - editor.select_ranges( - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ], - None, - cx, - ); - editor - }); - - // Refreshing selections is a no-op when excerpts haven't changed. - editor.update(cx, |editor, cx| { - editor.refresh_selections(cx); - assert_eq!( - editor.selected_ranges(cx), - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ] - ); - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); - }); - editor.update(cx, |editor, cx| { - // Removing an excerpt causes the first selection to become degenerate. - assert_eq!( - editor.selected_ranges(cx), - [ - Point::new(0, 0)..Point::new(0, 0), - Point::new(0, 1)..Point::new(0, 1) - ] - ); - - // Refreshing selections will relocate the first selection to the original buffer - // location. - editor.refresh_selections(cx); - assert_eq!( - editor.selected_ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(0, 3)..Point::new(0, 3) - ] - ); - }); - } - - #[gpui::test] - async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig { - brackets: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/* ".to_string(), - end: " */".to_string(), - close: true, - newline: true, - }, - ], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = concat!( - "{ }\n", // Suppress rustfmt - " x\n", // - " /* */\n", // - "x\n", // - "{{} }\n", // - ); - - 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, settings, cx)); - view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(&mut cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), - ], - cx, - ); - view.newline(&Newline, cx); - - assert_eq!( - view.buffer().read(cx).read(cx).text(), - concat!( - "{ \n", // Suppress rustfmt - "\n", // - "}\n", // - " x\n", // - " /* \n", // - " \n", // - " */\n", // - "x\n", // - "{{} \n", // - "}\n", // - ) - ); - }); - } - - #[gpui::test] - fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - let settings = Settings::test(&cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - editor.update(cx, |editor, cx| { - struct Type1; - struct Type2; - - let buffer = buffer.read(cx).snapshot(cx); - - let anchor_range = |range: Range| { - buffer.anchor_after(range.start)..buffer.anchor_after(range.end) - }; - - editor.highlight_ranges::( - vec![ - anchor_range(Point::new(2, 1)..Point::new(2, 3)), - anchor_range(Point::new(4, 2)..Point::new(4, 4)), - anchor_range(Point::new(6, 3)..Point::new(6, 5)), - anchor_range(Point::new(8, 4)..Point::new(8, 6)), - ], - Color::red(), - cx, - ); - editor.highlight_ranges::( - vec![ - anchor_range(Point::new(3, 2)..Point::new(3, 5)), - anchor_range(Point::new(5, 3)..Point::new(5, 6)), - anchor_range(Point::new(7, 4)..Point::new(7, 7)), - anchor_range(Point::new(9, 5)..Point::new(9, 8)), - ], - Color::green(), - cx, - ); - - let snapshot = editor.snapshot(cx); - let mut highlighted_ranges = editor.highlighted_ranges_in_range( - anchor_range(Point::new(3, 4)..Point::new(7, 4)), - &snapshot, - ); - // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-deterministic. - highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); - assert_eq!( - highlighted_ranges, - &[ - ( - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), - Color::green(), - ), - ( - DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), - Color::green(), - ), - ( - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), - Color::red(), - ), - ( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), - ), - ] - ); - assert_eq!( - editor.highlighted_ranges_in_range( - anchor_range(Point::new(5, 6)..Point::new(6, 4)), - &snapshot, - ), - &[( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), - )] - ); - }); - } - - #[test] - fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let default = HighlightStyle::default(); - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..8, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - &string, - default, - syntax_ranges.into_iter(), - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..5, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 5..6, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ( - 6..8, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 8..9, - HighlightStyle { - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ] - ); - } - - fn empty_range(row: usize, column: usize) -> Range { - let point = DisplayPoint::new(row as u32, column as u32); - point..point - } - - fn build_editor( - buffer: ModelHandle, - settings: Settings, - cx: &mut ViewContext, - ) -> Editor { - let settings = watch::channel_with(settings); - Editor::new(EditorMode::Full, buffer, None, settings.1, None, cx) - } -} - -trait RangeExt { - fn sorted(&self) -> Range; - fn to_inclusive(&self) -> RangeInclusive; -} - -impl RangeExt for Range { - fn sorted(&self) -> Self { - cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() - } - - fn to_inclusive(&self) -> RangeInclusive { - self.start.clone()..=self.end.clone() - } -} +pub mod display_map; +mod element; +pub mod items; +pub mod movement; +mod multi_buffer; + +#[cfg(test)] +mod test; + +use aho_corasick::AhoCorasick; +use anyhow::Result; +use clock::ReplicaId; +use collections::{BTreeMap, Bound, HashMap, HashSet}; +pub use display_map::DisplayPoint; +use display_map::*; +pub use element::*; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + action, + color::Color, + elements::*, + executor, + fonts::{self, HighlightStyle, TextStyle}, + geometry::vector::{vec2f, Vector2F}, + keymap::Binding, + platform::CursorStyle, + text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + WeakViewHandle, +}; +use items::{BufferItemHandle, MultiBufferItemHandle}; +use itertools::Itertools as _; +pub use language::{char_kind, CharKind}; +use language::{ + AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, + DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, +}; +use multi_buffer::MultiBufferChunks; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, +}; +use ordered_float::OrderedFloat; +use postage::watch; +use project::{Project, ProjectTransaction}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use smol::Timer; +use snippet::Snippet; +use std::{ + any::TypeId, + cmp::{self, Ordering, Reverse}, + iter::{self, FromIterator}, + mem, + ops::{Deref, DerefMut, Range, RangeInclusive, Sub}, + sync::Arc, + time::{Duration, Instant}, +}; +pub use sum_tree::Bias; +use text::rope::TextDimension; +use theme::DiagnosticStyle; +use util::{post_inc, ResultExt, TryFutureExt}; +use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace}; + +const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const MAX_LINE_LEN: usize = 1024; +const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; + +action!(Cancel); +action!(Backspace); +action!(Delete); +action!(Input, String); +action!(Newline); +action!(Tab); +action!(Outdent); +action!(DeleteLine); +action!(DeleteToPreviousWordBoundary); +action!(DeleteToNextWordBoundary); +action!(DeleteToBeginningOfLine); +action!(DeleteToEndOfLine); +action!(CutToEndOfLine); +action!(DuplicateLine); +action!(MoveLineUp); +action!(MoveLineDown); +action!(Cut); +action!(Copy); +action!(Paste); +action!(Undo); +action!(Redo); +action!(MoveUp); +action!(MoveDown); +action!(MoveLeft); +action!(MoveRight); +action!(MoveToPreviousWordBoundary); +action!(MoveToNextWordBoundary); +action!(MoveToBeginningOfLine); +action!(MoveToEndOfLine); +action!(MoveToBeginning); +action!(MoveToEnd); +action!(SelectUp); +action!(SelectDown); +action!(SelectLeft); +action!(SelectRight); +action!(SelectToPreviousWordBoundary); +action!(SelectToNextWordBoundary); +action!(SelectToBeginningOfLine, bool); +action!(SelectToEndOfLine, bool); +action!(SelectToBeginning); +action!(SelectToEnd); +action!(SelectAll); +action!(SelectLine); +action!(SplitSelectionIntoLines); +action!(AddSelectionAbove); +action!(AddSelectionBelow); +action!(SelectNext, bool); +action!(ToggleComments); +action!(SelectLargerSyntaxNode); +action!(SelectSmallerSyntaxNode); +action!(MoveToEnclosingBracket); +action!(ShowNextDiagnostic); +action!(GoToDefinition); +action!(FindAllReferences); +action!(Rename); +action!(ConfirmRename); +action!(PageUp); +action!(PageDown); +action!(Fold); +action!(Unfold); +action!(FoldSelectedRanges); +action!(Scroll, Vector2F); +action!(Select, SelectPhase); +action!(ShowCompletions); +action!(ToggleCodeActions, bool); +action!(ConfirmCompletion, Option); +action!(ConfirmCodeAction, Option); + +pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { + path_openers.push(Box::new(items::BufferOpener)); + cx.add_bindings(vec![ + Binding::new("escape", Cancel, Some("Editor")), + Binding::new("backspace", Backspace, Some("Editor")), + Binding::new("ctrl-h", Backspace, Some("Editor")), + Binding::new("delete", Delete, Some("Editor")), + Binding::new("ctrl-d", Delete, Some("Editor")), + Binding::new("enter", Newline, Some("Editor && mode == full")), + Binding::new( + "alt-enter", + Input("\n".into()), + Some("Editor && mode == auto_height"), + ), + Binding::new( + "enter", + ConfirmCompletion(None), + Some("Editor && showing_completions"), + ), + Binding::new( + "enter", + ConfirmCodeAction(None), + Some("Editor && showing_code_actions"), + ), + Binding::new("enter", ConfirmRename, Some("Editor && renaming")), + Binding::new("tab", Tab, Some("Editor")), + Binding::new( + "tab", + ConfirmCompletion(None), + Some("Editor && showing_completions"), + ), + Binding::new("shift-tab", Outdent, Some("Editor")), + Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), + Binding::new( + "alt-backspace", + DeleteToPreviousWordBoundary, + Some("Editor"), + ), + Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")), + Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")), + Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")), + Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")), + Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")), + Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")), + Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")), + Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")), + Binding::new("cmd-x", Cut, Some("Editor")), + Binding::new("cmd-c", Copy, Some("Editor")), + Binding::new("cmd-v", Paste, Some("Editor")), + Binding::new("cmd-z", Undo, Some("Editor")), + Binding::new("cmd-shift-Z", Redo, Some("Editor")), + Binding::new("up", MoveUp, Some("Editor")), + Binding::new("down", MoveDown, Some("Editor")), + Binding::new("left", MoveLeft, Some("Editor")), + Binding::new("right", MoveRight, Some("Editor")), + Binding::new("ctrl-p", MoveUp, Some("Editor")), + Binding::new("ctrl-n", MoveDown, Some("Editor")), + Binding::new("ctrl-b", MoveLeft, Some("Editor")), + Binding::new("ctrl-f", MoveRight, Some("Editor")), + Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")), + Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")), + Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")), + Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")), + Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")), + Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")), + Binding::new("cmd-up", MoveToBeginning, Some("Editor")), + Binding::new("cmd-down", MoveToEnd, Some("Editor")), + Binding::new("shift-up", SelectUp, Some("Editor")), + Binding::new("ctrl-shift-P", SelectUp, Some("Editor")), + Binding::new("shift-down", SelectDown, Some("Editor")), + Binding::new("ctrl-shift-N", SelectDown, Some("Editor")), + Binding::new("shift-left", SelectLeft, Some("Editor")), + Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")), + Binding::new("shift-right", SelectRight, Some("Editor")), + Binding::new("ctrl-shift-F", SelectRight, Some("Editor")), + Binding::new( + "alt-shift-left", + SelectToPreviousWordBoundary, + Some("Editor"), + ), + Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")), + Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")), + Binding::new( + "cmd-shift-left", + SelectToBeginningOfLine(true), + Some("Editor"), + ), + Binding::new( + "ctrl-shift-A", + SelectToBeginningOfLine(true), + Some("Editor"), + ), + Binding::new("cmd-shift-right", SelectToEndOfLine(true), Some("Editor")), + Binding::new("ctrl-shift-E", SelectToEndOfLine(true), Some("Editor")), + Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")), + Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")), + Binding::new("cmd-a", SelectAll, Some("Editor")), + Binding::new("cmd-l", SelectLine, Some("Editor")), + Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")), + Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")), + Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")), + Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")), + Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")), + Binding::new("cmd-d", SelectNext(false), Some("Editor")), + Binding::new("cmd-k cmd-d", SelectNext(true), Some("Editor")), + Binding::new("cmd-/", ToggleComments, Some("Editor")), + Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")), + Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), + Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("f8", ShowNextDiagnostic, Some("Editor")), + Binding::new("f2", Rename, Some("Editor")), + Binding::new("f12", GoToDefinition, Some("Editor")), + Binding::new("alt-shift-f12", FindAllReferences, Some("Editor")), + Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")), + Binding::new("pageup", PageUp, Some("Editor")), + Binding::new("pagedown", PageDown, Some("Editor")), + Binding::new("alt-cmd-[", Fold, Some("Editor")), + Binding::new("alt-cmd-]", Unfold, Some("Editor")), + Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")), + Binding::new("ctrl-space", ShowCompletions, Some("Editor")), + Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")), + ]); + + cx.add_action(Editor::open_new); + cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); + cx.add_action(Editor::select); + cx.add_action(Editor::cancel); + cx.add_action(Editor::handle_input); + cx.add_action(Editor::newline); + cx.add_action(Editor::backspace); + cx.add_action(Editor::delete); + cx.add_action(Editor::tab); + cx.add_action(Editor::outdent); + cx.add_action(Editor::delete_line); + cx.add_action(Editor::delete_to_previous_word_boundary); + cx.add_action(Editor::delete_to_next_word_boundary); + cx.add_action(Editor::delete_to_beginning_of_line); + cx.add_action(Editor::delete_to_end_of_line); + cx.add_action(Editor::cut_to_end_of_line); + cx.add_action(Editor::duplicate_line); + cx.add_action(Editor::move_line_up); + cx.add_action(Editor::move_line_down); + cx.add_action(Editor::cut); + cx.add_action(Editor::copy); + cx.add_action(Editor::paste); + cx.add_action(Editor::undo); + cx.add_action(Editor::redo); + cx.add_action(Editor::move_up); + cx.add_action(Editor::move_down); + cx.add_action(Editor::move_left); + cx.add_action(Editor::move_right); + cx.add_action(Editor::move_to_previous_word_boundary); + cx.add_action(Editor::move_to_next_word_boundary); + cx.add_action(Editor::move_to_beginning_of_line); + cx.add_action(Editor::move_to_end_of_line); + cx.add_action(Editor::move_to_beginning); + cx.add_action(Editor::move_to_end); + cx.add_action(Editor::select_up); + cx.add_action(Editor::select_down); + cx.add_action(Editor::select_left); + cx.add_action(Editor::select_right); + cx.add_action(Editor::select_to_previous_word_boundary); + cx.add_action(Editor::select_to_next_word_boundary); + cx.add_action(Editor::select_to_beginning_of_line); + cx.add_action(Editor::select_to_end_of_line); + cx.add_action(Editor::select_to_beginning); + cx.add_action(Editor::select_to_end); + cx.add_action(Editor::select_all); + cx.add_action(Editor::select_line); + cx.add_action(Editor::split_selection_into_lines); + cx.add_action(Editor::add_selection_above); + cx.add_action(Editor::add_selection_below); + cx.add_action(Editor::select_next); + cx.add_action(Editor::toggle_comments); + cx.add_action(Editor::select_larger_syntax_node); + cx.add_action(Editor::select_smaller_syntax_node); + cx.add_action(Editor::move_to_enclosing_bracket); + cx.add_action(Editor::show_next_diagnostic); + cx.add_action(Editor::go_to_definition); + cx.add_action(Editor::page_up); + cx.add_action(Editor::page_down); + cx.add_action(Editor::fold); + cx.add_action(Editor::unfold); + cx.add_action(Editor::fold_selected_ranges); + cx.add_action(Editor::show_completions); + cx.add_action(Editor::toggle_code_actions); + cx.add_async_action(Editor::confirm_completion); + cx.add_async_action(Editor::confirm_code_action); + cx.add_async_action(Editor::rename); + cx.add_async_action(Editor::confirm_rename); + cx.add_async_action(Editor::find_all_references); +} + +trait SelectionExt { + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn display_range(&self, map: &DisplaySnapshot) -> Range; + fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) + -> Range; +} + +trait InvalidationRegion { + fn ranges(&self) -> &[Range]; +} + +#[derive(Clone, Debug)] +pub enum SelectPhase { + Begin { + position: DisplayPoint, + add: bool, + click_count: usize, + }, + BeginColumnar { + position: DisplayPoint, + overshoot: u32, + }, + Extend { + position: DisplayPoint, + click_count: usize, + }, + Update { + position: DisplayPoint, + overshoot: u32, + scroll_position: Vector2F, + }, + End, +} + +#[derive(Clone, Debug)] +pub enum SelectMode { + Character, + Word(Range), + Line(Range), + All, +} + +#[derive(PartialEq, Eq)] +pub enum Autoscroll { + Fit, + Center, + Newest, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum EditorMode { + SingleLine, + AutoHeight { max_lines: usize }, + Full, +} + +#[derive(Clone)] +pub enum SoftWrap { + None, + EditorWidth, + Column(u32), +} + +#[derive(Clone)] +pub struct EditorStyle { + pub text: TextStyle, + pub placeholder_text: Option, + pub theme: theme::Editor, +} + +type CompletionId = usize; + +pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor; + +pub struct Editor { + handle: WeakViewHandle, + buffer: ModelHandle, + display_map: ModelHandle, + next_selection_id: usize, + selections: Arc<[Selection]>, + pending_selection: Option, + columnar_selection_tail: Option, + add_selections_state: Option, + select_next_state: Option, + selection_history: + HashMap]>, Option]>>)>, + autoclose_stack: InvalidationStack, + snippet_stack: InvalidationStack, + select_larger_syntax_node_stack: Vec]>>, + active_diagnostics: Option, + scroll_position: Vector2F, + scroll_top_anchor: Option, + autoscroll_request: Option, + settings: watch::Receiver, + soft_wrap_mode_override: Option, + get_field_editor_theme: Option, + project: Option>, + focused: bool, + show_local_cursors: bool, + blink_epoch: usize, + blinking_paused: bool, + mode: EditorMode, + vertical_scroll_margin: f32, + placeholder_text: Option>, + highlighted_rows: Option>, + highlighted_ranges: BTreeMap>)>, + nav_history: Option, + context_menu: Option, + completion_tasks: Vec<(CompletionId, Task>)>, + next_completion_id: CompletionId, + available_code_actions: Option<(ModelHandle, Arc<[CodeAction]>)>, + code_actions_task: Option>, + document_highlights_task: Option>, + pending_rename: Option, + searchable: bool, +} + +pub struct EditorSnapshot { + pub mode: EditorMode, + pub display_snapshot: DisplaySnapshot, + pub placeholder_text: Option>, + is_focused: bool, + scroll_position: Vector2F, + scroll_top_anchor: Option, +} + +#[derive(Clone)] +pub struct PendingSelection { + selection: Selection, + mode: SelectMode, +} + +struct AddSelectionsState { + above: bool, + stack: Vec, +} + +struct SelectNextState { + query: AhoCorasick, + wordwise: bool, + done: bool, +} + +struct BracketPairState { + ranges: Vec>, + pair: BracketPair, +} + +struct SnippetState { + ranges: Vec>>, + active_index: usize, +} + +pub struct RenameState { + pub range: Range, + pub old_name: String, + pub editor: ViewHandle, + block_id: BlockId, +} + +struct InvalidationStack(Vec); + +enum ContextMenu { + Completions(CompletionsMenu), + CodeActions(CodeActionsMenu), +} + +impl ContextMenu { + fn select_prev(&mut self, cx: &mut ViewContext) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_prev(cx), + ContextMenu::CodeActions(menu) => menu.select_prev(cx), + } + true + } else { + false + } + } + + fn select_next(&mut self, cx: &mut ViewContext) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_next(cx), + ContextMenu::CodeActions(menu) => menu.select_next(cx), + } + true + } else { + false + } + } + + fn visible(&self) -> bool { + match self { + ContextMenu::Completions(menu) => menu.visible(), + ContextMenu::CodeActions(menu) => menu.visible(), + } + } + + fn render( + &self, + cursor_position: DisplayPoint, + style: EditorStyle, + cx: &AppContext, + ) -> (DisplayPoint, ElementBox) { + match self { + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style), + } + } +} + +struct CompletionsMenu { + id: CompletionId, + initial_position: Anchor, + buffer: ModelHandle, + completions: Arc<[Completion]>, + match_candidates: Vec, + matches: Arc<[StringMatch]>, + selected_item: usize, + list: UniformListState, +} + +impl CompletionsMenu { + fn select_prev(&mut self, cx: &mut ViewContext) { + if self.selected_item > 0 { + self.selected_item -= 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + } + cx.notify(); + } + + fn select_next(&mut self, cx: &mut ViewContext) { + if self.selected_item + 1 < self.matches.len() { + self.selected_item += 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + } + cx.notify(); + } + + fn visible(&self) -> bool { + !self.matches.is_empty() + } + + fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox { + enum CompletionTag {} + + let completions = self.completions.clone(); + let matches = self.matches.clone(); + let selected_item = self.selected_item; + let container_style = style.autocomplete.container; + UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| { + let start_ix = range.start; + for (ix, mat) in matches[range].iter().enumerate() { + let completion = &completions[mat.candidate_id]; + let item_ix = start_ix + ix; + items.push( + MouseEventHandler::new::( + mat.candidate_id, + cx, + |state, _| { + let item_style = if item_ix == selected_item { + style.autocomplete.selected_item + } else if state.hovered { + style.autocomplete.hovered_item + } else { + style.autocomplete.item + }; + + Text::new(completion.label.text.clone(), style.text.clone()) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + style.text.color.into(), + styled_runs_for_code_label( + &completion.label, + style.text.color, + &style.syntax, + ), + &mat.positions, + )) + .contained() + .with_style(item_style) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(move |cx| { + cx.dispatch_action(ConfirmCompletion(Some(item_ix))); + }) + .boxed(), + ); + } + }) + .with_width_from_item( + self.matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + self.completions[mat.candidate_id] + .label + .text + .chars() + .count() + }) + .map(|(ix, _)| ix), + ) + .contained() + .with_style(container_style) + .boxed() + } + + pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { + let mut matches = if let Some(query) = query { + fuzzy::match_strings( + &self.match_candidates, + query, + false, + 100, + &Default::default(), + executor, + ) + .await + } else { + self.match_candidates + .iter() + .enumerate() + .map(|(candidate_id, candidate)| StringMatch { + candidate_id, + score: Default::default(), + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect() + }; + matches.sort_unstable_by_key(|mat| { + ( + Reverse(OrderedFloat(mat.score)), + self.completions[mat.candidate_id].sort_key(), + ) + }); + + for mat in &mut matches { + let filter_start = self.completions[mat.candidate_id].label.filter_range.start; + for position in &mut mat.positions { + *position += filter_start; + } + } + + self.matches = matches.into(); + } +} + +#[derive(Clone)] +struct CodeActionsMenu { + actions: Arc<[CodeAction]>, + buffer: ModelHandle, + selected_item: usize, + list: UniformListState, + deployed_from_indicator: bool, +} + +impl CodeActionsMenu { + fn select_prev(&mut self, cx: &mut ViewContext) { + if self.selected_item > 0 { + self.selected_item -= 1; + cx.notify() + } + } + + fn select_next(&mut self, cx: &mut ViewContext) { + if self.selected_item + 1 < self.actions.len() { + self.selected_item += 1; + cx.notify() + } + } + + fn visible(&self) -> bool { + !self.actions.is_empty() + } + + fn render( + &self, + mut cursor_position: DisplayPoint, + style: EditorStyle, + ) -> (DisplayPoint, ElementBox) { + enum ActionTag {} + + let container_style = style.autocomplete.container; + let actions = self.actions.clone(); + let selected_item = self.selected_item; + let element = + UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| { + let start_ix = range.start; + for (ix, action) in actions[range].iter().enumerate() { + let item_ix = start_ix + ix; + items.push( + MouseEventHandler::new::(item_ix, cx, |state, _| { + let item_style = if item_ix == selected_item { + style.autocomplete.selected_item + } else if state.hovered { + style.autocomplete.hovered_item + } else { + style.autocomplete.item + }; + + Text::new(action.lsp_action.title.clone(), style.text.clone()) + .with_soft_wrap(false) + .contained() + .with_style(item_style) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(move |cx| { + cx.dispatch_action(ConfirmCodeAction(Some(item_ix))); + }) + .boxed(), + ); + } + }) + .with_width_from_item( + self.actions + .iter() + .enumerate() + .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) + .map(|(ix, _)| ix), + ) + .contained() + .with_style(container_style) + .boxed(); + + if self.deployed_from_indicator { + *cursor_position.column_mut() = 0; + } + + (cursor_position, element) + } +} + +#[derive(Debug)] +struct ActiveDiagnosticGroup { + primary_range: Range, + primary_message: String, + blocks: HashMap, + is_valid: bool, +} + +#[derive(Serialize, Deserialize)] +struct ClipboardSelection { + len: usize, + is_entire_line: bool, +} + +pub struct NavigationData { + anchor: Anchor, + offset: usize, +} + +impl Editor { + pub fn single_line( + settings: watch::Receiver, + field_editor_style: Option, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::SingleLine, + buffer, + None, + settings, + field_editor_style, + cx, + ) + } + + pub fn auto_height( + max_lines: usize, + settings: watch::Receiver, + field_editor_style: Option, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::AutoHeight { max_lines }, + buffer, + None, + settings, + field_editor_style, + cx, + ) + } + + pub fn for_buffer( + buffer: ModelHandle, + project: Option>, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + Self::new(EditorMode::Full, buffer, project, settings, None, cx) + } + + pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { + let mut clone = Self::new( + self.mode, + self.buffer.clone(), + self.project.clone(), + self.settings.clone(), + self.get_field_editor_theme, + cx, + ); + clone.scroll_position = self.scroll_position; + clone.scroll_top_anchor = self.scroll_top_anchor.clone(); + clone.nav_history = Some(nav_history); + clone + } + + fn new( + mode: EditorMode, + buffer: ModelHandle, + project: Option>, + settings: watch::Receiver, + get_field_editor_theme: Option, + cx: &mut ViewContext, + ) -> Self { + let display_map = cx.add_model(|cx| { + let settings = settings.borrow(); + let style = build_style(&*settings, get_field_editor_theme, cx); + DisplayMap::new( + buffer.clone(), + settings.tab_size, + style.text.font_id, + style.text.font_size, + None, + 2, + 1, + cx, + ) + }); + cx.observe(&buffer, Self::on_buffer_changed).detach(); + cx.subscribe(&buffer, Self::on_buffer_event).detach(); + cx.observe(&display_map, Self::on_display_map_changed) + .detach(); + + let mut this = Self { + handle: cx.weak_handle(), + buffer, + display_map, + selections: Arc::from([]), + pending_selection: Some(PendingSelection { + selection: Selection { + id: 0, + start: Anchor::min(), + end: Anchor::min(), + reversed: false, + goal: SelectionGoal::None, + }, + mode: SelectMode::Character, + }), + columnar_selection_tail: None, + next_selection_id: 1, + add_selections_state: None, + select_next_state: None, + selection_history: Default::default(), + autoclose_stack: Default::default(), + snippet_stack: Default::default(), + select_larger_syntax_node_stack: Vec::new(), + active_diagnostics: None, + settings, + soft_wrap_mode_override: None, + get_field_editor_theme, + project, + scroll_position: Vector2F::zero(), + scroll_top_anchor: None, + autoscroll_request: None, + focused: false, + show_local_cursors: false, + blink_epoch: 0, + blinking_paused: false, + mode, + vertical_scroll_margin: 3.0, + placeholder_text: None, + highlighted_rows: None, + highlighted_ranges: Default::default(), + nav_history: None, + context_menu: None, + completion_tasks: Default::default(), + next_completion_id: 0, + available_code_actions: Default::default(), + code_actions_task: Default::default(), + document_highlights_task: Default::default(), + pending_rename: Default::default(), + searchable: true, + }; + this.end_selection(cx); + this + } + + pub fn open_new( + workspace: &mut Workspace, + _: &workspace::OpenNew, + cx: &mut ViewContext, + ) { + let buffer = cx + .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx)); + workspace.open_item(BufferItemHandle(buffer), cx); + } + + pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { + self.buffer.read(cx).replica_id() + } + + pub fn buffer(&self) -> &ModelHandle { + &self.buffer + } + + pub fn title(&self, cx: &AppContext) -> String { + self.buffer().read(cx).title(cx) + } + + pub fn snapshot(&mut self, cx: &mut MutableAppContext) -> EditorSnapshot { + EditorSnapshot { + mode: self.mode, + display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), + scroll_position: self.scroll_position, + scroll_top_anchor: self.scroll_top_anchor.clone(), + placeholder_text: self.placeholder_text.clone(), + is_focused: self + .handle + .upgrade(cx) + .map_or(false, |handle| handle.is_focused(cx)), + } + } + + pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { + self.buffer.read(cx).language(cx) + } + + fn style(&self, cx: &AppContext) -> EditorStyle { + build_style(&*self.settings.borrow(), self.get_field_editor_theme, cx) + } + + pub fn set_placeholder_text( + &mut self, + placeholder_text: impl Into>, + cx: &mut ViewContext, + ) { + self.placeholder_text = Some(placeholder_text.into()); + cx.notify(); + } + + pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext) { + self.vertical_scroll_margin = margin_rows as f32; + cx.notify(); + } + + pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if scroll_position.y() == 0. { + self.scroll_top_anchor = None; + self.scroll_position = scroll_position; + } else { + let scroll_top_buffer_offset = + DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); + let anchor = map + .buffer_snapshot + .anchor_at(scroll_top_buffer_offset, Bias::Right); + self.scroll_position = vec2f( + scroll_position.x(), + scroll_position.y() - anchor.to_display_point(&map).row() as f32, + ); + self.scroll_top_anchor = Some(anchor); + } + + cx.notify(); + } + + pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) + } + + pub fn clamp_scroll_left(&mut self, max: f32) -> bool { + if max < self.scroll_position.x() { + self.scroll_position.set_x(max); + true + } else { + false + } + } + + pub fn autoscroll_vertically( + &mut self, + viewport_height: f32, + line_height: f32, + cx: &mut ViewContext, + ) -> bool { + let visible_lines = viewport_height / line_height; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut scroll_position = + compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor); + let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) + } else { + display_map.max_point().row().saturating_sub(1) as f32 + }; + if scroll_position.y() > max_scroll_top { + scroll_position.set_y(max_scroll_top); + self.set_scroll_position(scroll_position, cx); + } + + let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() { + autoscroll + } else { + return false; + }; + + let first_cursor_top; + let last_cursor_bottom; + if let Some(highlighted_rows) = &self.highlighted_rows { + first_cursor_top = highlighted_rows.start as f32; + last_cursor_bottom = first_cursor_top + 1.; + } else if autoscroll == Autoscroll::Newest { + let newest_selection = self.newest_selection::(&display_map.buffer_snapshot); + first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; + last_cursor_bottom = first_cursor_top + 1.; + } else { + let selections = self.local_selections::(cx); + first_cursor_top = selections + .first() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + last_cursor_bottom = selections + .last() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32 + + 1.0; + } + + let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + 0. + } else { + ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() + }; + if margin < 0.0 { + return false; + } + + match autoscroll { + Autoscroll::Fit | Autoscroll::Newest => { + let margin = margin.min(self.vertical_scroll_margin); + let target_top = (first_cursor_top - margin).max(0.0); + let target_bottom = last_cursor_bottom + margin; + let start_row = scroll_position.y(); + let end_row = start_row + visible_lines; + + if target_top < start_row { + scroll_position.set_y(target_top); + self.set_scroll_position(scroll_position, cx); + } else if target_bottom >= end_row { + scroll_position.set_y(target_bottom - visible_lines); + self.set_scroll_position(scroll_position, cx); + } + } + Autoscroll::Center => { + scroll_position.set_y((first_cursor_top - margin).max(0.0)); + self.set_scroll_position(scroll_position, cx); + } + } + + true + } + + pub fn autoscroll_horizontally( + &mut self, + start_row: u32, + viewport_width: f32, + scroll_width: f32, + max_glyph_width: f32, + layouts: &[text_layout::Line], + cx: &mut ViewContext, + ) -> bool { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.local_selections::(cx); + + let mut target_left; + let mut target_right; + + if self.highlighted_rows.is_some() { + target_left = 0.0_f32; + target_right = 0.0_f32; + } else { + target_left = std::f32::INFINITY; + target_right = 0.0_f32; + for selection in selections { + let head = selection.head().to_display_point(&display_map); + if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { + let start_column = head.column().saturating_sub(3); + let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); + target_left = target_left.min( + layouts[(head.row() - start_row) as usize] + .x_for_index(start_column as usize), + ); + target_right = target_right.max( + layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) + + max_glyph_width, + ); + } + } + } + + target_right = target_right.min(scroll_width); + + if target_right - target_left > viewport_width { + return false; + } + + let scroll_left = self.scroll_position.x() * max_glyph_width; + let scroll_right = scroll_left + viewport_width; + + if target_left < scroll_left { + self.scroll_position.set_x(target_left / max_glyph_width); + true + } else if target_right > scroll_right { + self.scroll_position + .set_x((target_right - viewport_width) / max_glyph_width); + true + } else { + false + } + } + + fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { + self.hide_context_menu(cx); + + match phase { + SelectPhase::Begin { + position, + add, + click_count, + } => self.begin_selection(*position, *add, *click_count, cx), + SelectPhase::BeginColumnar { + position, + overshoot, + } => self.begin_columnar_selection(*position, *overshoot, cx), + SelectPhase::Extend { + position, + click_count, + } => self.extend_selection(*position, *click_count, cx), + SelectPhase::Update { + position, + overshoot, + scroll_position, + } => self.update_selection(*position, *overshoot, *scroll_position, cx), + SelectPhase::End => self.end_selection(cx), + } + } + + fn extend_selection( + &mut self, + position: DisplayPoint, + click_count: usize, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self + .newest_selection::(&display_map.buffer_snapshot) + .tail(); + self.begin_selection(position, false, click_count, cx); + + let position = position.to_offset(&display_map, Bias::Left); + let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); + let mut pending = self.pending_selection.clone().unwrap(); + + if position >= tail { + pending.selection.start = tail_anchor.clone(); + } else { + pending.selection.end = tail_anchor.clone(); + pending.selection.reversed = true; + } + + match &mut pending.mode { + SelectMode::Word(range) | SelectMode::Line(range) => { + *range = tail_anchor.clone()..tail_anchor + } + _ => {} + } + + self.set_selections(self.selections.clone(), Some(pending), cx); + } + + fn begin_selection( + &mut self, + position: DisplayPoint, + add: bool, + click_count: usize, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus_self(); + cx.emit(Event::Activate); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let newest_selection = self.newest_anchor_selection().clone(); + + let start; + let end; + let mode; + match click_count { + 1 => { + start = buffer.anchor_before(position.to_point(&display_map)); + end = start.clone(); + mode = SelectMode::Character; + } + 2 => { + let range = movement::surrounding_word(&display_map, position); + start = buffer.anchor_before(range.start.to_point(&display_map)); + end = buffer.anchor_before(range.end.to_point(&display_map)); + mode = SelectMode::Word(start.clone()..end.clone()); + } + 3 => { + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + start = buffer.anchor_before(line_start); + end = buffer.anchor_before(next_line_start); + mode = SelectMode::Line(start.clone()..end.clone()); + } + _ => { + start = buffer.anchor_before(0); + end = buffer.anchor_before(buffer.len()); + mode = SelectMode::All; + } + } + + self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); + + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start, + end, + reversed: false, + goal: SelectionGoal::None, + }; + + let mut selections; + if add { + selections = self.selections.clone(); + // Remove the newest selection if it was added due to a previous mouse up + // within this multi-click. + if click_count > 1 { + selections = self + .selections + .iter() + .filter(|selection| selection.id != newest_selection.id) + .cloned() + .collect(); + } + } else { + selections = Arc::from([]); + } + self.set_selections(selections, Some(PendingSelection { selection, mode }), cx); + + cx.notify(); + } + + fn begin_columnar_selection( + &mut self, + position: DisplayPoint, + overshoot: u32, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus_self(); + cx.emit(Event::Activate); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self + .newest_selection::(&display_map.buffer_snapshot) + .tail(); + self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); + + self.select_columns( + tail.to_display_point(&display_map), + position, + overshoot, + &display_map, + cx, + ); + } + + fn update_selection( + &mut self, + position: DisplayPoint, + overshoot: u32, + scroll_position: Vector2F, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(tail) = self.columnar_selection_tail.as_ref() { + let tail = tail.to_display_point(&display_map); + self.select_columns(tail, position, overshoot, &display_map, cx); + } else if let Some(mut pending) = self.pending_selection.clone() { + let buffer = self.buffer.read(cx).snapshot(cx); + let head; + let tail; + match &pending.mode { + SelectMode::Character => { + head = position.to_point(&display_map); + tail = pending.selection.tail().to_point(&buffer); + } + SelectMode::Word(original_range) => { + let original_display_range = original_range.start.to_display_point(&display_map) + ..original_range.end.to_display_point(&display_map); + let original_buffer_range = original_display_range.start.to_point(&display_map) + ..original_display_range.end.to_point(&display_map); + if movement::is_inside_word(&display_map, position) + || original_display_range.contains(&position) + { + let word_range = movement::surrounding_word(&display_map, position); + if word_range.start < original_display_range.start { + head = word_range.start.to_point(&display_map); + } else { + head = word_range.end.to_point(&display_map); + } + } else { + head = position.to_point(&display_map); + } + + if head <= original_buffer_range.start { + tail = original_buffer_range.end; + } else { + tail = original_buffer_range.start; + } + } + SelectMode::Line(original_range) => { + let original_range = original_range.to_point(&display_map.buffer_snapshot); + + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + + if line_start < original_range.start { + head = line_start + } else { + head = next_line_start + } + + if head <= original_range.start { + tail = original_range.end; + } else { + tail = original_range.start; + } + } + SelectMode::All => { + return; + } + }; + + if head < tail { + pending.selection.start = buffer.anchor_before(head); + pending.selection.end = buffer.anchor_before(tail); + pending.selection.reversed = true; + } else { + pending.selection.start = buffer.anchor_before(tail); + pending.selection.end = buffer.anchor_before(head); + pending.selection.reversed = false; + } + self.set_selections(self.selections.clone(), Some(pending), cx); + } else { + log::error!("update_selection dispatched with no pending selection"); + return; + } + + self.set_scroll_position(scroll_position, cx); + cx.notify(); + } + + fn end_selection(&mut self, cx: &mut ViewContext) { + self.columnar_selection_tail.take(); + if self.pending_selection.is_some() { + let selections = self.local_selections::(cx); + self.update_selections(selections, None, cx); + } + } + + fn select_columns( + &mut self, + tail: DisplayPoint, + head: DisplayPoint, + overshoot: u32, + display_map: &DisplaySnapshot, + cx: &mut ViewContext, + ) { + let start_row = cmp::min(tail.row(), head.row()); + let end_row = cmp::max(tail.row(), head.row()); + let start_column = cmp::min(tail.column(), head.column() + overshoot); + let end_column = cmp::max(tail.column(), head.column() + overshoot); + let reversed = start_column < tail.column(); + + let selections = (start_row..=end_row) + .filter_map(|row| { + if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { + let start = display_map + .clip_point(DisplayPoint::new(row, start_column), Bias::Left) + .to_point(&display_map); + let end = display_map + .clip_point(DisplayPoint::new(row, end_column), Bias::Right) + .to_point(&display_map); + Some(Selection { + id: post_inc(&mut self.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + }) + } else { + None + } + }) + .collect::>(); + + self.update_selections(selections, None, cx); + cx.notify(); + } + + pub fn is_selecting(&self) -> bool { + self.pending_selection.is_some() || self.columnar_selection_tail.is_some() + } + + pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.take_rename(cx).is_some() { + return; + } + + if self.hide_context_menu(cx).is_some() { + return; + } + + if self.snippet_stack.pop().is_some() { + return; + } + + if self.mode != EditorMode::Full { + cx.propagate_action(); + return; + } + + if self.active_diagnostics.is_some() { + self.dismiss_diagnostics(cx); + } else if let Some(pending) = self.pending_selection.clone() { + let mut selections = self.selections.clone(); + if selections.is_empty() { + selections = Arc::from([pending.selection]); + } + self.set_selections(selections, None, cx); + self.request_autoscroll(Autoscroll::Fit, cx); + } else { + let buffer = self.buffer.read(cx).snapshot(cx); + let mut oldest_selection = self.oldest_selection::(&buffer); + if self.selection_count() == 1 { + if oldest_selection.is_empty() { + cx.propagate_action(); + return; + } + + oldest_selection.start = oldest_selection.head().clone(); + oldest_selection.end = oldest_selection.head().clone(); + } + self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx); + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn selected_ranges>( + &self, + cx: &mut MutableAppContext, + ) -> Vec> { + self.local_selections::(cx) + .iter() + .map(|s| { + if s.reversed { + s.end.clone()..s.start.clone() + } else { + s.start.clone()..s.end.clone() + } + }) + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec> { + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + self.selections + .iter() + .chain( + self.pending_selection + .as_ref() + .map(|pending| &pending.selection), + ) + .map(|s| { + if s.reversed { + s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) + } else { + s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map) + } + }) + .collect() + } + + pub fn select_ranges( + &mut self, + ranges: I, + autoscroll: Option, + cx: &mut ViewContext, + ) where + I: IntoIterator>, + T: ToOffset, + { + let buffer = self.buffer.read(cx).snapshot(cx); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start.to_offset(&buffer); + let mut end = range.end.to_offset(&buffer); + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } + }) + .collect::>(); + self.update_selections(selections, autoscroll, cx); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext) + where + T: IntoIterator>, + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start; + let mut end = range.end; + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.next_selection_id), + start: start.to_point(&display_map), + end: end.to_point(&display_map), + reversed, + goal: SelectionGoal::None, + } + }) + .collect(); + self.update_selections(selections, None, cx); + } + + pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { + let text = action.0.as_ref(); + if !self.skip_autoclose_end(text, cx) { + self.start_transaction(cx); + self.insert(text, cx); + self.autoclose_pairs(cx); + self.end_transaction(cx); + self.trigger_completion_on_input(text, cx); + } + } + + pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { + self.start_transaction(cx); + let mut old_selections = SmallVec::<[_; 32]>::new(); + { + let selections = self.local_selections::(cx); + let buffer = self.buffer.read(cx).snapshot(cx); + for selection in selections.iter() { + let start_point = selection.start.to_point(&buffer); + let indent = buffer + .indent_column_for_line(start_point.row) + .min(start_point.column); + let start = selection.start; + let end = selection.end; + + let mut insert_extra_newline = false; + if let Some(language) = buffer.language() { + let leading_whitespace_len = buffer + .reversed_chars_at(start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let trailing_whitespace_len = buffer + .chars_at(end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + insert_extra_newline = language.brackets().iter().any(|pair| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + pair.newline + && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) + && buffer.contains_str_at( + (start - leading_whitespace_len).saturating_sub(pair_start.len()), + pair_start, + ) + }); + } + + old_selections.push(( + selection.id, + buffer.anchor_after(end), + start..end, + indent, + insert_extra_newline, + )); + } + } + + self.buffer.update(cx, |buffer, cx| { + let mut delta = 0_isize; + let mut pending_edit: Option = None; + for (_, _, range, indent, insert_extra_newline) in &old_selections { + if pending_edit.as_ref().map_or(false, |pending| { + pending.indent != *indent + || pending.insert_extra_newline != *insert_extra_newline + }) { + let pending = pending_edit.take().unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + delta += pending.delta; + } + + let start = (range.start as isize + delta) as usize; + let end = (range.end as isize + delta) as usize; + let mut text_len = *indent as usize + 1; + if *insert_extra_newline { + text_len *= 2; + } + + let pending = pending_edit.get_or_insert_with(Default::default); + pending.delta += text_len as isize - (end - start) as isize; + pending.indent = *indent; + pending.insert_extra_newline = *insert_extra_newline; + pending.ranges.push(start..end); + } + + let pending = pending_edit.unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + + let buffer = buffer.read(cx); + self.selections = self + .selections + .iter() + .cloned() + .zip(old_selections) + .map( + |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { + let mut cursor = end_anchor.to_point(&buffer); + if insert_extra_newline { + cursor.row -= 1; + cursor.column = buffer.line_len(cursor.row); + } + let anchor = buffer.anchor_after(cursor); + new_selection.start = anchor.clone(); + new_selection.end = anchor; + new_selection + }, + ) + .collect(); + }); + + self.request_autoscroll(Autoscroll::Fit, cx); + self.end_transaction(cx); + + #[derive(Default)] + struct PendingEdit { + indent: u32, + insert_extra_newline: bool, + delta: isize, + ranges: SmallVec<[Range; 32]>, + } + } + + pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { + self.start_transaction(cx); + + let old_selections = self.local_selections::(cx); + let selection_anchors = self.buffer.update(cx, |buffer, cx| { + let anchors = { + let snapshot = buffer.read(cx); + old_selections + .iter() + .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) + .collect::>() + }; + let edit_ranges = old_selections.iter().map(|s| s.start..s.end); + buffer.edit_with_autoindent(edit_ranges, text, cx); + anchors + }); + + let selections = { + let snapshot = self.buffer.read(cx).read(cx); + selection_anchors + .into_iter() + .map(|(id, goal, position)| { + let position = position.to_offset(&snapshot); + Selection { + id, + start: position, + end: position, + goal, + reversed: false, + } + }) + .collect() + }; + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { + let selection = self.newest_anchor_selection(); + if self + .buffer + .read(cx) + .is_completion_trigger(selection.head(), text, cx) + { + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_context_menu(cx); + } + } + + fn autoclose_pairs(&mut self, cx: &mut ViewContext) { + let selections = self.local_selections::(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 { + id: selection.id, + start: snapshot.anchor_before(selection.start), + end: snapshot.anchor_before(selection.end), + reversed: selection.reversed, + goal: selection.goal, + }) + .collect::>(); + + let autoclose_pair = snapshot.language().and_then(|language| { + let first_selection_start = selections.first().unwrap().start; + let pair = language.brackets().iter().find(|pair| { + snapshot.contains_str_at( + first_selection_start.saturating_sub(pair.start.len()), + &pair.start, + ) + }); + pair.and_then(|pair| { + let should_autoclose = selections[1..].iter().all(|selection| { + snapshot.contains_str_at( + selection.start.saturating_sub(pair.start.len()), + &pair.start, + ) + }); + + 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::>(); + + buffer.edit(selection_ranges, &pair.end, cx); + snapshot = buffer.snapshot(cx); + + new_selections = Some( + self.resolve_selections::(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, + }); + } + } + }); + + if let Some(new_selections) = new_selections { + self.update_selections(new_selections, None, cx); + } + if let Some(bracket_pair_state) = bracket_pair_state { + self.autoclose_stack.push(bracket_pair_state); + } + } + + fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { + let old_selections = self.local_selections::(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; + } + + debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); + + let buffer = self.buffer.read(cx).snapshot(cx); + 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.update_selections(new_selections, Some(Autoscroll::Fit), cx); + true + } else { + false + } + } + + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + let offset = position.to_offset(buffer); + let (word_range, kind) = buffer.surrounding_word(offset); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + } + + fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { + if self.pending_rename.is_some() { + return; + } + + let project = if let Some(project) = self.project.clone() { + project + } else { + return; + }; + + let position = self.newest_anchor_selection().head(); + let (buffer, buffer_position) = if let Some(output) = self + .buffer + .read(cx) + .text_anchor_for_position(position.clone(), cx) + { + output + } else { + return; + }; + + let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, buffer_position.clone(), cx) + }); + + let id = post_inc(&mut self.next_completion_id); + let task = cx.spawn_weak(|this, mut cx| { + async move { + let completions = completions.await?; + if completions.is_empty() { + return Ok(()); + } + + let mut menu = CompletionsMenu { + id, + initial_position: position, + match_candidates: completions + .iter() + .enumerate() + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label.text[completion.label.filter_range.clone()].into(), + ) + }) + .collect(), + buffer, + completions: completions.into(), + matches: Vec::new().into(), + selected_item: 0, + list: Default::default(), + }; + + menu.filter(query.as_deref(), cx.background()).await; + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + match this.context_menu.as_ref() { + None => {} + Some(ContextMenu::Completions(prev_menu)) => { + if prev_menu.id > menu.id { + return; + } + } + _ => return, + } + + this.completion_tasks.retain(|(id, _)| *id > menu.id); + if this.focused { + this.show_context_menu(ContextMenu::Completions(menu), cx); + } + + cx.notify(); + }); + } + Ok::<_, anyhow::Error>(()) + } + .log_err() + }); + self.completion_tasks.push((id, task)); + } + + pub fn confirm_completion( + &mut self, + ConfirmCompletion(completion_ix): &ConfirmCompletion, + cx: &mut ViewContext, + ) -> Option>> { + use language::ToOffset as _; + + let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; + + let mat = completions_menu + .matches + .get(completion_ix.unwrap_or(completions_menu.selected_item))?; + let buffer_handle = completions_menu.buffer; + let completion = completions_menu.completions.get(mat.candidate_id)?; + + let snippet; + let text; + if completion.is_snippet() { + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + text = completion.new_text.clone(); + }; + let buffer = buffer_handle.read(cx); + let old_range = completion.old_range.to_offset(&buffer); + let old_text = buffer.text_for_range(old_range.clone()).collect::(); + + let selections = self.local_selections::(cx); + let newest_selection = self.newest_anchor_selection(); + if newest_selection.start.buffer_id != Some(buffer_handle.id()) { + return None; + } + + let lookbehind = newest_selection + .start + .text_anchor + .to_offset(buffer) + .saturating_sub(old_range.start); + let lookahead = old_range + .end + .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); + let mut common_prefix_len = old_text + .bytes() + .zip(text.bytes()) + .take_while(|(a, b)| a == b) + .count(); + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut ranges = Vec::new(); + for selection in &selections { + if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { + let start = selection.start.saturating_sub(lookbehind); + let end = selection.end + lookahead; + ranges.push(start + common_prefix_len..end); + } else { + common_prefix_len = 0; + ranges.clear(); + ranges.extend(selections.iter().map(|s| { + if s.id == newest_selection.id { + old_range.clone() + } else { + s.start..s.end + } + })); + break; + } + } + let text = &text[common_prefix_len..]; + + self.start_transaction(cx); + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } + + self.insert_snippet(&ranges, snippet, cx).log_err(); + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent(ranges, text, cx); + }); + } + self.end_transaction(cx); + + let project = self.project.clone()?; + let apply_edits = project.update(cx, |project, cx| { + project.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ) + }); + Some(cx.foreground().spawn(async move { + apply_edits.await?; + Ok(()) + })) + } + + pub fn toggle_code_actions( + &mut self, + &ToggleCodeActions(deployed_from_indicator): &ToggleCodeActions, + cx: &mut ViewContext, + ) { + if matches!( + self.context_menu.as_ref(), + Some(ContextMenu::CodeActions(_)) + ) { + self.context_menu.take(); + cx.notify(); + return; + } + + let mut task = self.code_actions_task.take(); + cx.spawn_weak(|this, mut cx| async move { + while let Some(prev_task) = task { + prev_task.await; + task = this + .upgrade(&cx) + .and_then(|this| this.update(&mut cx, |this, _| this.code_actions_task.take())); + } + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if this.focused { + if let Some((buffer, actions)) = this.available_code_actions.clone() { + this.show_context_menu( + ContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions, + selected_item: Default::default(), + list: Default::default(), + deployed_from_indicator, + }), + cx, + ); + } + } + }) + } + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn confirm_code_action( + workspace: &mut Workspace, + ConfirmCodeAction(action_ix): &ConfirmCodeAction, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + let actions_menu = if let ContextMenu::CodeActions(menu) = + editor.update(cx, |editor, cx| editor.hide_context_menu(cx))? + { + menu + } else { + return None; + }; + let action_ix = action_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?.clone(); + let title = action.lsp_action.title.clone(); + let buffer = actions_menu.buffer; + + let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { + project.apply_code_action(buffer, action, true, cx) + }); + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = apply_code_actions.await?; + Self::open_project_transaction(editor, workspace, project_transaction, title, cx).await + })) + } + + async fn open_project_transaction( + this: ViewHandle, + workspace: ViewHandle, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncAppContext, + ) -> Result<()> { + let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); + + // If the code action's edits are all contained within this editor, then + // avoid opening a new editor to display them. + let mut entries = transaction.0.iter(); + if let Some((buffer, transaction)) = entries.next() { + if entries.next().is_none() { + let excerpt = this.read_with(&cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.newest_anchor_selection().head(), cx) + }); + if let Some((excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + let excerpt_range = excerpt_range.to_offset(&snapshot); + if snapshot + .edited_ranges_for_transaction(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + { + return Ok(()); + } + } + } + } + } + + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer, transaction) in &transaction.0 { + let snapshot = buffer.read(cx).snapshot(); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + snapshot + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(&transaction.0); + multibuffer + }); + + workspace.update(&mut cx, |workspace, cx| { + let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_ranges::(ranges_to_highlight, color, cx); + }); + } + }); + + Ok(()) + } + + fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { + let project = self.project.as_ref()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.newest_anchor_selection().clone(); + let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; + let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?; + if start_buffer != end_buffer { + return None; + } + + let actions = project.update(cx, |project, cx| { + project.code_actions(&start_buffer, start..end, cx) + }); + self.code_actions_task = Some(cx.spawn_weak(|this, mut cx| async move { + let actions = actions.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.available_code_actions = actions.log_err().and_then(|actions| { + if actions.is_empty() { + None + } else { + Some((start_buffer, actions.into())) + } + }); + cx.notify(); + }) + } + })); + None + } + + fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { + let project = self.project.as_ref()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.newest_anchor_selection().clone(); + let cursor_position = newest_selection.head(); + let (cursor_buffer, cursor_buffer_position) = + buffer.text_anchor_for_position(cursor_position.clone(), cx)?; + let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; + if cursor_buffer != tail_buffer { + return None; + } + + let highlights = project.update(cx, |project, cx| { + project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }); + + enum DocumentHighlightRead {} + enum DocumentHighlightWrite {} + + self.document_highlights_task = Some(cx.spawn_weak(|this, mut cx| async move { + let highlights = highlights.log_err().await; + if let Some((this, highlights)) = this.upgrade(&cx).zip(highlights) { + this.update(&mut cx, |this, cx| { + let buffer_id = cursor_position.buffer_id; + let excerpt_id = cursor_position.excerpt_id.clone(); + let style = this.style(cx); + let read_background = style.document_highlight_read_background; + let write_background = style.document_highlight_write_background; + let buffer = this.buffer.read(cx); + if !buffer + .text_anchor_for_position(cursor_position, cx) + .map_or(false, |(buffer, _)| buffer == cursor_buffer) + { + return; + } + + let mut write_ranges = Vec::new(); + let mut read_ranges = Vec::new(); + for highlight in highlights { + let range = Anchor { + buffer_id, + excerpt_id: excerpt_id.clone(), + text_anchor: highlight.range.start, + }..Anchor { + buffer_id, + excerpt_id: excerpt_id.clone(), + text_anchor: highlight.range.end, + }; + if highlight.kind == lsp::DocumentHighlightKind::WRITE { + write_ranges.push(range); + } else { + read_ranges.push(range); + } + } + + this.highlight_ranges::( + read_ranges, + read_background, + cx, + ); + this.highlight_ranges::( + write_ranges, + write_background, + cx, + ); + cx.notify(); + }); + } + })); + None + } + + pub fn render_code_actions_indicator( + &self, + style: &EditorStyle, + cx: &mut ViewContext, + ) -> Option { + if self.available_code_actions.is_some() { + enum Tag {} + Some( + MouseEventHandler::new::(0, cx, |_, _| { + Svg::new("icons/zap.svg") + .with_color(style.code_actions_indicator) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(3.)) + .on_mouse_down(|cx| { + cx.dispatch_action(ToggleCodeActions(true)); + }) + .boxed(), + ) + } else { + None + } + } + + pub fn context_menu_visible(&self) -> bool { + self.context_menu + .as_ref() + .map_or(false, |menu| menu.visible()) + } + + pub fn render_context_menu( + &self, + cursor_position: DisplayPoint, + style: EditorStyle, + cx: &AppContext, + ) -> Option<(DisplayPoint, ElementBox)> { + self.context_menu + .as_ref() + .map(|menu| menu.render(cursor_position, style, cx)) + } + + fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { + if !matches!(menu, ContextMenu::Completions(_)) { + self.completion_tasks.clear(); + } + self.context_menu = Some(menu); + cx.notify(); + } + + fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { + cx.notify(); + self.completion_tasks.clear(); + self.context_menu.take() + } + + pub fn insert_snippet( + &mut self, + insertion_ranges: &[Range], + snippet: Snippet, + cx: &mut ViewContext, + ) -> Result<()> { + let tabstops = self.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent(insertion_ranges.iter().cloned(), &snippet.text, cx); + + let snapshot = &*buffer.read(cx); + let snippet = &snippet; + snippet + .tabstops + .iter() + .map(|tabstop| { + let mut tabstop_ranges = tabstop + .iter() + .flat_map(|tabstop_range| { + let mut delta = 0 as isize; + insertion_ranges.iter().map(move |insertion_range| { + let insertion_start = insertion_range.start as isize + delta; + delta += + snippet.text.len() as isize - insertion_range.len() as isize; + + let start = snapshot.anchor_before( + (insertion_start + tabstop_range.start) as usize, + ); + let end = snapshot + .anchor_after((insertion_start + tabstop_range.end) as usize); + start..end + }) + }) + .collect::>(); + tabstop_ranges + .sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot).unwrap()); + tabstop_ranges + }) + .collect::>() + }); + + if let Some(tabstop) = tabstops.first() { + self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx); + self.snippet_stack.push(SnippetState { + active_index: 0, + ranges: tabstops, + }); + } + + Ok(()) + } + + pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { + self.move_to_snippet_tabstop(Bias::Right, cx) + } + + pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) { + self.move_to_snippet_tabstop(Bias::Left, cx); + } + + pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { + let buffer = self.buffer.read(cx).snapshot(cx); + + if let Some(snippet) = self.snippet_stack.last_mut() { + match bias { + Bias::Left => { + if snippet.active_index > 0 { + snippet.active_index -= 1; + } else { + return false; + } + } + Bias::Right => { + if snippet.active_index + 1 < snippet.ranges.len() { + snippet.active_index += 1; + } else { + return false; + } + } + } + if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { + let new_selections = current_ranges + .iter() + .map(|new_range| { + let new_range = new_range.to_offset(&buffer); + Selection { + id: post_inc(&mut self.next_selection_id), + start: new_range.start, + end: new_range.end, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + + // Remove the snippet state when moving to the last tabstop. + if snippet.active_index + 1 == snippet.ranges.len() { + self.snippet_stack.pop(); + } + + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + return true; + } + self.snippet_stack.pop(); + } + + false + } + + pub fn clear(&mut self, cx: &mut ViewContext) { + self.start_transaction(cx); + self.select_all(&SelectAll, cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { + self.start_transaction(cx); + let mut selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::left(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { + self.start_transaction(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::right(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert(&"", cx); + self.end_transaction(cx); + } + + pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + if self.move_to_next_snippet_tabstop(cx) { + return; + } + + self.start_transaction(cx); + let tab_size = self.settings.borrow().tab_size; + let mut selections = self.local_selections::(cx); + let mut last_indent = None; + self.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + if selection.is_empty() { + let char_column = buffer + .read(cx) + .text_for_range(Point::new(selection.start.row, 0)..selection.start) + .flat_map(str::chars) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column % tab_size); + buffer.edit( + [selection.start..selection.start], + " ".repeat(chars_to_next_tab_stop), + cx, + ); + selection.start.column += chars_to_next_tab_stop as u32; + selection.end = selection.start; + } else { + let mut start_row = selection.start.row; + let mut end_row = selection.end.row + 1; + + // If a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 { + end_row -= 1; + } + + // Avoid re-indenting a row that has already been indented by a + // previous selection, but still update this selection's column + // to reflect that indentation. + if let Some((last_indent_row, last_indent_len)) = last_indent { + if last_indent_row == selection.start.row { + selection.start.column += last_indent_len; + start_row += 1; + } + if last_indent_row == selection.end.row { + selection.end.column += last_indent_len; + } + } + + for row in start_row..end_row { + let indent_column = buffer.read(cx).indent_column_for_line(row) as usize; + let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); + let row_start = Point::new(row, 0); + buffer.edit( + [row_start..row_start], + " ".repeat(columns_to_next_tab_stop), + cx, + ); + + // Update this selection's endpoints to reflect the indentation. + if row == selection.start.row { + selection.start.column += columns_to_next_tab_stop as u32; + } + if row == selection.end.row { + selection.end.column += columns_to_next_tab_stop as u32; + } + + last_indent = Some((row, columns_to_next_tab_stop as u32)); + } + } + } + }); + + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { + if !self.snippet_stack.is_empty() { + self.move_to_prev_snippet_tabstop(cx); + return; + } + + self.start_transaction(cx); + let tab_size = self.settings.borrow().tab_size; + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut deletion_ranges = Vec::new(); + let mut last_outdent = None; + { + let buffer = self.buffer.read(cx).read(cx); + for selection in &selections { + let mut rows = selection.spanned_rows(false, &display_map); + + // Avoid re-outdenting a row that has already been outdented by a + // previous selection. + if let Some(last_row) = last_outdent { + if last_row == rows.start { + rows.start += 1; + } + } + + for row in rows { + let column = buffer.indent_column_for_line(row) as usize; + if column > 0 { + let mut deletion_len = (column % tab_size) as u32; + if deletion_len == 0 { + deletion_len = tab_size as u32; + } + deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len)); + last_outdent = Some(row); + } + } + } + } + self.buffer.update(cx, |buffer, cx| { + buffer.edit(deletion_ranges, "", cx); + }); + + self.update_selections( + self.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); + self.end_transaction(cx); + } + + pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { + self.start_transaction(cx); + + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut new_cursors = Vec::new(); + let mut edit_ranges = Vec::new(); + let mut selections = selections.iter().peekable(); + while let Some(selection) = selections.next() { + let mut rows = selection.spanned_rows(false, &display_map); + let goal_display_column = selection.head().to_display_point(&display_map).column(); + + // Accumulate contiguous regions of rows that we want to delete. + while let Some(next_selection) = selections.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start <= rows.end { + rows.end = next_rows.end; + selections.next().unwrap(); + } else { + break; + } + } + + let mut edit_start = Point::new(rows.start, 0).to_offset(&buffer); + let edit_end; + let cursor_buffer_row; + if buffer.max_point().row >= rows.end { + // If there's a line after the range, delete the \n from the end of the row range + // and position the cursor on the next line. + edit_end = Point::new(rows.end, 0).to_offset(&buffer); + cursor_buffer_row = rows.end; + } else { + // If there isn't a line after the range, delete the \n from the line before the + // start of the row range and position the cursor there. + edit_start = edit_start.saturating_sub(1); + edit_end = buffer.len(); + cursor_buffer_row = rows.start.saturating_sub(1); + } + + let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map); + *cursor.column_mut() = + cmp::min(goal_display_column, display_map.line_len(cursor.row())); + + new_cursors.push(( + selection.id, + buffer.anchor_after(cursor.to_point(&display_map)), + )); + edit_ranges.push(edit_start..edit_end); + } + + let buffer = self.buffer.update(cx, |buffer, cx| { + buffer.edit(edit_ranges, "", cx); + buffer.snapshot(cx) + }); + let new_selections = new_cursors + .into_iter() + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { + self.start_transaction(cx); + + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + + let mut edits = Vec::new(); + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + // Avoid duplicating the same lines twice. + let mut rows = selection.spanned_rows(false, &display_map); + + while let Some(next_selection) = selections_iter.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start <= rows.end - 1 { + rows.end = next_rows.end; + selections_iter.next().unwrap(); + } else { + break; + } + } + + // Copy the text from the selected row region and splice it at the start of the region. + let start = Point::new(rows.start, 0); + let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1)); + let text = buffer + .text_for_range(start..end) + .chain(Some("\n")) + .collect::(); + edits.push((start, text, rows.len() as u32)); + } + + self.buffer.update(cx, |buffer, cx| { + for (point, text, _) in edits.into_iter().rev() { + buffer.edit(Some(point..point), text, cx); + } + }); + + self.request_autoscroll(Autoscroll::Fit, cx); + self.end_transaction(cx); + } + + pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_ranges = Vec::new(); + + let selections = self.local_selections::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + contiguous_row_selections.push(selection.clone()); + let start_row = selection.start.row; + let mut end_row = if selection.end.column > 0 || selection.is_empty() { + display_map.next_line_boundary(selection.end).0.row + 1 + } else { + selection.end.row + }; + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row { + end_row = if next_selection.end.column > 0 || next_selection.is_empty() { + display_map.next_line_boundary(next_selection.end).0.row + 1 + } else { + next_selection.end.row + }; + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + + // Move the text spanned by the row range to be before the line preceding the row range + if start_row > 0 { + let range_to_move = Point::new(start_row - 1, buffer.line_len(start_row - 1)) + ..Point::new(end_row - 1, buffer.line_len(end_row - 1)); + let insertion_point = display_map + .prev_line_boundary(Point::new(start_row - 1, 0)) + .0; + + // Don't move lines across excerpts + if buffer + .excerpt_boundaries_in_range(( + Bound::Excluded(insertion_point), + Bound::Included(range_to_move.end), + )) + .next() + .is_none() + { + let text = buffer + .text_for_range(range_to_move.clone()) + .flat_map(|s| s.chars()) + .skip(1) + .chain(['\n']) + .collect::(); + + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor.clone()..insertion_anchor, text)); + + let row_delta = range_to_move.start.row - insertion_point.row + 1; + + // Move selections up + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row -= row_delta; + selection.end.row -= row_delta; + selection + }, + )); + + // Move folds up + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.start.to_point(&buffer); + let mut end = fold.end.to_point(&buffer); + start.row -= row_delta; + end.row -= row_delta; + refold_ranges.push(start..end); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.extend(contiguous_row_selections.drain(..)); + } + + self.start_transaction(cx); + self.unfold_ranges(unfold_ranges, cx); + self.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + self.fold_ranges(refold_ranges, cx); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_ranges = Vec::new(); + + let selections = self.local_selections::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + contiguous_row_selections.push(selection.clone()); + let start_row = selection.start.row; + let mut end_row = if selection.end.column > 0 || selection.is_empty() { + display_map.next_line_boundary(selection.end).0.row + 1 + } else { + selection.end.row + }; + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row { + end_row = if next_selection.end.column > 0 || next_selection.is_empty() { + display_map.next_line_boundary(next_selection.end).0.row + 1 + } else { + next_selection.end.row + }; + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + + // Move the text spanned by the row range to be after the last line of the row range + if end_row <= buffer.max_point().row { + let range_to_move = Point::new(start_row, 0)..Point::new(end_row, 0); + let insertion_point = display_map.next_line_boundary(Point::new(end_row, 0)).0; + + // Don't move lines across excerpt boundaries + if buffer + .excerpt_boundaries_in_range(( + Bound::Excluded(range_to_move.start), + Bound::Included(insertion_point), + )) + .next() + .is_none() + { + let mut text = String::from("\n"); + text.extend(buffer.text_for_range(range_to_move.clone())); + text.pop(); // Drop trailing newline + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor.clone()..insertion_anchor, text)); + + let row_delta = insertion_point.row - range_to_move.end.row + 1; + + // Move selections down + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row += row_delta; + selection.end.row += row_delta; + selection + }, + )); + + // Move folds down + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.start.to_point(&buffer); + let mut end = fold.end.to_point(&buffer); + start.row += row_delta; + end.row += row_delta; + refold_ranges.push(start..end); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.extend(contiguous_row_selections.drain(..)); + } + + self.start_transaction(cx); + self.unfold_ranges(unfold_ranges, cx); + self.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + self.fold_ranges(refold_ranges, cx); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + self.start_transaction(cx); + let mut text = String::new(); + let mut selections = self.local_selections::(cx); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let buffer = self.buffer.read(cx).read(cx); + let max_point = buffer.max_point(); + for selection in &mut selections { + let is_entire_line = selection.is_empty(); + if is_entire_line { + selection.start = Point::new(selection.start.row, 0); + selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); + selection.goal = SelectionGoal::None; + } + let mut len = 0; + for chunk in buffer.text_for_range(selection.start..selection.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + }); + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + + cx.as_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + } + + pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + let selections = self.local_selections::(cx); + let mut text = String::new(); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let buffer = self.buffer.read(cx).read(cx); + let max_point = buffer.max_point(); + for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty(); + if is_entire_line { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(start.row + 1, 0)); + } + let mut len = 0; + for chunk in buffer.text_for_range(start..end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + }); + } + } + + cx.as_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + } + + pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + if let Some(item) = cx.as_mut().read_from_clipboard() { + let clipboard_text = item.text(); + if let Some(mut clipboard_selections) = item.metadata::>() { + let mut selections = self.local_selections::(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + if clipboard_selections.len() != selections.len() { + clipboard_selections.clear(); + } + + let mut delta = 0_isize; + let mut start_offset = 0; + for (i, selection) in selections.iter_mut().enumerate() { + let to_insert; + let entire_line; + if let Some(clipboard_selection) = clipboard_selections.get(i) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + entire_line = clipboard_selection.is_entire_line; + start_offset = end_offset + } else { + to_insert = clipboard_text.as_str(); + entire_line = all_selections_were_entire_line; + } + + selection.start = (selection.start as isize + delta) as usize; + selection.end = (selection.end as isize + delta) as usize; + + self.buffer.update(cx, |buffer, cx| { + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let range = if selection.is_empty() && entire_line { + let column = selection.start.to_point(&buffer.read(cx)).column as usize; + let line_start = selection.start - column; + line_start..line_start + } else { + selection.start..selection.end + }; + + delta += to_insert.len() as isize - range.len() as isize; + buffer.edit([range], to_insert, cx); + selection.start += to_insert.len(); + selection.end = selection.start; + }); + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } else { + self.insert(clipboard_text, cx); + } + } + } + + pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { + if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { + self.set_selections(selections, None, cx); + } + self.request_autoscroll(Autoscroll::Fit, cx); + } + } + + pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { + if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { + self.set_selections(selections, None, cx); + } + self.request_autoscroll(Autoscroll::Fit, cx); + } + } + + pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + + if start != end { + selection.end = selection.start.clone(); + } else { + let cursor = movement::left(&display_map, start) + .unwrap() + .to_point(&display_map); + selection.start = cursor.clone(); + selection.end = cursor; + } + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::left(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + + if start != end { + selection.start = selection.end.clone(); + } else { + let cursor = movement::right(&display_map, end) + .unwrap() + .to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + } + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::right(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if self.take_rename(cx).is_some() { + return; + } + + if let Some(context_menu) = self.context_menu.as_mut() { + if context_menu.select_prev(cx) { + return; + } + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + if start != end { + selection.goal = SelectionGoal::None; + } + + let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap(); + let cursor = start.to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.goal = goal; + selection.reversed = false; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap(); + let cursor = head.to_point(&display_map); + selection.set_head(cursor); + selection.goal = goal; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + self.take_rename(cx); + + if let Some(context_menu) = self.context_menu.as_mut() { + if context_menu.select_next(cx) { + return; + } + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + if start != end { + selection.goal = SelectionGoal::None; + } + + let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap(); + let cursor = start.to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.goal = goal; + selection.reversed = false; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap(); + let cursor = head.to_point(&display_map); + selection.set_head(cursor); + selection.goal = goal; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_to_previous_word_boundary( + &mut self, + _: &MoveToPreviousWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); + selection.start = cursor.clone(); + selection.end = cursor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_previous_word_boundary( + &mut self, + _: &SelectToPreviousWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_previous_word_boundary( + &mut self, + _: &DeleteToPreviousWordBoundary, + cx: &mut ViewContext, + ) { + self.start_transaction(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = + movement::prev_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn move_to_next_word_boundary( + &mut self, + _: &MoveToNextWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_next_word_boundary( + &mut self, + _: &SelectToNextWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_next_word_boundary( + &mut self, + _: &DeleteToNextWordBoundary, + cx: &mut ViewContext, + ) { + self.start_transaction(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = + movement::next_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn move_to_beginning_of_line( + &mut self, + _: &MoveToBeginningOfLine, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_beginning(&display_map, head, true); + let cursor = new_head.to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_beginning_of_line( + &mut self, + SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_beginning(&display_map, head, *stop_at_soft_boundaries); + selection.set_head(new_head.to_point(&display_map)); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_beginning_of_line( + &mut self, + _: &DeleteToBeginningOfLine, + cx: &mut ViewContext, + ) { + self.start_transaction(cx); + self.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); + self.backspace(&Backspace, cx); + self.end_transaction(cx); + } + + pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + { + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_end(&display_map, head, true); + let anchor = new_head.to_point(&display_map); + selection.start = anchor.clone(); + selection.end = anchor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_end_of_line( + &mut self, + SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_end(&display_map, head, *stop_at_soft_boundaries); + selection.set_head(new_head.to_point(&display_map)); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { + self.start_transaction(cx); + self.select_to_end_of_line(&SelectToEndOfLine(false), cx); + self.delete(&Delete, cx); + self.end_transaction(cx); + } + + pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { + self.start_transaction(cx); + self.select_to_end_of_line(&SelectToEndOfLine(false), cx); + self.cut(&Cut, cx); + self.end_transaction(cx); + } + + pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start: 0, + end: 0, + reversed: false, + goal: SelectionGoal::None, + }; + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { + let mut selection = self.local_selections::(cx).last().unwrap().clone(); + selection.set_head(Point::zero()); + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let cursor = self.buffer.read(cx).read(cx).len(); + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + }; + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn set_nav_history(&mut self, nav_history: Option) { + self.nav_history = nav_history; + } + + pub fn nav_history(&self) -> Option<&ItemNavHistory> { + self.nav_history.as_ref() + } + + fn push_to_nav_history( + &self, + position: Anchor, + new_position: Option, + cx: &mut ViewContext, + ) { + if let Some(nav_history) = &self.nav_history { + let buffer = self.buffer.read(cx).read(cx); + let offset = position.to_offset(&buffer); + let point = position.to_point(&buffer); + drop(buffer); + + if let Some(new_position) = new_position { + let row_delta = (new_position.row as i64 - point.row as i64).abs(); + if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + return; + } + } + + nav_history.push(Some(NavigationData { + anchor: position, + offset, + })); + } + } + + pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { + let mut selection = self.local_selections::(cx).first().unwrap().clone(); + selection.set_head(self.buffer.read(cx).read(cx).len()); + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start: 0, + end: self.buffer.read(cx).read(cx).len(), + reversed: false, + goal: SelectionGoal::None, + }; + self.update_selections(vec![selection], None, cx); + } + + pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + let max_point = display_map.buffer_snapshot.max_point(); + for selection in &mut selections { + let rows = selection.spanned_rows(true, &display_map); + selection.start = Point::new(rows.start, 0); + selection.end = cmp::min(max_point, Point::new(rows.end, 0)); + selection.reversed = false; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn split_selection_into_lines( + &mut self, + _: &SplitSelectionIntoLines, + cx: &mut ViewContext, + ) { + let mut to_unfold = Vec::new(); + let mut new_selections = Vec::new(); + { + let selections = self.local_selections::(cx); + let buffer = self.buffer.read(cx).read(cx); + for selection in selections { + for row in selection.start.row..selection.end.row { + let cursor = Point::new(row, buffer.line_len(row)); + new_selections.push(Selection { + id: post_inc(&mut self.next_selection_id), + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + }); + } + new_selections.push(Selection { + id: selection.id, + start: selection.end, + end: selection.end, + reversed: false, + goal: SelectionGoal::None, + }); + to_unfold.push(selection.start..selection.end); + } + } + self.unfold_ranges(to_unfold, cx); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + } + + pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext) { + self.add_selection(true, cx); + } + + pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext) { + self.add_selection(false, cx); + } + + fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + let mut state = self.add_selections_state.take().unwrap_or_else(|| { + let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); + let range = oldest_selection.display_range(&display_map).sorted(); + let columns = cmp::min(range.start.column(), range.end.column()) + ..cmp::max(range.start.column(), range.end.column()); + + selections.clear(); + let mut stack = Vec::new(); + for row in range.start.row()..=range.end.row() { + if let Some(selection) = self.build_columnar_selection( + &display_map, + row, + &columns, + oldest_selection.reversed, + ) { + stack.push(selection.id); + selections.push(selection); + } + } + + if above { + stack.reverse(); + } + + AddSelectionsState { above, stack } + }); + + let last_added_selection = *state.stack.last().unwrap(); + let mut new_selections = Vec::new(); + if above == state.above { + let end_row = if above { + 0 + } else { + display_map.max_point().row() + }; + + 'outer: for selection in selections { + if selection.id == last_added_selection { + let range = selection.display_range(&display_map).sorted(); + debug_assert_eq!(range.start.row(), range.end.row()); + let mut row = range.start.row(); + let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal + { + start..end + } else { + cmp::min(range.start.column(), range.end.column()) + ..cmp::max(range.start.column(), range.end.column()) + }; + + while row != end_row { + if above { + row -= 1; + } else { + row += 1; + } + + if let Some(new_selection) = self.build_columnar_selection( + &display_map, + row, + &columns, + selection.reversed, + ) { + state.stack.push(new_selection.id); + if above { + new_selections.push(new_selection); + new_selections.push(selection); + } else { + new_selections.push(selection); + new_selections.push(new_selection); + } + + continue 'outer; + } + } + } + + new_selections.push(selection); + } + } else { + new_selections = selections; + new_selections.retain(|s| s.id != last_added_selection); + state.stack.pop(); + } + + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + if state.stack.len() > 1 { + self.add_selections_state = Some(state); + } + } + + pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) { + let replace_newest = action.0; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.local_selections::(cx); + if let Some(mut select_next_state) = self.select_next_state.take() { + let query = &select_next_state.query; + if !select_next_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + + let bytes_after_last_selection = + buffer.bytes_in_range(last_selection.end..buffer.len()); + let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); + let query_matches = query + .stream_find_iter(bytes_after_last_selection) + .map(|result| (last_selection.end, result)) + .chain( + query + .stream_find_iter(bytes_before_first_selection) + .map(|result| (0, result)), + ); + for (start_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + start_offset + query_match.start()..start_offset + query_match.end(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_next_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + if replace_newest { + if let Some(newest_id) = + selections.iter().max_by_key(|s| s.id).map(|s| s.id) + { + selections.retain(|s| s.id != newest_id); + } + } + selections.push(Selection { + id: post_inc(&mut self.next_selection_id), + start: next_selected_range.start, + end: next_selected_range.end, + reversed: false, + goal: SelectionGoal::None, + }); + self.update_selections(selections, Some(Autoscroll::Newest), cx); + } else { + select_next_state.done = true; + } + } + + self.select_next_state = Some(select_next_state); + } else if selections.len() == 1 { + let selection = selections.last_mut().unwrap(); + if selection.start == selection.end { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let select_state = SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: true, + done: false, + }; + self.update_selections(selections, Some(Autoscroll::Newest), cx); + self.select_next_state = Some(select_state); + } else { + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + self.select_next_state = Some(SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: false, + done: false, + }); + self.select_next(action, cx); + } + } + } + + pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { + // 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 = + if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) { + prefix.to_string() + } else { + return; + }; + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + + self.start_transaction(cx); + let mut selections = self.local_selections::(cx); + let mut all_selection_lines_are_comments = true; + let mut edit_ranges = Vec::new(); + let mut last_toggled_row = None; + self.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + 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); + } + + if snapshot.is_line_blank(row) { + continue; + } + + let start = Point::new(row, snapshot.indent_column_for_line(row)); + 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 { + all_selection_lines_are_comments = false; + edit_ranges.push(start..start); + } + } + + if !edit_ranges.is_empty() { + if all_selection_lines_are_comments { + buffer.edit(edit_ranges.iter().cloned(), "", cx); + } else { + let min_column = edit_ranges.iter().map(|r| r.start.column).min().unwrap(); + let edit_ranges = edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + position..position + }); + buffer.edit(edit_ranges, &full_comment_prefix, cx); + } + } + } + }); + + self.update_selections( + self.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); + self.end_transaction(cx); + } + + pub fn select_larger_syntax_node( + &mut self, + _: &SelectLargerSyntaxNode, + cx: &mut ViewContext, + ) { + let old_selections = self.local_selections::(cx).into_boxed_slice(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); + let mut selected_larger_node = false; + let new_selections = old_selections + .iter() + .map(|selection| { + let old_range = selection.start..selection.end; + let mut new_range = old_range.clone(); + while let Some(containing_range) = + buffer.range_for_syntax_ancestor(new_range.clone()) + { + new_range = containing_range; + if !display_map.intersects_fold(new_range.start) + && !display_map.intersects_fold(new_range.end) + { + break; + } + } + + selected_larger_node |= new_range != old_range; + Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + } + }) + .collect::>(); + + if selected_larger_node { + stack.push(old_selections); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + } + self.select_larger_syntax_node_stack = stack; + } + + pub fn select_smaller_syntax_node( + &mut self, + _: &SelectSmallerSyntaxNode, + cx: &mut ViewContext, + ) { + let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); + if let Some(selections) = stack.pop() { + self.update_selections(selections.to_vec(), Some(Autoscroll::Fit), cx); + } + self.select_larger_syntax_node_stack = stack; + } + + pub fn move_to_enclosing_bracket( + &mut self, + _: &MoveToEnclosingBracket, + cx: &mut ViewContext, + ) { + let mut selections = self.local_selections::(cx); + let buffer = self.buffer.read(cx).snapshot(cx); + for selection in &mut selections { + if let Some((open_range, close_range)) = + buffer.enclosing_bracket_ranges(selection.start..selection.end) + { + let close_range = close_range.to_inclusive(); + let destination = if close_range.contains(&selection.start) + && close_range.contains(&selection.end) + { + open_range.end + } else { + *close_range.start() + }; + selection.start = destination; + selection.end = destination; + } + } + + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selection = self.newest_selection::(&buffer); + let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { + active_diagnostics + .primary_range + .to_offset(&buffer) + .to_inclusive() + }); + let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() { + if active_primary_range.contains(&selection.head()) { + *active_primary_range.end() + } else { + selection.head() + } + } else { + selection.head() + }; + + loop { + let next_group = buffer + .diagnostics_in_range::<_, usize>(search_start..buffer.len()) + .find_map(|entry| { + if entry.diagnostic.is_primary + && !entry.range.is_empty() + && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) + { + Some((entry.range, entry.diagnostic.group_id)) + } else { + None + } + }); + + if let Some((primary_range, group_id)) = next_group { + self.activate_diagnostics(group_id, cx); + self.update_selections( + vec![Selection { + id: selection.id, + start: primary_range.start, + end: primary_range.start, + reversed: false, + goal: SelectionGoal::None, + }], + Some(Autoscroll::Center), + cx, + ); + break; + } else if search_start == 0 { + break; + } else { + // Cycle around to the start of the buffer, potentially moving back to the start of + // the currently active diagnostic. + search_start = 0; + active_primary_range.take(); + } + } + } + + pub fn go_to_definition( + workspace: &mut Workspace, + _: &GoToDefinition, + cx: &mut ViewContext, + ) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(cx)) + { + editor + } else { + return; + }; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + let head = editor.newest_selection::(&buffer.read(cx)).head(); + let (buffer, head) = + if let Some(text_anchor) = editor.buffer.read(cx).text_anchor_for_position(head, cx) { + text_anchor + } else { + return; + }; + + let definitions = workspace + .project() + .update(cx, |project, cx| project.definition(&buffer, head, cx)); + cx.spawn(|workspace, mut cx| async move { + let definitions = definitions.await?; + workspace.update(&mut cx, |workspace, cx| { + for definition in definitions { + let range = definition.range.to_offset(definition.buffer.read(cx)); + let target_editor_handle = workspace + .open_item(BufferItemHandle(definition.buffer), cx) + .downcast::() + .unwrap(); + + target_editor_handle.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + let disabled_history = if editor_handle == target_editor_handle { + None + } else { + target_editor.nav_history.take() + }; + target_editor.select_ranges([range], Some(Autoscroll::Center), cx); + if disabled_history.is_some() { + target_editor.nav_history = disabled_history; + } + }); + } + }); + + Ok::<(), anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn find_all_references( + workspace: &mut Workspace, + _: &FindAllReferences, + cx: &mut ViewContext, + ) -> Option>> { + let active_item = workspace.active_item(cx)?; + let editor_handle = active_item.act_as::(cx)?; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + let head = editor.newest_selection::(&buffer.read(cx)).head(); + let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; + let replica_id = editor.replica_id(cx); + + let references = workspace + .project() + .update(cx, |project, cx| project.references(&buffer, head, cx)); + Some(cx.spawn(|workspace, mut cx| async move { + let mut locations = references.await?; + if locations.is_empty() { + return Ok(()); + } + + locations.sort_by_key(|location| location.buffer.id()); + let mut locations = locations.into_iter().peekable(); + let mut ranges_to_highlight = Vec::new(); + + let excerpt_buffer = cx.add_model(|cx| { + let mut symbol_name = None; + let mut multibuffer = MultiBuffer::new(replica_id); + while let Some(location) = locations.next() { + let buffer = location.buffer.read(cx); + let mut ranges_for_buffer = Vec::new(); + let range = location.range.to_offset(buffer); + ranges_for_buffer.push(range.clone()); + if symbol_name.is_none() { + symbol_name = Some(buffer.text_for_range(range).collect::()); + } + + while let Some(next_location) = locations.peek() { + if next_location.buffer == location.buffer { + ranges_for_buffer.push(next_location.range.to_offset(buffer)); + locations.next(); + } else { + break; + } + } + + ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); + ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines( + location.buffer.clone(), + ranges_for_buffer, + 1, + cx, + )); + } + multibuffer.with_title(format!("References to `{}`", symbol_name.unwrap())) + }); + + workspace.update(&mut cx, |workspace, cx| { + let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_ranges::(ranges_to_highlight, color, cx); + }); + } + }); + + Ok(()) + })) + } + + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; + + let project = self.project.clone()?; + let selection = self.newest_anchor_selection().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, tail_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } + + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) + }); + + Some(cx.spawn(|this, mut cx| async move { + if let Some(rename_range) = prepare_rename.await? { + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + let tail_offset_in_rename_range = + tail_buffer_offset.saturating_sub(rename_buffer_range.start); + + this.update(&mut cx, |this, cx| { + this.take_rename(cx); + let style = this.style(cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let old_name = buffer + .text_for_range(rename_start..rename_end) + .collect::(); + drop(buffer); + + // Position the selection in the rename editor so that it matches the current selection. + let rename_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line(this.settings.clone(), None, cx); + editor + .buffer + .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx)); + editor.select_ranges( + [tail_offset_in_rename_range..cursor_offset_in_rename_range], + None, + cx, + ); + editor.highlight_ranges::( + vec![Anchor::min()..Anchor::max()], + style.diff_background_inserted, + cx, + ); + editor + }); + this.highlight_ranges::( + vec![range.clone()], + style.diff_background_deleted, + cx, + ); + this.update_selections( + vec![Selection { + id: selection.id, + start: rename_end, + end: rename_end, + reversed: false, + goal: SelectionGoal::None, + }], + None, + cx, + ); + cx.focus(&rename_editor); + let block_id = this.insert_blocks( + [BlockProperties { + position: range.start.clone(), + height: 1, + render: Arc::new({ + let editor = rename_editor.clone(); + move |cx: &BlockContext| { + ChildView::new(editor.clone()) + .contained() + .with_padding_left(cx.anchor_x) + .boxed() + } + }), + disposition: BlockDisposition::Below, + }], + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + }); + } + + Ok(()) + })) + } + + pub fn confirm_rename( + workspace: &mut Workspace, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { + let rename = editor.take_rename(cx)?; + let buffer = editor.buffer.read(cx); + let (start_buffer, start) = + buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; + if start_buffer == end_buffer { + let new_name = rename.editor.read(cx).text(cx); + Some((start_buffer, start..end, rename.old_name, new_name)) + } else { + None + } + })?; + + let rename = workspace.project().clone().update(cx, |project, cx| { + project.perform_rename( + buffer.clone(), + range.start.clone(), + new_name.clone(), + true, + cx, + ) + }); + + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx, + ) + .await + })) + } + + fn take_rename(&mut self, cx: &mut ViewContext) -> Option { + let rename = self.pending_rename.take()?; + self.remove_blocks([rename.block_id].into_iter().collect(), cx); + self.clear_highlighted_ranges::(cx); + + let editor = rename.editor.read(cx); + let buffer = editor.buffer.read(cx).snapshot(cx); + let selection = editor.newest_selection::(&buffer); + + // Update the selection to match the position of the selection inside + // the rename editor. + let snapshot = self.buffer.read(cx).snapshot(cx); + let rename_range = rename.range.to_offset(&snapshot); + let start = snapshot + .clip_offset(rename_range.start + selection.start, Bias::Left) + .min(rename_range.end); + let end = snapshot + .clip_offset(rename_range.start + selection.end, Bias::Left) + .min(rename_range.end); + self.update_selections( + vec![Selection { + id: self.newest_anchor_selection().id, + start, + end, + reversed: selection.reversed, + goal: SelectionGoal::None, + }], + None, + cx, + ); + + Some(rename) + } + + fn invalidate_rename_range( + &mut self, + buffer: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) { + if let Some(rename) = self.pending_rename.as_ref() { + if self.selections.len() == 1 { + let head = self.selections[0].head().to_offset(buffer); + let range = rename.range.to_offset(buffer).to_inclusive(); + if range.contains(&head) { + return; + } + } + let rename = self.pending_rename.take().unwrap(); + self.remove_blocks([rename.block_id].into_iter().collect(), cx); + self.clear_highlighted_ranges::(cx); + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn pending_rename(&self) -> Option<&RenameState> { + self.pending_rename.as_ref() + } + + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { + let buffer = self.buffer.read(cx).snapshot(cx); + let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); + let is_valid = buffer + .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) + .any(|entry| { + entry.diagnostic.is_primary + && !entry.range.is_empty() + && entry.range.start == primary_range_start + && entry.diagnostic.message == active_diagnostics.primary_message + }); + + if is_valid != active_diagnostics.is_valid { + active_diagnostics.is_valid = is_valid; + let mut new_styles = HashMap::default(); + for (block_id, diagnostic) in &active_diagnostics.blocks { + new_styles.insert( + *block_id, + diagnostic_block_renderer( + diagnostic.clone(), + is_valid, + self.settings.clone(), + ), + ); + } + self.display_map + .update(cx, |display_map, _| display_map.replace_blocks(new_styles)); + } + } + } + + fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { + self.dismiss_diagnostics(cx); + self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut primary_range = None; + let mut primary_message = None; + let mut group_end = Point::zero(); + let diagnostic_group = buffer + .diagnostic_group::(group_id) + .map(|entry| { + if entry.range.end > group_end { + group_end = entry.range.end; + } + if entry.diagnostic.is_primary { + primary_range = Some(entry.range.clone()); + primary_message = Some(entry.diagnostic.message.clone()); + } + entry + }) + .collect::>(); + let primary_range = primary_range.unwrap(); + let primary_message = primary_message.unwrap(); + let primary_range = + buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end); + + let blocks = display_map + .insert_blocks( + diagnostic_group.iter().map(|entry| { + let diagnostic = entry.diagnostic.clone(); + let message_height = diagnostic.message.lines().count() as u8; + BlockProperties { + position: buffer.anchor_after(entry.range.start), + height: message_height, + render: diagnostic_block_renderer( + diagnostic, + true, + self.settings.clone(), + ), + disposition: BlockDisposition::Below, + } + }), + cx, + ) + .into_iter() + .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic)) + .collect(); + + Some(ActiveDiagnosticGroup { + primary_range, + primary_message, + blocks, + is_valid: true, + }) + }); + } + + fn dismiss_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostic_group) = self.active_diagnostics.take() { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); + }); + cx.notify(); + } + } + + fn build_columnar_selection( + &mut self, + display_map: &DisplaySnapshot, + row: u32, + columns: &Range, + reversed: bool, + ) -> Option> { + let is_empty = columns.start == columns.end; + let line_len = display_map.line_len(row); + if columns.start < line_len || (is_empty && columns.start == line_len) { + let start = DisplayPoint::new(row, columns.start); + let end = DisplayPoint::new(row, cmp::min(columns.end, line_len)); + Some(Selection { + id: post_inc(&mut self.next_selection_id), + start: start.to_point(display_map), + end: end.to_point(display_map), + reversed, + goal: SelectionGoal::ColumnRange { + start: columns.start, + end: columns.end, + }, + }) + } else { + None + } + } + + pub fn local_selections_in_range( + &self, + range: Range, + display_map: &DisplaySnapshot, + ) -> Vec> { + let buffer = &display_map.buffer_snapshot; + + let start_ix = match self + .selections + .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer).unwrap()) + { + Ok(ix) | Err(ix) => ix, + }; + let end_ix = match self + .selections + .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer).unwrap()) + { + Ok(ix) => ix + 1, + Err(ix) => ix, + }; + + fn point_selection( + selection: &Selection, + buffer: &MultiBufferSnapshot, + ) -> Selection { + let start = selection.start.to_point(&buffer); + let end = selection.end.to_point(&buffer); + Selection { + id: selection.id, + start, + end, + reversed: selection.reversed, + goal: selection.goal, + } + } + + self.selections[start_ix..end_ix] + .iter() + .chain( + self.pending_selection + .as_ref() + .map(|pending| &pending.selection), + ) + .map(|s| point_selection(s, &buffer)) + .collect() + } + + pub fn local_selections<'a, D>(&self, cx: &'a AppContext) -> Vec> + where + D: 'a + TextDimension + Ord + Sub, + { + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selections = self + .resolve_selections::(self.selections.iter(), &buffer) + .peekable(); + + let mut pending_selection = self.pending_selection::(&buffer); + + iter::from_fn(move || { + if let Some(pending) = pending_selection.as_mut() { + while let Some(next_selection) = selections.peek() { + if pending.start <= next_selection.end && pending.end >= next_selection.start { + let next_selection = selections.next().unwrap(); + if next_selection.start < pending.start { + pending.start = next_selection.start; + } + if next_selection.end > pending.end { + pending.end = next_selection.end; + } + } else if next_selection.end < pending.start { + return selections.next(); + } else { + break; + } + } + + pending_selection.take() + } else { + selections.next() + } + }) + .collect() + } + + fn resolve_selections<'a, D, I>( + &self, + selections: I, + snapshot: &MultiBufferSnapshot, + ) -> impl 'a + Iterator> + where + D: TextDimension + Ord + Sub, + I: 'a + IntoIterator>, + { + let (to_summarize, selections) = selections.into_iter().tee(); + let mut summaries = snapshot + .summaries_for_anchors::(to_summarize.flat_map(|s| [&s.start, &s.end])) + .into_iter(); + selections.map(move |s| Selection { + id: s.id, + start: summaries.next().unwrap(), + end: summaries.next().unwrap(), + reversed: s.reversed, + goal: s.goal, + }) + } + + fn pending_selection>( + &self, + snapshot: &MultiBufferSnapshot, + ) -> Option> { + self.pending_selection + .as_ref() + .map(|pending| self.resolve_selection(&pending.selection, &snapshot)) + } + + fn resolve_selection>( + &self, + selection: &Selection, + buffer: &MultiBufferSnapshot, + ) -> Selection { + Selection { + id: selection.id, + start: selection.start.summary::(&buffer), + end: selection.end.summary::(&buffer), + reversed: selection.reversed, + goal: selection.goal, + } + } + + fn selection_count<'a>(&self) -> usize { + let mut count = self.selections.len(); + if self.pending_selection.is_some() { + count += 1; + } + count + } + + pub fn oldest_selection>( + &self, + snapshot: &MultiBufferSnapshot, + ) -> Selection { + self.selections + .iter() + .min_by_key(|s| s.id) + .map(|selection| self.resolve_selection(selection, snapshot)) + .or_else(|| self.pending_selection(snapshot)) + .unwrap() + } + + pub fn newest_selection>( + &self, + snapshot: &MultiBufferSnapshot, + ) -> Selection { + self.resolve_selection(self.newest_anchor_selection(), snapshot) + } + + pub fn newest_anchor_selection(&self) -> &Selection { + self.pending_selection + .as_ref() + .map(|s| &s.selection) + .or_else(|| self.selections.iter().max_by_key(|s| s.id)) + .unwrap() + } + + pub fn update_selections( + &mut self, + mut selections: Vec>, + autoscroll: Option, + cx: &mut ViewContext, + ) where + T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, + { + let buffer = self.buffer.read(cx).snapshot(cx); + selections.sort_unstable_by_key(|s| s.start); + + // Merge overlapping selections. + let mut i = 1; + while i < selections.len() { + if selections[i - 1].end >= selections[i].start { + let removed = selections.remove(i); + if removed.start < selections[i - 1].start { + selections[i - 1].start = removed.start; + } + if removed.end > selections[i - 1].end { + selections[i - 1].end = removed.end; + } + } else { + i += 1; + } + } + + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + + self.set_selections( + Arc::from_iter(selections.into_iter().map(|selection| { + let end_bias = if selection.end > selection.start { + Bias::Left + } else { + Bias::Right + }; + Selection { + id: selection.id, + start: buffer.anchor_after(selection.start), + end: buffer.anchor_at(selection.end, end_bias), + reversed: selection.reversed, + goal: selection.goal, + } + })), + None, + cx, + ); + } + + /// Compute new ranges for any selections that were located in excerpts that have + /// since been removed. + /// + /// Returns a `HashMap` indicating which selections whose former head position + /// was no longer present. The keys of the map are selection ids. The values are + /// the id of the new excerpt where the head of the selection has been moved. + pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { + let snapshot = self.buffer.read(cx).read(cx); + let anchors_with_status = snapshot.refresh_anchors( + self.selections + .iter() + .flat_map(|selection| [&selection.start, &selection.end]), + ); + let offsets = + snapshot.summaries_for_anchors::(anchors_with_status.iter().map(|a| &a.1)); + let offsets = offsets.chunks(2); + let statuses = anchors_with_status + .chunks(2) + .map(|a| (a[0].0 / 2, a[0].2, a[1].2)); + + let mut selections_with_lost_position = HashMap::default(); + let new_selections = offsets + .zip(statuses) + .map(|(offsets, (selection_ix, kept_start, kept_end))| { + let selection = &self.selections[selection_ix]; + let kept_head = if selection.reversed { + kept_start + } else { + kept_end + }; + if !kept_head { + selections_with_lost_position + .insert(selection.id, selection.head().excerpt_id.clone()); + } + + Selection { + id: selection.id, + start: offsets[0], + end: offsets[1], + reversed: selection.reversed, + goal: selection.goal, + } + }) + .collect(); + drop(snapshot); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + selections_with_lost_position + } + + fn set_selections( + &mut self, + selections: Arc<[Selection]>, + pending_selection: Option, + cx: &mut ViewContext, + ) { + let old_cursor_position = self.newest_anchor_selection().head(); + + self.selections = selections; + self.pending_selection = pending_selection; + if self.focused { + self.buffer.update(cx, |buffer, cx| { + buffer.set_active_selections(&self.selections, cx) + }); + } + + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + self.add_selections_state = None; + self.select_next_state = None; + self.select_larger_syntax_node_stack.clear(); + self.autoclose_stack.invalidate(&self.selections, &buffer); + self.snippet_stack.invalidate(&self.selections, &buffer); + self.invalidate_rename_range(&buffer, cx); + + let new_cursor_position = self.newest_anchor_selection().head(); + + self.push_to_nav_history( + old_cursor_position.clone(), + Some(new_cursor_position.to_point(&buffer)), + cx, + ); + + let completion_menu = match self.context_menu.as_mut() { + Some(ContextMenu::Completions(menu)) => Some(menu), + _ => { + self.context_menu.take(); + None + } + }; + + if let Some(completion_menu) = completion_menu { + let cursor_position = new_cursor_position.to_offset(&buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_menu.initial_position.clone()); + if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) + { + let query = Self::completion_query(&buffer, cursor_position); + cx.background() + .block(completion_menu.filter(query.as_deref(), cx.background().clone())); + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_context_menu(cx); + } + } + + if old_cursor_position.to_display_point(&display_map).row() + != new_cursor_position.to_display_point(&display_map).row() + { + self.available_code_actions.take(); + } + self.refresh_code_actions(cx); + self.refresh_document_highlights(cx); + + self.pause_cursor_blinking(cx); + cx.emit(Event::SelectionsChanged); + } + + pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + self.autoscroll_request = Some(autoscroll); + cx.notify(); + } + + fn start_transaction(&mut self, cx: &mut ViewContext) { + self.start_transaction_at(Instant::now(), cx); + } + + fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + self.end_selection(cx); + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) + { + self.selection_history + .insert(tx_id, (self.selections.clone(), None)); + } + } + + fn end_transaction(&mut self, cx: &mut ViewContext) { + self.end_transaction_at(Instant::now(), cx); + } + + fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { + *end_selections = Some(self.selections.clone()); + } else { + log::error!("unexpectedly ended a transaction that wasn't started by this editor"); + } + } + } + + pub fn page_up(&mut self, _: &PageUp, _: &mut ViewContext) { + log::info!("Editor::page_up"); + } + + pub fn page_down(&mut self, _: &PageDown, _: &mut ViewContext) { + log::info!("Editor::page_down"); + } + + pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { + let mut fold_ranges = Vec::new(); + + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in selections { + let range = selection.display_range(&display_map).sorted(); + let buffer_start_row = range.start.to_point(&display_map).row; + + for row in (0..=range.end.row()).rev() { + if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) { + let fold_range = self.foldable_range_for_line(&display_map, row); + if fold_range.end.row >= buffer_start_row { + fold_ranges.push(fold_range); + if row <= range.start.row() { + break; + } + } + } + } + } + + self.fold_ranges(fold_ranges, cx); + } + + pub fn unfold(&mut self, _: &Unfold, cx: &mut ViewContext) { + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let ranges = selections + .iter() + .map(|s| { + let range = s.display_range(&display_map).sorted(); + let mut start = range.start.to_point(&display_map); + let mut end = range.end.to_point(&display_map); + start.column = 0; + end.column = buffer.line_len(end.row); + start..end + }) + .collect::>(); + self.unfold_ranges(ranges, cx); + } + + fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool { + let max_point = display_map.max_point(); + if display_row >= max_point.row() { + false + } else { + let (start_indent, is_blank) = display_map.line_indent(display_row); + if is_blank { + false + } else { + for display_row in display_row + 1..=max_point.row() { + let (indent, is_blank) = display_map.line_indent(display_row); + if !is_blank { + return indent > start_indent; + } + } + false + } + } + } + + fn foldable_range_for_line( + &self, + display_map: &DisplaySnapshot, + start_row: u32, + ) -> Range { + let max_point = display_map.max_point(); + + let (start_indent, _) = display_map.line_indent(start_row); + let start = DisplayPoint::new(start_row, display_map.line_len(start_row)); + let mut end = None; + for row in start_row + 1..=max_point.row() { + let (indent, is_blank) = display_map.line_indent(row); + if !is_blank && indent <= start_indent { + end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1))); + break; + } + } + + let end = end.unwrap_or(max_point); + return start.to_point(display_map)..end.to_point(display_map); + } + + pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { + let selections = self.local_selections::(cx); + let ranges = selections.into_iter().map(|s| s.start..s.end); + self.fold_ranges(ranges, cx); + } + + fn fold_ranges( + &mut self, + ranges: impl IntoIterator>, + cx: &mut ViewContext, + ) { + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_some() { + self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); + self.request_autoscroll(Autoscroll::Fit, cx); + cx.notify(); + } + } + + fn unfold_ranges(&mut self, ranges: Vec>, cx: &mut ViewContext) { + if !ranges.is_empty() { + self.display_map + .update(cx, |map, cx| map.unfold(ranges, cx)); + self.request_autoscroll(Autoscroll::Fit, cx); + cx.notify(); + } + } + + pub fn insert_blocks( + &mut self, + blocks: impl IntoIterator>, + cx: &mut ViewContext, + ) -> Vec { + let blocks = self + .display_map + .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); + self.request_autoscroll(Autoscroll::Fit, cx); + blocks + } + + pub fn replace_blocks( + &mut self, + blocks: HashMap, + cx: &mut ViewContext, + ) { + self.display_map + .update(cx, |display_map, _| display_map.replace_blocks(blocks)); + self.request_autoscroll(Autoscroll::Fit, cx); + } + + pub fn remove_blocks(&mut self, block_ids: HashSet, cx: &mut ViewContext) { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(block_ids, cx) + }); + } + + pub fn longest_row(&self, cx: &mut MutableAppContext) -> u32 { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .longest_row() + } + + pub fn max_point(&self, cx: &mut MutableAppContext) -> DisplayPoint { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .max_point() + } + + pub fn text(&self, cx: &AppContext) -> String { + self.buffer.read(cx).read(cx).text() + } + + pub fn display_text(&self, cx: &mut MutableAppContext) -> String { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .text() + } + + pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { + let language = self.language(cx); + let settings = self.settings.borrow(); + let mode = self + .soft_wrap_mode_override + .unwrap_or_else(|| settings.soft_wrap(language)); + match mode { + settings::SoftWrap::None => SoftWrap::None, + settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length(language)) + } + } + } + + pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext) { + self.soft_wrap_mode_override = Some(mode); + cx.notify(); + } + + pub fn set_wrap_width(&self, width: Option, cx: &mut MutableAppContext) -> bool { + self.display_map + .update(cx, |map, cx| map.set_wrap_width(width, cx)) + } + + pub fn set_highlighted_rows(&mut self, rows: Option>) { + self.highlighted_rows = rows; + } + + pub fn highlighted_rows(&self) -> Option> { + self.highlighted_rows.clone() + } + + pub fn highlight_ranges( + &mut self, + ranges: Vec>, + color: Color, + cx: &mut ViewContext, + ) { + self.highlighted_ranges + .insert(TypeId::of::(), (color, ranges)); + cx.notify(); + } + + pub fn clear_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Color, Vec>)> { + cx.notify(); + self.highlighted_ranges.remove(&TypeId::of::()) + } + + #[cfg(feature = "test-support")] + pub fn all_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Vec<(Range, Color)> { + let snapshot = self.snapshot(cx); + let buffer = &snapshot.buffer_snapshot; + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + self.highlighted_ranges_in_range(start..end, &snapshot) + } + + pub fn highlighted_ranges_for_type(&self) -> Option<(Color, &[Range])> { + self.highlighted_ranges + .get(&TypeId::of::()) + .map(|(color, ranges)| (*color, ranges.as_slice())) + } + + pub fn highlighted_ranges_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + ) -> Vec<(Range, Color)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + for (color, ranges) in self.highlighted_ranges.values() { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((start..end, *color)) + } + } + results + } + + fn next_blink_epoch(&mut self) -> usize { + self.blink_epoch += 1; + self.blink_epoch + } + + fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { + if !self.focused { + return; + } + + self.show_local_cursors = true; + cx.notify(); + + let epoch = self.next_blink_epoch(); + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) + } + } + }) + .detach(); + } + + fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext) { + if epoch == self.blink_epoch { + self.blinking_paused = false; + self.blink_cursors(epoch, cx); + } + } + + fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext) { + if epoch == self.blink_epoch && self.focused && !self.blinking_paused { + self.show_local_cursors = !self.show_local_cursors; + cx.notify(); + + let epoch = self.next_blink_epoch(); + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); + } + } + }) + .detach(); + } + } + + pub fn show_local_cursors(&self) -> bool { + self.show_local_cursors + } + + fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + cx.notify(); + } + + fn on_buffer_event( + &mut self, + _: ModelHandle, + event: &language::Event, + cx: &mut ViewContext, + ) { + match event { + language::Event::Edited => { + self.refresh_active_diagnostics(cx); + self.refresh_code_actions(cx); + cx.emit(Event::Edited); + } + language::Event::Dirtied => cx.emit(Event::Dirtied), + language::Event::Saved => cx.emit(Event::Saved), + language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), + language::Event::Reloaded => cx.emit(Event::TitleChanged), + language::Event::Closed => cx.emit(Event::Closed), + language::Event::DiagnosticsUpdated => { + self.refresh_active_diagnostics(cx); + } + _ => {} + } + } + + fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + cx.notify(); + } + + pub fn set_searchable(&mut self, searchable: bool) { + self.searchable = searchable; + } + + pub fn searchable(&self) -> bool { + self.searchable + } +} + +impl EditorSnapshot { + pub fn is_focused(&self) -> bool { + self.is_focused + } + + pub fn placeholder_text(&self) -> Option<&Arc> { + self.placeholder_text.as_ref() + } + + pub fn scroll_position(&self) -> Vector2F { + compute_scroll_position( + &self.display_snapshot, + self.scroll_position, + &self.scroll_top_anchor, + ) + } +} + +impl Deref for EditorSnapshot { + type Target = DisplaySnapshot; + + fn deref(&self) -> &Self::Target { + &self.display_snapshot + } +} + +fn compute_scroll_position( + snapshot: &DisplaySnapshot, + mut scroll_position: Vector2F, + scroll_top_anchor: &Option, +) -> Vector2F { + if let Some(anchor) = scroll_top_anchor { + let scroll_top = anchor.to_display_point(snapshot).row() as f32; + scroll_position.set_y(scroll_top + scroll_position.y()); + } else { + scroll_position.set_y(0.); + } + scroll_position +} + +#[derive(Copy, Clone)] +pub enum Event { + Activate, + Edited, + Blurred, + Dirtied, + Saved, + TitleChanged, + SelectionsChanged, + Closed, +} + +impl Entity for Editor { + type Event = Event; +} + +impl View for Editor { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let style = self.style(cx); + self.display_map.update(cx, |map, cx| { + map.set_font(style.text.font_id, style.text.font_size, cx) + }); + EditorElement::new(self.handle.clone(), style.clone()).boxed() + } + + fn ui_name() -> &'static str { + "Editor" + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + self.focused = true; + self.blink_cursors(self.blink_epoch, cx); + self.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx); + buffer.set_active_selections(&self.selections, cx) + }); + } + + fn on_blur(&mut self, cx: &mut ViewContext) { + self.focused = false; + self.show_local_cursors = false; + self.buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + self.hide_context_menu(cx); + cx.emit(Event::Blurred); + cx.notify(); + } + + fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { + let mut cx = Self::default_keymap_context(); + let mode = match self.mode { + EditorMode::SingleLine => "single_line", + EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Full => "full", + }; + cx.map.insert("mode".into(), mode.into()); + if self.pending_rename.is_some() { + cx.set.insert("renaming".into()); + } + match self.context_menu.as_ref() { + Some(ContextMenu::Completions(_)) => { + cx.set.insert("showing_completions".into()); + } + Some(ContextMenu::CodeActions(_)) => { + cx.set.insert("showing_code_actions".into()); + } + None => {} + } + cx + } +} + +fn build_style( + settings: &Settings, + get_field_editor_theme: Option, + cx: &AppContext, +) -> EditorStyle { + let mut theme = settings.theme.editor.clone(); + if let Some(get_field_editor_theme) = get_field_editor_theme { + let field_editor_theme = get_field_editor_theme(&settings.theme); + if let Some(background) = field_editor_theme.container.background_color { + theme.background = background; + } + theme.text_color = field_editor_theme.text.color; + theme.selection = field_editor_theme.selection; + EditorStyle { + text: field_editor_theme.text, + placeholder_text: field_editor_theme.placeholder_text, + theme, + } + } else { + let font_cache = cx.font_cache(); + let font_family_id = settings.buffer_font_family; + let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); + let font_properties = Default::default(); + let font_id = font_cache + .select_font(font_family_id, &font_properties) + .unwrap(); + let font_size = settings.buffer_font_size; + EditorStyle { + text: TextStyle { + color: settings.theme.editor.text_color, + font_family_name, + font_family_id, + font_id, + font_size, + font_properties, + underline: None, + }, + placeholder_text: None, + theme, + } + } +} + +impl SelectionExt for Selection { + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range { + let start = self.start.to_point(buffer); + let end = self.end.to_point(buffer); + if self.reversed { + end..start + } else { + start..end + } + } + + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range { + let start = self.start.to_offset(buffer); + let end = self.end.to_offset(buffer); + if self.reversed { + end..start + } else { + start..end + } + } + + fn display_range(&self, map: &DisplaySnapshot) -> Range { + let start = self + .start + .to_point(&map.buffer_snapshot) + .to_display_point(map); + let end = self + .end + .to_point(&map.buffer_snapshot) + .to_display_point(map); + if self.reversed { + end..start + } else { + start..end + } + } + + fn spanned_rows( + &self, + include_end_if_at_line_start: bool, + map: &DisplaySnapshot, + ) -> Range { + let start = self.start.to_point(&map.buffer_snapshot); + let mut end = self.end.to_point(&map.buffer_snapshot); + if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { + end.row -= 1; + } + + let buffer_start = map.prev_line_boundary(start).0; + let buffer_end = map.next_line_boundary(end).0; + buffer_start.row..buffer_end.row + 1 + } +} + +impl InvalidationStack { + fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) + where + S: Clone + ToOffset, + { + while let Some(region) = self.last() { + let all_selections_inside_invalidation_ranges = + if selections.len() == region.ranges().len() { + selections + .iter() + .zip(region.ranges().iter().map(|r| r.to_offset(&buffer))) + .all(|(selection, invalidation_range)| { + let head = selection.head().to_offset(&buffer); + invalidation_range.start <= head && invalidation_range.end >= head + }) + } else { + false + }; + + if all_selections_inside_invalidation_ranges { + break; + } else { + self.pop(); + } + } + } +} + +impl Default for InvalidationStack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Deref for InvalidationStack { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for InvalidationStack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl InvalidationRegion for BracketPairState { + fn ranges(&self) -> &[Range] { + &self.ranges + } +} + +impl InvalidationRegion for SnippetState { + fn ranges(&self) -> &[Range] { + &self.ranges[self.active_index] + } +} + +impl Deref for EditorStyle { + type Target = theme::Editor; + + fn deref(&self) -> &Self::Target { + &self.theme + } +} + +pub fn diagnostic_block_renderer( + diagnostic: Diagnostic, + is_valid: bool, + settings: watch::Receiver, +) -> RenderBlock { + let mut highlighted_lines = Vec::new(); + for line in diagnostic.message.lines() { + highlighted_lines.push(highlight_diagnostic_message(line)); + } + + Arc::new(move |cx: &BlockContext| { + let settings = settings.borrow(); + let theme = &settings.theme.editor; + let style = diagnostic_style(diagnostic.severity, is_valid, theme); + let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); + Flex::column() + .with_children(highlighted_lines.iter().map(|(line, highlights)| { + Label::new( + line.clone(), + style.message.clone().with_font_size(font_size), + ) + .with_highlights(highlights.clone()) + .contained() + .with_margin_left(cx.anchor_x) + .boxed() + })) + .aligned() + .left() + .boxed() + }) +} + +pub fn highlight_diagnostic_message(message: &str) -> (String, Vec) { + let mut message_without_backticks = String::new(); + let mut prev_offset = 0; + let mut inside_block = false; + let mut highlights = Vec::new(); + for (match_ix, (offset, _)) in message + .match_indices('`') + .chain([(message.len(), "")]) + .enumerate() + { + message_without_backticks.push_str(&message[prev_offset..offset]); + if inside_block { + highlights.extend(prev_offset - match_ix..offset - match_ix); + } + + inside_block = !inside_block; + prev_offset = offset + 1; + } + + (message_without_backticks, highlights) +} + +pub fn diagnostic_style( + severity: DiagnosticSeverity, + valid: bool, + theme: &theme::Editor, +) -> DiagnosticStyle { + match (severity, valid) { + (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(), + (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(), + (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(), + (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(), + (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(), + (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(), + _ => theme.invalid_hint_diagnostic.clone(), + } +} + +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: HighlightStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); + + for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) + { + syntax_highlight.font_properties.weight(Default::default()); + + // Add highlights for any fuzzy match characters before the next + // syntax highlight range. + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.start { + break; + } + match_indices.next(); + let end_index = char_ix_after(match_index, text); + let mut match_style = default_style; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + } + + if range.start == usize::MAX { + break; + } + + // Add highlights for any fuzzy match characters within the + // syntax highlight range. + let mut offset = range.start; + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.end { + break; + } + + match_indices.next(); + if match_index > offset { + result.push((offset..match_index, syntax_highlight)); + } + + let mut end_index = char_ix_after(match_index, text); + while let Some(&next_match_index) = match_indices.peek() { + if next_match_index == end_index && next_match_index < range.end { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } + + let mut match_style = syntax_highlight; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + offset = end_index; + } + + if offset < range.end { + result.push((offset..range.end, syntax_highlight)); + } + } + + fn char_ix_after(ix: usize, text: &str) -> usize { + ix + text[ix..].chars().next().unwrap().len_utf8() + } + + result +} + +pub fn styled_runs_for_code_label<'a>( + label: &'a CodeLabel, + default_color: Color, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + const MUTED_OPACITY: usize = 165; + + let mut muted_default_style = HighlightStyle { + color: default_color, + ..Default::default() + }; + muted_default_style.color.a = ((default_color.a as usize * MUTED_OPACITY) / 255) as u8; + + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style.clone(); + muted_style.color.a = ((style.color.a as usize * MUTED_OPACITY) / 255) as u8; + + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, muted_default_style)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); + + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), muted_default_style)); + } + + runs + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use language::LanguageConfig; + use lsp::FakeLanguageServer; + use project::{FakeFs, ProjectPath}; + use smol::stream::StreamExt; + use std::{cell::RefCell, rc::Rc, time::Instant}; + use text::Point; + use unindent::Unindent; + use util::test::sample_text; + + #[gpui::test] + fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { + let mut now = Instant::now(); + let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + let group_interval = buffer.read(cx).transaction_group_interval(); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let settings = Settings::test(cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + editor.update(cx, |editor, cx| { + editor.start_transaction_at(now, cx); + editor.select_ranges([2..4], None, cx); + editor.insert("cd", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cd56"); + assert_eq!(editor.selected_ranges(cx), vec![4..4]); + + editor.start_transaction_at(now, cx); + editor.select_ranges([4..5], None, cx); + editor.insert("e", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selected_ranges(cx), vec![5..5]); + + now += group_interval + Duration::from_millis(1); + editor.select_ranges([2..2], None, cx); + + // Simulate an edit in another editor + buffer.update(cx, |buffer, cx| { + buffer.start_transaction_at(now, cx); + buffer.edit([0..1], "a", cx); + buffer.edit([1..1], "b", cx); + buffer.end_transaction_at(now, cx); + }); + + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selected_ranges(cx), vec![3..3]); + + // Last transaction happened past the group interval in a different editor. + // Undo it individually and don't restore selections. + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selected_ranges(cx), vec![2..2]); + + // First two transactions happened within the group interval in this editor. + // Undo them together and restore selections. + editor.undo(&Undo, cx); + editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. + assert_eq!(editor.text(cx), "123456"); + assert_eq!(editor.selected_ranges(cx), vec![0..0]); + + // Redo the first two transactions together. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selected_ranges(cx), vec![5..5]); + + // Redo the last transaction on its own. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selected_ranges(cx), vec![6..6]); + + // Test empty transactions. + editor.start_transaction_at(now, cx); + editor.end_transaction_at(now, cx); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + }); + } + + #[gpui::test] + fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + let settings = Settings::test(cx); + let (_, editor) = + cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [ + DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) + ] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] + ); + } + + #[gpui::test] + fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + let settings = Settings::test(cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + }); + + view.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); + } + + #[gpui::test] + fn test_navigation_history(cx: &mut gpui::MutableAppContext) { + cx.add_window(Default::default(), |cx| { + use workspace::ItemView; + let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), settings, cx); + editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle())); + + // Move the cursor a small distance. + // Nothing is added to the navigation history. + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + editor.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); + assert!(nav_history.borrow_mut().pop_backward().is_none()); + + // Move the cursor a large distance. + // The history can jump back to the previous position. + editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); + let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] + ); + + // Move the cursor a small distance via the mouse. + // Nothing is added to the navigation history. + editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(nav_history.borrow_mut().pop_backward().is_none()); + + // Move the cursor a large distance via the mouse. + // The history can jump back to the previous position. + editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] + ); + let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + + editor + }); + } + + #[gpui::test] + fn test_cancel(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + let settings = Settings::test(cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + view.end_selection(cx); + + view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx); + view.end_selection(cx); + assert_eq!( + view.selected_display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + ); + }); + } + + #[gpui::test] + fn test_fold(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple( + &" + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() { + 2 + } + + fn c() { + 3 + } + } + " + .unindent(), + cx, + ); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], cx); + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {… + } + + fn c() {… + } + } + " + .unindent(), + ); + + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo {… + } + " + .unindent(), + ); + + view.unfold(&Unfold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {… + } + + fn c() {… + } + } + " + .unindent(), + ); + + view.unfold(&Unfold, cx); + assert_eq!(view.display_text(cx), buffer.read(cx).read(cx).text()); + }); + } + + #[gpui::test] + fn test_move_cursor(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + Point::new(1, 0)..Point::new(1, 0), + Point::new(1, 1)..Point::new(1, 1), + ], + "\t", + cx, + ); + }); + + view.update(cx, |view, cx| { + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + ); + + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_to_end(&MoveToEnd, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] + ); + + view.move_to_beginning(&MoveToBeginning, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], cx); + view.select_to_beginning(&SelectToBeginning, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] + ); + + view.select_to_end(&SelectToEnd, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] + ); + }); + } + + #[gpui::test] + fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + assert_eq!('ⓐ'.len_utf8(), 3); + assert_eq!('α'.len_utf8(), 2); + + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 6)..Point::new(0, 12), + Point::new(1, 2)..Point::new(1, 4), + Point::new(2, 4)..Point::new(2, 8), + ], + cx, + ); + assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ…".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "ab…".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "ab".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "a".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "α".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβ…".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβ…ε".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "ab…e".len())] + ); + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ…ⓔ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ…".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + }); + } + + #[gpui::test] + fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + view.update(cx, |view, cx| { + view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + }); + } + + #[gpui::test] + fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ], + cx, + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + // Moving to the end of line again is a no-op. + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_left(&MoveLeft, cx); + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_end_of_line(&SelectToEndOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_end_of_line(&DeleteToEndOfLine, cx); + assert_eq!(view.display_text(cx), "ab\n de"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + assert_eq!(view.display_text(cx), "\n"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + } + + #[gpui::test] + fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ], + cx, + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_right(&MoveRight, cx); + view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), + ] + ); + }); + } + + #[gpui::test] + fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.set_wrap_width(Some(140.), cx); + assert_eq!( + view.display_text(cx), + "use one::{\n two::three::\n four::five\n};" + ); + + view.select_display_ranges(&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], cx); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] + ); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] + ); + + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + }); + } + + #[gpui::test] + fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("one two three four", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the preceding word fragment is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), + ], + cx, + ); + view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); + }); + + assert_eq!(buffer.read(cx).read(cx).text(), "e two te four"); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the following word fragment is deleted + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), + ], + cx, + ); + view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); + }); + + assert_eq!(buffer.read(cx).read(cx).text(), "e t te our"); + } + + #[gpui::test] + fn test_newline(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), + ], + cx, + ); + + view.newline(&Newline, cx); + assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); + }); + } + + #[gpui::test] + fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple( + " + a + b( + X + ) + c( + X + ) + " + .unindent() + .as_str(), + cx, + ); + + let settings = Settings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(buffer.clone(), settings, cx); + editor.select_ranges( + [ + Point::new(2, 4)..Point::new(2, 5), + Point::new(5, 4)..Point::new(5, 5), + ], + None, + cx, + ); + editor + }); + + // Edit the buffer directly, deleting ranges surrounding the editor's selections + buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + Point::new(1, 2)..Point::new(3, 0), + Point::new(4, 2)..Point::new(6, 0), + ], + "", + cx, + ); + assert_eq!( + buffer.read(cx).text(), + " + a + b() + c() + " + .unindent() + ); + }); + + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selected_ranges(cx), + &[ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2), + ], + ); + + editor.newline(&Newline, cx); + assert_eq!( + editor.text(cx), + " + a + b( + ) + c( + ) + " + .unindent() + ); + + // The selections are moved after the inserted newlines + assert_eq!( + editor.selected_ranges(cx), + &[ + Point::new(2, 0)..Point::new(2, 0), + Point::new(4, 0)..Point::new(4, 0), + ], + ); + }); + } + + #[gpui::test] + fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); + + let settings = Settings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(buffer.clone(), settings, cx); + editor.select_ranges([3..4, 11..12, 19..20], None, cx); + editor + }); + + // Edit the buffer directly, deleting ranges surrounding the editor's selections + buffer.update(cx, |buffer, cx| { + buffer.edit([2..5, 10..13, 18..21], "", cx); + assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); + }); + + editor.update(cx, |editor, cx| { + assert_eq!(editor.selected_ranges(cx), &[2..2, 7..7, 12..12],); + + editor.insert("Z", cx); + assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); + + // The selections are moved after the inserted characters + assert_eq!(editor.selected_ranges(cx), &[3..3, 9..9, 15..15],); + }); + } + + #[gpui::test] + fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(" one two\nthree\n four", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + // two selections on the same line + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5), + DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9), + ], + cx, + ); + + // indent from mid-tabstop to full tabstop + view.tab(&Tab, cx); + assert_eq!(view.text(cx), " one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), + DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11), + ] + ); + + // outdent from 1 tabstop to 0 tabstops + view.outdent(&Outdent, cx); + assert_eq!(view.text(cx), "one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3), + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), + ] + ); + + // select across line ending + view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); + + // indent and outdent affect only the preceding line + view.tab(&Tab, cx); + assert_eq!(view.text(cx), "one two\n three\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] + ); + view.outdent(&Outdent, cx); + assert_eq!(view.text(cx), "one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)] + ); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + view.tab(&Tab, cx); + assert_eq!(view.text(cx), "one two\n three\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + ); + + view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + view.outdent(&Outdent, cx); + assert_eq!(view.text(cx), "one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + }); + } + + #[gpui::test] + fn test_backspace(cx: &mut gpui::MutableAppContext) { + let buffer = + MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the preceding character is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.backspace(&Backspace, cx); + }); + + assert_eq!( + buffer.read(cx).read(cx).text(), + "oe two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete(cx: &mut gpui::MutableAppContext) { + let buffer = + MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the following character is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.delete(&Delete, cx); + }); + + assert_eq!( + buffer.read(cx).read(cx).text(), + "on two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete_line(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi"); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) + ] + ); + }); + + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi\n"); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] + ); + }); + } + + #[gpui::test] + fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), + ] + ); + }); + + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), + ], + cx, + ); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), + DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), + ] + ); + }); + } + + #[gpui::test] + fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + cx, + ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), + ], + cx, + ); + assert_eq!( + view.display_text(cx), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + ); + + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); + } + + #[gpui::test] + fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + let snapshot = buffer.read(cx).snapshot(cx); + let (_, editor) = + cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + editor.update(cx, |editor, cx| { + editor.insert_blocks( + [BlockProperties { + position: snapshot.anchor_after(Point::new(2, 0)), + disposition: BlockDisposition::Below, + height: 1, + render: Arc::new(|_| Empty::new().boxed()), + }], + cx, + ); + editor.select_ranges([Point::new(2, 0)..Point::new(2, 0)], None, cx); + editor.move_line_down(&MoveLineDown, cx); + }); + } + + #[gpui::test] + fn test_clipboard(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); + let settings = Settings::test(&cx); + let view = cx + .add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }) + .1; + + // Cut with three selections. Clipboard text is divided into three slices. + view.update(cx, |view, cx| { + view.select_ranges(vec![0..7, 11..17, 22..27], None, cx); + view.cut(&Cut, cx); + assert_eq!(view.display_text(cx), "two four six "); + }); + + // Paste with three cursors. Each cursor pastes one slice of the clipboard text. + view.update(cx, |view, cx| { + view.select_ranges(vec![4..4, 9..9, 13..13], None, cx); + view.paste(&Paste, cx); + assert_eq!(view.display_text(cx), "two one✅ four three six five "); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(0, 22)..DisplayPoint::new(0, 22), + DisplayPoint::new(0, 31)..DisplayPoint::new(0, 31) + ] + ); + }); + + // Paste again but with only two cursors. Since the number of cursors doesn't + // match the number of slices in the clipboard, the entire clipboard text + // is pasted at each cursor. + view.update(cx, |view, cx| { + view.select_ranges(vec![0..0, 31..31], None, cx); + view.handle_input(&Input("( ".into()), cx); + view.paste(&Paste, cx); + view.handle_input(&Input(") ".into()), cx); + assert_eq!( + view.display_text(cx), + "( one✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + }); + + view.update(cx, |view, cx| { + view.select_ranges(vec![0..0], None, cx); + view.handle_input(&Input("123\n4567\n89\n".into()), cx); + assert_eq!( + view.display_text(cx), + "123\n4567\n89\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + }); + + // Cut with three selections, one of which is full-line. + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ], + cx, + ); + view.cut(&Cut, cx); + assert_eq!( + view.display_text(cx), + "13\n9\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + }); + + // Paste with three selections, noticing how the copied selection that was full-line + // gets inserted before the second cursor. + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), + ], + cx, + ); + view.paste(&Paste, cx); + assert_eq!( + view.display_text(cx), + "123\n4567\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), + ] + ); + }); + + // Copy with a single cursor only, which writes the whole line into the clipboard. + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], cx); + view.copy(&Copy, cx); + }); + + // Paste with three selections, noticing how the copied full-line selection is inserted + // before the empty selections but replaces the selection that is non-empty. + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + ], + cx, + ); + view.paste(&Paste, cx); + assert_eq!( + view.display_text(cx), + "123\n123\n123\n67\n123\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), + ] + ); + }); + } + + #[gpui::test] + fn test_select_all(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_all(&SelectAll, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] + ); + }); + } + + #[gpui::test] + fn test_select_line(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ], + cx, + ); + view.select_line(&SelectLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] + ); + }); + } + + #[gpui::test] + fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + cx, + ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ], + cx, + ); + assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i"); + }); + + view.update(cx, |view, cx| { + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i" + ); + assert_eq!( + view.selected_display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)], cx); + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" + ); + assert_eq!( + view.selected_display_ranges(cx), + [ + DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), + DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), + DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), + DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), + DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) + ] + ); + }); + } + + #[gpui::test] + fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], cx); + }); + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], cx); + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], cx); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); + } + + #[gpui::test] + async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + + let text = r#" + use mod1::mod2::{mod3, mod4}; + + fn fn_1(param1: bool, param2: &str) { + let var1 = "text"; + } + "# + .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, settings, cx)); + view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(&mut cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ], + cx, + ); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + // Trying to expand the selected syntax node one more time has no effect. + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Trying to shrink the selected syntax node one more time has no effect. + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Ensure that we keep expanding the selection if the larger selection starts or ends within + // a fold. + view.update(&mut cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 21)..Point::new(0, 24), + Point::new(3, 20)..Point::new(3, 22), + ], + cx, + ); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), + ] + ); + } + + #[gpui::test] + async fn test_autoindent_selections(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + 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 (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + editor + .condition(&cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(&mut cx, |editor, cx| { + editor.select_ranges([5..5, 8..8, 9..9], None, cx); + editor.newline(&Newline, cx); + assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); + assert_eq!( + editor.selected_ranges(cx), + &[ + Point::new(1, 4)..Point::new(1, 4), + Point::new(3, 4)..Point::new(3, 4), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); + } + + #[gpui::test] + async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig { + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/*".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + + / + + "# + .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, settings, cx)); + view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(&mut cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ], + cx, + ); + view.handle_input(&Input("{".to_string()), cx); + view.handle_input(&Input("{".to_string()), cx); + view.handle_input(&Input("{".to_string()), cx); + assert_eq!( + view.text(cx), + " + {{{}}} + {{{}}} + / + + " + .unindent() + ); + + view.move_right(&MoveRight, cx); + view.handle_input(&Input("}".to_string()), cx); + view.handle_input(&Input("}".to_string()), cx); + view.handle_input(&Input("}".to_string()), cx); + assert_eq!( + view.text(cx), + " + {{{}}}} + {{{}}}} + / + + " + .unindent() + ); + + view.undo(&Undo, cx); + view.handle_input(&Input("/".to_string()), cx); + view.handle_input(&Input("*".to_string()), cx); + assert_eq!( + view.text(cx), + " + /* */ + /* */ + / + + " + .unindent() + ); + + view.undo(&Undo, cx); + view.select_display_ranges( + &[ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.handle_input(&Input("*".to_string()), cx); + assert_eq!( + view.text(cx), + " + a + + /* + * + " + .unindent() + ); + }); + } + + #[gpui::test] + async fn test_snippets(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + + let text = " + a. b + a. b + a. b + " + .unindent(); + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + editor.update(&mut cx, |editor, cx| { + let buffer = &editor.snapshot(cx).buffer_snapshot; + let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); + let insertion_ranges = [ + Point::new(0, 2).to_offset(buffer)..Point::new(0, 2).to_offset(buffer), + Point::new(1, 2).to_offset(buffer)..Point::new(1, 2).to_offset(buffer), + Point::new(2, 2).to_offset(buffer)..Point::new(2, 2).to_offset(buffer), + ]; + + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); + assert_eq!( + editor.text(cx), + " + a.f(one, two, three) b + a.f(one, two, three) b + a.f(one, two, three) b + " + .unindent() + ); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); + + // Can't move earlier than the first tab stop + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 9)..Point::new(0, 12), + Point::new(1, 9)..Point::new(1, 12), + Point::new(2, 9)..Point::new(2, 12) + ] + ); + + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 20)..Point::new(0, 20), + Point::new(1, 20)..Point::new(1, 20), + Point::new(2, 20)..Point::new(2, 20) + ] + ); + + // As soon as the last tab stop is reached, snippet state is gone + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 20)..Point::new(0, 20), + Point::new(1, 20)..Point::new(1, 20), + Point::new(2, 20)..Point::new(2, 20) + ] + ); + }); + } + + #[gpui::test] + async fn test_completion(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let (language_server, mut fake) = cx.update(|cx| { + lsp::LanguageServer::fake_with_capabilities( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + }); + + let text = " + one + two + three + " + .unindent(); + + let fs = FakeFs::new(cx.background().clone()); + fs.insert_file("/file", text).await; + + let project = Project::test(fs, &mut cx); + + let (worktree, relative_path) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/file", false, cx) + }) + .await + .unwrap(); + let project_path = ProjectPath { + worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), + path: relative_path.into(), + }; + let buffer = project + .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) + .await + .unwrap(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language_server(Some(language_server), cx); + }); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + buffer.next_notification(&cx).await; + + let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + editor.update(&mut cx, |editor, cx| { + editor.project = Some(project); + editor.select_ranges([Point::new(0, 3)..Point::new(0, 3)], None, cx); + editor.handle_input(&Input(".".to_string()), cx); + }); + + handle_completion_request( + &mut fake, + "/file", + Point::new(0, 4), + vec![ + (Point::new(0, 4)..Point::new(0, 4), "first_completion"), + (Point::new(0, 4)..Point::new(0, 4), "second_completion"), + ], + ) + .await; + editor + .condition(&cx, |editor, _| editor.context_menu_visible()) + .await; + + let apply_additional_edits = editor.update(&mut cx, |editor, cx| { + editor.move_down(&MoveDown, cx); + let apply_additional_edits = editor + .confirm_completion(&ConfirmCompletion(None), cx) + .unwrap(); + assert_eq!( + editor.text(cx), + " + one.second_completion + two + three + " + .unindent() + ); + apply_additional_edits + }); + + handle_resolve_completion_request( + &mut fake, + Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")), + ) + .await; + apply_additional_edits.await.unwrap(); + assert_eq!( + editor.read_with(&cx, |editor, cx| editor.text(cx)), + " + one.second_completion + two + three + additional edit + " + .unindent() + ); + + editor.update(&mut cx, |editor, cx| { + editor.select_ranges( + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 5)..Point::new(2, 5), + ], + None, + cx, + ); + + editor.handle_input(&Input(" ".to_string()), cx); + assert!(editor.context_menu.is_none()); + editor.handle_input(&Input("s".to_string()), cx); + assert!(editor.context_menu.is_none()); + }); + + handle_completion_request( + &mut fake, + "/file", + Point::new(2, 7), + vec![ + (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"), + (Point::new(2, 6)..Point::new(2, 7), "fifth_completion"), + (Point::new(2, 6)..Point::new(2, 7), "sixth_completion"), + ], + ) + .await; + editor + .condition(&cx, |editor, _| editor.context_menu_visible()) + .await; + + editor.update(&mut cx, |editor, cx| { + editor.handle_input(&Input("i".to_string()), cx); + }); + + handle_completion_request( + &mut fake, + "/file", + Point::new(2, 8), + vec![ + (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"), + (Point::new(2, 6)..Point::new(2, 8), "fifth_completion"), + (Point::new(2, 6)..Point::new(2, 8), "sixth_completion"), + ], + ) + .await; + editor + .condition(&cx, |editor, _| editor.context_menu_visible()) + .await; + + let apply_additional_edits = editor.update(&mut cx, |editor, cx| { + let apply_additional_edits = editor + .confirm_completion(&ConfirmCompletion(None), cx) + .unwrap(); + assert_eq!( + editor.text(cx), + " + one.second_completion + two sixth_completion + three sixth_completion + additional edit + " + .unindent() + ); + apply_additional_edits + }); + handle_resolve_completion_request(&mut fake, None).await; + apply_additional_edits.await.unwrap(); + + async fn handle_completion_request( + fake: &mut FakeLanguageServer, + path: &'static str, + position: Point, + completions: Vec<(Range, &'static str)>, + ) { + fake.handle_request::(move |params, _| { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path(path).unwrap() + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(position.row, position.column) + ); + Some(lsp::CompletionResponse::Array( + completions + .iter() + .map(|(range, new_text)| lsp::CompletionItem { + label: new_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.start.row, range.start.column), + ), + new_text: new_text.to_string(), + })), + ..Default::default() + }) + .collect(), + )) + }) + .next() + .await; + } + + async fn handle_resolve_completion_request( + fake: &mut FakeLanguageServer, + edit: Option<(Range, &'static str)>, + ) { + fake.handle_request::(move |_, _| { + lsp::CompletionItem { + additional_text_edits: edit.clone().map(|(range, new_text)| { + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.end.row, range.end.column), + ), + new_text.to_string(), + )] + }), + ..Default::default() + } + }) + .next() + .await; + } + } + + #[gpui::test] + async fn test_toggle_comment(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = " + fn a() { + //b(); + // c(); + // d(); + } + " + .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, settings, cx)); + + view.update(&mut cx, |editor, cx| { + // If multiple selections intersect a line, the line is only + // toggled once. + editor.select_display_ranges( + &[ + DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6), + ], + cx, + ); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + b(); + c(); + d(); + } + " + .unindent() + ); + + // The comment prefix is inserted at the same column for every line + // in a selection. + editor.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)], cx); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + // b(); + // c(); + // d(); + } + " + .unindent() + ); + + // If a selection ends at the beginning of a line, that line is not toggled. + editor.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)], cx); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + // b(); + c(); + // d(); + } + " + .unindent() + ); + }); + } + + #[gpui::test] + fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + Point::new(0, 0)..Point::new(0, 4), + Point::new(1, 0)..Point::new(1, 4), + ], + cx, + ); + multibuffer + }); + + assert_eq!(multibuffer.read(cx).read(cx).text(), "aaaa\nbbbb"); + + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(multibuffer, settings, cx) + }); + view.update(cx, |view, cx| { + assert_eq!(view.text(cx), "aaaa\nbbbb"); + view.select_ranges( + [ + Point::new(0, 0)..Point::new(0, 0), + Point::new(1, 0)..Point::new(1, 0), + ], + None, + cx, + ); + + view.handle_input(&Input("X".to_string()), cx); + assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); + assert_eq!( + view.selected_ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + ] + ) + }); + } + + #[gpui::test] + fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer, + [ + Point::new(0, 0)..Point::new(1, 4), + Point::new(1, 0)..Point::new(2, 4), + ], + cx, + ); + multibuffer + }); + + assert_eq!( + multibuffer.read(cx).read(cx).text(), + "aaaa\nbbbb\nbbbb\ncccc" + ); + + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(multibuffer, settings, cx) + }); + view.update(cx, |view, cx| { + view.select_ranges( + [ + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 3)..Point::new(2, 3), + ], + None, + cx, + ); + + view.handle_input(&Input("X".to_string()), cx); + assert_eq!(view.text(cx), "aaaa\nbXbbXb\nbXbbXb\ncccc"); + assert_eq!( + view.selected_ranges(cx), + [ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 5)..Point::new(2, 5), + ] + ); + + view.newline(&Newline, cx); + assert_eq!(view.text(cx), "aaaa\nbX\nbbX\nb\nbX\nbbX\nb\ncccc"); + assert_eq!( + view.selected_ranges(cx), + [ + Point::new(2, 0)..Point::new(2, 0), + Point::new(6, 0)..Point::new(6, 0), + ] + ); + }); + } + + #[gpui::test] + fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + Point::new(0, 0)..Point::new(1, 4), + Point::new(1, 0)..Point::new(2, 4), + ], + cx, + ) + .into_iter() + .next(); + multibuffer + }); + assert_eq!( + multibuffer.read(cx).read(cx).text(), + "aaaa\nbbbb\nbbbb\ncccc" + ); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(multibuffer.clone(), settings, cx); + editor.select_ranges( + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ], + None, + cx, + ); + editor + }); + + // Refreshing selections is a no-op when excerpts haven't changed. + editor.update(cx, |editor, cx| { + editor.refresh_selections(cx); + assert_eq!( + editor.selected_ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + // Removing an excerpt causes the first selection to become degenerate. + assert_eq!( + editor.selected_ranges(cx), + [ + Point::new(0, 0)..Point::new(0, 0), + Point::new(0, 1)..Point::new(0, 1) + ] + ); + + // Refreshing selections will relocate the first selection to the original buffer + // location. + editor.refresh_selections(cx); + assert_eq!( + editor.selected_ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(0, 3)..Point::new(0, 3) + ] + ); + }); + } + + #[gpui::test] + async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig { + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = concat!( + "{ }\n", // Suppress rustfmt + " x\n", // + " /* */\n", // + "x\n", // + "{{} }\n", // + ); + + 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, settings, cx)); + view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(&mut cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ], + cx, + ); + view.newline(&Newline, cx); + + assert_eq!( + view.buffer().read(cx).read(cx).text(), + concat!( + "{ \n", // Suppress rustfmt + "\n", // + "}\n", // + " x\n", // + " /* \n", // + " \n", // + " */\n", // + "x\n", // + "{{} \n", // + "}\n", // + ) + ); + }); + } + + #[gpui::test] + fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + let settings = Settings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; + + let buffer = buffer.read(cx).snapshot(cx); + + let anchor_range = |range: Range| { + buffer.anchor_after(range.start)..buffer.anchor_after(range.end) + }; + + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + Color::red(), + cx, + ); + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + Color::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + let mut highlighted_ranges = editor.highlighted_ranges_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + ); + // Enforce a consistent ordering based on color without relying on the ordering of the + // highlight's `TypeId` which is non-deterministic. + highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); + assert_eq!( + highlighted_ranges, + &[ + ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), + Color::green(), + ), + ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), + Color::green(), + ), + ( + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + Color::red(), + ), + ( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + ), + ] + ); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + ), + &[( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + )] + ); + }); + } + + #[test] + fn test_combine_syntax_and_fuzzy_match_highlights() { + let string = "abcdefghijklmnop"; + let default = HighlightStyle::default(); + let syntax_ranges = [ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..8, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ]; + let match_indices = [4, 6, 7, 8]; + assert_eq!( + combine_syntax_and_fuzzy_match_highlights( + &string, + default, + syntax_ranges.into_iter(), + &match_indices, + ), + &[ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..5, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 5..6, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ( + 6..8, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 8..9, + HighlightStyle { + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ] + ); + } + + fn empty_range(row: usize, column: usize) -> Range { + let point = DisplayPoint::new(row as u32, column as u32); + point..point + } + + fn build_editor( + buffer: ModelHandle, + settings: Settings, + cx: &mut ViewContext, + ) -> Editor { + let settings = watch::channel_with(settings); + Editor::new(EditorMode::Full, buffer, None, settings.1, None, cx) + } +} + +trait RangeExt { + fn sorted(&self) -> Range; + fn to_inclusive(&self) -> RangeInclusive; +} + +impl RangeExt for Range { + fn sorted(&self) -> Self { + cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() + } + + fn to_inclusive(&self) -> RangeInclusive { + self.start.clone()..=self.end.clone() + } +} diff --git a/crates/find/src/buffer_find.rs b/crates/find/src/buffer_find.rs index 41aa96fa688eaeb982a2a3944109da18d5feb38c..2926cb4066b49462cdf32dce164b941d27222d76 100644 --- a/crates/find/src/buffer_find.rs +++ b/crates/find/src/buffer_find.rs @@ -142,14 +142,15 @@ impl Toolbar for FindBar { self.pending_search.take(); if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { - self.active_editor_subscription = - Some(cx.subscribe(&editor, Self::on_active_editor_event)); - self.active_editor = Some(editor); - self.update_matches(false, cx); - true - } else { - false + if editor.read(cx).searchable() { + self.active_editor_subscription = + Some(cx.subscribe(&editor, Self::on_active_editor_event)); + self.active_editor = Some(editor); + self.update_matches(false, cx); + return true; + } } + false } fn on_dismiss(&mut self, cx: &mut ViewContext) { @@ -295,6 +296,8 @@ impl FindBar { }); cx.focus(&find_bar); } + } else { + cx.propagate_action(); } }); } diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index f98a12ec4ce0e4bd64b90e7e1c6ba008d61656ea..fd590e93b11582b6feade4510fac280a7cf490b7 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -23,6 +23,7 @@ action!(ToggleFocus); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), + Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), Binding::new("cmd-shift-F", Deploy, Some("Workspace")), Binding::new("enter", Search, Some("ProjectFindView")), ]); @@ -140,6 +141,7 @@ impl Item for ProjectFind { settings.clone(), cx, ); + editor.set_searchable(false); editor.set_nav_history(Some(nav_history)); editor }), @@ -305,6 +307,7 @@ impl ItemView for ProjectFindView { .update(cx, |editor, cx| editor.scroll_position(cx)); let mut editor = Editor::for_buffer(excerpts, Some(project), self.settings.clone(), cx); + editor.set_searchable(false); editor.set_nav_history(Some(nav_history)); editor.set_scroll_position(scroll_position, cx); editor From 721258911ca542c8da62c411dcd26d273e134177 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 16:49:28 -0700 Subject: [PATCH 34/65] Open excerpts on alt-enter Also: Remove special handling for alt-shift-D binding in diagnostics view that opens excerpts. Rely on alt-enter in all multi-buffers instead. Co-Authored-By: Max Brunsfeld --- crates/diagnostics/src/diagnostics.rs | 58 ++------------------ crates/editor/src/editor.rs | 76 ++++++++++++++++++++++++--- crates/workspace/src/pane.rs | 16 ++++++ 3 files changed, 88 insertions(+), 62 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e668ce326f126bff33d47360de3c8ad4e74dabb5..bb269fdbab1f1cf883337a7b46a7152a0ad8a76b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1,13 +1,11 @@ pub mod items; use anyhow::Result; -use collections::{BTreeSet, HashMap, HashSet}; +use collections::{BTreeSet, HashSet}; use editor::{ diagnostic_block_renderer, display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock}, - highlight_diagnostic_message, - items::BufferItemHandle, - Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset, + highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset, }; use gpui::{ action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity, @@ -31,21 +29,12 @@ use util::TryFutureExt; use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Workspace}; action!(Deploy); -action!(OpenExcerpts); const CONTEXT_LINE_COUNT: u32 = 1; pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("alt-shift-D", Deploy, Some("Workspace")), - Binding::new( - "alt-shift-D", - OpenExcerpts, - Some("ProjectDiagnosticsEditor"), - ), - ]); + cx.add_bindings([Binding::new("alt-shift-D", Deploy, Some("Workspace"))]); cx.add_action(ProjectDiagnosticsEditor::deploy); - cx.add_action(ProjectDiagnosticsEditor::open_excerpts); } type Event = editor::Event; @@ -180,47 +169,6 @@ impl ProjectDiagnosticsEditor { } } - fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { - if let Some(workspace) = self.workspace.upgrade(cx) { - let editor = self.editor.read(cx); - let excerpts = self.excerpts.read(cx); - let mut new_selections_by_buffer = HashMap::default(); - - for selection in editor.local_selections::(cx) { - for (buffer, mut range) in - excerpts.range_to_buffer_ranges(selection.start..selection.end, cx) - { - if selection.reversed { - mem::swap(&mut range.start, &mut range.end); - } - new_selections_by_buffer - .entry(buffer) - .or_insert(Vec::new()) - .push(range) - } - } - - // We defer the pane interaction because we ourselves are a workspace item - // and activating a new item causes the pane to call a method on us reentrantly, - // which panics if we're on the stack. - workspace.defer(cx, |workspace, cx| { - for (buffer, ranges) in new_selections_by_buffer { - let buffer = BufferItemHandle(buffer); - if !workspace.activate_pane_for_item(&buffer, cx) { - workspace.activate_next_pane(cx); - } - let editor = workspace - .open_item(buffer, cx) - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - editor.select_ranges(ranges, Some(Autoscroll::Center), cx) - }); - } - }); - } - } - fn update_excerpts(&mut self, cx: &mut ViewContext) { let paths = mem::take(&mut self.paths_to_update); let project = self.model.read(cx).project.clone(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 21f1d9a4ad7bdc4087198fa8ffcf8dce5bc03127..6919a85c55e582c5d0fc4195a545bfe982b1c682 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -132,6 +132,7 @@ action!(ShowCompletions); action!(ToggleCodeActions, bool); action!(ConfirmCompletion, Option); action!(ConfirmCodeAction, Option); +action!(OpenExcerpts); pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { path_openers.push(Box::new(items::BufferOpener)); @@ -259,6 +260,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec bool { self.searchable } + + fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(cx)) + { + editor + } else { + cx.propagate_action(); + return; + }; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + if buffer.is_singleton() { + cx.propagate_action(); + return; + } + + let mut new_selections_by_buffer = HashMap::default(); + for selection in editor.local_selections::(cx) { + for (buffer, mut range) in + buffer.range_to_buffer_ranges(selection.start..selection.end, cx) + { + if selection.reversed { + mem::swap(&mut range.start, &mut range.end); + } + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push(range) + } + } + + // We defer the pane interaction because we ourselves are a workspace item + // and activating a new item causes the pane to call a method on us reentrantly, + // which panics if we're on the stack. + cx.defer(|workspace, cx| { + for (buffer, ranges) in new_selections_by_buffer { + let buffer = BufferItemHandle(buffer); + if !workspace.activate_pane_for_item(&buffer, cx) { + workspace.activate_next_pane(cx); + } + let editor = workspace + .open_item(buffer, cx) + .downcast::() + .unwrap(); + editor.update(cx, |editor, cx| { + let prev_nav_history_len = + editor.nav_history().map_or(0, |history| history.len()); + editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); + if let Some(history) = editor.nav_history() { + history.truncate(prev_nav_history_len); + } + }); + } + }); + } } impl EditorSnapshot { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index beafd0eb8c7de83e529763264fec129aa8cd5894..c650a0240f182935b3858799aa23f24564154727 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -649,6 +649,14 @@ impl ToolbarHandle for ViewHandle { } impl ItemNavHistory { + pub fn len(&self) -> usize { + self.history.borrow().len() + } + + pub fn truncate(&self, len: usize) { + self.history.borrow_mut().truncate(len) + } + pub fn new(history: Rc>, item_view: &ViewHandle) -> Self { Self { history, @@ -666,6 +674,14 @@ impl ItemNavHistory { } impl NavHistory { + pub fn len(&self) -> usize { + self.backward_stack.len() + } + + pub fn truncate(&mut self, len: usize) { + self.backward_stack.truncate(len); + } + pub fn pop_backward(&mut self) -> Option { self.backward_stack.pop_back() } From 60710fa5d51c671e806a65a42b1619661b29dbdf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 19:26:15 -0700 Subject: [PATCH 35/65] Only store one nav history entry when opening excerpts Also: Introduce the ability to disable and enable the nav history directly. This allows us to explicitly push an entry when opening excerpts and then disable all pushes as we open individual buffers. --- crates/editor/src/editor.rs | 32 +++++++++++++++++--------------- crates/workspace/src/pane.rs | 22 ++++++++-------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6919a85c55e582c5d0fc4195a545bfe982b1c682..dff3518fcba4fa1df5f706b540ba3313e4b0b797 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4172,6 +4172,7 @@ impl Editor { cx.spawn(|workspace, mut cx| async move { let definitions = definitions.await?; workspace.update(&mut cx, |workspace, cx| { + let nav_history = workspace.active_pane().read(cx).nav_history().clone(); for definition in definitions { let range = definition.range.to_offset(definition.buffer.read(cx)); let target_editor_handle = workspace @@ -4182,15 +4183,11 @@ impl Editor { target_editor_handle.update(cx, |target_editor, cx| { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. - let prev_nav_history_len = target_editor - .nav_history() - .map_or(0, |history| history.len()); - target_editor.select_ranges([range], Some(Autoscroll::Center), cx); if editor_handle != target_editor_handle { - if let Some(history) = target_editor.nav_history() { - history.truncate(prev_nav_history_len); - } + nav_history.borrow_mut().disable(); } + target_editor.select_ranges([range], Some(Autoscroll::Center), cx); + nav_history.borrow_mut().enable(); }); } }); @@ -5389,28 +5386,33 @@ impl Editor { } } + editor_handle.update(cx, |editor, cx| { + editor.push_to_nav_history(editor.newest_anchor_selection().head(), None, cx); + }); + let nav_history = workspace.active_pane().read(cx).nav_history().clone(); + nav_history.borrow_mut().disable(); + // We defer the pane interaction because we ourselves are a workspace item // and activating a new item causes the pane to call a method on us reentrantly, // which panics if we're on the stack. - cx.defer(|workspace, cx| { - for (buffer, ranges) in new_selections_by_buffer { + cx.defer(move |workspace, cx| { + for (ix, (buffer, ranges)) in new_selections_by_buffer.into_iter().enumerate() { let buffer = BufferItemHandle(buffer); - if !workspace.activate_pane_for_item(&buffer, cx) { + if ix == 0 && !workspace.activate_pane_for_item(&buffer, cx) { workspace.activate_next_pane(cx); } + let editor = workspace .open_item(buffer, cx) .downcast::() .unwrap(); + editor.update(cx, |editor, cx| { - let prev_nav_history_len = - editor.nav_history().map_or(0, |history| history.len()); editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); - if let Some(history) = editor.nav_history() { - history.truncate(prev_nav_history_len); - } }); } + + nav_history.borrow_mut().enable(); }); } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c650a0240f182935b3858799aa23f24564154727..050cd7d5555af9a2432afbd323c8c3446d1589b8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -123,6 +123,7 @@ enum NavigationMode { Normal, GoingBack, GoingForward, + Disabled, } impl Default for NavigationMode { @@ -149,7 +150,7 @@ impl Pane { } } - pub(crate) fn nav_history(&self) -> &Rc> { + pub fn nav_history(&self) -> &Rc> { &self.nav_history } @@ -649,14 +650,6 @@ impl ToolbarHandle for ViewHandle { } impl ItemNavHistory { - pub fn len(&self) -> usize { - self.history.borrow().len() - } - - pub fn truncate(&self, len: usize) { - self.history.borrow_mut().truncate(len) - } - pub fn new(history: Rc>, item_view: &ViewHandle) -> Self { Self { history, @@ -674,12 +667,12 @@ impl ItemNavHistory { } impl NavHistory { - pub fn len(&self) -> usize { - self.backward_stack.len() + pub fn disable(&mut self) { + self.mode = NavigationMode::Disabled; } - pub fn truncate(&mut self, len: usize) { - self.backward_stack.truncate(len); + pub fn enable(&mut self) { + self.mode = NavigationMode::Normal; } pub fn pop_backward(&mut self) -> Option { @@ -692,7 +685,7 @@ impl NavHistory { fn pop(&mut self, mode: NavigationMode) -> Option { match mode { - NavigationMode::Normal => None, + NavigationMode::Normal | NavigationMode::Disabled => None, NavigationMode::GoingBack => self.pop_backward(), NavigationMode::GoingForward => self.pop_forward(), } @@ -708,6 +701,7 @@ impl NavHistory { item_view: Rc, ) { match self.mode { + NavigationMode::Disabled => {} NavigationMode::Normal => { if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { self.backward_stack.pop_front(); From f6b7cbd5cf41c5d8d059244832c1d4d06c025e51 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Feb 2022 19:48:43 -0700 Subject: [PATCH 36/65] Always open a new project find on `alt-cmd-shift-F` --- crates/find/src/project_find.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index fd590e93b11582b6feade4510fac280a7cf490b7..c97e37168c17e1c43a8dc95062d33d1c828d0765 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -15,16 +15,18 @@ use std::{ use util::ResultExt as _; use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; -action!(Deploy); +action!(Deploy, bool); action!(Search); action!(ToggleSearchOption, SearchOption); action!(ToggleFocus); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ + Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), - Binding::new("cmd-shift-F", Deploy, Some("Workspace")), + Binding::new("cmd-shift-F", Deploy(true), Some("Workspace")), + Binding::new("cmd-alt-shift-F", Deploy(false), Some("Workspace")), Binding::new("enter", Search, Some("ProjectFindView")), ]); cx.add_action(ProjectFindView::deploy); @@ -333,13 +335,19 @@ impl ItemView for ProjectFindView { } impl ProjectFindView { - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, cx); - } else { - let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); - workspace.open_item(model, cx); + fn deploy( + workspace: &mut Workspace, + &Deploy(activate_existing): &Deploy, + cx: &mut ViewContext, + ) { + if activate_existing { + if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_item(&existing, cx); + return; + } } + let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); + workspace.open_item(model, cx); } fn search(&mut self, _: &Search, cx: &mut ViewContext) { From afea5a3d5e70b3af63560e41fd8026dbe4cd4e5e Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Feb 2022 14:31:36 +0100 Subject: [PATCH 37/65] :art: --- crates/editor/src/multi_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index c5d38bed6a68a5ebc8a42eb223831b5e76528d4b..e05ddc56933bd83d67735a9e877252043f852f23 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -752,7 +752,7 @@ impl MultiBuffer { let edit_start = new_excerpts.summary().text.bytes; new_excerpts.update_last( |excerpt| { - excerpt.has_trailing_newline = ranges.peek().is_some(); + excerpt.has_trailing_newline = true; prev_id = excerpt.id.clone(); }, &(), From a78fe4ef6a19d502ca24acf90028f4f22c0dea48 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 26 Feb 2022 14:43:02 +0100 Subject: [PATCH 38/65] Don't focus results editor on `cmd-shift-f` when there are no results --- crates/find/src/project_find.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index c97e37168c17e1c43a8dc95062d33d1c828d0765..5a1214561effcd48f223d2bf55bc463bb62ae647 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -22,7 +22,6 @@ action!(ToggleFocus); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ - Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), Binding::new("cmd-shift-F", Deploy(true), Some("Workspace")), @@ -385,7 +384,9 @@ impl ProjectFindView { fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { if self.query_editor.is_focused(cx) { - cx.focus(&self.results_editor); + if !self.model.read(cx).highlighted_ranges.is_empty() { + cx.focus(&self.results_editor); + } } else { cx.focus(&self.query_editor); } From ae1a46a4e4a9279616d37badb9aa2865af13421b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Feb 2022 08:21:38 -0700 Subject: [PATCH 39/65] Render a magnifier icon and the query in project search tab Also: Wire up events so the modified status updates correctly. --- crates/find/src/project_find.rs | 63 +++++++++++++++++++++------ crates/theme/src/theme.rs | 2 + crates/zed/assets/icons/magnifier.svg | 3 ++ crates/zed/assets/themes/_base.toml | 2 + 4 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 crates/zed/assets/icons/magnifier.svg diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 5a1214561effcd48f223d2bf55bc463bb62ae647..00406d26a2ee28d54fe670c866bdd59bf477181c 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -39,6 +39,7 @@ struct ProjectFind { excerpts: ModelHandle, pending_search: Option>>, highlighted_ranges: Vec>, + active_query: Option, } struct ProjectFindView { @@ -64,6 +65,7 @@ impl ProjectFind { excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), pending_search: Default::default(), highlighted_ranges: Default::default(), + active_query: None, } } @@ -75,6 +77,7 @@ impl ProjectFind { .update(new_cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), pending_search: Default::default(), highlighted_ranges: self.highlighted_ranges.clone(), + active_query: self.active_query.clone(), } } @@ -104,6 +107,7 @@ impl ProjectFind { } }); this.pending_search.take(); + this.active_query = Some(query); cx.notify(); }); } @@ -124,8 +128,22 @@ impl Item for ProjectFind { ) -> Self::View { let settings = workspace.settings(); let excerpts = model.read(cx).excerpts.clone(); + let results_editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer( + excerpts, + Some(workspace.project().clone()), + settings.clone(), + cx, + ); + editor.set_searchable(false); + editor.set_nav_history(Some(nav_history)); + editor + }); + cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) + .detach(); cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) .detach(); + ProjectFindView { model, query_editor: cx.add_view(|cx| { @@ -135,17 +153,7 @@ impl Item for ProjectFind { cx, ) }), - results_editor: cx.add_view(|cx| { - let mut editor = Editor::for_buffer( - excerpts, - Some(workspace.project().clone()), - settings.clone(), - cx, - ); - editor.set_searchable(false); - editor.set_nav_history(Some(nav_history)); - editor - }), + results_editor, case_sensitive: false, whole_word: false, regex: false, @@ -159,8 +167,12 @@ impl Item for ProjectFind { } } +enum ViewEvent { + UpdateTab, +} + impl Entity for ProjectFindView { - type Event = (); + type Event = ViewEvent; } impl View for ProjectFindView { @@ -231,8 +243,26 @@ impl ItemView for ProjectFindView { Box::new(self.model.clone()) } - fn tab_content(&self, style: &theme::Tab, _: &gpui::AppContext) -> ElementBox { - Label::new("Project Find".to_string(), style.label.clone()).boxed() + fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = self.settings.borrow(); + let find_theme = &settings.theme.find; + Flex::row() + .with_child( + Svg::new("icons/magnifier.svg") + .with_color(style.label.text.color) + .constrained() + .with_width(find_theme.tab_icon_width) + .aligned() + .boxed(), + ) + .with_children(self.model.read(cx).active_query.as_ref().map(|query| { + Label::new(query.as_str().to_string(), style.label.clone()) + .aligned() + .contained() + .with_margin_left(find_theme.tab_icon_spacing) + .boxed() + })) + .boxed() } fn project_path(&self, _: &gpui::AppContext) -> Option { @@ -331,6 +361,10 @@ impl ItemView for ProjectFindView { self.results_editor .update(cx, |editor, cx| editor.navigate(data, cx)); } + + fn should_update_tab_on_event(event: &ViewEvent) -> bool { + matches!(event, ViewEvent::UpdateTab) + } } impl ProjectFindView { @@ -407,6 +441,7 @@ impl ProjectFindView { } } + cx.emit(ViewEvent::UpdateTab); cx.notify(); } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index babd9f6e2001690c9342f1f4c268ddc7464cb503..701047961947f7982b6058cab81ea1ce40b4ab79 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -108,6 +108,8 @@ pub struct Find { pub match_background: Color, pub match_index: ContainedText, pub results_status: TextStyle, + pub tab_icon_width: f32, + pub tab_icon_spacing: f32, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/zed/assets/icons/magnifier.svg b/crates/zed/assets/icons/magnifier.svg new file mode 100644 index 0000000000000000000000000000000000000000..dc27a594ee3d6996e6e5a0fd9922774a05129ada --- /dev/null +++ b/crates/zed/assets/icons/magnifier.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index f7fd023f23a1d8e8fcc3f45e7e2f70115d972ce7..b46f8efa5071dc738cab9f60bc897c96e93ca723 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -352,6 +352,8 @@ tab_summary_spacing = 10 match_background = "$state.highlighted_line" background = "$surface.1" results_status = { extends = "$text.0", size = 18 } +tab_icon_width = 14 +tab_icon_spacing = 4 [find.option_button] extends = "$text.1" From 2f427769df6372c9c07d3e0d20b1769bdda8ccd2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Feb 2022 13:23:05 -0700 Subject: [PATCH 40/65] Allow a new search to be created with cmd-enter This replaces the `cmd-alt-shift-F` binding to open a new search. Instead, you can preserve the existing search results by entering a query and then hitting `cmd-enter` instead of `enter`. This opens a new project find view and restores the previous view to whatever query it was previously displaying. It's a bit strange, but I don't want to rely on splitting as the only way of creating multiple sets of search results. --- crates/editor/src/editor.rs | 8 + crates/find/src/project_find.rs | 1077 ++++++++++++++++--------------- crates/language/src/buffer.rs | 7 + 3 files changed, 578 insertions(+), 514 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index dff3518fcba4fa1df5f706b540ba3313e4b0b797..1961438d0d4fd99fd7e59dcb03d894e4dde797fa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5144,6 +5144,14 @@ impl Editor { self.buffer.read(cx).read(cx).text() } + pub fn set_text(&mut self, text: impl Into, cx: &mut ViewContext) { + self.buffer + .read(cx) + .as_singleton() + .expect("you can only call set_text on editors for singleton buffers") + .update(cx, |buffer, cx| buffer.set_text(text, cx)); + } + pub fn display_text(&self, cx: &mut MutableAppContext) -> String { self.display_map .update(cx, |map, cx| map.snapshot(cx)) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 00406d26a2ee28d54fe670c866bdd59bf477181c..293422486030b47aef296d0ca78fc54f9f33a13a 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,514 +1,563 @@ -use crate::SearchOption; -use editor::{Anchor, Autoscroll, Editor, MultiBuffer}; -use gpui::{ - action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, - ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, - ViewHandle, -}; -use postage::watch; -use project::{search::SearchQuery, Project}; -use std::{ - any::{Any, TypeId}, - ops::Range, - path::PathBuf, -}; -use util::ResultExt as _; -use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; - -action!(Deploy, bool); -action!(Search); -action!(ToggleSearchOption, SearchOption); -action!(ToggleFocus); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), - Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), - Binding::new("cmd-shift-F", Deploy(true), Some("Workspace")), - Binding::new("cmd-alt-shift-F", Deploy(false), Some("Workspace")), - Binding::new("enter", Search, Some("ProjectFindView")), - ]); - cx.add_action(ProjectFindView::deploy); - cx.add_action(ProjectFindView::search); - cx.add_action(ProjectFindView::toggle_search_option); - cx.add_action(ProjectFindView::toggle_focus); -} - -struct ProjectFind { - project: ModelHandle, - excerpts: ModelHandle, - pending_search: Option>>, - highlighted_ranges: Vec>, - active_query: Option, -} - -struct ProjectFindView { - model: ModelHandle, - query_editor: ViewHandle, - results_editor: ViewHandle, - case_sensitive: bool, - whole_word: bool, - regex: bool, - query_contains_error: bool, - settings: watch::Receiver, -} - -impl Entity for ProjectFind { - type Event = (); -} - -impl ProjectFind { - fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { - let replica_id = project.read(cx).replica_id(); - Self { - project, - excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), - pending_search: Default::default(), - highlighted_ranges: Default::default(), - active_query: None, - } - } - - fn clone(&self, new_cx: &mut ModelContext) -> Self { - Self { - project: self.project.clone(), - excerpts: self - .excerpts - .update(new_cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), - pending_search: Default::default(), - highlighted_ranges: self.highlighted_ranges.clone(), - active_query: self.active_query.clone(), - } - } - - fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { - let search = self - .project - .update(cx, |project, cx| project.search(query.clone(), cx)); - self.highlighted_ranges.clear(); - self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let matches = search.await.log_err()?; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.highlighted_ranges.clear(); - let mut matches = matches.into_iter().collect::>(); - matches - .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - for (buffer, buffer_matches) in matches { - let ranges_to_highlight = excerpts.push_excerpts_with_context_lines( - buffer, - buffer_matches.clone(), - 1, - cx, - ); - this.highlighted_ranges.extend(ranges_to_highlight); - } - }); - this.pending_search.take(); - this.active_query = Some(query); - cx.notify(); - }); - } - None - })); - cx.notify(); - } -} - -impl Item for ProjectFind { - type View = ProjectFindView; - - fn build_view( - model: ModelHandle, - workspace: &Workspace, - nav_history: ItemNavHistory, - cx: &mut gpui::ViewContext, - ) -> Self::View { - let settings = workspace.settings(); - let excerpts = model.read(cx).excerpts.clone(); - let results_editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer( - excerpts, - Some(workspace.project().clone()), - settings.clone(), - cx, - ); - editor.set_searchable(false); - editor.set_nav_history(Some(nav_history)); - editor - }); - cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) - .detach(); - cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) - .detach(); - - ProjectFindView { - model, - query_editor: cx.add_view(|cx| { - Editor::single_line( - settings.clone(), - Some(|theme| theme.find.editor.input.clone()), - cx, - ) - }), - results_editor, - case_sensitive: false, - whole_word: false, - regex: false, - query_contains_error: false, - settings, - } - } - - fn project_path(&self) -> Option { - None - } -} - -enum ViewEvent { - UpdateTab, -} - -impl Entity for ProjectFindView { - type Event = ViewEvent; -} - -impl View for ProjectFindView { - fn ui_name() -> &'static str { - "ProjectFindView" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let model = &self.model.read(cx); - let results = if model.highlighted_ranges.is_empty() { - let theme = &self.settings.borrow().theme; - let text = if self.query_editor.read(cx).text(cx).is_empty() { - "" - } else if model.pending_search.is_some() { - "Searching..." - } else { - "No results" - }; - Label::new(text.to_string(), theme.find.results_status.clone()) - .aligned() - .contained() - .with_background_color(theme.editor.background) - .flexible(1., true) - .boxed() - } else { - ChildView::new(&self.results_editor) - .flexible(1., true) - .boxed() - }; - - Flex::column() - .with_child(self.render_query_editor(cx)) - .with_child(results) - .boxed() - } - - fn on_focus(&mut self, cx: &mut ViewContext) { - if self.model.read(cx).highlighted_ranges.is_empty() { - cx.focus(&self.query_editor); - } else { - cx.focus(&self.results_editor); - } - } -} - -impl ItemView for ProjectFindView { - fn act_as_type( - &self, - type_id: TypeId, - self_handle: &ViewHandle, - _: &gpui::AppContext, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.into()) - } else if type_id == TypeId::of::() { - Some((&self.results_editor).into()) - } else { - None - } - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - self.results_editor - .update(cx, |editor, cx| editor.deactivated(cx)); - } - - fn item(&self, _: &gpui::AppContext) -> Box { - Box::new(self.model.clone()) - } - - fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { - let settings = self.settings.borrow(); - let find_theme = &settings.theme.find; - Flex::row() - .with_child( - Svg::new("icons/magnifier.svg") - .with_color(style.label.text.color) - .constrained() - .with_width(find_theme.tab_icon_width) - .aligned() - .boxed(), - ) - .with_children(self.model.read(cx).active_query.as_ref().map(|query| { - Label::new(query.as_str().to_string(), style.label.clone()) - .aligned() - .contained() - .with_margin_left(find_theme.tab_icon_spacing) - .boxed() - })) - .boxed() - } - - fn project_path(&self, _: &gpui::AppContext) -> Option { - None - } - - fn can_save(&self, _: &gpui::AppContext) -> bool { - true - } - - fn is_dirty(&self, cx: &AppContext) -> bool { - self.results_editor.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &AppContext) -> bool { - self.results_editor.read(cx).has_conflict(cx) - } - - fn save( - &mut self, - project: ModelHandle, - cx: &mut ViewContext, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.save(project, cx)) - } - - fn can_save_as(&self, _: &gpui::AppContext) -> bool { - false - } - - fn save_as( - &mut self, - _: ModelHandle, - _: PathBuf, - _: &mut ViewContext, - ) -> Task> { - unreachable!("save_as should not have been called") - } - - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option - where - Self: Sized, - { - let query_editor = cx.add_view(|cx| { - let query = self.query_editor.read(cx).text(cx); - let editor = Editor::single_line( - self.settings.clone(), - Some(|theme| theme.find.editor.input.clone()), - cx, - ); - editor - .buffer() - .update(cx, |buffer, cx| buffer.edit([0..0], query, cx)); - editor - }); - let model = self - .model - .update(cx, |model, cx| cx.add_model(|cx| model.clone(cx))); - - cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) - .detach(); - let results_editor = cx.add_view(|cx| { - let model = model.read(cx); - let excerpts = model.excerpts.clone(); - let project = model.project.clone(); - let scroll_position = self - .results_editor - .update(cx, |editor, cx| editor.scroll_position(cx)); - - let mut editor = Editor::for_buffer(excerpts, Some(project), self.settings.clone(), cx); - editor.set_searchable(false); - editor.set_nav_history(Some(nav_history)); - editor.set_scroll_position(scroll_position, cx); - editor - }); - let mut view = Self { - model, - query_editor, - results_editor, - case_sensitive: self.case_sensitive, - whole_word: self.whole_word, - regex: self.regex, - query_contains_error: self.query_contains_error, - settings: self.settings.clone(), - }; - view.model_changed(false, cx); - Some(view) - } - - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { - self.results_editor - .update(cx, |editor, cx| editor.navigate(data, cx)); - } - - fn should_update_tab_on_event(event: &ViewEvent) -> bool { - matches!(event, ViewEvent::UpdateTab) - } -} - -impl ProjectFindView { - fn deploy( - workspace: &mut Workspace, - &Deploy(activate_existing): &Deploy, - cx: &mut ViewContext, - ) { - if activate_existing { - if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, cx); - return; - } - } - let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); - workspace.open_item(model, cx); - } - - fn search(&mut self, _: &Search, cx: &mut ViewContext) { - let text = self.query_editor.read(cx).text(cx); - let query = if self.regex { - match SearchQuery::regex(text, self.whole_word, self.case_sensitive) { - Ok(query) => query, - Err(_) => { - self.query_contains_error = true; - cx.notify(); - return; - } - } - } else { - SearchQuery::text(text, self.whole_word, self.case_sensitive) - }; - - self.model.update(cx, |model, cx| model.search(query, cx)); - } - - fn toggle_search_option( - &mut self, - ToggleSearchOption(option): &ToggleSearchOption, - cx: &mut ViewContext, - ) { - let value = match option { - SearchOption::WholeWord => &mut self.whole_word, - SearchOption::CaseSensitive => &mut self.case_sensitive, - SearchOption::Regex => &mut self.regex, - }; - *value = !*value; - self.search(&Search, cx); - cx.notify(); - } - - fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { - if self.query_editor.is_focused(cx) { - if !self.model.read(cx).highlighted_ranges.is_empty() { - cx.focus(&self.results_editor); - } - } else { - cx.focus(&self.query_editor); - } - } - - fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { - let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); - if !highlighted_ranges.is_empty() { - let theme = &self.settings.borrow().theme.find; - self.results_editor.update(cx, |editor, cx| { - editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); - if reset_selections { - editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); - } - }); - if self.query_editor.is_focused(cx) { - cx.focus(&self.results_editor); - } - } - - cx.emit(ViewEvent::UpdateTab); - cx.notify(); - } - - fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &self.settings.borrow().theme; - let editor_container = if self.query_contains_error { - theme.find.invalid_editor - } else { - theme.find.editor.input.container - }; - Flex::row() - .with_child( - ChildView::new(&self.query_editor) - .contained() - .with_style(editor_container) - .aligned() - .constrained() - .with_max_width(theme.find.editor.max_width) - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) - .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) - .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) - .contained() - .with_style(theme.find.option_button_group) - .aligned() - .boxed(), - ) - .contained() - .with_style(theme.find.container) - .constrained() - .with_height(theme.workspace.toolbar.height) - .named("find bar") - } - - fn render_option_button( - &self, - icon: &str, - option: SearchOption, - cx: &mut RenderContext, - ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; - let is_active = self.is_option_enabled(option); - MouseEventHandler::new::(option as usize, cx, |state, _| { - let style = match (is_active, state.hovered) { - (false, false) => &theme.option_button, - (false, true) => &theme.hovered_option_button, - (true, false) => &theme.active_option_button, - (true, true) => &theme.active_hovered_option_button, - }; - Label::new(icon.to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) - .with_cursor_style(CursorStyle::PointingHand) - .boxed() - } - - fn is_option_enabled(&self, option: SearchOption) -> bool { - match option { - SearchOption::WholeWord => self.whole_word, - SearchOption::CaseSensitive => self.case_sensitive, - SearchOption::Regex => self.regex, - } - } -} +use crate::SearchOption; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer}; +use gpui::{ + action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, + ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, +}; +use postage::watch; +use project::{search::SearchQuery, Project}; +use std::{ + any::{Any, TypeId}, + ops::Range, + path::PathBuf, +}; +use util::ResultExt as _; +use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; + +action!(Deploy); +action!(Search); +action!(SearchInNew); +action!(ToggleSearchOption, SearchOption); +action!(ToggleFocus); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), + Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), + Binding::new("cmd-shift-F", Deploy, Some("Workspace")), + Binding::new("enter", Search, Some("ProjectFindView")), + Binding::new("cmd-enter", SearchInNew, Some("ProjectFindView")), + ]); + cx.add_action(ProjectFindView::deploy); + cx.add_action(ProjectFindView::search); + cx.add_action(ProjectFindView::search_in_new); + cx.add_action(ProjectFindView::toggle_search_option); + cx.add_action(ProjectFindView::toggle_focus); +} + +struct ProjectFind { + project: ModelHandle, + excerpts: ModelHandle, + pending_search: Option>>, + highlighted_ranges: Vec>, + active_query: Option, +} + +struct ProjectFindView { + model: ModelHandle, + query_editor: ViewHandle, + results_editor: ViewHandle, + case_sensitive: bool, + whole_word: bool, + regex: bool, + query_contains_error: bool, + settings: watch::Receiver, +} + +impl Entity for ProjectFind { + type Event = (); +} + +impl ProjectFind { + fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + let replica_id = project.read(cx).replica_id(); + Self { + project, + excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), + pending_search: Default::default(), + highlighted_ranges: Default::default(), + active_query: None, + } + } + + fn clone(&self, new_cx: &mut ModelContext) -> Self { + Self { + project: self.project.clone(), + excerpts: self + .excerpts + .update(new_cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + pending_search: Default::default(), + highlighted_ranges: self.highlighted_ranges.clone(), + active_query: self.active_query.clone(), + } + } + + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + let search = self + .project + .update(cx, |project, cx| project.search(query.clone(), cx)); + self.active_query = Some(query); + self.highlighted_ranges.clear(); + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let matches = search.await.log_err()?; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.highlighted_ranges.clear(); + let mut matches = matches.into_iter().collect::>(); + matches + .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + for (buffer, buffer_matches) in matches { + let ranges_to_highlight = excerpts.push_excerpts_with_context_lines( + buffer, + buffer_matches.clone(), + 1, + cx, + ); + this.highlighted_ranges.extend(ranges_to_highlight); + } + }); + this.pending_search.take(); + cx.notify(); + }); + } + None + })); + cx.notify(); + } +} + +impl Item for ProjectFind { + type View = ProjectFindView; + + fn build_view( + model: ModelHandle, + workspace: &Workspace, + nav_history: ItemNavHistory, + cx: &mut gpui::ViewContext, + ) -> Self::View { + let settings = workspace.settings(); + let excerpts = model.read(cx).excerpts.clone(); + + let mut query_text = String::new(); + let mut regex = false; + let mut case_sensitive = false; + let mut whole_word = false; + if let Some(active_query) = model.read(cx).active_query.as_ref() { + query_text = active_query.as_str().to_string(); + regex = active_query.is_regex(); + case_sensitive = active_query.case_sensitive(); + whole_word = active_query.whole_word(); + } + + let query_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + settings.clone(), + Some(|theme| theme.find.editor.input.clone()), + cx, + ); + editor.set_text(query_text, cx); + editor + }); + let results_editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer( + excerpts, + Some(workspace.project().clone()), + settings.clone(), + cx, + ); + editor.set_searchable(false); + editor.set_nav_history(Some(nav_history)); + editor + }); + cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) + .detach(); + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) + .detach(); + + ProjectFindView { + model, + query_editor, + results_editor, + case_sensitive, + whole_word, + regex, + query_contains_error: false, + settings, + } + } + + fn project_path(&self) -> Option { + None + } +} + +enum ViewEvent { + UpdateTab, +} + +impl Entity for ProjectFindView { + type Event = ViewEvent; +} + +impl View for ProjectFindView { + fn ui_name() -> &'static str { + "ProjectFindView" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let model = &self.model.read(cx); + let results = if model.highlighted_ranges.is_empty() { + let theme = &self.settings.borrow().theme; + let text = if self.query_editor.read(cx).text(cx).is_empty() { + "" + } else if model.pending_search.is_some() { + "Searching..." + } else { + "No results" + }; + Label::new(text.to_string(), theme.find.results_status.clone()) + .aligned() + .contained() + .with_background_color(theme.editor.background) + .flexible(1., true) + .boxed() + } else { + ChildView::new(&self.results_editor) + .flexible(1., true) + .boxed() + }; + + Flex::column() + .with_child(self.render_query_editor(cx)) + .with_child(results) + .boxed() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + if self.model.read(cx).highlighted_ranges.is_empty() { + cx.focus(&self.query_editor); + } else { + cx.focus(&self.results_editor); + } + } +} + +impl ItemView for ProjectFindView { + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &gpui::AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.into()) + } else if type_id == TypeId::of::() { + Some((&self.results_editor).into()) + } else { + None + } + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.deactivated(cx)); + } + + fn item(&self, _: &gpui::AppContext) -> Box { + Box::new(self.model.clone()) + } + + fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = self.settings.borrow(); + let find_theme = &settings.theme.find; + Flex::row() + .with_child( + Svg::new("icons/magnifier.svg") + .with_color(style.label.text.color) + .constrained() + .with_width(find_theme.tab_icon_width) + .aligned() + .boxed(), + ) + .with_children(self.model.read(cx).active_query.as_ref().map(|query| { + Label::new(query.as_str().to_string(), style.label.clone()) + .aligned() + .contained() + .with_margin_left(find_theme.tab_icon_spacing) + .boxed() + })) + .boxed() + } + + fn project_path(&self, _: &gpui::AppContext) -> Option { + None + } + + fn can_save(&self, _: &gpui::AppContext) -> bool { + true + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).has_conflict(cx) + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.save(project, cx)) + } + + fn can_save_as(&self, _: &gpui::AppContext) -> bool { + false + } + + fn save_as( + &mut self, + _: ModelHandle, + _: PathBuf, + _: &mut ViewContext, + ) -> Task> { + unreachable!("save_as should not have been called") + } + + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option + where + Self: Sized, + { + let query_editor = cx.add_view(|cx| { + let query = self.query_editor.read(cx).text(cx); + let editor = Editor::single_line( + self.settings.clone(), + Some(|theme| theme.find.editor.input.clone()), + cx, + ); + editor + .buffer() + .update(cx, |buffer, cx| buffer.edit([0..0], query, cx)); + editor + }); + let model = self + .model + .update(cx, |model, cx| cx.add_model(|cx| model.clone(cx))); + + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) + .detach(); + let results_editor = cx.add_view(|cx| { + let model = model.read(cx); + let excerpts = model.excerpts.clone(); + let project = model.project.clone(); + let scroll_position = self + .results_editor + .update(cx, |editor, cx| editor.scroll_position(cx)); + + let mut editor = Editor::for_buffer(excerpts, Some(project), self.settings.clone(), cx); + editor.set_searchable(false); + editor.set_nav_history(Some(nav_history)); + editor.set_scroll_position(scroll_position, cx); + editor + }); + let mut view = Self { + model, + query_editor, + results_editor, + case_sensitive: self.case_sensitive, + whole_word: self.whole_word, + regex: self.regex, + query_contains_error: self.query_contains_error, + settings: self.settings.clone(), + }; + view.model_changed(false, cx); + Some(view) + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.navigate(data, cx)); + } + + fn should_update_tab_on_event(event: &ViewEvent) -> bool { + matches!(event, ViewEvent::UpdateTab) + } +} + +impl ProjectFindView { + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_item(&existing, cx); + } else { + let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); + workspace.open_item(model, cx); + } + } + + fn search(&mut self, _: &Search, cx: &mut ViewContext) { + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); + } + } + + fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { + if let Some(find_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let new_query = find_view.update(cx, |find_view, cx| { + let new_query = find_view.build_search_query(cx); + if new_query.is_some() { + if let Some(old_query) = find_view.model.read(cx).active_query.clone() { + find_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), cx); + }); + find_view.regex = old_query.is_regex(); + find_view.whole_word = old_query.whole_word(); + find_view.case_sensitive = old_query.case_sensitive(); + } + } + new_query + }); + if let Some(new_query) = new_query { + let model = cx.add_model(|cx| { + let mut model = ProjectFind::new(workspace.project().clone(), cx); + model.search(new_query, cx); + model + }); + workspace.open_item(model, cx); + } + } + } + + fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { + let text = self.query_editor.read(cx).text(cx); + if self.regex { + match SearchQuery::regex(text, self.whole_word, self.case_sensitive) { + Ok(query) => Some(query), + Err(_) => { + self.query_contains_error = true; + cx.notify(); + None + } + } + } else { + Some(SearchQuery::text( + text, + self.whole_word, + self.case_sensitive, + )) + } + } + + fn toggle_search_option( + &mut self, + ToggleSearchOption(option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + let value = match option { + SearchOption::WholeWord => &mut self.whole_word, + SearchOption::CaseSensitive => &mut self.case_sensitive, + SearchOption::Regex => &mut self.regex, + }; + *value = !*value; + self.search(&Search, cx); + cx.notify(); + } + + fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { + if self.query_editor.is_focused(cx) { + if !self.model.read(cx).highlighted_ranges.is_empty() { + cx.focus(&self.results_editor); + } + } else { + cx.focus(&self.query_editor); + } + } + + fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { + let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); + if !highlighted_ranges.is_empty() { + let theme = &self.settings.borrow().theme.find; + self.results_editor.update(cx, |editor, cx| { + editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); + if reset_selections { + editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + } + }); + if self.query_editor.is_focused(cx) { + cx.focus(&self.results_editor); + } + } + + cx.emit(ViewEvent::UpdateTab); + cx.notify(); + } + + fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let editor_container = if self.query_contains_error { + theme.find.invalid_editor + } else { + theme.find.editor.input.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.find.editor.max_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.find.option_button_group) + .aligned() + .boxed(), + ) + .contained() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") + } + + fn render_option_button( + &self, + icon: &str, + option: SearchOption, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + let is_active = self.is_option_enabled(option); + MouseEventHandler::new::(option as usize, cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn is_option_enabled(&self, option: SearchOption) -> bool { + match option { + SearchOption::WholeWord => self.whole_word, + SearchOption::CaseSensitive => self.case_sensitive, + SearchOption::Regex => self.regex, + } + } +} diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3d26da982718f1372e0b17eac0b90bb77fb148c1..ddc6fa7c93d6d73d915c1a36a56c925a265a04f0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1345,6 +1345,13 @@ impl Buffer { let _ = language_server.latest_snapshot.blocking_send(snapshot); } + pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option + where + T: Into, + { + self.edit_internal([0..self.len()], text, false, cx) + } + pub fn edit( &mut self, ranges_iter: I, From e96d0a9355713c07bbc3ad8b298b2c8ab0ab04d7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Feb 2022 14:03:14 -0700 Subject: [PATCH 41/65] Activate the *newest* existing project find view on cmd-shift-F --- crates/find/src/project_find.rs | 5 ++++- crates/workspace/src/workspace.rs | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 293422486030b47aef296d0ca78fc54f9f33a13a..af2d1856ccdbdb97a7426a7032029ab5e492741f 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -386,7 +386,10 @@ impl ItemView for ProjectFindView { impl ProjectFindView { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - if let Some(existing) = workspace.item_of_type::(cx) { + if let Some(existing) = workspace + .items_of_type::(cx) + .max_by_key(|existing| existing.id()) + { workspace.activate_item(&existing, cx); } else { let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bff2d2f94616f7120a83dc8d8b061b675c7dc161..585695c520c757b3dddc851571a13c519373598e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -826,6 +826,15 @@ impl Workspace { .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) } + pub fn items_of_type<'a, T: Item>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.items + .iter() + .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) + } + pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } From 28b71cbc03b1e86981789f0ece8ad4d8a59642c4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Feb 2022 14:12:31 -0700 Subject: [PATCH 42/65] Select query when focusing query editor Also: Clear the selection when we focus the results editor because we continue to render the selection even when the editor isn't focused and it looks awkward. Another approach we could take is to not render selections for non-focused editors, either always or with an option. But considering that we select all anyways next time we return focus to the query editor, I think this is ok for now. --- crates/find/src/project_find.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index af2d1856ccdbdb97a7426a7032029ab5e492741f..2d0466a8b25974c601c57eda67d16f255b14ecf9 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,5 +1,5 @@ use crate::SearchOption; -use editor::{Anchor, Autoscroll, Editor, MultiBuffer}; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, @@ -230,7 +230,7 @@ impl View for ProjectFindView { if self.model.read(cx).highlighted_ranges.is_empty() { cx.focus(&self.query_editor); } else { - cx.focus(&self.results_editor); + self.focus_results_editor(cx); } } } @@ -471,13 +471,24 @@ impl ProjectFindView { fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { if self.query_editor.is_focused(cx) { if !self.model.read(cx).highlighted_ranges.is_empty() { - cx.focus(&self.results_editor); + self.focus_results_editor(cx); } } else { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&SelectAll, cx); + }); cx.focus(&self.query_editor); } } + fn focus_results_editor(&self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + let head = query_editor.newest_anchor_selection().head(); + query_editor.select_ranges([head.clone()..head], None, cx); + }); + cx.focus(&self.results_editor); + } + fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); if !highlighted_ranges.is_empty() { @@ -489,7 +500,7 @@ impl ProjectFindView { } }); if self.query_editor.is_focused(cx) { - cx.focus(&self.results_editor); + self.focus_results_editor(cx); } } From dd6f8d20a32a1064f676286092e1f3e4c1dc9a7f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 07:47:46 -0700 Subject: [PATCH 43/65] Remove carriage returns --- crates/editor/src/editor.rs | 17834 +++++++++++++++--------------- crates/find/src/project_find.rs | 1154 +- crates/project/src/search.rs | 454 +- 3 files changed, 9721 insertions(+), 9721 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1961438d0d4fd99fd7e59dcb03d894e4dde797fa..c6e5f2b026387202c72e7828bb38931b7cd88aee 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1,8917 +1,8917 @@ -pub mod display_map; -mod element; -pub mod items; -pub mod movement; -mod multi_buffer; - -#[cfg(test)] -mod test; - -use aho_corasick::AhoCorasick; -use anyhow::Result; -use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; -pub use display_map::DisplayPoint; -use display_map::*; -pub use element::*; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - action, - color::Color, - elements::*, - executor, - fonts::{self, HighlightStyle, TextStyle}, - geometry::vector::{vec2f, Vector2F}, - keymap::Binding, - platform::CursorStyle, - text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, - ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, - WeakViewHandle, -}; -use items::{BufferItemHandle, MultiBufferItemHandle}; -use itertools::Itertools as _; -pub use language::{char_kind, CharKind}; -use language::{ - AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, -}; -use multi_buffer::MultiBufferChunks; -pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, -}; -use ordered_float::OrderedFloat; -use postage::watch; -use project::{Project, ProjectTransaction}; -use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; -use smol::Timer; -use snippet::Snippet; -use std::{ - any::TypeId, - cmp::{self, Ordering, Reverse}, - iter::{self, FromIterator}, - mem, - ops::{Deref, DerefMut, Range, RangeInclusive, Sub}, - sync::Arc, - time::{Duration, Instant}, -}; -pub use sum_tree::Bias; -use text::rope::TextDimension; -use theme::DiagnosticStyle; -use util::{post_inc, ResultExt, TryFutureExt}; -use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace}; - -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const MAX_LINE_LEN: usize = 1024; -const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; - -action!(Cancel); -action!(Backspace); -action!(Delete); -action!(Input, String); -action!(Newline); -action!(Tab); -action!(Outdent); -action!(DeleteLine); -action!(DeleteToPreviousWordBoundary); -action!(DeleteToNextWordBoundary); -action!(DeleteToBeginningOfLine); -action!(DeleteToEndOfLine); -action!(CutToEndOfLine); -action!(DuplicateLine); -action!(MoveLineUp); -action!(MoveLineDown); -action!(Cut); -action!(Copy); -action!(Paste); -action!(Undo); -action!(Redo); -action!(MoveUp); -action!(MoveDown); -action!(MoveLeft); -action!(MoveRight); -action!(MoveToPreviousWordBoundary); -action!(MoveToNextWordBoundary); -action!(MoveToBeginningOfLine); -action!(MoveToEndOfLine); -action!(MoveToBeginning); -action!(MoveToEnd); -action!(SelectUp); -action!(SelectDown); -action!(SelectLeft); -action!(SelectRight); -action!(SelectToPreviousWordBoundary); -action!(SelectToNextWordBoundary); -action!(SelectToBeginningOfLine, bool); -action!(SelectToEndOfLine, bool); -action!(SelectToBeginning); -action!(SelectToEnd); -action!(SelectAll); -action!(SelectLine); -action!(SplitSelectionIntoLines); -action!(AddSelectionAbove); -action!(AddSelectionBelow); -action!(SelectNext, bool); -action!(ToggleComments); -action!(SelectLargerSyntaxNode); -action!(SelectSmallerSyntaxNode); -action!(MoveToEnclosingBracket); -action!(ShowNextDiagnostic); -action!(GoToDefinition); -action!(FindAllReferences); -action!(Rename); -action!(ConfirmRename); -action!(PageUp); -action!(PageDown); -action!(Fold); -action!(Unfold); -action!(FoldSelectedRanges); -action!(Scroll, Vector2F); -action!(Select, SelectPhase); -action!(ShowCompletions); -action!(ToggleCodeActions, bool); -action!(ConfirmCompletion, Option); -action!(ConfirmCodeAction, Option); -action!(OpenExcerpts); - -pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { - path_openers.push(Box::new(items::BufferOpener)); - cx.add_bindings(vec![ - Binding::new("escape", Cancel, Some("Editor")), - Binding::new("backspace", Backspace, Some("Editor")), - Binding::new("ctrl-h", Backspace, Some("Editor")), - Binding::new("delete", Delete, Some("Editor")), - Binding::new("ctrl-d", Delete, Some("Editor")), - Binding::new("enter", Newline, Some("Editor && mode == full")), - Binding::new( - "alt-enter", - Input("\n".into()), - Some("Editor && mode == auto_height"), - ), - Binding::new( - "enter", - ConfirmCompletion(None), - Some("Editor && showing_completions"), - ), - Binding::new( - "enter", - ConfirmCodeAction(None), - Some("Editor && showing_code_actions"), - ), - Binding::new("enter", ConfirmRename, Some("Editor && renaming")), - Binding::new("tab", Tab, Some("Editor")), - Binding::new( - "tab", - ConfirmCompletion(None), - Some("Editor && showing_completions"), - ), - Binding::new("shift-tab", Outdent, Some("Editor")), - Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), - Binding::new( - "alt-backspace", - DeleteToPreviousWordBoundary, - Some("Editor"), - ), - Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")), - Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")), - Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")), - Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")), - Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")), - Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")), - Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")), - Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")), - Binding::new("cmd-x", Cut, Some("Editor")), - Binding::new("cmd-c", Copy, Some("Editor")), - Binding::new("cmd-v", Paste, Some("Editor")), - Binding::new("cmd-z", Undo, Some("Editor")), - Binding::new("cmd-shift-Z", Redo, Some("Editor")), - Binding::new("up", MoveUp, Some("Editor")), - Binding::new("down", MoveDown, Some("Editor")), - Binding::new("left", MoveLeft, Some("Editor")), - Binding::new("right", MoveRight, Some("Editor")), - Binding::new("ctrl-p", MoveUp, Some("Editor")), - Binding::new("ctrl-n", MoveDown, Some("Editor")), - Binding::new("ctrl-b", MoveLeft, Some("Editor")), - Binding::new("ctrl-f", MoveRight, Some("Editor")), - Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")), - Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")), - Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")), - Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")), - Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")), - Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")), - Binding::new("cmd-up", MoveToBeginning, Some("Editor")), - Binding::new("cmd-down", MoveToEnd, Some("Editor")), - Binding::new("shift-up", SelectUp, Some("Editor")), - Binding::new("ctrl-shift-P", SelectUp, Some("Editor")), - Binding::new("shift-down", SelectDown, Some("Editor")), - Binding::new("ctrl-shift-N", SelectDown, Some("Editor")), - Binding::new("shift-left", SelectLeft, Some("Editor")), - Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")), - Binding::new("shift-right", SelectRight, Some("Editor")), - Binding::new("ctrl-shift-F", SelectRight, Some("Editor")), - Binding::new( - "alt-shift-left", - SelectToPreviousWordBoundary, - Some("Editor"), - ), - Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")), - Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")), - Binding::new( - "cmd-shift-left", - SelectToBeginningOfLine(true), - Some("Editor"), - ), - Binding::new( - "ctrl-shift-A", - SelectToBeginningOfLine(true), - Some("Editor"), - ), - Binding::new("cmd-shift-right", SelectToEndOfLine(true), Some("Editor")), - Binding::new("ctrl-shift-E", SelectToEndOfLine(true), Some("Editor")), - Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")), - Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")), - Binding::new("cmd-a", SelectAll, Some("Editor")), - Binding::new("cmd-l", SelectLine, Some("Editor")), - Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")), - Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")), - Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")), - Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")), - Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")), - Binding::new("cmd-d", SelectNext(false), Some("Editor")), - Binding::new("cmd-k cmd-d", SelectNext(true), Some("Editor")), - Binding::new("cmd-/", ToggleComments, Some("Editor")), - Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")), - Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), - Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), - Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), - Binding::new("f8", ShowNextDiagnostic, Some("Editor")), - Binding::new("f2", Rename, Some("Editor")), - Binding::new("f12", GoToDefinition, Some("Editor")), - Binding::new("alt-shift-f12", FindAllReferences, Some("Editor")), - Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")), - Binding::new("pageup", PageUp, Some("Editor")), - Binding::new("pagedown", PageDown, Some("Editor")), - Binding::new("alt-cmd-[", Fold, Some("Editor")), - Binding::new("alt-cmd-]", Unfold, Some("Editor")), - Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")), - Binding::new("ctrl-space", ShowCompletions, Some("Editor")), - Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")), - Binding::new("alt-enter", OpenExcerpts, Some("Editor")), - ]); - - cx.add_action(Editor::open_new); - cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); - cx.add_action(Editor::select); - cx.add_action(Editor::cancel); - cx.add_action(Editor::handle_input); - cx.add_action(Editor::newline); - cx.add_action(Editor::backspace); - cx.add_action(Editor::delete); - cx.add_action(Editor::tab); - cx.add_action(Editor::outdent); - cx.add_action(Editor::delete_line); - cx.add_action(Editor::delete_to_previous_word_boundary); - cx.add_action(Editor::delete_to_next_word_boundary); - cx.add_action(Editor::delete_to_beginning_of_line); - cx.add_action(Editor::delete_to_end_of_line); - cx.add_action(Editor::cut_to_end_of_line); - cx.add_action(Editor::duplicate_line); - cx.add_action(Editor::move_line_up); - cx.add_action(Editor::move_line_down); - cx.add_action(Editor::cut); - cx.add_action(Editor::copy); - cx.add_action(Editor::paste); - cx.add_action(Editor::undo); - cx.add_action(Editor::redo); - cx.add_action(Editor::move_up); - cx.add_action(Editor::move_down); - cx.add_action(Editor::move_left); - cx.add_action(Editor::move_right); - cx.add_action(Editor::move_to_previous_word_boundary); - cx.add_action(Editor::move_to_next_word_boundary); - cx.add_action(Editor::move_to_beginning_of_line); - cx.add_action(Editor::move_to_end_of_line); - cx.add_action(Editor::move_to_beginning); - cx.add_action(Editor::move_to_end); - cx.add_action(Editor::select_up); - cx.add_action(Editor::select_down); - cx.add_action(Editor::select_left); - cx.add_action(Editor::select_right); - cx.add_action(Editor::select_to_previous_word_boundary); - cx.add_action(Editor::select_to_next_word_boundary); - cx.add_action(Editor::select_to_beginning_of_line); - cx.add_action(Editor::select_to_end_of_line); - cx.add_action(Editor::select_to_beginning); - cx.add_action(Editor::select_to_end); - cx.add_action(Editor::select_all); - cx.add_action(Editor::select_line); - cx.add_action(Editor::split_selection_into_lines); - cx.add_action(Editor::add_selection_above); - cx.add_action(Editor::add_selection_below); - cx.add_action(Editor::select_next); - cx.add_action(Editor::toggle_comments); - cx.add_action(Editor::select_larger_syntax_node); - cx.add_action(Editor::select_smaller_syntax_node); - cx.add_action(Editor::move_to_enclosing_bracket); - cx.add_action(Editor::show_next_diagnostic); - cx.add_action(Editor::go_to_definition); - cx.add_action(Editor::page_up); - cx.add_action(Editor::page_down); - cx.add_action(Editor::fold); - cx.add_action(Editor::unfold); - cx.add_action(Editor::fold_selected_ranges); - cx.add_action(Editor::show_completions); - cx.add_action(Editor::toggle_code_actions); - cx.add_action(Editor::open_excerpts); - cx.add_async_action(Editor::confirm_completion); - cx.add_async_action(Editor::confirm_code_action); - cx.add_async_action(Editor::rename); - cx.add_async_action(Editor::confirm_rename); - cx.add_async_action(Editor::find_all_references); -} - -trait SelectionExt { - fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; - fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; - fn display_range(&self, map: &DisplaySnapshot) -> Range; - fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) - -> Range; -} - -trait InvalidationRegion { - fn ranges(&self) -> &[Range]; -} - -#[derive(Clone, Debug)] -pub enum SelectPhase { - Begin { - position: DisplayPoint, - add: bool, - click_count: usize, - }, - BeginColumnar { - position: DisplayPoint, - overshoot: u32, - }, - Extend { - position: DisplayPoint, - click_count: usize, - }, - Update { - position: DisplayPoint, - overshoot: u32, - scroll_position: Vector2F, - }, - End, -} - -#[derive(Clone, Debug)] -pub enum SelectMode { - Character, - Word(Range), - Line(Range), - All, -} - -#[derive(PartialEq, Eq)] -pub enum Autoscroll { - Fit, - Center, - Newest, -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum EditorMode { - SingleLine, - AutoHeight { max_lines: usize }, - Full, -} - -#[derive(Clone)] -pub enum SoftWrap { - None, - EditorWidth, - Column(u32), -} - -#[derive(Clone)] -pub struct EditorStyle { - pub text: TextStyle, - pub placeholder_text: Option, - pub theme: theme::Editor, -} - -type CompletionId = usize; - -pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor; - -pub struct Editor { - handle: WeakViewHandle, - buffer: ModelHandle, - display_map: ModelHandle, - next_selection_id: usize, - selections: Arc<[Selection]>, - pending_selection: Option, - columnar_selection_tail: Option, - add_selections_state: Option, - select_next_state: Option, - selection_history: - HashMap]>, Option]>>)>, - autoclose_stack: InvalidationStack, - snippet_stack: InvalidationStack, - select_larger_syntax_node_stack: Vec]>>, - active_diagnostics: Option, - scroll_position: Vector2F, - scroll_top_anchor: Option, - autoscroll_request: Option, - settings: watch::Receiver, - soft_wrap_mode_override: Option, - get_field_editor_theme: Option, - project: Option>, - focused: bool, - show_local_cursors: bool, - blink_epoch: usize, - blinking_paused: bool, - mode: EditorMode, - vertical_scroll_margin: f32, - placeholder_text: Option>, - highlighted_rows: Option>, - highlighted_ranges: BTreeMap>)>, - nav_history: Option, - context_menu: Option, - completion_tasks: Vec<(CompletionId, Task>)>, - next_completion_id: CompletionId, - available_code_actions: Option<(ModelHandle, Arc<[CodeAction]>)>, - code_actions_task: Option>, - document_highlights_task: Option>, - pending_rename: Option, - searchable: bool, -} - -pub struct EditorSnapshot { - pub mode: EditorMode, - pub display_snapshot: DisplaySnapshot, - pub placeholder_text: Option>, - is_focused: bool, - scroll_position: Vector2F, - scroll_top_anchor: Option, -} - -#[derive(Clone)] -pub struct PendingSelection { - selection: Selection, - mode: SelectMode, -} - -struct AddSelectionsState { - above: bool, - stack: Vec, -} - -struct SelectNextState { - query: AhoCorasick, - wordwise: bool, - done: bool, -} - -struct BracketPairState { - ranges: Vec>, - pair: BracketPair, -} - -struct SnippetState { - ranges: Vec>>, - active_index: usize, -} - -pub struct RenameState { - pub range: Range, - pub old_name: String, - pub editor: ViewHandle, - block_id: BlockId, -} - -struct InvalidationStack(Vec); - -enum ContextMenu { - Completions(CompletionsMenu), - CodeActions(CodeActionsMenu), -} - -impl ContextMenu { - fn select_prev(&mut self, cx: &mut ViewContext) -> bool { - if self.visible() { - match self { - ContextMenu::Completions(menu) => menu.select_prev(cx), - ContextMenu::CodeActions(menu) => menu.select_prev(cx), - } - true - } else { - false - } - } - - fn select_next(&mut self, cx: &mut ViewContext) -> bool { - if self.visible() { - match self { - ContextMenu::Completions(menu) => menu.select_next(cx), - ContextMenu::CodeActions(menu) => menu.select_next(cx), - } - true - } else { - false - } - } - - fn visible(&self) -> bool { - match self { - ContextMenu::Completions(menu) => menu.visible(), - ContextMenu::CodeActions(menu) => menu.visible(), - } - } - - fn render( - &self, - cursor_position: DisplayPoint, - style: EditorStyle, - cx: &AppContext, - ) -> (DisplayPoint, ElementBox) { - match self { - ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), - ContextMenu::CodeActions(menu) => menu.render(cursor_position, style), - } - } -} - -struct CompletionsMenu { - id: CompletionId, - initial_position: Anchor, - buffer: ModelHandle, - completions: Arc<[Completion]>, - match_candidates: Vec, - matches: Arc<[StringMatch]>, - selected_item: usize, - list: UniformListState, -} - -impl CompletionsMenu { - fn select_prev(&mut self, cx: &mut ViewContext) { - if self.selected_item > 0 { - self.selected_item -= 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - } - cx.notify(); - } - - fn select_next(&mut self, cx: &mut ViewContext) { - if self.selected_item + 1 < self.matches.len() { - self.selected_item += 1; - self.list.scroll_to(ScrollTarget::Show(self.selected_item)); - } - cx.notify(); - } - - fn visible(&self) -> bool { - !self.matches.is_empty() - } - - fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox { - enum CompletionTag {} - - let completions = self.completions.clone(); - let matches = self.matches.clone(); - let selected_item = self.selected_item; - let container_style = style.autocomplete.container; - UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| { - let start_ix = range.start; - for (ix, mat) in matches[range].iter().enumerate() { - let completion = &completions[mat.candidate_id]; - let item_ix = start_ix + ix; - items.push( - MouseEventHandler::new::( - mat.candidate_id, - cx, - |state, _| { - let item_style = if item_ix == selected_item { - style.autocomplete.selected_item - } else if state.hovered { - style.autocomplete.hovered_item - } else { - style.autocomplete.item - }; - - Text::new(completion.label.text.clone(), style.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &completion.label.text, - style.text.color.into(), - styled_runs_for_code_label( - &completion.label, - style.text.color, - &style.syntax, - ), - &mat.positions, - )) - .contained() - .with_style(item_style) - .boxed() - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { - cx.dispatch_action(ConfirmCompletion(Some(item_ix))); - }) - .boxed(), - ); - } - }) - .with_width_from_item( - self.matches - .iter() - .enumerate() - .max_by_key(|(_, mat)| { - self.completions[mat.candidate_id] - .label - .text - .chars() - .count() - }) - .map(|(ix, _)| ix), - ) - .contained() - .with_style(container_style) - .boxed() - } - - pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { - let mut matches = if let Some(query) = query { - fuzzy::match_strings( - &self.match_candidates, - query, - false, - 100, - &Default::default(), - executor, - ) - .await - } else { - self.match_candidates - .iter() - .enumerate() - .map(|(candidate_id, candidate)| StringMatch { - candidate_id, - score: Default::default(), - positions: Default::default(), - string: candidate.string.clone(), - }) - .collect() - }; - matches.sort_unstable_by_key(|mat| { - ( - Reverse(OrderedFloat(mat.score)), - self.completions[mat.candidate_id].sort_key(), - ) - }); - - for mat in &mut matches { - let filter_start = self.completions[mat.candidate_id].label.filter_range.start; - for position in &mut mat.positions { - *position += filter_start; - } - } - - self.matches = matches.into(); - } -} - -#[derive(Clone)] -struct CodeActionsMenu { - actions: Arc<[CodeAction]>, - buffer: ModelHandle, - selected_item: usize, - list: UniformListState, - deployed_from_indicator: bool, -} - -impl CodeActionsMenu { - fn select_prev(&mut self, cx: &mut ViewContext) { - if self.selected_item > 0 { - self.selected_item -= 1; - cx.notify() - } - } - - fn select_next(&mut self, cx: &mut ViewContext) { - if self.selected_item + 1 < self.actions.len() { - self.selected_item += 1; - cx.notify() - } - } - - fn visible(&self) -> bool { - !self.actions.is_empty() - } - - fn render( - &self, - mut cursor_position: DisplayPoint, - style: EditorStyle, - ) -> (DisplayPoint, ElementBox) { - enum ActionTag {} - - let container_style = style.autocomplete.container; - let actions = self.actions.clone(); - let selected_item = self.selected_item; - let element = - UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| { - let start_ix = range.start; - for (ix, action) in actions[range].iter().enumerate() { - let item_ix = start_ix + ix; - items.push( - MouseEventHandler::new::(item_ix, cx, |state, _| { - let item_style = if item_ix == selected_item { - style.autocomplete.selected_item - } else if state.hovered { - style.autocomplete.hovered_item - } else { - style.autocomplete.item - }; - - Text::new(action.lsp_action.title.clone(), style.text.clone()) - .with_soft_wrap(false) - .contained() - .with_style(item_style) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_mouse_down(move |cx| { - cx.dispatch_action(ConfirmCodeAction(Some(item_ix))); - }) - .boxed(), - ); - } - }) - .with_width_from_item( - self.actions - .iter() - .enumerate() - .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) - .map(|(ix, _)| ix), - ) - .contained() - .with_style(container_style) - .boxed(); - - if self.deployed_from_indicator { - *cursor_position.column_mut() = 0; - } - - (cursor_position, element) - } -} - -#[derive(Debug)] -struct ActiveDiagnosticGroup { - primary_range: Range, - primary_message: String, - blocks: HashMap, - is_valid: bool, -} - -#[derive(Serialize, Deserialize)] -struct ClipboardSelection { - len: usize, - is_entire_line: bool, -} - -pub struct NavigationData { - anchor: Anchor, - offset: usize, -} - -impl Editor { - pub fn single_line( - settings: watch::Receiver, - field_editor_style: Option, - cx: &mut ViewContext, - ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine, - buffer, - None, - settings, - field_editor_style, - cx, - ) - } - - pub fn auto_height( - max_lines: usize, - settings: watch::Receiver, - field_editor_style: Option, - cx: &mut ViewContext, - ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::AutoHeight { max_lines }, - buffer, - None, - settings, - field_editor_style, - cx, - ) - } - - pub fn for_buffer( - buffer: ModelHandle, - project: Option>, - settings: watch::Receiver, - cx: &mut ViewContext, - ) -> Self { - Self::new(EditorMode::Full, buffer, project, settings, None, cx) - } - - pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { - let mut clone = Self::new( - self.mode, - self.buffer.clone(), - self.project.clone(), - self.settings.clone(), - self.get_field_editor_theme, - cx, - ); - clone.scroll_position = self.scroll_position; - clone.scroll_top_anchor = self.scroll_top_anchor.clone(); - clone.nav_history = Some(nav_history); - clone - } - - fn new( - mode: EditorMode, - buffer: ModelHandle, - project: Option>, - settings: watch::Receiver, - get_field_editor_theme: Option, - cx: &mut ViewContext, - ) -> Self { - let display_map = cx.add_model(|cx| { - let settings = settings.borrow(); - let style = build_style(&*settings, get_field_editor_theme, cx); - DisplayMap::new( - buffer.clone(), - settings.tab_size, - style.text.font_id, - style.text.font_size, - None, - 2, - 1, - cx, - ) - }); - cx.observe(&buffer, Self::on_buffer_changed).detach(); - cx.subscribe(&buffer, Self::on_buffer_event).detach(); - cx.observe(&display_map, Self::on_display_map_changed) - .detach(); - - let mut this = Self { - handle: cx.weak_handle(), - buffer, - display_map, - selections: Arc::from([]), - pending_selection: Some(PendingSelection { - selection: Selection { - id: 0, - start: Anchor::min(), - end: Anchor::min(), - reversed: false, - goal: SelectionGoal::None, - }, - mode: SelectMode::Character, - }), - columnar_selection_tail: None, - next_selection_id: 1, - add_selections_state: None, - select_next_state: None, - selection_history: Default::default(), - autoclose_stack: Default::default(), - snippet_stack: Default::default(), - select_larger_syntax_node_stack: Vec::new(), - active_diagnostics: None, - settings, - soft_wrap_mode_override: None, - get_field_editor_theme, - project, - scroll_position: Vector2F::zero(), - scroll_top_anchor: None, - autoscroll_request: None, - focused: false, - show_local_cursors: false, - blink_epoch: 0, - blinking_paused: false, - mode, - vertical_scroll_margin: 3.0, - placeholder_text: None, - highlighted_rows: None, - highlighted_ranges: Default::default(), - nav_history: None, - context_menu: None, - completion_tasks: Default::default(), - next_completion_id: 0, - available_code_actions: Default::default(), - code_actions_task: Default::default(), - document_highlights_task: Default::default(), - pending_rename: Default::default(), - searchable: true, - }; - this.end_selection(cx); - this - } - - pub fn open_new( - workspace: &mut Workspace, - _: &workspace::OpenNew, - cx: &mut ViewContext, - ) { - let buffer = cx - .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx)); - workspace.open_item(BufferItemHandle(buffer), cx); - } - - pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { - self.buffer.read(cx).replica_id() - } - - pub fn buffer(&self) -> &ModelHandle { - &self.buffer - } - - pub fn title(&self, cx: &AppContext) -> String { - self.buffer().read(cx).title(cx) - } - - pub fn snapshot(&mut self, cx: &mut MutableAppContext) -> EditorSnapshot { - EditorSnapshot { - mode: self.mode, - display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), - scroll_position: self.scroll_position, - scroll_top_anchor: self.scroll_top_anchor.clone(), - placeholder_text: self.placeholder_text.clone(), - is_focused: self - .handle - .upgrade(cx) - .map_or(false, |handle| handle.is_focused(cx)), - } - } - - pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { - self.buffer.read(cx).language(cx) - } - - fn style(&self, cx: &AppContext) -> EditorStyle { - build_style(&*self.settings.borrow(), self.get_field_editor_theme, cx) - } - - pub fn set_placeholder_text( - &mut self, - placeholder_text: impl Into>, - cx: &mut ViewContext, - ) { - self.placeholder_text = Some(placeholder_text.into()); - cx.notify(); - } - - pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext) { - self.vertical_scroll_margin = margin_rows as f32; - cx.notify(); - } - - pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { - let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if scroll_position.y() == 0. { - self.scroll_top_anchor = None; - self.scroll_position = scroll_position; - } else { - let scroll_top_buffer_offset = - DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); - let anchor = map - .buffer_snapshot - .anchor_at(scroll_top_buffer_offset, Bias::Right); - self.scroll_position = vec2f( - scroll_position.x(), - scroll_position.y() - anchor.to_display_point(&map).row() as f32, - ); - self.scroll_top_anchor = Some(anchor); - } - - cx.notify(); - } - - pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) - } - - pub fn clamp_scroll_left(&mut self, max: f32) -> bool { - if max < self.scroll_position.x() { - self.scroll_position.set_x(max); - true - } else { - false - } - } - - pub fn autoscroll_vertically( - &mut self, - viewport_height: f32, - line_height: f32, - cx: &mut ViewContext, - ) -> bool { - let visible_lines = viewport_height / line_height; - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut scroll_position = - compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor); - let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { - (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) - } else { - display_map.max_point().row().saturating_sub(1) as f32 - }; - if scroll_position.y() > max_scroll_top { - scroll_position.set_y(max_scroll_top); - self.set_scroll_position(scroll_position, cx); - } - - let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() { - autoscroll - } else { - return false; - }; - - let first_cursor_top; - let last_cursor_bottom; - if let Some(highlighted_rows) = &self.highlighted_rows { - first_cursor_top = highlighted_rows.start as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else if autoscroll == Autoscroll::Newest { - let newest_selection = self.newest_selection::(&display_map.buffer_snapshot); - first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else { - let selections = self.local_selections::(cx); - first_cursor_top = selections - .first() - .unwrap() - .head() - .to_display_point(&display_map) - .row() as f32; - last_cursor_bottom = selections - .last() - .unwrap() - .head() - .to_display_point(&display_map) - .row() as f32 - + 1.0; - } - - let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { - 0. - } else { - ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() - }; - if margin < 0.0 { - return false; - } - - match autoscroll { - Autoscroll::Fit | Autoscroll::Newest => { - let margin = margin.min(self.vertical_scroll_margin); - let target_top = (first_cursor_top - margin).max(0.0); - let target_bottom = last_cursor_bottom + margin; - let start_row = scroll_position.y(); - let end_row = start_row + visible_lines; - - if target_top < start_row { - scroll_position.set_y(target_top); - self.set_scroll_position(scroll_position, cx); - } else if target_bottom >= end_row { - scroll_position.set_y(target_bottom - visible_lines); - self.set_scroll_position(scroll_position, cx); - } - } - Autoscroll::Center => { - scroll_position.set_y((first_cursor_top - margin).max(0.0)); - self.set_scroll_position(scroll_position, cx); - } - } - - true - } - - pub fn autoscroll_horizontally( - &mut self, - start_row: u32, - viewport_width: f32, - scroll_width: f32, - max_glyph_width: f32, - layouts: &[text_layout::Line], - cx: &mut ViewContext, - ) -> bool { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.local_selections::(cx); - - let mut target_left; - let mut target_right; - - if self.highlighted_rows.is_some() { - target_left = 0.0_f32; - target_right = 0.0_f32; - } else { - target_left = std::f32::INFINITY; - target_right = 0.0_f32; - for selection in selections { - let head = selection.head().to_display_point(&display_map); - if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { - let start_column = head.column().saturating_sub(3); - let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); - target_left = target_left.min( - layouts[(head.row() - start_row) as usize] - .x_for_index(start_column as usize), - ); - target_right = target_right.max( - layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) - + max_glyph_width, - ); - } - } - } - - target_right = target_right.min(scroll_width); - - if target_right - target_left > viewport_width { - return false; - } - - let scroll_left = self.scroll_position.x() * max_glyph_width; - let scroll_right = scroll_left + viewport_width; - - if target_left < scroll_left { - self.scroll_position.set_x(target_left / max_glyph_width); - true - } else if target_right > scroll_right { - self.scroll_position - .set_x((target_right - viewport_width) / max_glyph_width); - true - } else { - false - } - } - - fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { - self.hide_context_menu(cx); - - match phase { - SelectPhase::Begin { - position, - add, - click_count, - } => self.begin_selection(*position, *add, *click_count, cx), - SelectPhase::BeginColumnar { - position, - overshoot, - } => self.begin_columnar_selection(*position, *overshoot, cx), - SelectPhase::Extend { - position, - click_count, - } => self.extend_selection(*position, *click_count, cx), - SelectPhase::Update { - position, - overshoot, - scroll_position, - } => self.update_selection(*position, *overshoot, *scroll_position, cx), - SelectPhase::End => self.end_selection(cx), - } - } - - fn extend_selection( - &mut self, - position: DisplayPoint, - click_count: usize, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self - .newest_selection::(&display_map.buffer_snapshot) - .tail(); - self.begin_selection(position, false, click_count, cx); - - let position = position.to_offset(&display_map, Bias::Left); - let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); - let mut pending = self.pending_selection.clone().unwrap(); - - if position >= tail { - pending.selection.start = tail_anchor.clone(); - } else { - pending.selection.end = tail_anchor.clone(); - pending.selection.reversed = true; - } - - match &mut pending.mode { - SelectMode::Word(range) | SelectMode::Line(range) => { - *range = tail_anchor.clone()..tail_anchor - } - _ => {} - } - - self.set_selections(self.selections.clone(), Some(pending), cx); - } - - fn begin_selection( - &mut self, - position: DisplayPoint, - add: bool, - click_count: usize, - cx: &mut ViewContext, - ) { - if !self.focused { - cx.focus_self(); - cx.emit(Event::Activate); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let newest_selection = self.newest_anchor_selection().clone(); - - let start; - let end; - let mode; - match click_count { - 1 => { - start = buffer.anchor_before(position.to_point(&display_map)); - end = start.clone(); - mode = SelectMode::Character; - } - 2 => { - let range = movement::surrounding_word(&display_map, position); - start = buffer.anchor_before(range.start.to_point(&display_map)); - end = buffer.anchor_before(range.end.to_point(&display_map)); - mode = SelectMode::Word(start.clone()..end.clone()); - } - 3 => { - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - start = buffer.anchor_before(line_start); - end = buffer.anchor_before(next_line_start); - mode = SelectMode::Line(start.clone()..end.clone()); - } - _ => { - start = buffer.anchor_before(0); - end = buffer.anchor_before(buffer.len()); - mode = SelectMode::All; - } - } - - self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); - - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start, - end, - reversed: false, - goal: SelectionGoal::None, - }; - - let mut selections; - if add { - selections = self.selections.clone(); - // Remove the newest selection if it was added due to a previous mouse up - // within this multi-click. - if click_count > 1 { - selections = self - .selections - .iter() - .filter(|selection| selection.id != newest_selection.id) - .cloned() - .collect(); - } - } else { - selections = Arc::from([]); - } - self.set_selections(selections, Some(PendingSelection { selection, mode }), cx); - - cx.notify(); - } - - fn begin_columnar_selection( - &mut self, - position: DisplayPoint, - overshoot: u32, - cx: &mut ViewContext, - ) { - if !self.focused { - cx.focus_self(); - cx.emit(Event::Activate); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self - .newest_selection::(&display_map.buffer_snapshot) - .tail(); - self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); - - self.select_columns( - tail.to_display_point(&display_map), - position, - overshoot, - &display_map, - cx, - ); - } - - fn update_selection( - &mut self, - position: DisplayPoint, - overshoot: u32, - scroll_position: Vector2F, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if let Some(tail) = self.columnar_selection_tail.as_ref() { - let tail = tail.to_display_point(&display_map); - self.select_columns(tail, position, overshoot, &display_map, cx); - } else if let Some(mut pending) = self.pending_selection.clone() { - let buffer = self.buffer.read(cx).snapshot(cx); - let head; - let tail; - match &pending.mode { - SelectMode::Character => { - head = position.to_point(&display_map); - tail = pending.selection.tail().to_point(&buffer); - } - SelectMode::Word(original_range) => { - let original_display_range = original_range.start.to_display_point(&display_map) - ..original_range.end.to_display_point(&display_map); - let original_buffer_range = original_display_range.start.to_point(&display_map) - ..original_display_range.end.to_point(&display_map); - if movement::is_inside_word(&display_map, position) - || original_display_range.contains(&position) - { - let word_range = movement::surrounding_word(&display_map, position); - if word_range.start < original_display_range.start { - head = word_range.start.to_point(&display_map); - } else { - head = word_range.end.to_point(&display_map); - } - } else { - head = position.to_point(&display_map); - } - - if head <= original_buffer_range.start { - tail = original_buffer_range.end; - } else { - tail = original_buffer_range.start; - } - } - SelectMode::Line(original_range) => { - let original_range = original_range.to_point(&display_map.buffer_snapshot); - - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - - if line_start < original_range.start { - head = line_start - } else { - head = next_line_start - } - - if head <= original_range.start { - tail = original_range.end; - } else { - tail = original_range.start; - } - } - SelectMode::All => { - return; - } - }; - - if head < tail { - pending.selection.start = buffer.anchor_before(head); - pending.selection.end = buffer.anchor_before(tail); - pending.selection.reversed = true; - } else { - pending.selection.start = buffer.anchor_before(tail); - pending.selection.end = buffer.anchor_before(head); - pending.selection.reversed = false; - } - self.set_selections(self.selections.clone(), Some(pending), cx); - } else { - log::error!("update_selection dispatched with no pending selection"); - return; - } - - self.set_scroll_position(scroll_position, cx); - cx.notify(); - } - - fn end_selection(&mut self, cx: &mut ViewContext) { - self.columnar_selection_tail.take(); - if self.pending_selection.is_some() { - let selections = self.local_selections::(cx); - self.update_selections(selections, None, cx); - } - } - - fn select_columns( - &mut self, - tail: DisplayPoint, - head: DisplayPoint, - overshoot: u32, - display_map: &DisplaySnapshot, - cx: &mut ViewContext, - ) { - let start_row = cmp::min(tail.row(), head.row()); - let end_row = cmp::max(tail.row(), head.row()); - let start_column = cmp::min(tail.column(), head.column() + overshoot); - let end_column = cmp::max(tail.column(), head.column() + overshoot); - let reversed = start_column < tail.column(); - - let selections = (start_row..=end_row) - .filter_map(|row| { - if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { - let start = display_map - .clip_point(DisplayPoint::new(row, start_column), Bias::Left) - .to_point(&display_map); - let end = display_map - .clip_point(DisplayPoint::new(row, end_column), Bias::Right) - .to_point(&display_map); - Some(Selection { - id: post_inc(&mut self.next_selection_id), - start, - end, - reversed, - goal: SelectionGoal::None, - }) - } else { - None - } - }) - .collect::>(); - - self.update_selections(selections, None, cx); - cx.notify(); - } - - pub fn is_selecting(&self) -> bool { - self.pending_selection.is_some() || self.columnar_selection_tail.is_some() - } - - pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if self.take_rename(cx).is_some() { - return; - } - - if self.hide_context_menu(cx).is_some() { - return; - } - - if self.snippet_stack.pop().is_some() { - return; - } - - if self.mode != EditorMode::Full { - cx.propagate_action(); - return; - } - - if self.active_diagnostics.is_some() { - self.dismiss_diagnostics(cx); - } else if let Some(pending) = self.pending_selection.clone() { - let mut selections = self.selections.clone(); - if selections.is_empty() { - selections = Arc::from([pending.selection]); - } - self.set_selections(selections, None, cx); - self.request_autoscroll(Autoscroll::Fit, cx); - } else { - let buffer = self.buffer.read(cx).snapshot(cx); - let mut oldest_selection = self.oldest_selection::(&buffer); - if self.selection_count() == 1 { - if oldest_selection.is_empty() { - cx.propagate_action(); - return; - } - - oldest_selection.start = oldest_selection.head().clone(); - oldest_selection.end = oldest_selection.head().clone(); - } - self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx); - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn selected_ranges>( - &self, - cx: &mut MutableAppContext, - ) -> Vec> { - self.local_selections::(cx) - .iter() - .map(|s| { - if s.reversed { - s.end.clone()..s.start.clone() - } else { - s.start.clone()..s.end.clone() - } - }) - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec> { - let display_map = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); - self.selections - .iter() - .chain( - self.pending_selection - .as_ref() - .map(|pending| &pending.selection), - ) - .map(|s| { - if s.reversed { - s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) - } else { - s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map) - } - }) - .collect() - } - - pub fn select_ranges( - &mut self, - ranges: I, - autoscroll: Option, - cx: &mut ViewContext, - ) where - I: IntoIterator>, - T: ToOffset, - { - let buffer = self.buffer.read(cx).snapshot(cx); - let selections = ranges - .into_iter() - .map(|range| { - let mut start = range.start.to_offset(&buffer); - let mut end = range.end.to_offset(&buffer); - let reversed = if start > end { - mem::swap(&mut start, &mut end); - true - } else { - false - }; - Selection { - id: post_inc(&mut self.next_selection_id), - start, - end, - reversed, - goal: SelectionGoal::None, - } - }) - .collect::>(); - self.update_selections(selections, autoscroll, cx); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext) - where - T: IntoIterator>, - { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = ranges - .into_iter() - .map(|range| { - let mut start = range.start; - let mut end = range.end; - let reversed = if start > end { - mem::swap(&mut start, &mut end); - true - } else { - false - }; - Selection { - id: post_inc(&mut self.next_selection_id), - start: start.to_point(&display_map), - end: end.to_point(&display_map), - reversed, - goal: SelectionGoal::None, - } - }) - .collect(); - self.update_selections(selections, None, cx); - } - - pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { - let text = action.0.as_ref(); - if !self.skip_autoclose_end(text, cx) { - self.start_transaction(cx); - self.insert(text, cx); - self.autoclose_pairs(cx); - self.end_transaction(cx); - self.trigger_completion_on_input(text, cx); - } - } - - pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut old_selections = SmallVec::<[_; 32]>::new(); - { - let selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).snapshot(cx); - for selection in selections.iter() { - let start_point = selection.start.to_point(&buffer); - let indent = buffer - .indent_column_for_line(start_point.row) - .min(start_point.column); - let start = selection.start; - let end = selection.end; - - let mut insert_extra_newline = false; - if let Some(language) = buffer.language() { - let leading_whitespace_len = buffer - .reversed_chars_at(start) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - let trailing_whitespace_len = buffer - .chars_at(end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - - insert_extra_newline = language.brackets().iter().any(|pair| { - let pair_start = pair.start.trim_end(); - let pair_end = pair.end.trim_start(); - - pair.newline - && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) - && buffer.contains_str_at( - (start - leading_whitespace_len).saturating_sub(pair_start.len()), - pair_start, - ) - }); - } - - old_selections.push(( - selection.id, - buffer.anchor_after(end), - start..end, - indent, - insert_extra_newline, - )); - } - } - - self.buffer.update(cx, |buffer, cx| { - let mut delta = 0_isize; - let mut pending_edit: Option = None; - for (_, _, range, indent, insert_extra_newline) in &old_selections { - if pending_edit.as_ref().map_or(false, |pending| { - pending.indent != *indent - || pending.insert_extra_newline != *insert_extra_newline - }) { - let pending = pending_edit.take().unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); - } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); - delta += pending.delta; - } - - let start = (range.start as isize + delta) as usize; - let end = (range.end as isize + delta) as usize; - let mut text_len = *indent as usize + 1; - if *insert_extra_newline { - text_len *= 2; - } - - let pending = pending_edit.get_or_insert_with(Default::default); - pending.delta += text_len as isize - (end - start) as isize; - pending.indent = *indent; - pending.insert_extra_newline = *insert_extra_newline; - pending.ranges.push(start..end); - } - - let pending = pending_edit.unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); - } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); - - let buffer = buffer.read(cx); - self.selections = self - .selections - .iter() - .cloned() - .zip(old_selections) - .map( - |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { - let mut cursor = end_anchor.to_point(&buffer); - if insert_extra_newline { - cursor.row -= 1; - cursor.column = buffer.line_len(cursor.row); - } - let anchor = buffer.anchor_after(cursor); - new_selection.start = anchor.clone(); - new_selection.end = anchor; - new_selection - }, - ) - .collect(); - }); - - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); - - #[derive(Default)] - struct PendingEdit { - indent: u32, - insert_extra_newline: bool, - delta: isize, - ranges: SmallVec<[Range; 32]>, - } - } - - pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { - self.start_transaction(cx); - - let old_selections = self.local_selections::(cx); - let selection_anchors = self.buffer.update(cx, |buffer, cx| { - let anchors = { - let snapshot = buffer.read(cx); - old_selections - .iter() - .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) - .collect::>() - }; - let edit_ranges = old_selections.iter().map(|s| s.start..s.end); - buffer.edit_with_autoindent(edit_ranges, text, cx); - anchors - }); - - let selections = { - let snapshot = self.buffer.read(cx).read(cx); - selection_anchors - .into_iter() - .map(|(id, goal, position)| { - let position = position.to_offset(&snapshot); - Selection { - id, - start: position, - end: position, - goal, - reversed: false, - } - }) - .collect() - }; - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { - let selection = self.newest_anchor_selection(); - if self - .buffer - .read(cx) - .is_completion_trigger(selection.head(), text, cx) - { - self.show_completions(&ShowCompletions, cx); - } else { - self.hide_context_menu(cx); - } - } - - fn autoclose_pairs(&mut self, cx: &mut ViewContext) { - let selections = self.local_selections::(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 { - id: selection.id, - start: snapshot.anchor_before(selection.start), - end: snapshot.anchor_before(selection.end), - reversed: selection.reversed, - goal: selection.goal, - }) - .collect::>(); - - let autoclose_pair = snapshot.language().and_then(|language| { - let first_selection_start = selections.first().unwrap().start; - let pair = language.brackets().iter().find(|pair| { - snapshot.contains_str_at( - first_selection_start.saturating_sub(pair.start.len()), - &pair.start, - ) - }); - pair.and_then(|pair| { - let should_autoclose = selections[1..].iter().all(|selection| { - snapshot.contains_str_at( - selection.start.saturating_sub(pair.start.len()), - &pair.start, - ) - }); - - 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::>(); - - buffer.edit(selection_ranges, &pair.end, cx); - snapshot = buffer.snapshot(cx); - - new_selections = Some( - self.resolve_selections::(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, - }); - } - } - }); - - if let Some(new_selections) = new_selections { - self.update_selections(new_selections, None, cx); - } - if let Some(bracket_pair_state) = bracket_pair_state { - self.autoclose_stack.push(bracket_pair_state); - } - } - - fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { - let old_selections = self.local_selections::(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; - } - - debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); - - let buffer = self.buffer.read(cx).snapshot(cx); - 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.update_selections(new_selections, Some(Autoscroll::Fit), cx); - true - } else { - false - } - } - - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { - let offset = position.to_offset(buffer); - let (word_range, kind) = buffer.surrounding_word(offset); - if offset > word_range.start && kind == Some(CharKind::Word) { - Some( - buffer - .text_for_range(word_range.start..offset) - .collect::(), - ) - } else { - None - } - } - - fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { - if self.pending_rename.is_some() { - return; - } - - let project = if let Some(project) = self.project.clone() { - project - } else { - return; - }; - - let position = self.newest_anchor_selection().head(); - let (buffer, buffer_position) = if let Some(output) = self - .buffer - .read(cx) - .text_anchor_for_position(position.clone(), cx) - { - output - } else { - return; - }; - - let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); - let completions = project.update(cx, |project, cx| { - project.completions(&buffer, buffer_position.clone(), cx) - }); - - let id = post_inc(&mut self.next_completion_id); - let task = cx.spawn_weak(|this, mut cx| { - async move { - let completions = completions.await?; - if completions.is_empty() { - return Ok(()); - } - - let mut menu = CompletionsMenu { - id, - initial_position: position, - match_candidates: completions - .iter() - .enumerate() - .map(|(id, completion)| { - StringMatchCandidate::new( - id, - completion.label.text[completion.label.filter_range.clone()].into(), - ) - }) - .collect(), - buffer, - completions: completions.into(), - matches: Vec::new().into(), - selected_item: 0, - list: Default::default(), - }; - - menu.filter(query.as_deref(), cx.background()).await; - - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - match this.context_menu.as_ref() { - None => {} - Some(ContextMenu::Completions(prev_menu)) => { - if prev_menu.id > menu.id { - return; - } - } - _ => return, - } - - this.completion_tasks.retain(|(id, _)| *id > menu.id); - if this.focused { - this.show_context_menu(ContextMenu::Completions(menu), cx); - } - - cx.notify(); - }); - } - Ok::<_, anyhow::Error>(()) - } - .log_err() - }); - self.completion_tasks.push((id, task)); - } - - pub fn confirm_completion( - &mut self, - ConfirmCompletion(completion_ix): &ConfirmCompletion, - cx: &mut ViewContext, - ) -> Option>> { - use language::ToOffset as _; - - let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { - menu - } else { - return None; - }; - - let mat = completions_menu - .matches - .get(completion_ix.unwrap_or(completions_menu.selected_item))?; - let buffer_handle = completions_menu.buffer; - let completion = completions_menu.completions.get(mat.candidate_id)?; - - let snippet; - let text; - if completion.is_snippet() { - snippet = Some(Snippet::parse(&completion.new_text).log_err()?); - text = snippet.as_ref().unwrap().text.clone(); - } else { - snippet = None; - text = completion.new_text.clone(); - }; - let buffer = buffer_handle.read(cx); - let old_range = completion.old_range.to_offset(&buffer); - let old_text = buffer.text_for_range(old_range.clone()).collect::(); - - let selections = self.local_selections::(cx); - let newest_selection = self.newest_anchor_selection(); - if newest_selection.start.buffer_id != Some(buffer_handle.id()) { - return None; - } - - let lookbehind = newest_selection - .start - .text_anchor - .to_offset(buffer) - .saturating_sub(old_range.start); - let lookahead = old_range - .end - .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); - let mut common_prefix_len = old_text - .bytes() - .zip(text.bytes()) - .take_while(|(a, b)| a == b) - .count(); - - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut ranges = Vec::new(); - for selection in &selections { - if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { - let start = selection.start.saturating_sub(lookbehind); - let end = selection.end + lookahead; - ranges.push(start + common_prefix_len..end); - } else { - common_prefix_len = 0; - ranges.clear(); - ranges.extend(selections.iter().map(|s| { - if s.id == newest_selection.id { - old_range.clone() - } else { - s.start..s.end - } - })); - break; - } - } - let text = &text[common_prefix_len..]; - - self.start_transaction(cx); - if let Some(mut snippet) = snippet { - snippet.text = text.to_string(); - for tabstop in snippet.tabstops.iter_mut().flatten() { - tabstop.start -= common_prefix_len as isize; - tabstop.end -= common_prefix_len as isize; - } - - self.insert_snippet(&ranges, snippet, cx).log_err(); - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(ranges, text, cx); - }); - } - self.end_transaction(cx); - - let project = self.project.clone()?; - let apply_edits = project.update(cx, |project, cx| { - project.apply_additional_edits_for_completion( - buffer_handle, - completion.clone(), - true, - cx, - ) - }); - Some(cx.foreground().spawn(async move { - apply_edits.await?; - Ok(()) - })) - } - - pub fn toggle_code_actions( - &mut self, - &ToggleCodeActions(deployed_from_indicator): &ToggleCodeActions, - cx: &mut ViewContext, - ) { - if matches!( - self.context_menu.as_ref(), - Some(ContextMenu::CodeActions(_)) - ) { - self.context_menu.take(); - cx.notify(); - return; - } - - let mut task = self.code_actions_task.take(); - cx.spawn_weak(|this, mut cx| async move { - while let Some(prev_task) = task { - prev_task.await; - task = this - .upgrade(&cx) - .and_then(|this| this.update(&mut cx, |this, _| this.code_actions_task.take())); - } - - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if this.focused { - if let Some((buffer, actions)) = this.available_code_actions.clone() { - this.show_context_menu( - ContextMenu::CodeActions(CodeActionsMenu { - buffer, - actions, - selected_item: Default::default(), - list: Default::default(), - deployed_from_indicator, - }), - cx, - ); - } - } - }) - } - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - } - - pub fn confirm_code_action( - workspace: &mut Workspace, - ConfirmCodeAction(action_ix): &ConfirmCodeAction, - cx: &mut ViewContext, - ) -> Option>> { - let editor = workspace.active_item(cx)?.act_as::(cx)?; - let actions_menu = if let ContextMenu::CodeActions(menu) = - editor.update(cx, |editor, cx| editor.hide_context_menu(cx))? - { - menu - } else { - return None; - }; - let action_ix = action_ix.unwrap_or(actions_menu.selected_item); - let action = actions_menu.actions.get(action_ix)?.clone(); - let title = action.lsp_action.title.clone(); - let buffer = actions_menu.buffer; - - let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { - project.apply_code_action(buffer, action, true, cx) - }); - Some(cx.spawn(|workspace, cx| async move { - let project_transaction = apply_code_actions.await?; - Self::open_project_transaction(editor, workspace, project_transaction, title, cx).await - })) - } - - async fn open_project_transaction( - this: ViewHandle, - workspace: ViewHandle, - transaction: ProjectTransaction, - title: String, - mut cx: AsyncAppContext, - ) -> Result<()> { - let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); - - // If the code action's edits are all contained within this editor, then - // avoid opening a new editor to display them. - let mut entries = transaction.0.iter(); - if let Some((buffer, transaction)) = entries.next() { - if entries.next().is_none() { - let excerpt = this.read_with(&cx, |editor, cx| { - editor - .buffer() - .read(cx) - .excerpt_containing(editor.newest_anchor_selection().head(), cx) - }); - if let Some((excerpted_buffer, excerpt_range)) = excerpt { - if excerpted_buffer == *buffer { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - let excerpt_range = excerpt_range.to_offset(&snapshot); - if snapshot - .edited_ranges_for_transaction(transaction) - .all(|range| { - excerpt_range.start <= range.start && excerpt_range.end >= range.end - }) - { - return Ok(()); - } - } - } - } - } - - let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - for (buffer, transaction) in &transaction.0 { - let snapshot = buffer.read(cx).snapshot(); - ranges_to_highlight.extend( - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - snapshot - .edited_ranges_for_transaction::(transaction) - .collect(), - 1, - cx, - ), - ); - } - multibuffer.push_transaction(&transaction.0); - multibuffer - }); - - workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_ranges::(ranges_to_highlight, color, cx); - }); - } - }); - - Ok(()) - } - - fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { - let project = self.project.as_ref()?; - let buffer = self.buffer.read(cx); - let newest_selection = self.newest_anchor_selection().clone(); - let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; - let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?; - if start_buffer != end_buffer { - return None; - } - - let actions = project.update(cx, |project, cx| { - project.code_actions(&start_buffer, start..end, cx) - }); - self.code_actions_task = Some(cx.spawn_weak(|this, mut cx| async move { - let actions = actions.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.available_code_actions = actions.log_err().and_then(|actions| { - if actions.is_empty() { - None - } else { - Some((start_buffer, actions.into())) - } - }); - cx.notify(); - }) - } - })); - None - } - - fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { - let project = self.project.as_ref()?; - let buffer = self.buffer.read(cx); - let newest_selection = self.newest_anchor_selection().clone(); - let cursor_position = newest_selection.head(); - let (cursor_buffer, cursor_buffer_position) = - buffer.text_anchor_for_position(cursor_position.clone(), cx)?; - let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; - if cursor_buffer != tail_buffer { - return None; - } - - let highlights = project.update(cx, |project, cx| { - project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - }); - - enum DocumentHighlightRead {} - enum DocumentHighlightWrite {} - - self.document_highlights_task = Some(cx.spawn_weak(|this, mut cx| async move { - let highlights = highlights.log_err().await; - if let Some((this, highlights)) = this.upgrade(&cx).zip(highlights) { - this.update(&mut cx, |this, cx| { - let buffer_id = cursor_position.buffer_id; - let excerpt_id = cursor_position.excerpt_id.clone(); - let style = this.style(cx); - let read_background = style.document_highlight_read_background; - let write_background = style.document_highlight_write_background; - let buffer = this.buffer.read(cx); - if !buffer - .text_anchor_for_position(cursor_position, cx) - .map_or(false, |(buffer, _)| buffer == cursor_buffer) - { - return; - } - - let mut write_ranges = Vec::new(); - let mut read_ranges = Vec::new(); - for highlight in highlights { - let range = Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.start, - }..Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.end, - }; - if highlight.kind == lsp::DocumentHighlightKind::WRITE { - write_ranges.push(range); - } else { - read_ranges.push(range); - } - } - - this.highlight_ranges::( - read_ranges, - read_background, - cx, - ); - this.highlight_ranges::( - write_ranges, - write_background, - cx, - ); - cx.notify(); - }); - } - })); - None - } - - pub fn render_code_actions_indicator( - &self, - style: &EditorStyle, - cx: &mut ViewContext, - ) -> Option { - if self.available_code_actions.is_some() { - enum Tag {} - Some( - MouseEventHandler::new::(0, cx, |_, _| { - Svg::new("icons/zap.svg") - .with_color(style.code_actions_indicator) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .with_padding(Padding::uniform(3.)) - .on_mouse_down(|cx| { - cx.dispatch_action(ToggleCodeActions(true)); - }) - .boxed(), - ) - } else { - None - } - } - - pub fn context_menu_visible(&self) -> bool { - self.context_menu - .as_ref() - .map_or(false, |menu| menu.visible()) - } - - pub fn render_context_menu( - &self, - cursor_position: DisplayPoint, - style: EditorStyle, - cx: &AppContext, - ) -> Option<(DisplayPoint, ElementBox)> { - self.context_menu - .as_ref() - .map(|menu| menu.render(cursor_position, style, cx)) - } - - fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { - if !matches!(menu, ContextMenu::Completions(_)) { - self.completion_tasks.clear(); - } - self.context_menu = Some(menu); - cx.notify(); - } - - fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { - cx.notify(); - self.completion_tasks.clear(); - self.context_menu.take() - } - - pub fn insert_snippet( - &mut self, - insertion_ranges: &[Range], - snippet: Snippet, - cx: &mut ViewContext, - ) -> Result<()> { - let tabstops = self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(insertion_ranges.iter().cloned(), &snippet.text, cx); - - let snapshot = &*buffer.read(cx); - let snippet = &snippet; - snippet - .tabstops - .iter() - .map(|tabstop| { - let mut tabstop_ranges = tabstop - .iter() - .flat_map(|tabstop_range| { - let mut delta = 0 as isize; - insertion_ranges.iter().map(move |insertion_range| { - let insertion_start = insertion_range.start as isize + delta; - delta += - snippet.text.len() as isize - insertion_range.len() as isize; - - let start = snapshot.anchor_before( - (insertion_start + tabstop_range.start) as usize, - ); - let end = snapshot - .anchor_after((insertion_start + tabstop_range.end) as usize); - start..end - }) - }) - .collect::>(); - tabstop_ranges - .sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot).unwrap()); - tabstop_ranges - }) - .collect::>() - }); - - if let Some(tabstop) = tabstops.first() { - self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx); - self.snippet_stack.push(SnippetState { - active_index: 0, - ranges: tabstops, - }); - } - - Ok(()) - } - - pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { - self.move_to_snippet_tabstop(Bias::Right, cx) - } - - pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) { - self.move_to_snippet_tabstop(Bias::Left, cx); - } - - pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { - let buffer = self.buffer.read(cx).snapshot(cx); - - if let Some(snippet) = self.snippet_stack.last_mut() { - match bias { - Bias::Left => { - if snippet.active_index > 0 { - snippet.active_index -= 1; - } else { - return false; - } - } - Bias::Right => { - if snippet.active_index + 1 < snippet.ranges.len() { - snippet.active_index += 1; - } else { - return false; - } - } - } - if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { - let new_selections = current_ranges - .iter() - .map(|new_range| { - let new_range = new_range.to_offset(&buffer); - Selection { - id: post_inc(&mut self.next_selection_id), - start: new_range.start, - end: new_range.end, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - - // Remove the snippet state when moving to the last tabstop. - if snippet.active_index + 1 == snippet.ranges.len() { - self.snippet_stack.pop(); - } - - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - return true; - } - self.snippet_stack.pop(); - } - - false - } - - pub fn clear(&mut self, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_all(&SelectAll, cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert(&"", cx); - self.end_transaction(cx); - } - - pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if self.move_to_next_snippet_tabstop(cx) { - return; - } - - self.start_transaction(cx); - let tab_size = self.settings.borrow().tab_size; - let mut selections = self.local_selections::(cx); - let mut last_indent = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - if selection.is_empty() { - let char_column = buffer - .read(cx) - .text_for_range(Point::new(selection.start.row, 0)..selection.start) - .flat_map(str::chars) - .count(); - let chars_to_next_tab_stop = tab_size - (char_column % tab_size); - buffer.edit( - [selection.start..selection.start], - " ".repeat(chars_to_next_tab_stop), - cx, - ); - selection.start.column += chars_to_next_tab_stop as u32; - selection.end = selection.start; - } else { - let mut start_row = selection.start.row; - let mut end_row = selection.end.row + 1; - - // If a selection ends at the beginning of a line, don't indent - // that last line. - if selection.end.column == 0 { - end_row -= 1; - } - - // Avoid re-indenting a row that has already been indented by a - // previous selection, but still update this selection's column - // to reflect that indentation. - if let Some((last_indent_row, last_indent_len)) = last_indent { - if last_indent_row == selection.start.row { - selection.start.column += last_indent_len; - start_row += 1; - } - if last_indent_row == selection.end.row { - selection.end.column += last_indent_len; - } - } - - for row in start_row..end_row { - let indent_column = buffer.read(cx).indent_column_for_line(row) as usize; - let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); - let row_start = Point::new(row, 0); - buffer.edit( - [row_start..row_start], - " ".repeat(columns_to_next_tab_stop), - cx, - ); - - // Update this selection's endpoints to reflect the indentation. - if row == selection.start.row { - selection.start.column += columns_to_next_tab_stop as u32; - } - if row == selection.end.row { - selection.end.column += columns_to_next_tab_stop as u32; - } - - last_indent = Some((row, columns_to_next_tab_stop as u32)); - } - } - } - }); - - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { - if !self.snippet_stack.is_empty() { - self.move_to_prev_snippet_tabstop(cx); - return; - } - - self.start_transaction(cx); - let tab_size = self.settings.borrow().tab_size; - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut deletion_ranges = Vec::new(); - let mut last_outdent = None; - { - let buffer = self.buffer.read(cx).read(cx); - for selection in &selections { - let mut rows = selection.spanned_rows(false, &display_map); - - // Avoid re-outdenting a row that has already been outdented by a - // previous selection. - if let Some(last_row) = last_outdent { - if last_row == rows.start { - rows.start += 1; - } - } - - for row in rows { - let column = buffer.indent_column_for_line(row) as usize; - if column > 0 { - let mut deletion_len = (column % tab_size) as u32; - if deletion_len == 0 { - deletion_len = tab_size as u32; - } - deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len)); - last_outdent = Some(row); - } - } - } - } - self.buffer.update(cx, |buffer, cx| { - buffer.edit(deletion_ranges, "", cx); - }); - - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); - } - - pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { - self.start_transaction(cx); - - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut new_cursors = Vec::new(); - let mut edit_ranges = Vec::new(); - let mut selections = selections.iter().peekable(); - while let Some(selection) = selections.next() { - let mut rows = selection.spanned_rows(false, &display_map); - let goal_display_column = selection.head().to_display_point(&display_map).column(); - - // Accumulate contiguous regions of rows that we want to delete. - while let Some(next_selection) = selections.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start <= rows.end { - rows.end = next_rows.end; - selections.next().unwrap(); - } else { - break; - } - } - - let mut edit_start = Point::new(rows.start, 0).to_offset(&buffer); - let edit_end; - let cursor_buffer_row; - if buffer.max_point().row >= rows.end { - // If there's a line after the range, delete the \n from the end of the row range - // and position the cursor on the next line. - edit_end = Point::new(rows.end, 0).to_offset(&buffer); - cursor_buffer_row = rows.end; - } else { - // If there isn't a line after the range, delete the \n from the line before the - // start of the row range and position the cursor there. - edit_start = edit_start.saturating_sub(1); - edit_end = buffer.len(); - cursor_buffer_row = rows.start.saturating_sub(1); - } - - let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map); - *cursor.column_mut() = - cmp::min(goal_display_column, display_map.line_len(cursor.row())); - - new_cursors.push(( - selection.id, - buffer.anchor_after(cursor.to_point(&display_map)), - )); - edit_ranges.push(edit_start..edit_end); - } - - let buffer = self.buffer.update(cx, |buffer, cx| { - buffer.edit(edit_ranges, "", cx); - buffer.snapshot(cx) - }); - let new_selections = new_cursors - .into_iter() - .map(|(id, cursor)| { - let cursor = cursor.to_point(&buffer); - Selection { - id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { - self.start_transaction(cx); - - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - - let mut edits = Vec::new(); - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - // Avoid duplicating the same lines twice. - let mut rows = selection.spanned_rows(false, &display_map); - - while let Some(next_selection) = selections_iter.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start <= rows.end - 1 { - rows.end = next_rows.end; - selections_iter.next().unwrap(); - } else { - break; - } - } - - // Copy the text from the selected row region and splice it at the start of the region. - let start = Point::new(rows.start, 0); - let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1)); - let text = buffer - .text_for_range(start..end) - .chain(Some("\n")) - .collect::(); - edits.push((start, text, rows.len() as u32)); - } - - self.buffer.update(cx, |buffer, cx| { - for (point, text, _) in edits.into_iter().rev() { - buffer.edit(Some(point..point), text, cx); - } - }); - - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); - } - - pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_ranges = Vec::new(); - - let selections = self.local_selections::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - contiguous_row_selections.push(selection.clone()); - let start_row = selection.start.row; - let mut end_row = if selection.end.column > 0 || selection.is_empty() { - display_map.next_line_boundary(selection.end).0.row + 1 - } else { - selection.end.row - }; - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row { - end_row = if next_selection.end.column > 0 || next_selection.is_empty() { - display_map.next_line_boundary(next_selection.end).0.row + 1 - } else { - next_selection.end.row - }; - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } - - // Move the text spanned by the row range to be before the line preceding the row range - if start_row > 0 { - let range_to_move = Point::new(start_row - 1, buffer.line_len(start_row - 1)) - ..Point::new(end_row - 1, buffer.line_len(end_row - 1)); - let insertion_point = display_map - .prev_line_boundary(Point::new(start_row - 1, 0)) - .0; - - // Don't move lines across excerpts - if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(insertion_point), - Bound::Included(range_to_move.end), - )) - .next() - .is_none() - { - let text = buffer - .text_for_range(range_to_move.clone()) - .flat_map(|s| s.chars()) - .skip(1) - .chain(['\n']) - .collect::(); - - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor.clone()..insertion_anchor, text)); - - let row_delta = range_to_move.start.row - insertion_point.row + 1; - - // Move selections up - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row -= row_delta; - selection.end.row -= row_delta; - selection - }, - )); - - // Move folds up - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.start.to_point(&buffer); - let mut end = fold.end.to_point(&buffer); - start.row -= row_delta; - end.row -= row_delta; - refold_ranges.push(start..end); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.extend(contiguous_row_selections.drain(..)); - } - - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } - }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_ranges = Vec::new(); - - let selections = self.local_selections::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - contiguous_row_selections.push(selection.clone()); - let start_row = selection.start.row; - let mut end_row = if selection.end.column > 0 || selection.is_empty() { - display_map.next_line_boundary(selection.end).0.row + 1 - } else { - selection.end.row - }; - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row { - end_row = if next_selection.end.column > 0 || next_selection.is_empty() { - display_map.next_line_boundary(next_selection.end).0.row + 1 - } else { - next_selection.end.row - }; - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } - - // Move the text spanned by the row range to be after the last line of the row range - if end_row <= buffer.max_point().row { - let range_to_move = Point::new(start_row, 0)..Point::new(end_row, 0); - let insertion_point = display_map.next_line_boundary(Point::new(end_row, 0)).0; - - // Don't move lines across excerpt boundaries - if buffer - .excerpt_boundaries_in_range(( - Bound::Excluded(range_to_move.start), - Bound::Included(insertion_point), - )) - .next() - .is_none() - { - let mut text = String::from("\n"); - text.extend(buffer.text_for_range(range_to_move.clone())); - text.pop(); // Drop trailing newline - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor.clone()..insertion_anchor, text)); - - let row_delta = insertion_point.row - range_to_move.end.row + 1; - - // Move selections down - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row += row_delta; - selection.end.row += row_delta; - selection - }, - )); - - // Move folds down - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.start.to_point(&buffer); - let mut end = fold.end.to_point(&buffer); - start.row += row_delta; - end.row += row_delta; - refold_ranges.push(start..end); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.extend(contiguous_row_selections.drain(..)); - } - - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } - }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); - } - - pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut text = String::new(); - let mut selections = self.local_selections::(cx); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let buffer = self.buffer.read(cx).read(cx); - let max_point = buffer.max_point(); - for selection in &mut selections { - let is_entire_line = selection.is_empty(); - if is_entire_line { - selection.start = Point::new(selection.start.row, 0); - selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); - selection.goal = SelectionGoal::None; - } - let mut len = 0; - for chunk in buffer.text_for_range(selection.start..selection.end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - }); - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); - } - - pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - let selections = self.local_selections::(cx); - let mut text = String::new(); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let buffer = self.buffer.read(cx).read(cx); - let max_point = buffer.max_point(); - for selection in selections.iter() { - let mut start = selection.start; - let mut end = selection.end; - let is_entire_line = selection.is_empty(); - if is_entire_line { - start = Point::new(start.row, 0); - end = cmp::min(max_point, Point::new(start.row + 1, 0)); - } - let mut len = 0; - for chunk in buffer.text_for_range(start..end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - }); - } - } - - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); - } - - pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.as_mut().read_from_clipboard() { - let clipboard_text = item.text(); - if let Some(mut clipboard_selections) = item.metadata::>() { - let mut selections = self.local_selections::(cx); - let all_selections_were_entire_line = - clipboard_selections.iter().all(|s| s.is_entire_line); - if clipboard_selections.len() != selections.len() { - clipboard_selections.clear(); - } - - let mut delta = 0_isize; - let mut start_offset = 0; - for (i, selection) in selections.iter_mut().enumerate() { - let to_insert; - let entire_line; - if let Some(clipboard_selection) = clipboard_selections.get(i) { - let end_offset = start_offset + clipboard_selection.len; - to_insert = &clipboard_text[start_offset..end_offset]; - entire_line = clipboard_selection.is_entire_line; - start_offset = end_offset - } else { - to_insert = clipboard_text.as_str(); - entire_line = all_selections_were_entire_line; - } - - selection.start = (selection.start as isize + delta) as usize; - selection.end = (selection.end as isize + delta) as usize; - - self.buffer.update(cx, |buffer, cx| { - // If the corresponding selection was empty when this slice of the - // clipboard text was written, then the entire line containing the - // selection was copied. If this selection is also currently empty, - // then paste the line before the current line of the buffer. - let range = if selection.is_empty() && entire_line { - let column = selection.start.to_point(&buffer.read(cx)).column as usize; - let line_start = selection.start - column; - line_start..line_start - } else { - selection.start..selection.end - }; - - delta += to_insert.len() as isize - range.len() as isize; - buffer.edit([range], to_insert, cx); - selection.start += to_insert.len(); - selection.end = selection.start; - }); - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } else { - self.insert(clipboard_text, cx); - } - } - } - - pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { - if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { - if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); - } - self.request_autoscroll(Autoscroll::Fit, cx); - } - } - - pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { - if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { - if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); - } - self.request_autoscroll(Autoscroll::Fit, cx); - } - } - - pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.end = selection.start.clone(); - } else { - let cursor = movement::left(&display_map, start) - .unwrap() - .to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.start = selection.end.clone(); - } else { - let cursor = movement::right(&display_map, end) - .unwrap() - .to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - if self.take_rename(cx).is_some() { - return; - } - - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_prev(cx) { - return; - } - } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { - selection.goal = SelectionGoal::None; - } - - let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap(); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { - self.take_rename(cx); - - if let Some(context_menu) = self.context_menu.as_mut() { - if context_menu.select_next(cx) { - return; - } - } - - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { - selection.goal = SelectionGoal::None; - } - - let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap(); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn move_to_previous_word_boundary( - &mut self, - _: &MoveToPreviousWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_previous_word_boundary( - &mut self, - _: &SelectToPreviousWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_previous_word_boundary( - &mut self, - _: &DeleteToPreviousWordBoundary, - cx: &mut ViewContext, - ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = - movement::prev_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn move_to_next_word_boundary( - &mut self, - _: &MoveToNextWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_next_word_boundary( - &mut self, - _: &SelectToNextWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_next_word_boundary( - &mut self, - _: &DeleteToNextWordBoundary, - cx: &mut ViewContext, - ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = - movement::next_word_boundary(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - } - - pub fn move_to_beginning_of_line( - &mut self, - _: &MoveToBeginningOfLine, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, true); - let cursor = new_head.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_beginning_of_line( - &mut self, - SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_beginning_of_line( - &mut self, - _: &DeleteToBeginningOfLine, - cx: &mut ViewContext, - ) { - self.start_transaction(cx); - self.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); - self.backspace(&Backspace, cx); - self.end_transaction(cx); - } - - pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - { - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, true); - let anchor = new_head.to_point(&display_map); - selection.start = anchor.clone(); - selection.end = anchor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn select_to_end_of_line( - &mut self, - SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.delete(&Delete, cx); - self.end_transaction(cx); - } - - pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.cut(&Cut, cx); - self.end_transaction(cx); - } - - pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start: 0, - end: 0, - reversed: false, - goal: SelectionGoal::None, - }; - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { - let mut selection = self.local_selections::(cx).last().unwrap().clone(); - selection.set_head(Point::zero()); - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { - if matches!(self.mode, EditorMode::SingleLine) { - cx.propagate_action(); - return; - } - - let cursor = self.buffer.read(cx).read(cx).len(); - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - }; - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn set_nav_history(&mut self, nav_history: Option) { - self.nav_history = nav_history; - } - - pub fn nav_history(&self) -> Option<&ItemNavHistory> { - self.nav_history.as_ref() - } - - fn push_to_nav_history( - &self, - position: Anchor, - new_position: Option, - cx: &mut ViewContext, - ) { - if let Some(nav_history) = &self.nav_history { - let buffer = self.buffer.read(cx).read(cx); - let offset = position.to_offset(&buffer); - let point = position.to_point(&buffer); - drop(buffer); - - if let Some(new_position) = new_position { - let row_delta = (new_position.row as i64 - point.row as i64).abs(); - if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { - return; - } - } - - nav_history.push(Some(NavigationData { - anchor: position, - offset, - })); - } - } - - pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { - let mut selection = self.local_selections::(cx).first().unwrap().clone(); - selection.set_head(self.buffer.read(cx).read(cx).len()); - self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); - } - - pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { - let selection = Selection { - id: post_inc(&mut self.next_selection_id), - start: 0, - end: self.buffer.read(cx).read(cx).len(), - reversed: false, - goal: SelectionGoal::None, - }; - self.update_selections(vec![selection], None, cx); - } - - pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - let max_point = display_map.buffer_snapshot.max_point(); - for selection in &mut selections { - let rows = selection.spanned_rows(true, &display_map); - selection.start = Point::new(rows.start, 0); - selection.end = cmp::min(max_point, Point::new(rows.end, 0)); - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn split_selection_into_lines( - &mut self, - _: &SplitSelectionIntoLines, - cx: &mut ViewContext, - ) { - let mut to_unfold = Vec::new(); - let mut new_selections = Vec::new(); - { - let selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).read(cx); - for selection in selections { - for row in selection.start.row..selection.end.row { - let cursor = Point::new(row, buffer.line_len(row)); - new_selections.push(Selection { - id: post_inc(&mut self.next_selection_id), - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - }); - } - new_selections.push(Selection { - id: selection.id, - start: selection.end, - end: selection.end, - reversed: false, - goal: SelectionGoal::None, - }); - to_unfold.push(selection.start..selection.end); - } - } - self.unfold_ranges(to_unfold, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - } - - pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext) { - self.add_selection(true, cx); - } - - pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext) { - self.add_selection(false, cx); - } - - fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - let mut state = self.add_selections_state.take().unwrap_or_else(|| { - let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); - let range = oldest_selection.display_range(&display_map).sorted(); - let columns = cmp::min(range.start.column(), range.end.column()) - ..cmp::max(range.start.column(), range.end.column()); - - selections.clear(); - let mut stack = Vec::new(); - for row in range.start.row()..=range.end.row() { - if let Some(selection) = self.build_columnar_selection( - &display_map, - row, - &columns, - oldest_selection.reversed, - ) { - stack.push(selection.id); - selections.push(selection); - } - } - - if above { - stack.reverse(); - } - - AddSelectionsState { above, stack } - }); - - let last_added_selection = *state.stack.last().unwrap(); - let mut new_selections = Vec::new(); - if above == state.above { - let end_row = if above { - 0 - } else { - display_map.max_point().row() - }; - - 'outer: for selection in selections { - if selection.id == last_added_selection { - let range = selection.display_range(&display_map).sorted(); - debug_assert_eq!(range.start.row(), range.end.row()); - let mut row = range.start.row(); - let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal - { - start..end - } else { - cmp::min(range.start.column(), range.end.column()) - ..cmp::max(range.start.column(), range.end.column()) - }; - - while row != end_row { - if above { - row -= 1; - } else { - row += 1; - } - - if let Some(new_selection) = self.build_columnar_selection( - &display_map, - row, - &columns, - selection.reversed, - ) { - state.stack.push(new_selection.id); - if above { - new_selections.push(new_selection); - new_selections.push(selection); - } else { - new_selections.push(selection); - new_selections.push(new_selection); - } - - continue 'outer; - } - } - } - - new_selections.push(selection); - } - } else { - new_selections = selections; - new_selections.retain(|s| s.id != last_added_selection); - state.stack.pop(); - } - - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - if state.stack.len() > 1 { - self.add_selections_state = Some(state); - } - } - - pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) { - let replace_newest = action.0; - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let mut selections = self.local_selections::(cx); - if let Some(mut select_next_state) = self.select_next_state.take() { - let query = &select_next_state.query; - if !select_next_state.done { - let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); - let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); - let mut next_selected_range = None; - - let bytes_after_last_selection = - buffer.bytes_in_range(last_selection.end..buffer.len()); - let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); - let query_matches = query - .stream_find_iter(bytes_after_last_selection) - .map(|result| (last_selection.end, result)) - .chain( - query - .stream_find_iter(bytes_before_first_selection) - .map(|result| (0, result)), - ); - for (start_offset, query_match) in query_matches { - let query_match = query_match.unwrap(); // can only fail due to I/O - let offset_range = - start_offset + query_match.start()..start_offset + query_match.end(); - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); - - if !select_next_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) - { - next_selected_range = Some(offset_range); - break; - } - } - - if let Some(next_selected_range) = next_selected_range { - if replace_newest { - if let Some(newest_id) = - selections.iter().max_by_key(|s| s.id).map(|s| s.id) - { - selections.retain(|s| s.id != newest_id); - } - } - selections.push(Selection { - id: post_inc(&mut self.next_selection_id), - start: next_selected_range.start, - end: next_selected_range.end, - reversed: false, - goal: SelectionGoal::None, - }); - self.update_selections(selections, Some(Autoscroll::Newest), cx); - } else { - select_next_state.done = true; - } - } - - self.select_next_state = Some(select_next_state); - } else if selections.len() == 1 { - let selection = selections.last_mut().unwrap(); - if selection.start == selection.end { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let select_state = SelectNextState { - query: AhoCorasick::new_auto_configured(&[query]), - wordwise: true, - done: false, - }; - self.update_selections(selections, Some(Autoscroll::Newest), cx); - self.select_next_state = Some(select_state); - } else { - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - self.select_next_state = Some(SelectNextState { - query: AhoCorasick::new_auto_configured(&[query]), - wordwise: false, - done: false, - }); - self.select_next(action, cx); - } - } - } - - pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { - // 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 = - if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) { - prefix.to_string() - } else { - return; - }; - let comment_prefix = full_comment_prefix.trim_end_matches(' '); - let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - - self.start_transaction(cx); - let mut selections = self.local_selections::(cx); - let mut all_selection_lines_are_comments = true; - let mut edit_ranges = Vec::new(); - let mut last_toggled_row = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - 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); - } - - if snapshot.is_line_blank(row) { - continue; - } - - let start = Point::new(row, snapshot.indent_column_for_line(row)); - 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 { - all_selection_lines_are_comments = false; - edit_ranges.push(start..start); - } - } - - if !edit_ranges.is_empty() { - if all_selection_lines_are_comments { - buffer.edit(edit_ranges.iter().cloned(), "", cx); - } else { - let min_column = edit_ranges.iter().map(|r| r.start.column).min().unwrap(); - let edit_ranges = edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - position..position - }); - buffer.edit(edit_ranges, &full_comment_prefix, cx); - } - } - } - }); - - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); - } - - pub fn select_larger_syntax_node( - &mut self, - _: &SelectLargerSyntaxNode, - cx: &mut ViewContext, - ) { - let old_selections = self.local_selections::(cx).into_boxed_slice(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); - let mut selected_larger_node = false; - let new_selections = old_selections - .iter() - .map(|selection| { - let old_range = selection.start..selection.end; - let mut new_range = old_range.clone(); - while let Some(containing_range) = - buffer.range_for_syntax_ancestor(new_range.clone()) - { - new_range = containing_range; - if !display_map.intersects_fold(new_range.start) - && !display_map.intersects_fold(new_range.end) - { - break; - } - } - - selected_larger_node |= new_range != old_range; - Selection { - id: selection.id, - start: new_range.start, - end: new_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - } - }) - .collect::>(); - - if selected_larger_node { - stack.push(old_selections); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - } - self.select_larger_syntax_node_stack = stack; - } - - pub fn select_smaller_syntax_node( - &mut self, - _: &SelectSmallerSyntaxNode, - cx: &mut ViewContext, - ) { - let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); - if let Some(selections) = stack.pop() { - self.update_selections(selections.to_vec(), Some(Autoscroll::Fit), cx); - } - self.select_larger_syntax_node_stack = stack; - } - - pub fn move_to_enclosing_bracket( - &mut self, - _: &MoveToEnclosingBracket, - cx: &mut ViewContext, - ) { - let mut selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).snapshot(cx); - for selection in &mut selections { - if let Some((open_range, close_range)) = - buffer.enclosing_bracket_ranges(selection.start..selection.end) - { - let close_range = close_range.to_inclusive(); - let destination = if close_range.contains(&selection.start) - && close_range.contains(&selection.end) - { - open_range.end - } else { - *close_range.start() - }; - selection.start = destination; - selection.end = destination; - } - } - - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } - - pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext) { - let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self.newest_selection::(&buffer); - let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { - active_diagnostics - .primary_range - .to_offset(&buffer) - .to_inclusive() - }); - let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() { - if active_primary_range.contains(&selection.head()) { - *active_primary_range.end() - } else { - selection.head() - } - } else { - selection.head() - }; - - loop { - let next_group = buffer - .diagnostics_in_range::<_, usize>(search_start..buffer.len()) - .find_map(|entry| { - if entry.diagnostic.is_primary - && !entry.range.is_empty() - && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) - { - Some((entry.range, entry.diagnostic.group_id)) - } else { - None - } - }); - - if let Some((primary_range, group_id)) = next_group { - self.activate_diagnostics(group_id, cx); - self.update_selections( - vec![Selection { - id: selection.id, - start: primary_range.start, - end: primary_range.start, - reversed: false, - goal: SelectionGoal::None, - }], - Some(Autoscroll::Center), - cx, - ); - break; - } else if search_start == 0 { - break; - } else { - // Cycle around to the start of the buffer, potentially moving back to the start of - // the currently active diagnostic. - search_start = 0; - active_primary_range.take(); - } - } - } - - pub fn go_to_definition( - workspace: &mut Workspace, - _: &GoToDefinition, - cx: &mut ViewContext, - ) { - let active_item = workspace.active_item(cx); - let editor_handle = if let Some(editor) = active_item - .as_ref() - .and_then(|item| item.act_as::(cx)) - { - editor - } else { - return; - }; - - let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); - let (buffer, head) = - if let Some(text_anchor) = editor.buffer.read(cx).text_anchor_for_position(head, cx) { - text_anchor - } else { - return; - }; - - let definitions = workspace - .project() - .update(cx, |project, cx| project.definition(&buffer, head, cx)); - cx.spawn(|workspace, mut cx| async move { - let definitions = definitions.await?; - workspace.update(&mut cx, |workspace, cx| { - let nav_history = workspace.active_pane().read(cx).nav_history().clone(); - for definition in definitions { - let range = definition.range.to_offset(definition.buffer.read(cx)); - let target_editor_handle = workspace - .open_item(BufferItemHandle(definition.buffer), cx) - .downcast::() - .unwrap(); - - target_editor_handle.update(cx, |target_editor, cx| { - // When selecting a definition in a different buffer, disable the nav history - // to avoid creating a history entry at the previous cursor location. - if editor_handle != target_editor_handle { - nav_history.borrow_mut().disable(); - } - target_editor.select_ranges([range], Some(Autoscroll::Center), cx); - nav_history.borrow_mut().enable(); - }); - } - }); - - Ok::<(), anyhow::Error>(()) - }) - .detach_and_log_err(cx); - } - - pub fn find_all_references( - workspace: &mut Workspace, - _: &FindAllReferences, - cx: &mut ViewContext, - ) -> Option>> { - let active_item = workspace.active_item(cx)?; - let editor_handle = active_item.act_as::(cx)?; - - let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); - let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; - let replica_id = editor.replica_id(cx); - - let references = workspace - .project() - .update(cx, |project, cx| project.references(&buffer, head, cx)); - Some(cx.spawn(|workspace, mut cx| async move { - let mut locations = references.await?; - if locations.is_empty() { - return Ok(()); - } - - locations.sort_by_key(|location| location.buffer.id()); - let mut locations = locations.into_iter().peekable(); - let mut ranges_to_highlight = Vec::new(); - - let excerpt_buffer = cx.add_model(|cx| { - let mut symbol_name = None; - let mut multibuffer = MultiBuffer::new(replica_id); - while let Some(location) = locations.next() { - let buffer = location.buffer.read(cx); - let mut ranges_for_buffer = Vec::new(); - let range = location.range.to_offset(buffer); - ranges_for_buffer.push(range.clone()); - if symbol_name.is_none() { - symbol_name = Some(buffer.text_for_range(range).collect::()); - } - - while let Some(next_location) = locations.peek() { - if next_location.buffer == location.buffer { - ranges_for_buffer.push(next_location.range.to_offset(buffer)); - locations.next(); - } else { - break; - } - } - - ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); - ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines( - location.buffer.clone(), - ranges_for_buffer, - 1, - cx, - )); - } - multibuffer.with_title(format!("References to `{}`", symbol_name.unwrap())) - }); - - workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_ranges::(ranges_to_highlight, color, cx); - }); - } - }); - - Ok(()) - })) - } - - pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { - use language::ToOffset as _; - - let project = self.project.clone()?; - let selection = self.newest_anchor_selection().clone(); - let (cursor_buffer, cursor_buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.head(), cx)?; - let (tail_buffer, tail_buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.tail(), cx)?; - if tail_buffer != cursor_buffer { - return None; - } - - let snapshot = cursor_buffer.read(cx).snapshot(); - let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); - let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot); - let prepare_rename = project.update(cx, |project, cx| { - project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) - }); - - Some(cx.spawn(|this, mut cx| async move { - if let Some(rename_range) = prepare_rename.await? { - let rename_buffer_range = rename_range.to_offset(&snapshot); - let cursor_offset_in_rename_range = - cursor_buffer_offset.saturating_sub(rename_buffer_range.start); - let tail_offset_in_rename_range = - tail_buffer_offset.saturating_sub(rename_buffer_range.start); - - this.update(&mut cx, |this, cx| { - this.take_rename(cx); - let style = this.style(cx); - let buffer = this.buffer.read(cx).read(cx); - let cursor_offset = selection.head().to_offset(&buffer); - let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); - let rename_end = rename_start + rename_buffer_range.len(); - let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); - let old_name = buffer - .text_for_range(rename_start..rename_end) - .collect::(); - drop(buffer); - - // Position the selection in the rename editor so that it matches the current selection. - let rename_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line(this.settings.clone(), None, cx); - editor - .buffer - .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx)); - editor.select_ranges( - [tail_offset_in_rename_range..cursor_offset_in_rename_range], - None, - cx, - ); - editor.highlight_ranges::( - vec![Anchor::min()..Anchor::max()], - style.diff_background_inserted, - cx, - ); - editor - }); - this.highlight_ranges::( - vec![range.clone()], - style.diff_background_deleted, - cx, - ); - this.update_selections( - vec![Selection { - id: selection.id, - start: rename_end, - end: rename_end, - reversed: false, - goal: SelectionGoal::None, - }], - None, - cx, - ); - cx.focus(&rename_editor); - let block_id = this.insert_blocks( - [BlockProperties { - position: range.start.clone(), - height: 1, - render: Arc::new({ - let editor = rename_editor.clone(); - move |cx: &BlockContext| { - ChildView::new(editor.clone()) - .contained() - .with_padding_left(cx.anchor_x) - .boxed() - } - }), - disposition: BlockDisposition::Below, - }], - cx, - )[0]; - this.pending_rename = Some(RenameState { - range, - old_name, - editor: rename_editor, - block_id, - }); - }); - } - - Ok(()) - })) - } - - pub fn confirm_rename( - workspace: &mut Workspace, - _: &ConfirmRename, - cx: &mut ViewContext, - ) -> Option>> { - let editor = workspace.active_item(cx)?.act_as::(cx)?; - - let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { - let rename = editor.take_rename(cx)?; - let buffer = editor.buffer.read(cx); - let (start_buffer, start) = - buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; - let (end_buffer, end) = - buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; - if start_buffer == end_buffer { - let new_name = rename.editor.read(cx).text(cx); - Some((start_buffer, start..end, rename.old_name, new_name)) - } else { - None - } - })?; - - let rename = workspace.project().clone().update(cx, |project, cx| { - project.perform_rename( - buffer.clone(), - range.start.clone(), - new_name.clone(), - true, - cx, - ) - }); - - Some(cx.spawn(|workspace, cx| async move { - let project_transaction = rename.await?; - Self::open_project_transaction( - editor, - workspace, - project_transaction, - format!("Rename: {} → {}", old_name, new_name), - cx, - ) - .await - })) - } - - fn take_rename(&mut self, cx: &mut ViewContext) -> Option { - let rename = self.pending_rename.take()?; - self.remove_blocks([rename.block_id].into_iter().collect(), cx); - self.clear_highlighted_ranges::(cx); - - let editor = rename.editor.read(cx); - let buffer = editor.buffer.read(cx).snapshot(cx); - let selection = editor.newest_selection::(&buffer); - - // Update the selection to match the position of the selection inside - // the rename editor. - let snapshot = self.buffer.read(cx).snapshot(cx); - let rename_range = rename.range.to_offset(&snapshot); - let start = snapshot - .clip_offset(rename_range.start + selection.start, Bias::Left) - .min(rename_range.end); - let end = snapshot - .clip_offset(rename_range.start + selection.end, Bias::Left) - .min(rename_range.end); - self.update_selections( - vec![Selection { - id: self.newest_anchor_selection().id, - start, - end, - reversed: selection.reversed, - goal: SelectionGoal::None, - }], - None, - cx, - ); - - Some(rename) - } - - fn invalidate_rename_range( - &mut self, - buffer: &MultiBufferSnapshot, - cx: &mut ViewContext, - ) { - if let Some(rename) = self.pending_rename.as_ref() { - if self.selections.len() == 1 { - let head = self.selections[0].head().to_offset(buffer); - let range = rename.range.to_offset(buffer).to_inclusive(); - if range.contains(&head) { - return; - } - } - let rename = self.pending_rename.take().unwrap(); - self.remove_blocks([rename.block_id].into_iter().collect(), cx); - self.clear_highlighted_ranges::(cx); - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn pending_rename(&self) -> Option<&RenameState> { - self.pending_rename.as_ref() - } - - fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { - if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { - let buffer = self.buffer.read(cx).snapshot(cx); - let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); - let is_valid = buffer - .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) - .any(|entry| { - entry.diagnostic.is_primary - && !entry.range.is_empty() - && entry.range.start == primary_range_start - && entry.diagnostic.message == active_diagnostics.primary_message - }); - - if is_valid != active_diagnostics.is_valid { - active_diagnostics.is_valid = is_valid; - let mut new_styles = HashMap::default(); - for (block_id, diagnostic) in &active_diagnostics.blocks { - new_styles.insert( - *block_id, - diagnostic_block_renderer( - diagnostic.clone(), - is_valid, - self.settings.clone(), - ), - ); - } - self.display_map - .update(cx, |display_map, _| display_map.replace_blocks(new_styles)); - } - } - } - - fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { - self.dismiss_diagnostics(cx); - self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut primary_range = None; - let mut primary_message = None; - let mut group_end = Point::zero(); - let diagnostic_group = buffer - .diagnostic_group::(group_id) - .map(|entry| { - if entry.range.end > group_end { - group_end = entry.range.end; - } - if entry.diagnostic.is_primary { - primary_range = Some(entry.range.clone()); - primary_message = Some(entry.diagnostic.message.clone()); - } - entry - }) - .collect::>(); - let primary_range = primary_range.unwrap(); - let primary_message = primary_message.unwrap(); - let primary_range = - buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end); - - let blocks = display_map - .insert_blocks( - diagnostic_group.iter().map(|entry| { - let diagnostic = entry.diagnostic.clone(); - let message_height = diagnostic.message.lines().count() as u8; - BlockProperties { - position: buffer.anchor_after(entry.range.start), - height: message_height, - render: diagnostic_block_renderer( - diagnostic, - true, - self.settings.clone(), - ), - disposition: BlockDisposition::Below, - } - }), - cx, - ) - .into_iter() - .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic)) - .collect(); - - Some(ActiveDiagnosticGroup { - primary_range, - primary_message, - blocks, - is_valid: true, - }) - }); - } - - fn dismiss_diagnostics(&mut self, cx: &mut ViewContext) { - if let Some(active_diagnostic_group) = self.active_diagnostics.take() { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); - }); - cx.notify(); - } - } - - fn build_columnar_selection( - &mut self, - display_map: &DisplaySnapshot, - row: u32, - columns: &Range, - reversed: bool, - ) -> Option> { - let is_empty = columns.start == columns.end; - let line_len = display_map.line_len(row); - if columns.start < line_len || (is_empty && columns.start == line_len) { - let start = DisplayPoint::new(row, columns.start); - let end = DisplayPoint::new(row, cmp::min(columns.end, line_len)); - Some(Selection { - id: post_inc(&mut self.next_selection_id), - start: start.to_point(display_map), - end: end.to_point(display_map), - reversed, - goal: SelectionGoal::ColumnRange { - start: columns.start, - end: columns.end, - }, - }) - } else { - None - } - } - - pub fn local_selections_in_range( - &self, - range: Range, - display_map: &DisplaySnapshot, - ) -> Vec> { - let buffer = &display_map.buffer_snapshot; - - let start_ix = match self - .selections - .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer).unwrap()) - { - Ok(ix) | Err(ix) => ix, - }; - let end_ix = match self - .selections - .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer).unwrap()) - { - Ok(ix) => ix + 1, - Err(ix) => ix, - }; - - fn point_selection( - selection: &Selection, - buffer: &MultiBufferSnapshot, - ) -> Selection { - let start = selection.start.to_point(&buffer); - let end = selection.end.to_point(&buffer); - Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - } - } - - self.selections[start_ix..end_ix] - .iter() - .chain( - self.pending_selection - .as_ref() - .map(|pending| &pending.selection), - ) - .map(|s| point_selection(s, &buffer)) - .collect() - } - - pub fn local_selections<'a, D>(&self, cx: &'a AppContext) -> Vec> - where - D: 'a + TextDimension + Ord + Sub, - { - let buffer = self.buffer.read(cx).snapshot(cx); - let mut selections = self - .resolve_selections::(self.selections.iter(), &buffer) - .peekable(); - - let mut pending_selection = self.pending_selection::(&buffer); - - iter::from_fn(move || { - if let Some(pending) = pending_selection.as_mut() { - while let Some(next_selection) = selections.peek() { - if pending.start <= next_selection.end && pending.end >= next_selection.start { - let next_selection = selections.next().unwrap(); - if next_selection.start < pending.start { - pending.start = next_selection.start; - } - if next_selection.end > pending.end { - pending.end = next_selection.end; - } - } else if next_selection.end < pending.start { - return selections.next(); - } else { - break; - } - } - - pending_selection.take() - } else { - selections.next() - } - }) - .collect() - } - - fn resolve_selections<'a, D, I>( - &self, - selections: I, - snapshot: &MultiBufferSnapshot, - ) -> impl 'a + Iterator> - where - D: TextDimension + Ord + Sub, - I: 'a + IntoIterator>, - { - let (to_summarize, selections) = selections.into_iter().tee(); - let mut summaries = snapshot - .summaries_for_anchors::(to_summarize.flat_map(|s| [&s.start, &s.end])) - .into_iter(); - selections.map(move |s| Selection { - id: s.id, - start: summaries.next().unwrap(), - end: summaries.next().unwrap(), - reversed: s.reversed, - goal: s.goal, - }) - } - - fn pending_selection>( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Option> { - self.pending_selection - .as_ref() - .map(|pending| self.resolve_selection(&pending.selection, &snapshot)) - } - - fn resolve_selection>( - &self, - selection: &Selection, - buffer: &MultiBufferSnapshot, - ) -> Selection { - Selection { - id: selection.id, - start: selection.start.summary::(&buffer), - end: selection.end.summary::(&buffer), - reversed: selection.reversed, - goal: selection.goal, - } - } - - fn selection_count<'a>(&self) -> usize { - let mut count = self.selections.len(); - if self.pending_selection.is_some() { - count += 1; - } - count - } - - pub fn oldest_selection>( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Selection { - self.selections - .iter() - .min_by_key(|s| s.id) - .map(|selection| self.resolve_selection(selection, snapshot)) - .or_else(|| self.pending_selection(snapshot)) - .unwrap() - } - - pub fn newest_selection>( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Selection { - self.resolve_selection(self.newest_anchor_selection(), snapshot) - } - - pub fn newest_anchor_selection(&self) -> &Selection { - self.pending_selection - .as_ref() - .map(|s| &s.selection) - .or_else(|| self.selections.iter().max_by_key(|s| s.id)) - .unwrap() - } - - pub fn update_selections( - &mut self, - mut selections: Vec>, - autoscroll: Option, - cx: &mut ViewContext, - ) where - T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, - { - let buffer = self.buffer.read(cx).snapshot(cx); - selections.sort_unstable_by_key(|s| s.start); - - // Merge overlapping selections. - let mut i = 1; - while i < selections.len() { - if selections[i - 1].end >= selections[i].start { - let removed = selections.remove(i); - if removed.start < selections[i - 1].start { - selections[i - 1].start = removed.start; - } - if removed.end > selections[i - 1].end { - selections[i - 1].end = removed.end; - } - } else { - i += 1; - } - } - - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - - self.set_selections( - Arc::from_iter(selections.into_iter().map(|selection| { - let end_bias = if selection.end > selection.start { - Bias::Left - } else { - Bias::Right - }; - Selection { - id: selection.id, - start: buffer.anchor_after(selection.start), - end: buffer.anchor_at(selection.end, end_bias), - reversed: selection.reversed, - goal: selection.goal, - } - })), - None, - cx, - ); - } - - /// Compute new ranges for any selections that were located in excerpts that have - /// since been removed. - /// - /// Returns a `HashMap` indicating which selections whose former head position - /// was no longer present. The keys of the map are selection ids. The values are - /// the id of the new excerpt where the head of the selection has been moved. - pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { - let snapshot = self.buffer.read(cx).read(cx); - let anchors_with_status = snapshot.refresh_anchors( - self.selections - .iter() - .flat_map(|selection| [&selection.start, &selection.end]), - ); - let offsets = - snapshot.summaries_for_anchors::(anchors_with_status.iter().map(|a| &a.1)); - let offsets = offsets.chunks(2); - let statuses = anchors_with_status - .chunks(2) - .map(|a| (a[0].0 / 2, a[0].2, a[1].2)); - - let mut selections_with_lost_position = HashMap::default(); - let new_selections = offsets - .zip(statuses) - .map(|(offsets, (selection_ix, kept_start, kept_end))| { - let selection = &self.selections[selection_ix]; - let kept_head = if selection.reversed { - kept_start - } else { - kept_end - }; - if !kept_head { - selections_with_lost_position - .insert(selection.id, selection.head().excerpt_id.clone()); - } - - Selection { - id: selection.id, - start: offsets[0], - end: offsets[1], - reversed: selection.reversed, - goal: selection.goal, - } - }) - .collect(); - drop(snapshot); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - selections_with_lost_position - } - - fn set_selections( - &mut self, - selections: Arc<[Selection]>, - pending_selection: Option, - cx: &mut ViewContext, - ) { - let old_cursor_position = self.newest_anchor_selection().head(); - - self.selections = selections; - self.pending_selection = pending_selection; - if self.focused { - self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections(&self.selections, cx) - }); - } - - let display_map = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - self.add_selections_state = None; - self.select_next_state = None; - self.select_larger_syntax_node_stack.clear(); - self.autoclose_stack.invalidate(&self.selections, &buffer); - self.snippet_stack.invalidate(&self.selections, &buffer); - self.invalidate_rename_range(&buffer, cx); - - let new_cursor_position = self.newest_anchor_selection().head(); - - self.push_to_nav_history( - old_cursor_position.clone(), - Some(new_cursor_position.to_point(&buffer)), - cx, - ); - - let completion_menu = match self.context_menu.as_mut() { - Some(ContextMenu::Completions(menu)) => Some(menu), - _ => { - self.context_menu.take(); - None - } - }; - - if let Some(completion_menu) = completion_menu { - let cursor_position = new_cursor_position.to_offset(&buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position.clone()); - if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) - { - let query = Self::completion_query(&buffer, cursor_position); - cx.background() - .block(completion_menu.filter(query.as_deref(), cx.background().clone())); - self.show_completions(&ShowCompletions, cx); - } else { - self.hide_context_menu(cx); - } - } - - if old_cursor_position.to_display_point(&display_map).row() - != new_cursor_position.to_display_point(&display_map).row() - { - self.available_code_actions.take(); - } - self.refresh_code_actions(cx); - self.refresh_document_highlights(cx); - - self.pause_cursor_blinking(cx); - cx.emit(Event::SelectionsChanged); - } - - pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { - self.autoscroll_request = Some(autoscroll); - cx.notify(); - } - - fn start_transaction(&mut self, cx: &mut ViewContext) { - self.start_transaction_at(Instant::now(), cx); - } - - fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { - self.end_selection(cx); - if let Some(tx_id) = self - .buffer - .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) - { - self.selection_history - .insert(tx_id, (self.selections.clone(), None)); - } - } - - fn end_transaction(&mut self, cx: &mut ViewContext) { - self.end_transaction_at(Instant::now(), cx); - } - - fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { - if let Some(tx_id) = self - .buffer - .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { - *end_selections = Some(self.selections.clone()); - } else { - log::error!("unexpectedly ended a transaction that wasn't started by this editor"); - } - } - } - - pub fn page_up(&mut self, _: &PageUp, _: &mut ViewContext) { - log::info!("Editor::page_up"); - } - - pub fn page_down(&mut self, _: &PageDown, _: &mut ViewContext) { - log::info!("Editor::page_down"); - } - - pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { - let mut fold_ranges = Vec::new(); - - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in selections { - let range = selection.display_range(&display_map).sorted(); - let buffer_start_row = range.start.to_point(&display_map).row; - - for row in (0..=range.end.row()).rev() { - if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) { - let fold_range = self.foldable_range_for_line(&display_map, row); - if fold_range.end.row >= buffer_start_row { - fold_ranges.push(fold_range); - if row <= range.start.row() { - break; - } - } - } - } - } - - self.fold_ranges(fold_ranges, cx); - } - - pub fn unfold(&mut self, _: &Unfold, cx: &mut ViewContext) { - let selections = self.local_selections::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let ranges = selections - .iter() - .map(|s| { - let range = s.display_range(&display_map).sorted(); - let mut start = range.start.to_point(&display_map); - let mut end = range.end.to_point(&display_map); - start.column = 0; - end.column = buffer.line_len(end.row); - start..end - }) - .collect::>(); - self.unfold_ranges(ranges, cx); - } - - fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool { - let max_point = display_map.max_point(); - if display_row >= max_point.row() { - false - } else { - let (start_indent, is_blank) = display_map.line_indent(display_row); - if is_blank { - false - } else { - for display_row in display_row + 1..=max_point.row() { - let (indent, is_blank) = display_map.line_indent(display_row); - if !is_blank { - return indent > start_indent; - } - } - false - } - } - } - - fn foldable_range_for_line( - &self, - display_map: &DisplaySnapshot, - start_row: u32, - ) -> Range { - let max_point = display_map.max_point(); - - let (start_indent, _) = display_map.line_indent(start_row); - let start = DisplayPoint::new(start_row, display_map.line_len(start_row)); - let mut end = None; - for row in start_row + 1..=max_point.row() { - let (indent, is_blank) = display_map.line_indent(row); - if !is_blank && indent <= start_indent { - end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1))); - break; - } - } - - let end = end.unwrap_or(max_point); - return start.to_point(display_map)..end.to_point(display_map); - } - - pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { - let selections = self.local_selections::(cx); - let ranges = selections.into_iter().map(|s| s.start..s.end); - self.fold_ranges(ranges, cx); - } - - fn fold_ranges( - &mut self, - ranges: impl IntoIterator>, - cx: &mut ViewContext, - ) { - let mut ranges = ranges.into_iter().peekable(); - if ranges.peek().is_some() { - self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); - self.request_autoscroll(Autoscroll::Fit, cx); - cx.notify(); - } - } - - fn unfold_ranges(&mut self, ranges: Vec>, cx: &mut ViewContext) { - if !ranges.is_empty() { - self.display_map - .update(cx, |map, cx| map.unfold(ranges, cx)); - self.request_autoscroll(Autoscroll::Fit, cx); - cx.notify(); - } - } - - pub fn insert_blocks( - &mut self, - blocks: impl IntoIterator>, - cx: &mut ViewContext, - ) -> Vec { - let blocks = self - .display_map - .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); - self.request_autoscroll(Autoscroll::Fit, cx); - blocks - } - - pub fn replace_blocks( - &mut self, - blocks: HashMap, - cx: &mut ViewContext, - ) { - self.display_map - .update(cx, |display_map, _| display_map.replace_blocks(blocks)); - self.request_autoscroll(Autoscroll::Fit, cx); - } - - pub fn remove_blocks(&mut self, block_ids: HashSet, cx: &mut ViewContext) { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(block_ids, cx) - }); - } - - pub fn longest_row(&self, cx: &mut MutableAppContext) -> u32 { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .longest_row() - } - - pub fn max_point(&self, cx: &mut MutableAppContext) -> DisplayPoint { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .max_point() - } - - pub fn text(&self, cx: &AppContext) -> String { - self.buffer.read(cx).read(cx).text() - } - - pub fn set_text(&mut self, text: impl Into, cx: &mut ViewContext) { - self.buffer - .read(cx) - .as_singleton() - .expect("you can only call set_text on editors for singleton buffers") - .update(cx, |buffer, cx| buffer.set_text(text, cx)); - } - - pub fn display_text(&self, cx: &mut MutableAppContext) -> String { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .text() - } - - pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { - let language = self.language(cx); - let settings = self.settings.borrow(); - let mode = self - .soft_wrap_mode_override - .unwrap_or_else(|| settings.soft_wrap(language)); - match mode { - settings::SoftWrap::None => SoftWrap::None, - settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length(language)) - } - } - } - - pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext) { - self.soft_wrap_mode_override = Some(mode); - cx.notify(); - } - - pub fn set_wrap_width(&self, width: Option, cx: &mut MutableAppContext) -> bool { - self.display_map - .update(cx, |map, cx| map.set_wrap_width(width, cx)) - } - - pub fn set_highlighted_rows(&mut self, rows: Option>) { - self.highlighted_rows = rows; - } - - pub fn highlighted_rows(&self) -> Option> { - self.highlighted_rows.clone() - } - - pub fn highlight_ranges( - &mut self, - ranges: Vec>, - color: Color, - cx: &mut ViewContext, - ) { - self.highlighted_ranges - .insert(TypeId::of::(), (color, ranges)); - cx.notify(); - } - - pub fn clear_highlighted_ranges( - &mut self, - cx: &mut ViewContext, - ) -> Option<(Color, Vec>)> { - cx.notify(); - self.highlighted_ranges.remove(&TypeId::of::()) - } - - #[cfg(feature = "test-support")] - pub fn all_highlighted_ranges( - &mut self, - cx: &mut ViewContext, - ) -> Vec<(Range, Color)> { - let snapshot = self.snapshot(cx); - let buffer = &snapshot.buffer_snapshot; - let start = buffer.anchor_before(0); - let end = buffer.anchor_after(buffer.len()); - self.highlighted_ranges_in_range(start..end, &snapshot) - } - - pub fn highlighted_ranges_for_type(&self) -> Option<(Color, &[Range])> { - self.highlighted_ranges - .get(&TypeId::of::()) - .map(|(color, ranges)| (*color, ranges.as_slice())) - } - - pub fn highlighted_ranges_in_range( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - ) -> Vec<(Range, Color)> { - let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - for (color, ranges) in self.highlighted_ranges.values() { - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { - break; - } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, *color)) - } - } - results - } - - fn next_blink_epoch(&mut self) -> usize { - self.blink_epoch += 1; - self.blink_epoch - } - - fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { - if !self.focused { - return; - } - - self.show_local_cursors = true; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| { - let this = this.downgrade(); - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) - } - } - }) - .detach(); - } - - fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch { - self.blinking_paused = false; - self.blink_cursors(epoch, cx); - } - } - - fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext) { - if epoch == self.blink_epoch && self.focused && !self.blinking_paused { - self.show_local_cursors = !self.show_local_cursors; - cx.notify(); - - let epoch = self.next_blink_epoch(); - cx.spawn(|this, mut cx| { - let this = this.downgrade(); - async move { - Timer::after(CURSOR_BLINK_INTERVAL).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); - } - } - }) - .detach(); - } - } - - pub fn show_local_cursors(&self) -> bool { - self.show_local_cursors - } - - fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { - cx.notify(); - } - - fn on_buffer_event( - &mut self, - _: ModelHandle, - event: &language::Event, - cx: &mut ViewContext, - ) { - match event { - language::Event::Edited => { - self.refresh_active_diagnostics(cx); - self.refresh_code_actions(cx); - cx.emit(Event::Edited); - } - language::Event::Dirtied => cx.emit(Event::Dirtied), - language::Event::Saved => cx.emit(Event::Saved), - language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), - language::Event::Reloaded => cx.emit(Event::TitleChanged), - language::Event::Closed => cx.emit(Event::Closed), - language::Event::DiagnosticsUpdated => { - self.refresh_active_diagnostics(cx); - } - _ => {} - } - } - - fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { - cx.notify(); - } - - pub fn set_searchable(&mut self, searchable: bool) { - self.searchable = searchable; - } - - pub fn searchable(&self) -> bool { - self.searchable - } - - fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext) { - let active_item = workspace.active_item(cx); - let editor_handle = if let Some(editor) = active_item - .as_ref() - .and_then(|item| item.act_as::(cx)) - { - editor - } else { - cx.propagate_action(); - return; - }; - - let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - if buffer.is_singleton() { - cx.propagate_action(); - return; - } - - let mut new_selections_by_buffer = HashMap::default(); - for selection in editor.local_selections::(cx) { - for (buffer, mut range) in - buffer.range_to_buffer_ranges(selection.start..selection.end, cx) - { - if selection.reversed { - mem::swap(&mut range.start, &mut range.end); - } - new_selections_by_buffer - .entry(buffer) - .or_insert(Vec::new()) - .push(range) - } - } - - editor_handle.update(cx, |editor, cx| { - editor.push_to_nav_history(editor.newest_anchor_selection().head(), None, cx); - }); - let nav_history = workspace.active_pane().read(cx).nav_history().clone(); - nav_history.borrow_mut().disable(); - - // We defer the pane interaction because we ourselves are a workspace item - // and activating a new item causes the pane to call a method on us reentrantly, - // which panics if we're on the stack. - cx.defer(move |workspace, cx| { - for (ix, (buffer, ranges)) in new_selections_by_buffer.into_iter().enumerate() { - let buffer = BufferItemHandle(buffer); - if ix == 0 && !workspace.activate_pane_for_item(&buffer, cx) { - workspace.activate_next_pane(cx); - } - - let editor = workspace - .open_item(buffer, cx) - .downcast::() - .unwrap(); - - editor.update(cx, |editor, cx| { - editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); - }); - } - - nav_history.borrow_mut().enable(); - }); - } -} - -impl EditorSnapshot { - pub fn is_focused(&self) -> bool { - self.is_focused - } - - pub fn placeholder_text(&self) -> Option<&Arc> { - self.placeholder_text.as_ref() - } - - pub fn scroll_position(&self) -> Vector2F { - compute_scroll_position( - &self.display_snapshot, - self.scroll_position, - &self.scroll_top_anchor, - ) - } -} - -impl Deref for EditorSnapshot { - type Target = DisplaySnapshot; - - fn deref(&self) -> &Self::Target { - &self.display_snapshot - } -} - -fn compute_scroll_position( - snapshot: &DisplaySnapshot, - mut scroll_position: Vector2F, - scroll_top_anchor: &Option, -) -> Vector2F { - if let Some(anchor) = scroll_top_anchor { - let scroll_top = anchor.to_display_point(snapshot).row() as f32; - scroll_position.set_y(scroll_top + scroll_position.y()); - } else { - scroll_position.set_y(0.); - } - scroll_position -} - -#[derive(Copy, Clone)] -pub enum Event { - Activate, - Edited, - Blurred, - Dirtied, - Saved, - TitleChanged, - SelectionsChanged, - Closed, -} - -impl Entity for Editor { - type Event = Event; -} - -impl View for Editor { - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let style = self.style(cx); - self.display_map.update(cx, |map, cx| { - map.set_font(style.text.font_id, style.text.font_size, cx) - }); - EditorElement::new(self.handle.clone(), style.clone()).boxed() - } - - fn ui_name() -> &'static str { - "Editor" - } - - fn on_focus(&mut self, cx: &mut ViewContext) { - self.focused = true; - self.blink_cursors(self.blink_epoch, cx); - self.buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx); - buffer.set_active_selections(&self.selections, cx) - }); - } - - fn on_blur(&mut self, cx: &mut ViewContext) { - self.focused = false; - self.show_local_cursors = false; - self.buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - self.hide_context_menu(cx); - cx.emit(Event::Blurred); - cx.notify(); - } - - fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { - let mut cx = Self::default_keymap_context(); - let mode = match self.mode { - EditorMode::SingleLine => "single_line", - EditorMode::AutoHeight { .. } => "auto_height", - EditorMode::Full => "full", - }; - cx.map.insert("mode".into(), mode.into()); - if self.pending_rename.is_some() { - cx.set.insert("renaming".into()); - } - match self.context_menu.as_ref() { - Some(ContextMenu::Completions(_)) => { - cx.set.insert("showing_completions".into()); - } - Some(ContextMenu::CodeActions(_)) => { - cx.set.insert("showing_code_actions".into()); - } - None => {} - } - cx - } -} - -fn build_style( - settings: &Settings, - get_field_editor_theme: Option, - cx: &AppContext, -) -> EditorStyle { - let mut theme = settings.theme.editor.clone(); - if let Some(get_field_editor_theme) = get_field_editor_theme { - let field_editor_theme = get_field_editor_theme(&settings.theme); - if let Some(background) = field_editor_theme.container.background_color { - theme.background = background; - } - theme.text_color = field_editor_theme.text.color; - theme.selection = field_editor_theme.selection; - EditorStyle { - text: field_editor_theme.text, - placeholder_text: field_editor_theme.placeholder_text, - theme, - } - } else { - let font_cache = cx.font_cache(); - let font_family_id = settings.buffer_font_family; - let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); - let font_properties = Default::default(); - let font_id = font_cache - .select_font(font_family_id, &font_properties) - .unwrap(); - let font_size = settings.buffer_font_size; - EditorStyle { - text: TextStyle { - color: settings.theme.editor.text_color, - font_family_name, - font_family_id, - font_id, - font_size, - font_properties, - underline: None, - }, - placeholder_text: None, - theme, - } - } -} - -impl SelectionExt for Selection { - fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range { - let start = self.start.to_point(buffer); - let end = self.end.to_point(buffer); - if self.reversed { - end..start - } else { - start..end - } - } - - fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range { - let start = self.start.to_offset(buffer); - let end = self.end.to_offset(buffer); - if self.reversed { - end..start - } else { - start..end - } - } - - fn display_range(&self, map: &DisplaySnapshot) -> Range { - let start = self - .start - .to_point(&map.buffer_snapshot) - .to_display_point(map); - let end = self - .end - .to_point(&map.buffer_snapshot) - .to_display_point(map); - if self.reversed { - end..start - } else { - start..end - } - } - - fn spanned_rows( - &self, - include_end_if_at_line_start: bool, - map: &DisplaySnapshot, - ) -> Range { - let start = self.start.to_point(&map.buffer_snapshot); - let mut end = self.end.to_point(&map.buffer_snapshot); - if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { - end.row -= 1; - } - - let buffer_start = map.prev_line_boundary(start).0; - let buffer_end = map.next_line_boundary(end).0; - buffer_start.row..buffer_end.row + 1 - } -} - -impl InvalidationStack { - fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) - where - S: Clone + ToOffset, - { - while let Some(region) = self.last() { - let all_selections_inside_invalidation_ranges = - if selections.len() == region.ranges().len() { - selections - .iter() - .zip(region.ranges().iter().map(|r| r.to_offset(&buffer))) - .all(|(selection, invalidation_range)| { - let head = selection.head().to_offset(&buffer); - invalidation_range.start <= head && invalidation_range.end >= head - }) - } else { - false - }; - - if all_selections_inside_invalidation_ranges { - break; - } else { - self.pop(); - } - } - } -} - -impl Default for InvalidationStack { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Deref for InvalidationStack { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for InvalidationStack { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl InvalidationRegion for BracketPairState { - fn ranges(&self) -> &[Range] { - &self.ranges - } -} - -impl InvalidationRegion for SnippetState { - fn ranges(&self) -> &[Range] { - &self.ranges[self.active_index] - } -} - -impl Deref for EditorStyle { - type Target = theme::Editor; - - fn deref(&self) -> &Self::Target { - &self.theme - } -} - -pub fn diagnostic_block_renderer( - diagnostic: Diagnostic, - is_valid: bool, - settings: watch::Receiver, -) -> RenderBlock { - let mut highlighted_lines = Vec::new(); - for line in diagnostic.message.lines() { - highlighted_lines.push(highlight_diagnostic_message(line)); - } - - Arc::new(move |cx: &BlockContext| { - let settings = settings.borrow(); - let theme = &settings.theme.editor; - let style = diagnostic_style(diagnostic.severity, is_valid, theme); - let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); - Flex::column() - .with_children(highlighted_lines.iter().map(|(line, highlights)| { - Label::new( - line.clone(), - style.message.clone().with_font_size(font_size), - ) - .with_highlights(highlights.clone()) - .contained() - .with_margin_left(cx.anchor_x) - .boxed() - })) - .aligned() - .left() - .boxed() - }) -} - -pub fn highlight_diagnostic_message(message: &str) -> (String, Vec) { - let mut message_without_backticks = String::new(); - let mut prev_offset = 0; - let mut inside_block = false; - let mut highlights = Vec::new(); - for (match_ix, (offset, _)) in message - .match_indices('`') - .chain([(message.len(), "")]) - .enumerate() - { - message_without_backticks.push_str(&message[prev_offset..offset]); - if inside_block { - highlights.extend(prev_offset - match_ix..offset - match_ix); - } - - inside_block = !inside_block; - prev_offset = offset + 1; - } - - (message_without_backticks, highlights) -} - -pub fn diagnostic_style( - severity: DiagnosticSeverity, - valid: bool, - theme: &theme::Editor, -) -> DiagnosticStyle { - match (severity, valid) { - (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(), - (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(), - (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(), - (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(), - (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(), - (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(), - (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(), - (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(), - _ => theme.invalid_hint_diagnostic.clone(), - } -} - -pub fn combine_syntax_and_fuzzy_match_highlights( - text: &str, - default_style: HighlightStyle, - syntax_ranges: impl Iterator, HighlightStyle)>, - match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut result = Vec::new(); - let mut match_indices = match_indices.iter().copied().peekable(); - - for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) - { - syntax_highlight.font_properties.weight(Default::default()); - - // Add highlights for any fuzzy match characters before the next - // syntax highlight range. - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.start { - break; - } - match_indices.next(); - let end_index = char_ix_after(match_index, text); - let mut match_style = default_style; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - } - - if range.start == usize::MAX { - break; - } - - // Add highlights for any fuzzy match characters within the - // syntax highlight range. - let mut offset = range.start; - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.end { - break; - } - - match_indices.next(); - if match_index > offset { - result.push((offset..match_index, syntax_highlight)); - } - - let mut end_index = char_ix_after(match_index, text); - while let Some(&next_match_index) = match_indices.peek() { - if next_match_index == end_index && next_match_index < range.end { - end_index = char_ix_after(next_match_index, text); - match_indices.next(); - } else { - break; - } - } - - let mut match_style = syntax_highlight; - match_style.font_properties.weight(fonts::Weight::BOLD); - result.push((match_index..end_index, match_style)); - offset = end_index; - } - - if offset < range.end { - result.push((offset..range.end, syntax_highlight)); - } - } - - fn char_ix_after(ix: usize, text: &str) -> usize { - ix + text[ix..].chars().next().unwrap().len_utf8() - } - - result -} - -pub fn styled_runs_for_code_label<'a>( - label: &'a CodeLabel, - default_color: Color, - syntax_theme: &'a theme::SyntaxTheme, -) -> impl 'a + Iterator, HighlightStyle)> { - const MUTED_OPACITY: usize = 165; - - let mut muted_default_style = HighlightStyle { - color: default_color, - ..Default::default() - }; - muted_default_style.color.a = ((default_color.a as usize * MUTED_OPACITY) / 255) as u8; - - let mut prev_end = label.filter_range.end; - label - .runs - .iter() - .enumerate() - .flat_map(move |(ix, (range, highlight_id))| { - let style = if let Some(style) = highlight_id.style(syntax_theme) { - style - } else { - return Default::default(); - }; - let mut muted_style = style.clone(); - muted_style.color.a = ((style.color.a as usize * MUTED_OPACITY) / 255) as u8; - - let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); - if range.start >= label.filter_range.end { - if range.start > prev_end { - runs.push((prev_end..range.start, muted_default_style)); - } - runs.push((range.clone(), muted_style)); - } else if range.end <= label.filter_range.end { - runs.push((range.clone(), style)); - } else { - runs.push((range.start..label.filter_range.end, style)); - runs.push((label.filter_range.end..range.end, muted_style)); - } - prev_end = cmp::max(prev_end, range.end); - - if ix + 1 == label.runs.len() && label.text.len() > prev_end { - runs.push((prev_end..label.text.len(), muted_default_style)); - } - - runs - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use language::LanguageConfig; - use lsp::FakeLanguageServer; - use project::{FakeFs, ProjectPath}; - use smol::stream::StreamExt; - use std::{cell::RefCell, rc::Rc, time::Instant}; - use text::Point; - use unindent::Unindent; - use util::test::sample_text; - - #[gpui::test] - fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { - let mut now = Instant::now(); - let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); - let group_interval = buffer.read(cx).transaction_group_interval(); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let settings = Settings::test(cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - editor.update(cx, |editor, cx| { - editor.start_transaction_at(now, cx); - editor.select_ranges([2..4], None, cx); - editor.insert("cd", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cd56"); - assert_eq!(editor.selected_ranges(cx), vec![4..4]); - - editor.start_transaction_at(now, cx); - editor.select_ranges([4..5], None, cx); - editor.insert("e", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selected_ranges(cx), vec![5..5]); - - now += group_interval + Duration::from_millis(1); - editor.select_ranges([2..2], None, cx); - - // Simulate an edit in another editor - buffer.update(cx, |buffer, cx| { - buffer.start_transaction_at(now, cx); - buffer.edit([0..1], "a", cx); - buffer.edit([1..1], "b", cx); - buffer.end_transaction_at(now, cx); - }); - - assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selected_ranges(cx), vec![3..3]); - - // Last transaction happened past the group interval in a different editor. - // Undo it individually and don't restore selections. - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selected_ranges(cx), vec![2..2]); - - // First two transactions happened within the group interval in this editor. - // Undo them together and restore selections. - editor.undo(&Undo, cx); - editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. - assert_eq!(editor.text(cx), "123456"); - assert_eq!(editor.selected_ranges(cx), vec![0..0]); - - // Redo the first two transactions together. - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selected_ranges(cx), vec![5..5]); - - // Redo the last transaction on its own. - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selected_ranges(cx), vec![6..6]); - - // Test empty transactions. - editor.start_transaction_at(now, cx); - editor.end_transaction_at(now, cx); - editor.undo(&Undo, cx); - assert_eq!(editor.text(cx), "12cde6"); - }); - } - - #[gpui::test] - fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let settings = Settings::test(cx); - let (_, editor) = - cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - editor.update(cx, |view, cx| { - view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] - ); - - editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [ - DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) - ] - ); - - editor.update(cx, |view, cx| { - view.end_selection(cx); - }); - - assert_eq!( - editor.update(cx, |view, cx| view.selected_display_ranges(cx)), - [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] - ); - } - - #[gpui::test] - fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let settings = Settings::test(cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - }); - - view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - }); - } - - #[gpui::test] - fn test_navigation_history(cx: &mut gpui::MutableAppContext) { - cx.add_window(Default::default(), |cx| { - use workspace::ItemView; - let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), settings, cx); - editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle())); - - // Move the cursor a small distance. - // Nothing is added to the navigation history. - editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - editor.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); - assert!(nav_history.borrow_mut().pop_backward().is_none()); - - // Move the cursor a large distance. - // The history can jump back to the previous position. - editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); - let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] - ); - - // Move the cursor a small distance via the mouse. - // Nothing is added to the navigation history. - editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); - editor.end_selection(cx); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - assert!(nav_history.borrow_mut().pop_backward().is_none()); - - // Move the cursor a large distance via the mouse. - // The history can jump back to the previous position. - editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); - editor.end_selection(cx); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] - ); - let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); - assert_eq!( - editor.selected_display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - - editor - }); - } - - #[gpui::test] - fn test_cancel(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let settings = Settings::test(cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); - view.end_selection(cx); - - view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx); - view.end_selection(cx); - assert_eq!( - view.selected_display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), - ] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] - ); - }); - - view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - assert_eq!( - view.selected_display_ranges(cx), - [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] - ); - }); - } - - #[gpui::test] - fn test_fold(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple( - &" - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() { - 2 - } - - fn c() { - 3 - } - } - " - .unindent(), - cx, - ); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], cx); - view.fold(&Fold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() {… - } - - fn c() {… - } - } - " - .unindent(), - ); - - view.fold(&Fold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo {… - } - " - .unindent(), - ); - - view.unfold(&Unfold, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() {… - } - - fn c() {… - } - } - " - .unindent(), - ); - - view.unfold(&Unfold, cx); - assert_eq!(view.display_text(cx), buffer.read(cx).read(cx).text()); - }); - } - - #[gpui::test] - fn test_move_cursor(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - vec![ - Point::new(1, 0)..Point::new(1, 0), - Point::new(1, 1)..Point::new(1, 1), - ], - "\t", - cx, - ); - }); - - view.update(cx, |view, cx| { - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_to_end(&MoveToEnd, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] - ); - - view.move_to_beginning(&MoveToBeginning, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], cx); - view.select_to_beginning(&SelectToBeginning, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] - ); - - view.select_to_end(&SelectToEnd, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] - ); - }); - } - - #[gpui::test] - fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - assert_eq!('ⓐ'.len_utf8(), 3); - assert_eq!('α'.len_utf8(), 2); - - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 6)..Point::new(0, 12), - Point::new(1, 2)..Point::new(1, 4), - Point::new(2, 4)..Point::new(2, 8), - ], - cx, - ); - assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ…".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "ab…".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "ab".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "a".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "α".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβ…".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβ…ε".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "ab…e".len())] - ); - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ…ⓔ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ…".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - }); - } - - #[gpui::test] - fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - view.update(cx, |view, cx| { - view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx); - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(1, "abcd".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβγ".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(3, "abcd".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(3, "abcd".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[empty_range(2, "αβγ".len())] - ); - }); - } - - #[gpui::test] - fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("abc\n def", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ], - cx, - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_end_of_line(&MoveToEndOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - }); - - // Moving to the end of line again is a no-op. - view.update(cx, |view, cx| { - view.move_to_end_of_line(&MoveToEndOfLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_left(&MoveLeft, cx); - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_end_of_line(&SelectToEndOfLine(true), cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.delete_to_end_of_line(&DeleteToEndOfLine, cx); - assert_eq!(view.display_text(cx), "ab\n de"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ] - ); - }); - - view.update(cx, |view, cx| { - view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); - assert_eq!(view.display_text(cx), "\n"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - } - - #[gpui::test] - fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), - ], - cx, - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_right(&MoveRight, cx); - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] - ); - }); - } - - #[gpui::test] - fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.), cx); - assert_eq!( - view.display_text(cx), - "use one::{\n two::three::\n four::five\n};" - ); - - view.select_display_ranges(&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], cx); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] - ); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] - ); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] - ); - - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] - ); - - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] - ); - - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] - ); - }); - } - - #[gpui::test] - fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("one two three four", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the preceding word fragment is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // characters selected - they are deleted - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), - ], - cx, - ); - view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); - }); - - assert_eq!(buffer.read(cx).read(cx).text(), "e two te four"); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the following word fragment is deleted - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - // characters selected - they are deleted - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), - ], - cx, - ); - view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); - }); - - assert_eq!(buffer.read(cx).read(cx).text(), "e t te our"); - } - - #[gpui::test] - fn test_newline(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), - ], - cx, - ); - - view.newline(&Newline, cx); - assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); - }); - } - - #[gpui::test] - fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple( - " - a - b( - X - ) - c( - X - ) - " - .unindent() - .as_str(), - cx, - ); - - let settings = Settings::test(&cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - let mut editor = build_editor(buffer.clone(), settings, cx); - editor.select_ranges( - [ - Point::new(2, 4)..Point::new(2, 5), - Point::new(5, 4)..Point::new(5, 5), - ], - None, - cx, - ); - editor - }); - - // Edit the buffer directly, deleting ranges surrounding the editor's selections - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - Point::new(1, 2)..Point::new(3, 0), - Point::new(4, 2)..Point::new(6, 0), - ], - "", - cx, - ); - assert_eq!( - buffer.read(cx).text(), - " - a - b() - c() - " - .unindent() - ); - }); - - editor.update(cx, |editor, cx| { - assert_eq!( - editor.selected_ranges(cx), - &[ - Point::new(1, 2)..Point::new(1, 2), - Point::new(2, 2)..Point::new(2, 2), - ], - ); - - editor.newline(&Newline, cx); - assert_eq!( - editor.text(cx), - " - a - b( - ) - c( - ) - " - .unindent() - ); - - // The selections are moved after the inserted newlines - assert_eq!( - editor.selected_ranges(cx), - &[ - Point::new(2, 0)..Point::new(2, 0), - Point::new(4, 0)..Point::new(4, 0), - ], - ); - }); - } - - #[gpui::test] - fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - - let settings = Settings::test(&cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - let mut editor = build_editor(buffer.clone(), settings, cx); - editor.select_ranges([3..4, 11..12, 19..20], None, cx); - editor - }); - - // Edit the buffer directly, deleting ranges surrounding the editor's selections - buffer.update(cx, |buffer, cx| { - buffer.edit([2..5, 10..13, 18..21], "", cx); - assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); - }); - - editor.update(cx, |editor, cx| { - assert_eq!(editor.selected_ranges(cx), &[2..2, 7..7, 12..12],); - - editor.insert("Z", cx); - assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); - - // The selections are moved after the inserted characters - assert_eq!(editor.selected_ranges(cx), &[3..3, 9..9, 15..15],); - }); - } - - #[gpui::test] - fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple(" one two\nthree\n four", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - // two selections on the same line - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9), - ], - cx, - ); - - // indent from mid-tabstop to full tabstop - view.tab(&Tab, cx); - assert_eq!(view.text(cx), " one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11), - ] - ); - - // outdent from 1 tabstop to 0 tabstops - view.outdent(&Outdent, cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), - ] - ); - - // select across line ending - view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); - - // indent and outdent affect only the preceding line - view.tab(&Tab, cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] - ); - view.outdent(&Outdent, cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)] - ); - - // Ensure that indenting/outdenting works when the cursor is at column 0. - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.tab(&Tab, cx); - assert_eq!(view.text(cx), "one two\n three\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.outdent(&Outdent, cx); - assert_eq!(view.text(cx), "one two\nthree\n four"); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - }); - } - - #[gpui::test] - fn test_backspace(cx: &mut gpui::MutableAppContext) { - let buffer = - MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the preceding character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.backspace(&Backspace, cx); - }); - - assert_eq!( - buffer.read(cx).read(cx).text(), - "oe two three\nfou five six\nseven ten\n" - ); - } - - #[gpui::test] - fn test_delete(cx: &mut gpui::MutableAppContext) { - let buffer = - MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - // an empty selection - the following character is deleted - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - // one character selected - it is deleted - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - // a line suffix selected - it is deleted - DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.delete(&Delete, cx); - }); - - assert_eq!( - buffer.read(cx).read(cx).text(), - "on two three\nfou five six\nseven ten\n" - ); - } - - #[gpui::test] - fn test_delete_line(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi"); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) - ] - ); - }); - - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi\n"); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] - ); - }); - } - - #[gpui::test] - fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), - ] - ); - }); - - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), - ], - cx, - ); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), - DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[gpui::test] - fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - cx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), - ], - cx, - ); - assert_eq!( - view.display_text(cx), - "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" - ); - - view.move_line_up(&MoveLineUp, cx); - assert_eq!( - view.display_text(cx), - "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); - assert_eq!( - view.display_text(cx), - "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_down(&MoveLineDown, cx); - assert_eq!( - view.display_text(cx), - "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), - DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) - ] - ); - }); - - view.update(cx, |view, cx| { - view.move_line_up(&MoveLineUp, cx); - assert_eq!( - view.display_text(cx), - "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" - ); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), - DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) - ] - ); - }); - } - - #[gpui::test] - fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - let snapshot = buffer.read(cx).snapshot(cx); - let (_, editor) = - cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - editor.update(cx, |editor, cx| { - editor.insert_blocks( - [BlockProperties { - position: snapshot.anchor_after(Point::new(2, 0)), - disposition: BlockDisposition::Below, - height: 1, - render: Arc::new(|_| Empty::new().boxed()), - }], - cx, - ); - editor.select_ranges([Point::new(2, 0)..Point::new(2, 0)], None, cx); - editor.move_line_down(&MoveLineDown, cx); - }); - } - - #[gpui::test] - fn test_clipboard(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); - let settings = Settings::test(&cx); - let view = cx - .add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }) - .1; - - // Cut with three selections. Clipboard text is divided into three slices. - view.update(cx, |view, cx| { - view.select_ranges(vec![0..7, 11..17, 22..27], None, cx); - view.cut(&Cut, cx); - assert_eq!(view.display_text(cx), "two four six "); - }); - - // Paste with three cursors. Each cursor pastes one slice of the clipboard text. - view.update(cx, |view, cx| { - view.select_ranges(vec![4..4, 9..9, 13..13], None, cx); - view.paste(&Paste, cx); - assert_eq!(view.display_text(cx), "two one✅ four three six five "); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(0, 22)..DisplayPoint::new(0, 22), - DisplayPoint::new(0, 31)..DisplayPoint::new(0, 31) - ] - ); - }); - - // Paste again but with only two cursors. Since the number of cursors doesn't - // match the number of slices in the clipboard, the entire clipboard text - // is pasted at each cursor. - view.update(cx, |view, cx| { - view.select_ranges(vec![0..0, 31..31], None, cx); - view.handle_input(&Input("( ".into()), cx); - view.paste(&Paste, cx); - view.handle_input(&Input(") ".into()), cx); - assert_eq!( - view.display_text(cx), - "( one✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - }); - - view.update(cx, |view, cx| { - view.select_ranges(vec![0..0], None, cx); - view.handle_input(&Input("123\n4567\n89\n".into()), cx); - assert_eq!( - view.display_text(cx), - "123\n4567\n89\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - }); - - // Cut with three selections, one of which is full-line. - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), - ], - cx, - ); - view.cut(&Cut, cx); - assert_eq!( - view.display_text(cx), - "13\n9\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - }); - - // Paste with three selections, noticing how the copied selection that was full-line - // gets inserted before the second cursor. - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), - ], - cx, - ); - view.paste(&Paste, cx); - assert_eq!( - view.display_text(cx), - "123\n4567\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), - ] - ); - }); - - // Copy with a single cursor only, which writes the whole line into the clipboard. - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], cx); - view.copy(&Copy, cx); - }); - - // Paste with three selections, noticing how the copied full-line selection is inserted - // before the empty selections but replaces the selection that is non-empty. - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - ], - cx, - ); - view.paste(&Paste, cx); - assert_eq!( - view.display_text(cx), - "123\n123\n123\n67\n123\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " - ); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), - ] - ); - }); - } - - #[gpui::test] - fn test_select_all(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); - let settings = Settings::test(&cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_all(&SelectAll, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] - ); - }); - } - - #[gpui::test] - fn test_select_line(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ], - cx, - ); - view.select_line(&SelectLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), - DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_line(&SelectLine, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] - ); - }); - } - - #[gpui::test] - fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - view.update(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), - ], - cx, - ); - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), - ], - cx, - ); - assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i"); - }); - - view.update(cx, |view, cx| { - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); - assert_eq!( - view.display_text(cx), - "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i" - ); - assert_eq!( - view.selected_display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)], cx); - view.split_selection_into_lines(&SplitSelectionIntoLines, cx); - assert_eq!( - view.display_text(cx), - "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" - ); - assert_eq!( - view.selected_display_ranges(cx), - [ - DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), - DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), - DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), - DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), - DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), - DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) - ] - ); - }); - } - - #[gpui::test] - fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(&cx); - let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); - let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx); - }); - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], cx); - }); - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], cx); - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), - DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), - ] - ); - }); - - view.update(cx, |view, cx| { - view.select_display_ranges(&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], cx); - }); - view.update(cx, |view, cx| { - view.add_selection_above(&AddSelectionAbove, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - }); - - view.update(cx, |view, cx| { - view.add_selection_below(&AddSelectionBelow, cx); - assert_eq!( - view.selected_display_ranges(cx), - vec![ - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), - DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), - ] - ); - }); - } - - #[gpui::test] - async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - )); - - let text = r#" - use mod1::mod2::{mod3, mod4}; - - fn fn_1(param1: bool, param2: &str) { - let var1 = "text"; - } - "# - .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, settings, cx)); - view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(&mut cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ], - cx, - ); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] - ); - - // Trying to expand the selected syntax node one more time has no effect. - view.update(&mut cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] - ); - - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), - ] - ); - - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ] - ); - - // Trying to shrink the selected syntax node one more time has no effect. - view.update(&mut cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ] - ); - - // Ensure that we keep expanding the selection if the larger selection starts or ends within - // a fold. - view.update(&mut cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 21)..Point::new(0, 24), - Point::new(3, 20)..Point::new(3, 22), - ], - cx, - ); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), - DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), - ] - ); - } - - #[gpui::test] - async fn test_autoindent_selections(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new( - Language::new( - LanguageConfig { - brackets: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: false, - newline: true, - }, - BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: false, - newline: true, - }, - ], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_indents_query( - r#" - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap(), - ); - - let text = "fn a() {}"; - - 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 (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); - editor - .condition(&cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) - .await; - - editor.update(&mut cx, |editor, cx| { - editor.select_ranges([5..5, 8..8, 9..9], None, cx); - editor.newline(&Newline, cx); - assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); - assert_eq!( - editor.selected_ranges(cx), - &[ - Point::new(1, 4)..Point::new(1, 4), - Point::new(3, 4)..Point::new(3, 4), - Point::new(5, 0)..Point::new(5, 0) - ] - ); - }); - } - - #[gpui::test] - async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig { - brackets: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/*".to_string(), - end: " */".to_string(), - close: true, - newline: true, - }, - ], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = r#" - a - - / - - "# - .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, settings, cx)); - view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(&mut cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ], - cx, - ); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - view.handle_input(&Input("{".to_string()), cx); - assert_eq!( - view.text(cx), - " - {{{}}} - {{{}}} - / - - " - .unindent() - ); - - view.move_right(&MoveRight, cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); - view.handle_input(&Input("}".to_string()), cx); - assert_eq!( - view.text(cx), - " - {{{}}}} - {{{}}}} - / - - " - .unindent() - ); - - view.undo(&Undo, cx); - view.handle_input(&Input("/".to_string()), cx); - view.handle_input(&Input("*".to_string()), cx); - assert_eq!( - view.text(cx), - " - /* */ - /* */ - / - - " - .unindent() - ); - - view.undo(&Undo, cx); - view.select_display_ranges( - &[ - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ], - cx, - ); - view.handle_input(&Input("*".to_string()), cx); - assert_eq!( - view.text(cx), - " - a - - /* - * - " - .unindent() - ); - }); - } - - #[gpui::test] - async fn test_snippets(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - - let text = " - a. b - a. b - a. b - " - .unindent(); - let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); - - editor.update(&mut cx, |editor, cx| { - let buffer = &editor.snapshot(cx).buffer_snapshot; - let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); - let insertion_ranges = [ - Point::new(0, 2).to_offset(buffer)..Point::new(0, 2).to_offset(buffer), - Point::new(1, 2).to_offset(buffer)..Point::new(1, 2).to_offset(buffer), - Point::new(2, 2).to_offset(buffer)..Point::new(2, 2).to_offset(buffer), - ]; - - editor - .insert_snippet(&insertion_ranges, snippet, cx) - .unwrap(); - assert_eq!( - editor.text(cx), - " - a.f(one, two, three) b - a.f(one, two, three) b - a.f(one, two, three) b - " - .unindent() - ); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 4)..Point::new(0, 7), - Point::new(0, 14)..Point::new(0, 19), - Point::new(1, 4)..Point::new(1, 7), - Point::new(1, 14)..Point::new(1, 19), - Point::new(2, 4)..Point::new(2, 7), - Point::new(2, 14)..Point::new(2, 19), - ] - ); - - // Can't move earlier than the first tab stop - editor.move_to_prev_snippet_tabstop(cx); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 4)..Point::new(0, 7), - Point::new(0, 14)..Point::new(0, 19), - Point::new(1, 4)..Point::new(1, 7), - Point::new(1, 14)..Point::new(1, 19), - Point::new(2, 4)..Point::new(2, 7), - Point::new(2, 14)..Point::new(2, 19), - ] - ); - - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 9)..Point::new(0, 12), - Point::new(1, 9)..Point::new(1, 12), - Point::new(2, 9)..Point::new(2, 12) - ] - ); - - editor.move_to_prev_snippet_tabstop(cx); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 4)..Point::new(0, 7), - Point::new(0, 14)..Point::new(0, 19), - Point::new(1, 4)..Point::new(1, 7), - Point::new(1, 14)..Point::new(1, 19), - Point::new(2, 4)..Point::new(2, 7), - Point::new(2, 14)..Point::new(2, 19), - ] - ); - - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 20)..Point::new(0, 20), - Point::new(1, 20)..Point::new(1, 20), - Point::new(2, 20)..Point::new(2, 20) - ] - ); - - // As soon as the last tab stop is reached, snippet state is gone - editor.move_to_prev_snippet_tabstop(cx); - assert_eq!( - editor.selected_ranges::(cx), - &[ - Point::new(0, 20)..Point::new(0, 20), - Point::new(1, 20)..Point::new(1, 20), - Point::new(2, 20)..Point::new(2, 20) - ] - ); - }); - } - - #[gpui::test] - async fn test_completion(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let (language_server, mut fake) = cx.update(|cx| { - lsp::LanguageServer::fake_with_capabilities( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - }); - - let text = " - one - two - three - " - .unindent(); - - let fs = FakeFs::new(cx.background().clone()); - fs.insert_file("/file", text).await; - - let project = Project::test(fs, &mut cx); - - let (worktree, relative_path) = project - .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree("/file", false, cx) - }) - .await - .unwrap(); - let project_path = ProjectPath { - worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), - path: relative_path.into(), - }; - let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) - .await - .unwrap(); - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language_server(Some(language_server), cx); - }); - - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - buffer.next_notification(&cx).await; - - let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); - - editor.update(&mut cx, |editor, cx| { - editor.project = Some(project); - editor.select_ranges([Point::new(0, 3)..Point::new(0, 3)], None, cx); - editor.handle_input(&Input(".".to_string()), cx); - }); - - handle_completion_request( - &mut fake, - "/file", - Point::new(0, 4), - vec![ - (Point::new(0, 4)..Point::new(0, 4), "first_completion"), - (Point::new(0, 4)..Point::new(0, 4), "second_completion"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - let apply_additional_edits = editor.update(&mut cx, |editor, cx| { - editor.move_down(&MoveDown, cx); - let apply_additional_edits = editor - .confirm_completion(&ConfirmCompletion(None), cx) - .unwrap(); - assert_eq!( - editor.text(cx), - " - one.second_completion - two - three - " - .unindent() - ); - apply_additional_edits - }); - - handle_resolve_completion_request( - &mut fake, - Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")), - ) - .await; - apply_additional_edits.await.unwrap(); - assert_eq!( - editor.read_with(&cx, |editor, cx| editor.text(cx)), - " - one.second_completion - two - three - additional edit - " - .unindent() - ); - - editor.update(&mut cx, |editor, cx| { - editor.select_ranges( - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 5)..Point::new(2, 5), - ], - None, - cx, - ); - - editor.handle_input(&Input(" ".to_string()), cx); - assert!(editor.context_menu.is_none()); - editor.handle_input(&Input("s".to_string()), cx); - assert!(editor.context_menu.is_none()); - }); - - handle_completion_request( - &mut fake, - "/file", - Point::new(2, 7), - vec![ - (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"), - (Point::new(2, 6)..Point::new(2, 7), "fifth_completion"), - (Point::new(2, 6)..Point::new(2, 7), "sixth_completion"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - editor.update(&mut cx, |editor, cx| { - editor.handle_input(&Input("i".to_string()), cx); - }); - - handle_completion_request( - &mut fake, - "/file", - Point::new(2, 8), - vec![ - (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"), - (Point::new(2, 6)..Point::new(2, 8), "fifth_completion"), - (Point::new(2, 6)..Point::new(2, 8), "sixth_completion"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - let apply_additional_edits = editor.update(&mut cx, |editor, cx| { - let apply_additional_edits = editor - .confirm_completion(&ConfirmCompletion(None), cx) - .unwrap(); - assert_eq!( - editor.text(cx), - " - one.second_completion - two sixth_completion - three sixth_completion - additional edit - " - .unindent() - ); - apply_additional_edits - }); - handle_resolve_completion_request(&mut fake, None).await; - apply_additional_edits.await.unwrap(); - - async fn handle_completion_request( - fake: &mut FakeLanguageServer, - path: &'static str, - position: Point, - completions: Vec<(Range, &'static str)>, - ) { - fake.handle_request::(move |params, _| { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path).unwrap() - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(position.row, position.column) - ); - Some(lsp::CompletionResponse::Array( - completions - .iter() - .map(|(range, new_text)| lsp::CompletionItem { - label: new_text.to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.start.row, range.start.column), - ), - new_text: new_text.to_string(), - })), - ..Default::default() - }) - .collect(), - )) - }) - .next() - .await; - } - - async fn handle_resolve_completion_request( - fake: &mut FakeLanguageServer, - edit: Option<(Range, &'static str)>, - ) { - fake.handle_request::(move |_, _| { - lsp::CompletionItem { - additional_text_edits: edit.clone().map(|(range, new_text)| { - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.end.row, range.end.column), - ), - new_text.to_string(), - )] - }), - ..Default::default() - } - }) - .next() - .await; - } - } - - #[gpui::test] - async fn test_toggle_comment(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("// ".to_string()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = " - fn a() { - //b(); - // c(); - // d(); - } - " - .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, settings, cx)); - - view.update(&mut cx, |editor, cx| { - // If multiple selections intersect a line, the line is only - // toggled once. - editor.select_display_ranges( - &[ - DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3), - DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6), - ], - cx, - ); - editor.toggle_comments(&ToggleComments, cx); - assert_eq!( - editor.text(cx), - " - fn a() { - b(); - c(); - d(); - } - " - .unindent() - ); - - // The comment prefix is inserted at the same column for every line - // in a selection. - editor.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)], cx); - editor.toggle_comments(&ToggleComments, cx); - assert_eq!( - editor.text(cx), - " - fn a() { - // b(); - // c(); - // d(); - } - " - .unindent() - ); - - // If a selection ends at the beginning of a line, that line is not toggled. - editor.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)], cx); - editor.toggle_comments(&ToggleComments, cx); - assert_eq!( - editor.text(cx), - " - fn a() { - // b(); - c(); - // d(); - } - " - .unindent() - ); - }); - } - - #[gpui::test] - fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer.clone(), - [ - Point::new(0, 0)..Point::new(0, 4), - Point::new(1, 0)..Point::new(1, 4), - ], - cx, - ); - multibuffer - }); - - assert_eq!(multibuffer.read(cx).read(cx).text(), "aaaa\nbbbb"); - - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(multibuffer, settings, cx) - }); - view.update(cx, |view, cx| { - assert_eq!(view.text(cx), "aaaa\nbbbb"); - view.select_ranges( - [ - Point::new(0, 0)..Point::new(0, 0), - Point::new(1, 0)..Point::new(1, 0), - ], - None, - cx, - ); - - view.handle_input(&Input("X".to_string()), cx); - assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); - assert_eq!( - view.selected_ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - ] - ) - }); - } - - #[gpui::test] - fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer, - [ - Point::new(0, 0)..Point::new(1, 4), - Point::new(1, 0)..Point::new(2, 4), - ], - cx, - ); - multibuffer - }); - - assert_eq!( - multibuffer.read(cx).read(cx).text(), - "aaaa\nbbbb\nbbbb\ncccc" - ); - - let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(multibuffer, settings, cx) - }); - view.update(cx, |view, cx| { - view.select_ranges( - [ - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 3)..Point::new(2, 3), - ], - None, - cx, - ); - - view.handle_input(&Input("X".to_string()), cx); - assert_eq!(view.text(cx), "aaaa\nbXbbXb\nbXbbXb\ncccc"); - assert_eq!( - view.selected_ranges(cx), - [ - Point::new(1, 2)..Point::new(1, 2), - Point::new(2, 5)..Point::new(2, 5), - ] - ); - - view.newline(&Newline, cx); - assert_eq!(view.text(cx), "aaaa\nbX\nbbX\nb\nbX\nbbX\nb\ncccc"); - assert_eq!( - view.selected_ranges(cx), - [ - Point::new(2, 0)..Point::new(2, 0), - Point::new(6, 0)..Point::new(6, 0), - ] - ); - }); - } - - #[gpui::test] - fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { - let settings = Settings::test(cx); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); - let mut excerpt1_id = None; - let multibuffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - excerpt1_id = multibuffer - .push_excerpts( - buffer.clone(), - [ - Point::new(0, 0)..Point::new(1, 4), - Point::new(1, 0)..Point::new(2, 4), - ], - cx, - ) - .into_iter() - .next(); - multibuffer - }); - assert_eq!( - multibuffer.read(cx).read(cx).text(), - "aaaa\nbbbb\nbbbb\ncccc" - ); - let (_, editor) = cx.add_window(Default::default(), |cx| { - let mut editor = build_editor(multibuffer.clone(), settings, cx); - editor.select_ranges( - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ], - None, - cx, - ); - editor - }); - - // Refreshing selections is a no-op when excerpts haven't changed. - editor.update(cx, |editor, cx| { - editor.refresh_selections(cx); - assert_eq!( - editor.selected_ranges(cx), - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ] - ); - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); - }); - editor.update(cx, |editor, cx| { - // Removing an excerpt causes the first selection to become degenerate. - assert_eq!( - editor.selected_ranges(cx), - [ - Point::new(0, 0)..Point::new(0, 0), - Point::new(0, 1)..Point::new(0, 1) - ] - ); - - // Refreshing selections will relocate the first selection to the original buffer - // location. - editor.refresh_selections(cx); - assert_eq!( - editor.selected_ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(0, 3)..Point::new(0, 3) - ] - ); - }); - } - - #[gpui::test] - async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { - let settings = cx.read(Settings::test); - let language = Arc::new(Language::new( - LanguageConfig { - brackets: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/* ".to_string(), - end: " */".to_string(), - close: true, - newline: true, - }, - ], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = concat!( - "{ }\n", // Suppress rustfmt - " x\n", // - " /* */\n", // - "x\n", // - "{{} }\n", // - ); - - 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, settings, cx)); - view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - view.update(&mut cx, |view, cx| { - view.select_display_ranges( - &[ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), - ], - cx, - ); - view.newline(&Newline, cx); - - assert_eq!( - view.buffer().read(cx).read(cx).text(), - concat!( - "{ \n", // Suppress rustfmt - "\n", // - "}\n", // - " x\n", // - " /* \n", // - " \n", // - " */\n", // - "x\n", // - "{{} \n", // - "}\n", // - ) - ); - }); - } - - #[gpui::test] - fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { - let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - let settings = Settings::test(&cx); - let (_, editor) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) - }); - - editor.update(cx, |editor, cx| { - struct Type1; - struct Type2; - - let buffer = buffer.read(cx).snapshot(cx); - - let anchor_range = |range: Range| { - buffer.anchor_after(range.start)..buffer.anchor_after(range.end) - }; - - editor.highlight_ranges::( - vec![ - anchor_range(Point::new(2, 1)..Point::new(2, 3)), - anchor_range(Point::new(4, 2)..Point::new(4, 4)), - anchor_range(Point::new(6, 3)..Point::new(6, 5)), - anchor_range(Point::new(8, 4)..Point::new(8, 6)), - ], - Color::red(), - cx, - ); - editor.highlight_ranges::( - vec![ - anchor_range(Point::new(3, 2)..Point::new(3, 5)), - anchor_range(Point::new(5, 3)..Point::new(5, 6)), - anchor_range(Point::new(7, 4)..Point::new(7, 7)), - anchor_range(Point::new(9, 5)..Point::new(9, 8)), - ], - Color::green(), - cx, - ); - - let snapshot = editor.snapshot(cx); - let mut highlighted_ranges = editor.highlighted_ranges_in_range( - anchor_range(Point::new(3, 4)..Point::new(7, 4)), - &snapshot, - ); - // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-deterministic. - highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); - assert_eq!( - highlighted_ranges, - &[ - ( - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), - Color::green(), - ), - ( - DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), - Color::green(), - ), - ( - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), - Color::red(), - ), - ( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), - ), - ] - ); - assert_eq!( - editor.highlighted_ranges_in_range( - anchor_range(Point::new(5, 6)..Point::new(6, 4)), - &snapshot, - ), - &[( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Color::red(), - )] - ); - }); - } - - #[test] - fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let default = HighlightStyle::default(); - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..8, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - &string, - default, - syntax_ranges.into_iter(), - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Color::red(), - ..default - }, - ), - ( - 4..5, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 5..6, - HighlightStyle { - color: Color::green(), - ..default - }, - ), - ( - 6..8, - HighlightStyle { - color: Color::green(), - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ( - 8..9, - HighlightStyle { - font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), - ..default - }, - ), - ] - ); - } - - fn empty_range(row: usize, column: usize) -> Range { - let point = DisplayPoint::new(row as u32, column as u32); - point..point - } - - fn build_editor( - buffer: ModelHandle, - settings: Settings, - cx: &mut ViewContext, - ) -> Editor { - let settings = watch::channel_with(settings); - Editor::new(EditorMode::Full, buffer, None, settings.1, None, cx) - } -} - -trait RangeExt { - fn sorted(&self) -> Range; - fn to_inclusive(&self) -> RangeInclusive; -} - -impl RangeExt for Range { - fn sorted(&self) -> Self { - cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() - } - - fn to_inclusive(&self) -> RangeInclusive { - self.start.clone()..=self.end.clone() - } -} +pub mod display_map; +mod element; +pub mod items; +pub mod movement; +mod multi_buffer; + +#[cfg(test)] +mod test; + +use aho_corasick::AhoCorasick; +use anyhow::Result; +use clock::ReplicaId; +use collections::{BTreeMap, Bound, HashMap, HashSet}; +pub use display_map::DisplayPoint; +use display_map::*; +pub use element::*; +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + action, + color::Color, + elements::*, + executor, + fonts::{self, HighlightStyle, TextStyle}, + geometry::vector::{vec2f, Vector2F}, + keymap::Binding, + platform::CursorStyle, + text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + WeakViewHandle, +}; +use items::{BufferItemHandle, MultiBufferItemHandle}; +use itertools::Itertools as _; +pub use language::{char_kind, CharKind}; +use language::{ + AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, + DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, +}; +use multi_buffer::MultiBufferChunks; +pub use multi_buffer::{ + Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, +}; +use ordered_float::OrderedFloat; +use postage::watch; +use project::{Project, ProjectTransaction}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use smol::Timer; +use snippet::Snippet; +use std::{ + any::TypeId, + cmp::{self, Ordering, Reverse}, + iter::{self, FromIterator}, + mem, + ops::{Deref, DerefMut, Range, RangeInclusive, Sub}, + sync::Arc, + time::{Duration, Instant}, +}; +pub use sum_tree::Bias; +use text::rope::TextDimension; +use theme::DiagnosticStyle; +use util::{post_inc, ResultExt, TryFutureExt}; +use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace}; + +const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const MAX_LINE_LEN: usize = 1024; +const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; + +action!(Cancel); +action!(Backspace); +action!(Delete); +action!(Input, String); +action!(Newline); +action!(Tab); +action!(Outdent); +action!(DeleteLine); +action!(DeleteToPreviousWordBoundary); +action!(DeleteToNextWordBoundary); +action!(DeleteToBeginningOfLine); +action!(DeleteToEndOfLine); +action!(CutToEndOfLine); +action!(DuplicateLine); +action!(MoveLineUp); +action!(MoveLineDown); +action!(Cut); +action!(Copy); +action!(Paste); +action!(Undo); +action!(Redo); +action!(MoveUp); +action!(MoveDown); +action!(MoveLeft); +action!(MoveRight); +action!(MoveToPreviousWordBoundary); +action!(MoveToNextWordBoundary); +action!(MoveToBeginningOfLine); +action!(MoveToEndOfLine); +action!(MoveToBeginning); +action!(MoveToEnd); +action!(SelectUp); +action!(SelectDown); +action!(SelectLeft); +action!(SelectRight); +action!(SelectToPreviousWordBoundary); +action!(SelectToNextWordBoundary); +action!(SelectToBeginningOfLine, bool); +action!(SelectToEndOfLine, bool); +action!(SelectToBeginning); +action!(SelectToEnd); +action!(SelectAll); +action!(SelectLine); +action!(SplitSelectionIntoLines); +action!(AddSelectionAbove); +action!(AddSelectionBelow); +action!(SelectNext, bool); +action!(ToggleComments); +action!(SelectLargerSyntaxNode); +action!(SelectSmallerSyntaxNode); +action!(MoveToEnclosingBracket); +action!(ShowNextDiagnostic); +action!(GoToDefinition); +action!(FindAllReferences); +action!(Rename); +action!(ConfirmRename); +action!(PageUp); +action!(PageDown); +action!(Fold); +action!(Unfold); +action!(FoldSelectedRanges); +action!(Scroll, Vector2F); +action!(Select, SelectPhase); +action!(ShowCompletions); +action!(ToggleCodeActions, bool); +action!(ConfirmCompletion, Option); +action!(ConfirmCodeAction, Option); +action!(OpenExcerpts); + +pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { + path_openers.push(Box::new(items::BufferOpener)); + cx.add_bindings(vec![ + Binding::new("escape", Cancel, Some("Editor")), + Binding::new("backspace", Backspace, Some("Editor")), + Binding::new("ctrl-h", Backspace, Some("Editor")), + Binding::new("delete", Delete, Some("Editor")), + Binding::new("ctrl-d", Delete, Some("Editor")), + Binding::new("enter", Newline, Some("Editor && mode == full")), + Binding::new( + "alt-enter", + Input("\n".into()), + Some("Editor && mode == auto_height"), + ), + Binding::new( + "enter", + ConfirmCompletion(None), + Some("Editor && showing_completions"), + ), + Binding::new( + "enter", + ConfirmCodeAction(None), + Some("Editor && showing_code_actions"), + ), + Binding::new("enter", ConfirmRename, Some("Editor && renaming")), + Binding::new("tab", Tab, Some("Editor")), + Binding::new( + "tab", + ConfirmCompletion(None), + Some("Editor && showing_completions"), + ), + Binding::new("shift-tab", Outdent, Some("Editor")), + Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), + Binding::new( + "alt-backspace", + DeleteToPreviousWordBoundary, + Some("Editor"), + ), + Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")), + Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")), + Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")), + Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")), + Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")), + Binding::new("cmd-shift-D", DuplicateLine, Some("Editor")), + Binding::new("ctrl-cmd-up", MoveLineUp, Some("Editor")), + Binding::new("ctrl-cmd-down", MoveLineDown, Some("Editor")), + Binding::new("cmd-x", Cut, Some("Editor")), + Binding::new("cmd-c", Copy, Some("Editor")), + Binding::new("cmd-v", Paste, Some("Editor")), + Binding::new("cmd-z", Undo, Some("Editor")), + Binding::new("cmd-shift-Z", Redo, Some("Editor")), + Binding::new("up", MoveUp, Some("Editor")), + Binding::new("down", MoveDown, Some("Editor")), + Binding::new("left", MoveLeft, Some("Editor")), + Binding::new("right", MoveRight, Some("Editor")), + Binding::new("ctrl-p", MoveUp, Some("Editor")), + Binding::new("ctrl-n", MoveDown, Some("Editor")), + Binding::new("ctrl-b", MoveLeft, Some("Editor")), + Binding::new("ctrl-f", MoveRight, Some("Editor")), + Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")), + Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")), + Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")), + Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")), + Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")), + Binding::new("ctrl-e", MoveToEndOfLine, Some("Editor")), + Binding::new("cmd-up", MoveToBeginning, Some("Editor")), + Binding::new("cmd-down", MoveToEnd, Some("Editor")), + Binding::new("shift-up", SelectUp, Some("Editor")), + Binding::new("ctrl-shift-P", SelectUp, Some("Editor")), + Binding::new("shift-down", SelectDown, Some("Editor")), + Binding::new("ctrl-shift-N", SelectDown, Some("Editor")), + Binding::new("shift-left", SelectLeft, Some("Editor")), + Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")), + Binding::new("shift-right", SelectRight, Some("Editor")), + Binding::new("ctrl-shift-F", SelectRight, Some("Editor")), + Binding::new( + "alt-shift-left", + SelectToPreviousWordBoundary, + Some("Editor"), + ), + Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")), + Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")), + Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")), + Binding::new( + "cmd-shift-left", + SelectToBeginningOfLine(true), + Some("Editor"), + ), + Binding::new( + "ctrl-shift-A", + SelectToBeginningOfLine(true), + Some("Editor"), + ), + Binding::new("cmd-shift-right", SelectToEndOfLine(true), Some("Editor")), + Binding::new("ctrl-shift-E", SelectToEndOfLine(true), Some("Editor")), + Binding::new("cmd-shift-up", SelectToBeginning, Some("Editor")), + Binding::new("cmd-shift-down", SelectToEnd, Some("Editor")), + Binding::new("cmd-a", SelectAll, Some("Editor")), + Binding::new("cmd-l", SelectLine, Some("Editor")), + Binding::new("cmd-shift-L", SplitSelectionIntoLines, Some("Editor")), + Binding::new("cmd-alt-up", AddSelectionAbove, Some("Editor")), + Binding::new("cmd-ctrl-p", AddSelectionAbove, Some("Editor")), + Binding::new("cmd-alt-down", AddSelectionBelow, Some("Editor")), + Binding::new("cmd-ctrl-n", AddSelectionBelow, Some("Editor")), + Binding::new("cmd-d", SelectNext(false), Some("Editor")), + Binding::new("cmd-k cmd-d", SelectNext(true), Some("Editor")), + Binding::new("cmd-/", ToggleComments, Some("Editor")), + Binding::new("alt-up", SelectLargerSyntaxNode, Some("Editor")), + Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), + Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("f8", ShowNextDiagnostic, Some("Editor")), + Binding::new("f2", Rename, Some("Editor")), + Binding::new("f12", GoToDefinition, Some("Editor")), + Binding::new("alt-shift-f12", FindAllReferences, Some("Editor")), + Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")), + Binding::new("pageup", PageUp, Some("Editor")), + Binding::new("pagedown", PageDown, Some("Editor")), + Binding::new("alt-cmd-[", Fold, Some("Editor")), + Binding::new("alt-cmd-]", Unfold, Some("Editor")), + Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")), + Binding::new("ctrl-space", ShowCompletions, Some("Editor")), + Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")), + Binding::new("alt-enter", OpenExcerpts, Some("Editor")), + ]); + + cx.add_action(Editor::open_new); + cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx)); + cx.add_action(Editor::select); + cx.add_action(Editor::cancel); + cx.add_action(Editor::handle_input); + cx.add_action(Editor::newline); + cx.add_action(Editor::backspace); + cx.add_action(Editor::delete); + cx.add_action(Editor::tab); + cx.add_action(Editor::outdent); + cx.add_action(Editor::delete_line); + cx.add_action(Editor::delete_to_previous_word_boundary); + cx.add_action(Editor::delete_to_next_word_boundary); + cx.add_action(Editor::delete_to_beginning_of_line); + cx.add_action(Editor::delete_to_end_of_line); + cx.add_action(Editor::cut_to_end_of_line); + cx.add_action(Editor::duplicate_line); + cx.add_action(Editor::move_line_up); + cx.add_action(Editor::move_line_down); + cx.add_action(Editor::cut); + cx.add_action(Editor::copy); + cx.add_action(Editor::paste); + cx.add_action(Editor::undo); + cx.add_action(Editor::redo); + cx.add_action(Editor::move_up); + cx.add_action(Editor::move_down); + cx.add_action(Editor::move_left); + cx.add_action(Editor::move_right); + cx.add_action(Editor::move_to_previous_word_boundary); + cx.add_action(Editor::move_to_next_word_boundary); + cx.add_action(Editor::move_to_beginning_of_line); + cx.add_action(Editor::move_to_end_of_line); + cx.add_action(Editor::move_to_beginning); + cx.add_action(Editor::move_to_end); + cx.add_action(Editor::select_up); + cx.add_action(Editor::select_down); + cx.add_action(Editor::select_left); + cx.add_action(Editor::select_right); + cx.add_action(Editor::select_to_previous_word_boundary); + cx.add_action(Editor::select_to_next_word_boundary); + cx.add_action(Editor::select_to_beginning_of_line); + cx.add_action(Editor::select_to_end_of_line); + cx.add_action(Editor::select_to_beginning); + cx.add_action(Editor::select_to_end); + cx.add_action(Editor::select_all); + cx.add_action(Editor::select_line); + cx.add_action(Editor::split_selection_into_lines); + cx.add_action(Editor::add_selection_above); + cx.add_action(Editor::add_selection_below); + cx.add_action(Editor::select_next); + cx.add_action(Editor::toggle_comments); + cx.add_action(Editor::select_larger_syntax_node); + cx.add_action(Editor::select_smaller_syntax_node); + cx.add_action(Editor::move_to_enclosing_bracket); + cx.add_action(Editor::show_next_diagnostic); + cx.add_action(Editor::go_to_definition); + cx.add_action(Editor::page_up); + cx.add_action(Editor::page_down); + cx.add_action(Editor::fold); + cx.add_action(Editor::unfold); + cx.add_action(Editor::fold_selected_ranges); + cx.add_action(Editor::show_completions); + cx.add_action(Editor::toggle_code_actions); + cx.add_action(Editor::open_excerpts); + cx.add_async_action(Editor::confirm_completion); + cx.add_async_action(Editor::confirm_code_action); + cx.add_async_action(Editor::rename); + cx.add_async_action(Editor::confirm_rename); + cx.add_async_action(Editor::find_all_references); +} + +trait SelectionExt { + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn display_range(&self, map: &DisplaySnapshot) -> Range; + fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) + -> Range; +} + +trait InvalidationRegion { + fn ranges(&self) -> &[Range]; +} + +#[derive(Clone, Debug)] +pub enum SelectPhase { + Begin { + position: DisplayPoint, + add: bool, + click_count: usize, + }, + BeginColumnar { + position: DisplayPoint, + overshoot: u32, + }, + Extend { + position: DisplayPoint, + click_count: usize, + }, + Update { + position: DisplayPoint, + overshoot: u32, + scroll_position: Vector2F, + }, + End, +} + +#[derive(Clone, Debug)] +pub enum SelectMode { + Character, + Word(Range), + Line(Range), + All, +} + +#[derive(PartialEq, Eq)] +pub enum Autoscroll { + Fit, + Center, + Newest, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum EditorMode { + SingleLine, + AutoHeight { max_lines: usize }, + Full, +} + +#[derive(Clone)] +pub enum SoftWrap { + None, + EditorWidth, + Column(u32), +} + +#[derive(Clone)] +pub struct EditorStyle { + pub text: TextStyle, + pub placeholder_text: Option, + pub theme: theme::Editor, +} + +type CompletionId = usize; + +pub type GetFieldEditorTheme = fn(&theme::Theme) -> theme::FieldEditor; + +pub struct Editor { + handle: WeakViewHandle, + buffer: ModelHandle, + display_map: ModelHandle, + next_selection_id: usize, + selections: Arc<[Selection]>, + pending_selection: Option, + columnar_selection_tail: Option, + add_selections_state: Option, + select_next_state: Option, + selection_history: + HashMap]>, Option]>>)>, + autoclose_stack: InvalidationStack, + snippet_stack: InvalidationStack, + select_larger_syntax_node_stack: Vec]>>, + active_diagnostics: Option, + scroll_position: Vector2F, + scroll_top_anchor: Option, + autoscroll_request: Option, + settings: watch::Receiver, + soft_wrap_mode_override: Option, + get_field_editor_theme: Option, + project: Option>, + focused: bool, + show_local_cursors: bool, + blink_epoch: usize, + blinking_paused: bool, + mode: EditorMode, + vertical_scroll_margin: f32, + placeholder_text: Option>, + highlighted_rows: Option>, + highlighted_ranges: BTreeMap>)>, + nav_history: Option, + context_menu: Option, + completion_tasks: Vec<(CompletionId, Task>)>, + next_completion_id: CompletionId, + available_code_actions: Option<(ModelHandle, Arc<[CodeAction]>)>, + code_actions_task: Option>, + document_highlights_task: Option>, + pending_rename: Option, + searchable: bool, +} + +pub struct EditorSnapshot { + pub mode: EditorMode, + pub display_snapshot: DisplaySnapshot, + pub placeholder_text: Option>, + is_focused: bool, + scroll_position: Vector2F, + scroll_top_anchor: Option, +} + +#[derive(Clone)] +pub struct PendingSelection { + selection: Selection, + mode: SelectMode, +} + +struct AddSelectionsState { + above: bool, + stack: Vec, +} + +struct SelectNextState { + query: AhoCorasick, + wordwise: bool, + done: bool, +} + +struct BracketPairState { + ranges: Vec>, + pair: BracketPair, +} + +struct SnippetState { + ranges: Vec>>, + active_index: usize, +} + +pub struct RenameState { + pub range: Range, + pub old_name: String, + pub editor: ViewHandle, + block_id: BlockId, +} + +struct InvalidationStack(Vec); + +enum ContextMenu { + Completions(CompletionsMenu), + CodeActions(CodeActionsMenu), +} + +impl ContextMenu { + fn select_prev(&mut self, cx: &mut ViewContext) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_prev(cx), + ContextMenu::CodeActions(menu) => menu.select_prev(cx), + } + true + } else { + false + } + } + + fn select_next(&mut self, cx: &mut ViewContext) -> bool { + if self.visible() { + match self { + ContextMenu::Completions(menu) => menu.select_next(cx), + ContextMenu::CodeActions(menu) => menu.select_next(cx), + } + true + } else { + false + } + } + + fn visible(&self) -> bool { + match self { + ContextMenu::Completions(menu) => menu.visible(), + ContextMenu::CodeActions(menu) => menu.visible(), + } + } + + fn render( + &self, + cursor_position: DisplayPoint, + style: EditorStyle, + cx: &AppContext, + ) -> (DisplayPoint, ElementBox) { + match self { + ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style), + } + } +} + +struct CompletionsMenu { + id: CompletionId, + initial_position: Anchor, + buffer: ModelHandle, + completions: Arc<[Completion]>, + match_candidates: Vec, + matches: Arc<[StringMatch]>, + selected_item: usize, + list: UniformListState, +} + +impl CompletionsMenu { + fn select_prev(&mut self, cx: &mut ViewContext) { + if self.selected_item > 0 { + self.selected_item -= 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + } + cx.notify(); + } + + fn select_next(&mut self, cx: &mut ViewContext) { + if self.selected_item + 1 < self.matches.len() { + self.selected_item += 1; + self.list.scroll_to(ScrollTarget::Show(self.selected_item)); + } + cx.notify(); + } + + fn visible(&self) -> bool { + !self.matches.is_empty() + } + + fn render(&self, style: EditorStyle, _: &AppContext) -> ElementBox { + enum CompletionTag {} + + let completions = self.completions.clone(); + let matches = self.matches.clone(); + let selected_item = self.selected_item; + let container_style = style.autocomplete.container; + UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| { + let start_ix = range.start; + for (ix, mat) in matches[range].iter().enumerate() { + let completion = &completions[mat.candidate_id]; + let item_ix = start_ix + ix; + items.push( + MouseEventHandler::new::( + mat.candidate_id, + cx, + |state, _| { + let item_style = if item_ix == selected_item { + style.autocomplete.selected_item + } else if state.hovered { + style.autocomplete.hovered_item + } else { + style.autocomplete.item + }; + + Text::new(completion.label.text.clone(), style.text.clone()) + .with_soft_wrap(false) + .with_highlights(combine_syntax_and_fuzzy_match_highlights( + &completion.label.text, + style.text.color.into(), + styled_runs_for_code_label( + &completion.label, + style.text.color, + &style.syntax, + ), + &mat.positions, + )) + .contained() + .with_style(item_style) + .boxed() + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(move |cx| { + cx.dispatch_action(ConfirmCompletion(Some(item_ix))); + }) + .boxed(), + ); + } + }) + .with_width_from_item( + self.matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + self.completions[mat.candidate_id] + .label + .text + .chars() + .count() + }) + .map(|(ix, _)| ix), + ) + .contained() + .with_style(container_style) + .boxed() + } + + pub async fn filter(&mut self, query: Option<&str>, executor: Arc) { + let mut matches = if let Some(query) = query { + fuzzy::match_strings( + &self.match_candidates, + query, + false, + 100, + &Default::default(), + executor, + ) + .await + } else { + self.match_candidates + .iter() + .enumerate() + .map(|(candidate_id, candidate)| StringMatch { + candidate_id, + score: Default::default(), + positions: Default::default(), + string: candidate.string.clone(), + }) + .collect() + }; + matches.sort_unstable_by_key(|mat| { + ( + Reverse(OrderedFloat(mat.score)), + self.completions[mat.candidate_id].sort_key(), + ) + }); + + for mat in &mut matches { + let filter_start = self.completions[mat.candidate_id].label.filter_range.start; + for position in &mut mat.positions { + *position += filter_start; + } + } + + self.matches = matches.into(); + } +} + +#[derive(Clone)] +struct CodeActionsMenu { + actions: Arc<[CodeAction]>, + buffer: ModelHandle, + selected_item: usize, + list: UniformListState, + deployed_from_indicator: bool, +} + +impl CodeActionsMenu { + fn select_prev(&mut self, cx: &mut ViewContext) { + if self.selected_item > 0 { + self.selected_item -= 1; + cx.notify() + } + } + + fn select_next(&mut self, cx: &mut ViewContext) { + if self.selected_item + 1 < self.actions.len() { + self.selected_item += 1; + cx.notify() + } + } + + fn visible(&self) -> bool { + !self.actions.is_empty() + } + + fn render( + &self, + mut cursor_position: DisplayPoint, + style: EditorStyle, + ) -> (DisplayPoint, ElementBox) { + enum ActionTag {} + + let container_style = style.autocomplete.container; + let actions = self.actions.clone(); + let selected_item = self.selected_item; + let element = + UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| { + let start_ix = range.start; + for (ix, action) in actions[range].iter().enumerate() { + let item_ix = start_ix + ix; + items.push( + MouseEventHandler::new::(item_ix, cx, |state, _| { + let item_style = if item_ix == selected_item { + style.autocomplete.selected_item + } else if state.hovered { + style.autocomplete.hovered_item + } else { + style.autocomplete.item + }; + + Text::new(action.lsp_action.title.clone(), style.text.clone()) + .with_soft_wrap(false) + .contained() + .with_style(item_style) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_mouse_down(move |cx| { + cx.dispatch_action(ConfirmCodeAction(Some(item_ix))); + }) + .boxed(), + ); + } + }) + .with_width_from_item( + self.actions + .iter() + .enumerate() + .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) + .map(|(ix, _)| ix), + ) + .contained() + .with_style(container_style) + .boxed(); + + if self.deployed_from_indicator { + *cursor_position.column_mut() = 0; + } + + (cursor_position, element) + } +} + +#[derive(Debug)] +struct ActiveDiagnosticGroup { + primary_range: Range, + primary_message: String, + blocks: HashMap, + is_valid: bool, +} + +#[derive(Serialize, Deserialize)] +struct ClipboardSelection { + len: usize, + is_entire_line: bool, +} + +pub struct NavigationData { + anchor: Anchor, + offset: usize, +} + +impl Editor { + pub fn single_line( + settings: watch::Receiver, + field_editor_style: Option, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::SingleLine, + buffer, + None, + settings, + field_editor_style, + cx, + ) + } + + pub fn auto_height( + max_lines: usize, + settings: watch::Receiver, + field_editor_style: Option, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new( + EditorMode::AutoHeight { max_lines }, + buffer, + None, + settings, + field_editor_style, + cx, + ) + } + + pub fn for_buffer( + buffer: ModelHandle, + project: Option>, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + Self::new(EditorMode::Full, buffer, project, settings, None, cx) + } + + pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { + let mut clone = Self::new( + self.mode, + self.buffer.clone(), + self.project.clone(), + self.settings.clone(), + self.get_field_editor_theme, + cx, + ); + clone.scroll_position = self.scroll_position; + clone.scroll_top_anchor = self.scroll_top_anchor.clone(); + clone.nav_history = Some(nav_history); + clone + } + + fn new( + mode: EditorMode, + buffer: ModelHandle, + project: Option>, + settings: watch::Receiver, + get_field_editor_theme: Option, + cx: &mut ViewContext, + ) -> Self { + let display_map = cx.add_model(|cx| { + let settings = settings.borrow(); + let style = build_style(&*settings, get_field_editor_theme, cx); + DisplayMap::new( + buffer.clone(), + settings.tab_size, + style.text.font_id, + style.text.font_size, + None, + 2, + 1, + cx, + ) + }); + cx.observe(&buffer, Self::on_buffer_changed).detach(); + cx.subscribe(&buffer, Self::on_buffer_event).detach(); + cx.observe(&display_map, Self::on_display_map_changed) + .detach(); + + let mut this = Self { + handle: cx.weak_handle(), + buffer, + display_map, + selections: Arc::from([]), + pending_selection: Some(PendingSelection { + selection: Selection { + id: 0, + start: Anchor::min(), + end: Anchor::min(), + reversed: false, + goal: SelectionGoal::None, + }, + mode: SelectMode::Character, + }), + columnar_selection_tail: None, + next_selection_id: 1, + add_selections_state: None, + select_next_state: None, + selection_history: Default::default(), + autoclose_stack: Default::default(), + snippet_stack: Default::default(), + select_larger_syntax_node_stack: Vec::new(), + active_diagnostics: None, + settings, + soft_wrap_mode_override: None, + get_field_editor_theme, + project, + scroll_position: Vector2F::zero(), + scroll_top_anchor: None, + autoscroll_request: None, + focused: false, + show_local_cursors: false, + blink_epoch: 0, + blinking_paused: false, + mode, + vertical_scroll_margin: 3.0, + placeholder_text: None, + highlighted_rows: None, + highlighted_ranges: Default::default(), + nav_history: None, + context_menu: None, + completion_tasks: Default::default(), + next_completion_id: 0, + available_code_actions: Default::default(), + code_actions_task: Default::default(), + document_highlights_task: Default::default(), + pending_rename: Default::default(), + searchable: true, + }; + this.end_selection(cx); + this + } + + pub fn open_new( + workspace: &mut Workspace, + _: &workspace::OpenNew, + cx: &mut ViewContext, + ) { + let buffer = cx + .add_model(|cx| Buffer::new(0, "", cx).with_language(language::PLAIN_TEXT.clone(), cx)); + workspace.open_item(BufferItemHandle(buffer), cx); + } + + pub fn replica_id(&self, cx: &AppContext) -> ReplicaId { + self.buffer.read(cx).replica_id() + } + + pub fn buffer(&self) -> &ModelHandle { + &self.buffer + } + + pub fn title(&self, cx: &AppContext) -> String { + self.buffer().read(cx).title(cx) + } + + pub fn snapshot(&mut self, cx: &mut MutableAppContext) -> EditorSnapshot { + EditorSnapshot { + mode: self.mode, + display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), + scroll_position: self.scroll_position, + scroll_top_anchor: self.scroll_top_anchor.clone(), + placeholder_text: self.placeholder_text.clone(), + is_focused: self + .handle + .upgrade(cx) + .map_or(false, |handle| handle.is_focused(cx)), + } + } + + pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc> { + self.buffer.read(cx).language(cx) + } + + fn style(&self, cx: &AppContext) -> EditorStyle { + build_style(&*self.settings.borrow(), self.get_field_editor_theme, cx) + } + + pub fn set_placeholder_text( + &mut self, + placeholder_text: impl Into>, + cx: &mut ViewContext, + ) { + self.placeholder_text = Some(placeholder_text.into()); + cx.notify(); + } + + pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext) { + self.vertical_scroll_margin = margin_rows as f32; + cx.notify(); + } + + pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if scroll_position.y() == 0. { + self.scroll_top_anchor = None; + self.scroll_position = scroll_position; + } else { + let scroll_top_buffer_offset = + DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); + let anchor = map + .buffer_snapshot + .anchor_at(scroll_top_buffer_offset, Bias::Right); + self.scroll_position = vec2f( + scroll_position.x(), + scroll_position.y() - anchor.to_display_point(&map).row() as f32, + ); + self.scroll_top_anchor = Some(anchor); + } + + cx.notify(); + } + + pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) + } + + pub fn clamp_scroll_left(&mut self, max: f32) -> bool { + if max < self.scroll_position.x() { + self.scroll_position.set_x(max); + true + } else { + false + } + } + + pub fn autoscroll_vertically( + &mut self, + viewport_height: f32, + line_height: f32, + cx: &mut ViewContext, + ) -> bool { + let visible_lines = viewport_height / line_height; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut scroll_position = + compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor); + let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) + } else { + display_map.max_point().row().saturating_sub(1) as f32 + }; + if scroll_position.y() > max_scroll_top { + scroll_position.set_y(max_scroll_top); + self.set_scroll_position(scroll_position, cx); + } + + let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() { + autoscroll + } else { + return false; + }; + + let first_cursor_top; + let last_cursor_bottom; + if let Some(highlighted_rows) = &self.highlighted_rows { + first_cursor_top = highlighted_rows.start as f32; + last_cursor_bottom = first_cursor_top + 1.; + } else if autoscroll == Autoscroll::Newest { + let newest_selection = self.newest_selection::(&display_map.buffer_snapshot); + first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; + last_cursor_bottom = first_cursor_top + 1.; + } else { + let selections = self.local_selections::(cx); + first_cursor_top = selections + .first() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + last_cursor_bottom = selections + .last() + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32 + + 1.0; + } + + let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { + 0. + } else { + ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() + }; + if margin < 0.0 { + return false; + } + + match autoscroll { + Autoscroll::Fit | Autoscroll::Newest => { + let margin = margin.min(self.vertical_scroll_margin); + let target_top = (first_cursor_top - margin).max(0.0); + let target_bottom = last_cursor_bottom + margin; + let start_row = scroll_position.y(); + let end_row = start_row + visible_lines; + + if target_top < start_row { + scroll_position.set_y(target_top); + self.set_scroll_position(scroll_position, cx); + } else if target_bottom >= end_row { + scroll_position.set_y(target_bottom - visible_lines); + self.set_scroll_position(scroll_position, cx); + } + } + Autoscroll::Center => { + scroll_position.set_y((first_cursor_top - margin).max(0.0)); + self.set_scroll_position(scroll_position, cx); + } + } + + true + } + + pub fn autoscroll_horizontally( + &mut self, + start_row: u32, + viewport_width: f32, + scroll_width: f32, + max_glyph_width: f32, + layouts: &[text_layout::Line], + cx: &mut ViewContext, + ) -> bool { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.local_selections::(cx); + + let mut target_left; + let mut target_right; + + if self.highlighted_rows.is_some() { + target_left = 0.0_f32; + target_right = 0.0_f32; + } else { + target_left = std::f32::INFINITY; + target_right = 0.0_f32; + for selection in selections { + let head = selection.head().to_display_point(&display_map); + if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 { + let start_column = head.column().saturating_sub(3); + let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); + target_left = target_left.min( + layouts[(head.row() - start_row) as usize] + .x_for_index(start_column as usize), + ); + target_right = target_right.max( + layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize) + + max_glyph_width, + ); + } + } + } + + target_right = target_right.min(scroll_width); + + if target_right - target_left > viewport_width { + return false; + } + + let scroll_left = self.scroll_position.x() * max_glyph_width; + let scroll_right = scroll_left + viewport_width; + + if target_left < scroll_left { + self.scroll_position.set_x(target_left / max_glyph_width); + true + } else if target_right > scroll_right { + self.scroll_position + .set_x((target_right - viewport_width) / max_glyph_width); + true + } else { + false + } + } + + fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { + self.hide_context_menu(cx); + + match phase { + SelectPhase::Begin { + position, + add, + click_count, + } => self.begin_selection(*position, *add, *click_count, cx), + SelectPhase::BeginColumnar { + position, + overshoot, + } => self.begin_columnar_selection(*position, *overshoot, cx), + SelectPhase::Extend { + position, + click_count, + } => self.extend_selection(*position, *click_count, cx), + SelectPhase::Update { + position, + overshoot, + scroll_position, + } => self.update_selection(*position, *overshoot, *scroll_position, cx), + SelectPhase::End => self.end_selection(cx), + } + } + + fn extend_selection( + &mut self, + position: DisplayPoint, + click_count: usize, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self + .newest_selection::(&display_map.buffer_snapshot) + .tail(); + self.begin_selection(position, false, click_count, cx); + + let position = position.to_offset(&display_map, Bias::Left); + let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); + let mut pending = self.pending_selection.clone().unwrap(); + + if position >= tail { + pending.selection.start = tail_anchor.clone(); + } else { + pending.selection.end = tail_anchor.clone(); + pending.selection.reversed = true; + } + + match &mut pending.mode { + SelectMode::Word(range) | SelectMode::Line(range) => { + *range = tail_anchor.clone()..tail_anchor + } + _ => {} + } + + self.set_selections(self.selections.clone(), Some(pending), cx); + } + + fn begin_selection( + &mut self, + position: DisplayPoint, + add: bool, + click_count: usize, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus_self(); + cx.emit(Event::Activate); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let newest_selection = self.newest_anchor_selection().clone(); + + let start; + let end; + let mode; + match click_count { + 1 => { + start = buffer.anchor_before(position.to_point(&display_map)); + end = start.clone(); + mode = SelectMode::Character; + } + 2 => { + let range = movement::surrounding_word(&display_map, position); + start = buffer.anchor_before(range.start.to_point(&display_map)); + end = buffer.anchor_before(range.end.to_point(&display_map)); + mode = SelectMode::Word(start.clone()..end.clone()); + } + 3 => { + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + start = buffer.anchor_before(line_start); + end = buffer.anchor_before(next_line_start); + mode = SelectMode::Line(start.clone()..end.clone()); + } + _ => { + start = buffer.anchor_before(0); + end = buffer.anchor_before(buffer.len()); + mode = SelectMode::All; + } + } + + self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); + + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start, + end, + reversed: false, + goal: SelectionGoal::None, + }; + + let mut selections; + if add { + selections = self.selections.clone(); + // Remove the newest selection if it was added due to a previous mouse up + // within this multi-click. + if click_count > 1 { + selections = self + .selections + .iter() + .filter(|selection| selection.id != newest_selection.id) + .cloned() + .collect(); + } + } else { + selections = Arc::from([]); + } + self.set_selections(selections, Some(PendingSelection { selection, mode }), cx); + + cx.notify(); + } + + fn begin_columnar_selection( + &mut self, + position: DisplayPoint, + overshoot: u32, + cx: &mut ViewContext, + ) { + if !self.focused { + cx.focus_self(); + cx.emit(Event::Activate); + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let tail = self + .newest_selection::(&display_map.buffer_snapshot) + .tail(); + self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); + + self.select_columns( + tail.to_display_point(&display_map), + position, + overshoot, + &display_map, + cx, + ); + } + + fn update_selection( + &mut self, + position: DisplayPoint, + overshoot: u32, + scroll_position: Vector2F, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + + if let Some(tail) = self.columnar_selection_tail.as_ref() { + let tail = tail.to_display_point(&display_map); + self.select_columns(tail, position, overshoot, &display_map, cx); + } else if let Some(mut pending) = self.pending_selection.clone() { + let buffer = self.buffer.read(cx).snapshot(cx); + let head; + let tail; + match &pending.mode { + SelectMode::Character => { + head = position.to_point(&display_map); + tail = pending.selection.tail().to_point(&buffer); + } + SelectMode::Word(original_range) => { + let original_display_range = original_range.start.to_display_point(&display_map) + ..original_range.end.to_display_point(&display_map); + let original_buffer_range = original_display_range.start.to_point(&display_map) + ..original_display_range.end.to_point(&display_map); + if movement::is_inside_word(&display_map, position) + || original_display_range.contains(&position) + { + let word_range = movement::surrounding_word(&display_map, position); + if word_range.start < original_display_range.start { + head = word_range.start.to_point(&display_map); + } else { + head = word_range.end.to_point(&display_map); + } + } else { + head = position.to_point(&display_map); + } + + if head <= original_buffer_range.start { + tail = original_buffer_range.end; + } else { + tail = original_buffer_range.start; + } + } + SelectMode::Line(original_range) => { + let original_range = original_range.to_point(&display_map.buffer_snapshot); + + let position = display_map + .clip_point(position, Bias::Left) + .to_point(&display_map); + let line_start = display_map.prev_line_boundary(position).0; + let next_line_start = buffer.clip_point( + display_map.next_line_boundary(position).0 + Point::new(1, 0), + Bias::Left, + ); + + if line_start < original_range.start { + head = line_start + } else { + head = next_line_start + } + + if head <= original_range.start { + tail = original_range.end; + } else { + tail = original_range.start; + } + } + SelectMode::All => { + return; + } + }; + + if head < tail { + pending.selection.start = buffer.anchor_before(head); + pending.selection.end = buffer.anchor_before(tail); + pending.selection.reversed = true; + } else { + pending.selection.start = buffer.anchor_before(tail); + pending.selection.end = buffer.anchor_before(head); + pending.selection.reversed = false; + } + self.set_selections(self.selections.clone(), Some(pending), cx); + } else { + log::error!("update_selection dispatched with no pending selection"); + return; + } + + self.set_scroll_position(scroll_position, cx); + cx.notify(); + } + + fn end_selection(&mut self, cx: &mut ViewContext) { + self.columnar_selection_tail.take(); + if self.pending_selection.is_some() { + let selections = self.local_selections::(cx); + self.update_selections(selections, None, cx); + } + } + + fn select_columns( + &mut self, + tail: DisplayPoint, + head: DisplayPoint, + overshoot: u32, + display_map: &DisplaySnapshot, + cx: &mut ViewContext, + ) { + let start_row = cmp::min(tail.row(), head.row()); + let end_row = cmp::max(tail.row(), head.row()); + let start_column = cmp::min(tail.column(), head.column() + overshoot); + let end_column = cmp::max(tail.column(), head.column() + overshoot); + let reversed = start_column < tail.column(); + + let selections = (start_row..=end_row) + .filter_map(|row| { + if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { + let start = display_map + .clip_point(DisplayPoint::new(row, start_column), Bias::Left) + .to_point(&display_map); + let end = display_map + .clip_point(DisplayPoint::new(row, end_column), Bias::Right) + .to_point(&display_map); + Some(Selection { + id: post_inc(&mut self.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + }) + } else { + None + } + }) + .collect::>(); + + self.update_selections(selections, None, cx); + cx.notify(); + } + + pub fn is_selecting(&self) -> bool { + self.pending_selection.is_some() || self.columnar_selection_tail.is_some() + } + + pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.take_rename(cx).is_some() { + return; + } + + if self.hide_context_menu(cx).is_some() { + return; + } + + if self.snippet_stack.pop().is_some() { + return; + } + + if self.mode != EditorMode::Full { + cx.propagate_action(); + return; + } + + if self.active_diagnostics.is_some() { + self.dismiss_diagnostics(cx); + } else if let Some(pending) = self.pending_selection.clone() { + let mut selections = self.selections.clone(); + if selections.is_empty() { + selections = Arc::from([pending.selection]); + } + self.set_selections(selections, None, cx); + self.request_autoscroll(Autoscroll::Fit, cx); + } else { + let buffer = self.buffer.read(cx).snapshot(cx); + let mut oldest_selection = self.oldest_selection::(&buffer); + if self.selection_count() == 1 { + if oldest_selection.is_empty() { + cx.propagate_action(); + return; + } + + oldest_selection.start = oldest_selection.head().clone(); + oldest_selection.end = oldest_selection.head().clone(); + } + self.update_selections(vec![oldest_selection], Some(Autoscroll::Fit), cx); + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn selected_ranges>( + &self, + cx: &mut MutableAppContext, + ) -> Vec> { + self.local_selections::(cx) + .iter() + .map(|s| { + if s.reversed { + s.end.clone()..s.start.clone() + } else { + s.start.clone()..s.end.clone() + } + }) + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn selected_display_ranges(&self, cx: &mut MutableAppContext) -> Vec> { + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + self.selections + .iter() + .chain( + self.pending_selection + .as_ref() + .map(|pending| &pending.selection), + ) + .map(|s| { + if s.reversed { + s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map) + } else { + s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map) + } + }) + .collect() + } + + pub fn select_ranges( + &mut self, + ranges: I, + autoscroll: Option, + cx: &mut ViewContext, + ) where + I: IntoIterator>, + T: ToOffset, + { + let buffer = self.buffer.read(cx).snapshot(cx); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start.to_offset(&buffer); + let mut end = range.end.to_offset(&buffer); + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.next_selection_id), + start, + end, + reversed, + goal: SelectionGoal::None, + } + }) + .collect::>(); + self.update_selections(selections, autoscroll, cx); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn select_display_ranges<'a, T>(&mut self, ranges: T, cx: &mut ViewContext) + where + T: IntoIterator>, + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = ranges + .into_iter() + .map(|range| { + let mut start = range.start; + let mut end = range.end; + let reversed = if start > end { + mem::swap(&mut start, &mut end); + true + } else { + false + }; + Selection { + id: post_inc(&mut self.next_selection_id), + start: start.to_point(&display_map), + end: end.to_point(&display_map), + reversed, + goal: SelectionGoal::None, + } + }) + .collect(); + self.update_selections(selections, None, cx); + } + + pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { + let text = action.0.as_ref(); + if !self.skip_autoclose_end(text, cx) { + self.start_transaction(cx); + self.insert(text, cx); + self.autoclose_pairs(cx); + self.end_transaction(cx); + self.trigger_completion_on_input(text, cx); + } + } + + pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { + self.start_transaction(cx); + let mut old_selections = SmallVec::<[_; 32]>::new(); + { + let selections = self.local_selections::(cx); + let buffer = self.buffer.read(cx).snapshot(cx); + for selection in selections.iter() { + let start_point = selection.start.to_point(&buffer); + let indent = buffer + .indent_column_for_line(start_point.row) + .min(start_point.column); + let start = selection.start; + let end = selection.end; + + let mut insert_extra_newline = false; + if let Some(language) = buffer.language() { + let leading_whitespace_len = buffer + .reversed_chars_at(start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + let trailing_whitespace_len = buffer + .chars_at(end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); + + insert_extra_newline = language.brackets().iter().any(|pair| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); + + pair.newline + && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) + && buffer.contains_str_at( + (start - leading_whitespace_len).saturating_sub(pair_start.len()), + pair_start, + ) + }); + } + + old_selections.push(( + selection.id, + buffer.anchor_after(end), + start..end, + indent, + insert_extra_newline, + )); + } + } + + self.buffer.update(cx, |buffer, cx| { + let mut delta = 0_isize; + let mut pending_edit: Option = None; + for (_, _, range, indent, insert_extra_newline) in &old_selections { + if pending_edit.as_ref().map_or(false, |pending| { + pending.indent != *indent + || pending.insert_extra_newline != *insert_extra_newline + }) { + let pending = pending_edit.take().unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + delta += pending.delta; + } + + let start = (range.start as isize + delta) as usize; + let end = (range.end as isize + delta) as usize; + let mut text_len = *indent as usize + 1; + if *insert_extra_newline { + text_len *= 2; + } + + let pending = pending_edit.get_or_insert_with(Default::default); + pending.delta += text_len as isize - (end - start) as isize; + pending.indent = *indent; + pending.insert_extra_newline = *insert_extra_newline; + pending.ranges.push(start..end); + } + + let pending = pending_edit.unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + + let buffer = buffer.read(cx); + self.selections = self + .selections + .iter() + .cloned() + .zip(old_selections) + .map( + |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { + let mut cursor = end_anchor.to_point(&buffer); + if insert_extra_newline { + cursor.row -= 1; + cursor.column = buffer.line_len(cursor.row); + } + let anchor = buffer.anchor_after(cursor); + new_selection.start = anchor.clone(); + new_selection.end = anchor; + new_selection + }, + ) + .collect(); + }); + + self.request_autoscroll(Autoscroll::Fit, cx); + self.end_transaction(cx); + + #[derive(Default)] + struct PendingEdit { + indent: u32, + insert_extra_newline: bool, + delta: isize, + ranges: SmallVec<[Range; 32]>, + } + } + + pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { + self.start_transaction(cx); + + let old_selections = self.local_selections::(cx); + let selection_anchors = self.buffer.update(cx, |buffer, cx| { + let anchors = { + let snapshot = buffer.read(cx); + old_selections + .iter() + .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) + .collect::>() + }; + let edit_ranges = old_selections.iter().map(|s| s.start..s.end); + buffer.edit_with_autoindent(edit_ranges, text, cx); + anchors + }); + + let selections = { + let snapshot = self.buffer.read(cx).read(cx); + selection_anchors + .into_iter() + .map(|(id, goal, position)| { + let position = position.to_offset(&snapshot); + Selection { + id, + start: position, + end: position, + goal, + reversed: false, + } + }) + .collect() + }; + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { + let selection = self.newest_anchor_selection(); + if self + .buffer + .read(cx) + .is_completion_trigger(selection.head(), text, cx) + { + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_context_menu(cx); + } + } + + fn autoclose_pairs(&mut self, cx: &mut ViewContext) { + let selections = self.local_selections::(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 { + id: selection.id, + start: snapshot.anchor_before(selection.start), + end: snapshot.anchor_before(selection.end), + reversed: selection.reversed, + goal: selection.goal, + }) + .collect::>(); + + let autoclose_pair = snapshot.language().and_then(|language| { + let first_selection_start = selections.first().unwrap().start; + let pair = language.brackets().iter().find(|pair| { + snapshot.contains_str_at( + first_selection_start.saturating_sub(pair.start.len()), + &pair.start, + ) + }); + pair.and_then(|pair| { + let should_autoclose = selections[1..].iter().all(|selection| { + snapshot.contains_str_at( + selection.start.saturating_sub(pair.start.len()), + &pair.start, + ) + }); + + 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::>(); + + buffer.edit(selection_ranges, &pair.end, cx); + snapshot = buffer.snapshot(cx); + + new_selections = Some( + self.resolve_selections::(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, + }); + } + } + }); + + if let Some(new_selections) = new_selections { + self.update_selections(new_selections, None, cx); + } + if let Some(bracket_pair_state) = bracket_pair_state { + self.autoclose_stack.push(bracket_pair_state); + } + } + + fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext) -> bool { + let old_selections = self.local_selections::(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; + } + + debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len()); + + let buffer = self.buffer.read(cx).snapshot(cx); + 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.update_selections(new_selections, Some(Autoscroll::Fit), cx); + true + } else { + false + } + } + + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + let offset = position.to_offset(buffer); + let (word_range, kind) = buffer.surrounding_word(offset); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + } + + fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { + if self.pending_rename.is_some() { + return; + } + + let project = if let Some(project) = self.project.clone() { + project + } else { + return; + }; + + let position = self.newest_anchor_selection().head(); + let (buffer, buffer_position) = if let Some(output) = self + .buffer + .read(cx) + .text_anchor_for_position(position.clone(), cx) + { + output + } else { + return; + }; + + let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); + let completions = project.update(cx, |project, cx| { + project.completions(&buffer, buffer_position.clone(), cx) + }); + + let id = post_inc(&mut self.next_completion_id); + let task = cx.spawn_weak(|this, mut cx| { + async move { + let completions = completions.await?; + if completions.is_empty() { + return Ok(()); + } + + let mut menu = CompletionsMenu { + id, + initial_position: position, + match_candidates: completions + .iter() + .enumerate() + .map(|(id, completion)| { + StringMatchCandidate::new( + id, + completion.label.text[completion.label.filter_range.clone()].into(), + ) + }) + .collect(), + buffer, + completions: completions.into(), + matches: Vec::new().into(), + selected_item: 0, + list: Default::default(), + }; + + menu.filter(query.as_deref(), cx.background()).await; + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + match this.context_menu.as_ref() { + None => {} + Some(ContextMenu::Completions(prev_menu)) => { + if prev_menu.id > menu.id { + return; + } + } + _ => return, + } + + this.completion_tasks.retain(|(id, _)| *id > menu.id); + if this.focused { + this.show_context_menu(ContextMenu::Completions(menu), cx); + } + + cx.notify(); + }); + } + Ok::<_, anyhow::Error>(()) + } + .log_err() + }); + self.completion_tasks.push((id, task)); + } + + pub fn confirm_completion( + &mut self, + ConfirmCompletion(completion_ix): &ConfirmCompletion, + cx: &mut ViewContext, + ) -> Option>> { + use language::ToOffset as _; + + let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; + + let mat = completions_menu + .matches + .get(completion_ix.unwrap_or(completions_menu.selected_item))?; + let buffer_handle = completions_menu.buffer; + let completion = completions_menu.completions.get(mat.candidate_id)?; + + let snippet; + let text; + if completion.is_snippet() { + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + text = completion.new_text.clone(); + }; + let buffer = buffer_handle.read(cx); + let old_range = completion.old_range.to_offset(&buffer); + let old_text = buffer.text_for_range(old_range.clone()).collect::(); + + let selections = self.local_selections::(cx); + let newest_selection = self.newest_anchor_selection(); + if newest_selection.start.buffer_id != Some(buffer_handle.id()) { + return None; + } + + let lookbehind = newest_selection + .start + .text_anchor + .to_offset(buffer) + .saturating_sub(old_range.start); + let lookahead = old_range + .end + .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); + let mut common_prefix_len = old_text + .bytes() + .zip(text.bytes()) + .take_while(|(a, b)| a == b) + .count(); + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut ranges = Vec::new(); + for selection in &selections { + if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { + let start = selection.start.saturating_sub(lookbehind); + let end = selection.end + lookahead; + ranges.push(start + common_prefix_len..end); + } else { + common_prefix_len = 0; + ranges.clear(); + ranges.extend(selections.iter().map(|s| { + if s.id == newest_selection.id { + old_range.clone() + } else { + s.start..s.end + } + })); + break; + } + } + let text = &text[common_prefix_len..]; + + self.start_transaction(cx); + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } + + self.insert_snippet(&ranges, snippet, cx).log_err(); + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent(ranges, text, cx); + }); + } + self.end_transaction(cx); + + let project = self.project.clone()?; + let apply_edits = project.update(cx, |project, cx| { + project.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ) + }); + Some(cx.foreground().spawn(async move { + apply_edits.await?; + Ok(()) + })) + } + + pub fn toggle_code_actions( + &mut self, + &ToggleCodeActions(deployed_from_indicator): &ToggleCodeActions, + cx: &mut ViewContext, + ) { + if matches!( + self.context_menu.as_ref(), + Some(ContextMenu::CodeActions(_)) + ) { + self.context_menu.take(); + cx.notify(); + return; + } + + let mut task = self.code_actions_task.take(); + cx.spawn_weak(|this, mut cx| async move { + while let Some(prev_task) = task { + prev_task.await; + task = this + .upgrade(&cx) + .and_then(|this| this.update(&mut cx, |this, _| this.code_actions_task.take())); + } + + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if this.focused { + if let Some((buffer, actions)) = this.available_code_actions.clone() { + this.show_context_menu( + ContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions, + selected_item: Default::default(), + list: Default::default(), + deployed_from_indicator, + }), + cx, + ); + } + } + }) + } + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn confirm_code_action( + workspace: &mut Workspace, + ConfirmCodeAction(action_ix): &ConfirmCodeAction, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + let actions_menu = if let ContextMenu::CodeActions(menu) = + editor.update(cx, |editor, cx| editor.hide_context_menu(cx))? + { + menu + } else { + return None; + }; + let action_ix = action_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?.clone(); + let title = action.lsp_action.title.clone(); + let buffer = actions_menu.buffer; + + let apply_code_actions = workspace.project().clone().update(cx, |project, cx| { + project.apply_code_action(buffer, action, true, cx) + }); + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = apply_code_actions.await?; + Self::open_project_transaction(editor, workspace, project_transaction, title, cx).await + })) + } + + async fn open_project_transaction( + this: ViewHandle, + workspace: ViewHandle, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncAppContext, + ) -> Result<()> { + let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); + + // If the code action's edits are all contained within this editor, then + // avoid opening a new editor to display them. + let mut entries = transaction.0.iter(); + if let Some((buffer, transaction)) = entries.next() { + if entries.next().is_none() { + let excerpt = this.read_with(&cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.newest_anchor_selection().head(), cx) + }); + if let Some((excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + let excerpt_range = excerpt_range.to_offset(&snapshot); + if snapshot + .edited_ranges_for_transaction(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + { + return Ok(()); + } + } + } + } + } + + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer, transaction) in &transaction.0 { + let snapshot = buffer.read(cx).snapshot(); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + snapshot + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(&transaction.0); + multibuffer + }); + + workspace.update(&mut cx, |workspace, cx| { + let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_ranges::(ranges_to_highlight, color, cx); + }); + } + }); + + Ok(()) + } + + fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { + let project = self.project.as_ref()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.newest_anchor_selection().clone(); + let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; + let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?; + if start_buffer != end_buffer { + return None; + } + + let actions = project.update(cx, |project, cx| { + project.code_actions(&start_buffer, start..end, cx) + }); + self.code_actions_task = Some(cx.spawn_weak(|this, mut cx| async move { + let actions = actions.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.available_code_actions = actions.log_err().and_then(|actions| { + if actions.is_empty() { + None + } else { + Some((start_buffer, actions.into())) + } + }); + cx.notify(); + }) + } + })); + None + } + + fn refresh_document_highlights(&mut self, cx: &mut ViewContext) -> Option<()> { + let project = self.project.as_ref()?; + let buffer = self.buffer.read(cx); + let newest_selection = self.newest_anchor_selection().clone(); + let cursor_position = newest_selection.head(); + let (cursor_buffer, cursor_buffer_position) = + buffer.text_anchor_for_position(cursor_position.clone(), cx)?; + let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; + if cursor_buffer != tail_buffer { + return None; + } + + let highlights = project.update(cx, |project, cx| { + project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }); + + enum DocumentHighlightRead {} + enum DocumentHighlightWrite {} + + self.document_highlights_task = Some(cx.spawn_weak(|this, mut cx| async move { + let highlights = highlights.log_err().await; + if let Some((this, highlights)) = this.upgrade(&cx).zip(highlights) { + this.update(&mut cx, |this, cx| { + let buffer_id = cursor_position.buffer_id; + let excerpt_id = cursor_position.excerpt_id.clone(); + let style = this.style(cx); + let read_background = style.document_highlight_read_background; + let write_background = style.document_highlight_write_background; + let buffer = this.buffer.read(cx); + if !buffer + .text_anchor_for_position(cursor_position, cx) + .map_or(false, |(buffer, _)| buffer == cursor_buffer) + { + return; + } + + let mut write_ranges = Vec::new(); + let mut read_ranges = Vec::new(); + for highlight in highlights { + let range = Anchor { + buffer_id, + excerpt_id: excerpt_id.clone(), + text_anchor: highlight.range.start, + }..Anchor { + buffer_id, + excerpt_id: excerpt_id.clone(), + text_anchor: highlight.range.end, + }; + if highlight.kind == lsp::DocumentHighlightKind::WRITE { + write_ranges.push(range); + } else { + read_ranges.push(range); + } + } + + this.highlight_ranges::( + read_ranges, + read_background, + cx, + ); + this.highlight_ranges::( + write_ranges, + write_background, + cx, + ); + cx.notify(); + }); + } + })); + None + } + + pub fn render_code_actions_indicator( + &self, + style: &EditorStyle, + cx: &mut ViewContext, + ) -> Option { + if self.available_code_actions.is_some() { + enum Tag {} + Some( + MouseEventHandler::new::(0, cx, |_, _| { + Svg::new("icons/zap.svg") + .with_color(style.code_actions_indicator) + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_padding(Padding::uniform(3.)) + .on_mouse_down(|cx| { + cx.dispatch_action(ToggleCodeActions(true)); + }) + .boxed(), + ) + } else { + None + } + } + + pub fn context_menu_visible(&self) -> bool { + self.context_menu + .as_ref() + .map_or(false, |menu| menu.visible()) + } + + pub fn render_context_menu( + &self, + cursor_position: DisplayPoint, + style: EditorStyle, + cx: &AppContext, + ) -> Option<(DisplayPoint, ElementBox)> { + self.context_menu + .as_ref() + .map(|menu| menu.render(cursor_position, style, cx)) + } + + fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext) { + if !matches!(menu, ContextMenu::Completions(_)) { + self.completion_tasks.clear(); + } + self.context_menu = Some(menu); + cx.notify(); + } + + fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { + cx.notify(); + self.completion_tasks.clear(); + self.context_menu.take() + } + + pub fn insert_snippet( + &mut self, + insertion_ranges: &[Range], + snippet: Snippet, + cx: &mut ViewContext, + ) -> Result<()> { + let tabstops = self.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent(insertion_ranges.iter().cloned(), &snippet.text, cx); + + let snapshot = &*buffer.read(cx); + let snippet = &snippet; + snippet + .tabstops + .iter() + .map(|tabstop| { + let mut tabstop_ranges = tabstop + .iter() + .flat_map(|tabstop_range| { + let mut delta = 0 as isize; + insertion_ranges.iter().map(move |insertion_range| { + let insertion_start = insertion_range.start as isize + delta; + delta += + snippet.text.len() as isize - insertion_range.len() as isize; + + let start = snapshot.anchor_before( + (insertion_start + tabstop_range.start) as usize, + ); + let end = snapshot + .anchor_after((insertion_start + tabstop_range.end) as usize); + start..end + }) + }) + .collect::>(); + tabstop_ranges + .sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot).unwrap()); + tabstop_ranges + }) + .collect::>() + }); + + if let Some(tabstop) = tabstops.first() { + self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx); + self.snippet_stack.push(SnippetState { + active_index: 0, + ranges: tabstops, + }); + } + + Ok(()) + } + + pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext) -> bool { + self.move_to_snippet_tabstop(Bias::Right, cx) + } + + pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext) { + self.move_to_snippet_tabstop(Bias::Left, cx); + } + + pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext) -> bool { + let buffer = self.buffer.read(cx).snapshot(cx); + + if let Some(snippet) = self.snippet_stack.last_mut() { + match bias { + Bias::Left => { + if snippet.active_index > 0 { + snippet.active_index -= 1; + } else { + return false; + } + } + Bias::Right => { + if snippet.active_index + 1 < snippet.ranges.len() { + snippet.active_index += 1; + } else { + return false; + } + } + } + if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { + let new_selections = current_ranges + .iter() + .map(|new_range| { + let new_range = new_range.to_offset(&buffer); + Selection { + id: post_inc(&mut self.next_selection_id), + start: new_range.start, + end: new_range.end, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + + // Remove the snippet state when moving to the last tabstop. + if snippet.active_index + 1 == snippet.ranges.len() { + self.snippet_stack.pop(); + } + + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + return true; + } + self.snippet_stack.pop(); + } + + false + } + + pub fn clear(&mut self, cx: &mut ViewContext) { + self.start_transaction(cx); + self.select_all(&SelectAll, cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { + self.start_transaction(cx); + let mut selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::left(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { + self.start_transaction(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::right(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert(&"", cx); + self.end_transaction(cx); + } + + pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { + if self.move_to_next_snippet_tabstop(cx) { + return; + } + + self.start_transaction(cx); + let tab_size = self.settings.borrow().tab_size; + let mut selections = self.local_selections::(cx); + let mut last_indent = None; + self.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + if selection.is_empty() { + let char_column = buffer + .read(cx) + .text_for_range(Point::new(selection.start.row, 0)..selection.start) + .flat_map(str::chars) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column % tab_size); + buffer.edit( + [selection.start..selection.start], + " ".repeat(chars_to_next_tab_stop), + cx, + ); + selection.start.column += chars_to_next_tab_stop as u32; + selection.end = selection.start; + } else { + let mut start_row = selection.start.row; + let mut end_row = selection.end.row + 1; + + // If a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 { + end_row -= 1; + } + + // Avoid re-indenting a row that has already been indented by a + // previous selection, but still update this selection's column + // to reflect that indentation. + if let Some((last_indent_row, last_indent_len)) = last_indent { + if last_indent_row == selection.start.row { + selection.start.column += last_indent_len; + start_row += 1; + } + if last_indent_row == selection.end.row { + selection.end.column += last_indent_len; + } + } + + for row in start_row..end_row { + let indent_column = buffer.read(cx).indent_column_for_line(row) as usize; + let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); + let row_start = Point::new(row, 0); + buffer.edit( + [row_start..row_start], + " ".repeat(columns_to_next_tab_stop), + cx, + ); + + // Update this selection's endpoints to reflect the indentation. + if row == selection.start.row { + selection.start.column += columns_to_next_tab_stop as u32; + } + if row == selection.end.row { + selection.end.column += columns_to_next_tab_stop as u32; + } + + last_indent = Some((row, columns_to_next_tab_stop as u32)); + } + } + } + }); + + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { + if !self.snippet_stack.is_empty() { + self.move_to_prev_snippet_tabstop(cx); + return; + } + + self.start_transaction(cx); + let tab_size = self.settings.borrow().tab_size; + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut deletion_ranges = Vec::new(); + let mut last_outdent = None; + { + let buffer = self.buffer.read(cx).read(cx); + for selection in &selections { + let mut rows = selection.spanned_rows(false, &display_map); + + // Avoid re-outdenting a row that has already been outdented by a + // previous selection. + if let Some(last_row) = last_outdent { + if last_row == rows.start { + rows.start += 1; + } + } + + for row in rows { + let column = buffer.indent_column_for_line(row) as usize; + if column > 0 { + let mut deletion_len = (column % tab_size) as u32; + if deletion_len == 0 { + deletion_len = tab_size as u32; + } + deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len)); + last_outdent = Some(row); + } + } + } + } + self.buffer.update(cx, |buffer, cx| { + buffer.edit(deletion_ranges, "", cx); + }); + + self.update_selections( + self.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); + self.end_transaction(cx); + } + + pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { + self.start_transaction(cx); + + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut new_cursors = Vec::new(); + let mut edit_ranges = Vec::new(); + let mut selections = selections.iter().peekable(); + while let Some(selection) = selections.next() { + let mut rows = selection.spanned_rows(false, &display_map); + let goal_display_column = selection.head().to_display_point(&display_map).column(); + + // Accumulate contiguous regions of rows that we want to delete. + while let Some(next_selection) = selections.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start <= rows.end { + rows.end = next_rows.end; + selections.next().unwrap(); + } else { + break; + } + } + + let mut edit_start = Point::new(rows.start, 0).to_offset(&buffer); + let edit_end; + let cursor_buffer_row; + if buffer.max_point().row >= rows.end { + // If there's a line after the range, delete the \n from the end of the row range + // and position the cursor on the next line. + edit_end = Point::new(rows.end, 0).to_offset(&buffer); + cursor_buffer_row = rows.end; + } else { + // If there isn't a line after the range, delete the \n from the line before the + // start of the row range and position the cursor there. + edit_start = edit_start.saturating_sub(1); + edit_end = buffer.len(); + cursor_buffer_row = rows.start.saturating_sub(1); + } + + let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map); + *cursor.column_mut() = + cmp::min(goal_display_column, display_map.line_len(cursor.row())); + + new_cursors.push(( + selection.id, + buffer.anchor_after(cursor.to_point(&display_map)), + )); + edit_ranges.push(edit_start..edit_end); + } + + let buffer = self.buffer.update(cx, |buffer, cx| { + buffer.edit(edit_ranges, "", cx); + buffer.snapshot(cx) + }); + let new_selections = new_cursors + .into_iter() + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { + self.start_transaction(cx); + + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + + let mut edits = Vec::new(); + let mut selections_iter = selections.iter().peekable(); + while let Some(selection) = selections_iter.next() { + // Avoid duplicating the same lines twice. + let mut rows = selection.spanned_rows(false, &display_map); + + while let Some(next_selection) = selections_iter.peek() { + let next_rows = next_selection.spanned_rows(false, &display_map); + if next_rows.start <= rows.end - 1 { + rows.end = next_rows.end; + selections_iter.next().unwrap(); + } else { + break; + } + } + + // Copy the text from the selected row region and splice it at the start of the region. + let start = Point::new(rows.start, 0); + let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1)); + let text = buffer + .text_for_range(start..end) + .chain(Some("\n")) + .collect::(); + edits.push((start, text, rows.len() as u32)); + } + + self.buffer.update(cx, |buffer, cx| { + for (point, text, _) in edits.into_iter().rev() { + buffer.edit(Some(point..point), text, cx); + } + }); + + self.request_autoscroll(Autoscroll::Fit, cx); + self.end_transaction(cx); + } + + pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_ranges = Vec::new(); + + let selections = self.local_selections::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + contiguous_row_selections.push(selection.clone()); + let start_row = selection.start.row; + let mut end_row = if selection.end.column > 0 || selection.is_empty() { + display_map.next_line_boundary(selection.end).0.row + 1 + } else { + selection.end.row + }; + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row { + end_row = if next_selection.end.column > 0 || next_selection.is_empty() { + display_map.next_line_boundary(next_selection.end).0.row + 1 + } else { + next_selection.end.row + }; + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + + // Move the text spanned by the row range to be before the line preceding the row range + if start_row > 0 { + let range_to_move = Point::new(start_row - 1, buffer.line_len(start_row - 1)) + ..Point::new(end_row - 1, buffer.line_len(end_row - 1)); + let insertion_point = display_map + .prev_line_boundary(Point::new(start_row - 1, 0)) + .0; + + // Don't move lines across excerpts + if buffer + .excerpt_boundaries_in_range(( + Bound::Excluded(insertion_point), + Bound::Included(range_to_move.end), + )) + .next() + .is_none() + { + let text = buffer + .text_for_range(range_to_move.clone()) + .flat_map(|s| s.chars()) + .skip(1) + .chain(['\n']) + .collect::(); + + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor.clone()..insertion_anchor, text)); + + let row_delta = range_to_move.start.row - insertion_point.row + 1; + + // Move selections up + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row -= row_delta; + selection.end.row -= row_delta; + selection + }, + )); + + // Move folds up + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.start.to_point(&buffer); + let mut end = fold.end.to_point(&buffer); + start.row -= row_delta; + end.row -= row_delta; + refold_ranges.push(start..end); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.extend(contiguous_row_selections.drain(..)); + } + + self.start_transaction(cx); + self.unfold_ranges(unfold_ranges, cx); + self.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + self.fold_ranges(refold_ranges, cx); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + let mut unfold_ranges = Vec::new(); + let mut refold_ranges = Vec::new(); + + let selections = self.local_selections::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + contiguous_row_selections.push(selection.clone()); + let start_row = selection.start.row; + let mut end_row = if selection.end.column > 0 || selection.is_empty() { + display_map.next_line_boundary(selection.end).0.row + 1 + } else { + selection.end.row + }; + + while let Some(next_selection) = selections.peek() { + if next_selection.start.row <= end_row { + end_row = if next_selection.end.column > 0 || next_selection.is_empty() { + display_map.next_line_boundary(next_selection.end).0.row + 1 + } else { + next_selection.end.row + }; + contiguous_row_selections.push(selections.next().unwrap().clone()); + } else { + break; + } + } + + // Move the text spanned by the row range to be after the last line of the row range + if end_row <= buffer.max_point().row { + let range_to_move = Point::new(start_row, 0)..Point::new(end_row, 0); + let insertion_point = display_map.next_line_boundary(Point::new(end_row, 0)).0; + + // Don't move lines across excerpt boundaries + if buffer + .excerpt_boundaries_in_range(( + Bound::Excluded(range_to_move.start), + Bound::Included(insertion_point), + )) + .next() + .is_none() + { + let mut text = String::from("\n"); + text.extend(buffer.text_for_range(range_to_move.clone())); + text.pop(); // Drop trailing newline + edits.push(( + buffer.anchor_after(range_to_move.start) + ..buffer.anchor_before(range_to_move.end), + String::new(), + )); + let insertion_anchor = buffer.anchor_after(insertion_point); + edits.push((insertion_anchor.clone()..insertion_anchor, text)); + + let row_delta = insertion_point.row - range_to_move.end.row + 1; + + // Move selections down + new_selections.extend(contiguous_row_selections.drain(..).map( + |mut selection| { + selection.start.row += row_delta; + selection.end.row += row_delta; + selection + }, + )); + + // Move folds down + unfold_ranges.push(range_to_move.clone()); + for fold in display_map.folds_in_range( + buffer.anchor_before(range_to_move.start) + ..buffer.anchor_after(range_to_move.end), + ) { + let mut start = fold.start.to_point(&buffer); + let mut end = fold.end.to_point(&buffer); + start.row += row_delta; + end.row += row_delta; + refold_ranges.push(start..end); + } + } + } + + // If we didn't move line(s), preserve the existing selections + new_selections.extend(contiguous_row_selections.drain(..)); + } + + self.start_transaction(cx); + self.unfold_ranges(unfold_ranges, cx); + self.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + self.fold_ranges(refold_ranges, cx); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.end_transaction(cx); + } + + pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + self.start_transaction(cx); + let mut text = String::new(); + let mut selections = self.local_selections::(cx); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let buffer = self.buffer.read(cx).read(cx); + let max_point = buffer.max_point(); + for selection in &mut selections { + let is_entire_line = selection.is_empty(); + if is_entire_line { + selection.start = Point::new(selection.start.row, 0); + selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); + selection.goal = SelectionGoal::None; + } + let mut len = 0; + for chunk in buffer.text_for_range(selection.start..selection.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + }); + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + + cx.as_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + } + + pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + let selections = self.local_selections::(cx); + let mut text = String::new(); + let mut clipboard_selections = Vec::with_capacity(selections.len()); + { + let buffer = self.buffer.read(cx).read(cx); + let max_point = buffer.max_point(); + for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty(); + if is_entire_line { + start = Point::new(start.row, 0); + end = cmp::min(max_point, Point::new(start.row + 1, 0)); + } + let mut len = 0; + for chunk in buffer.text_for_range(start..end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + }); + } + } + + cx.as_mut() + .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + } + + pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + if let Some(item) = cx.as_mut().read_from_clipboard() { + let clipboard_text = item.text(); + if let Some(mut clipboard_selections) = item.metadata::>() { + let mut selections = self.local_selections::(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + if clipboard_selections.len() != selections.len() { + clipboard_selections.clear(); + } + + let mut delta = 0_isize; + let mut start_offset = 0; + for (i, selection) in selections.iter_mut().enumerate() { + let to_insert; + let entire_line; + if let Some(clipboard_selection) = clipboard_selections.get(i) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + entire_line = clipboard_selection.is_entire_line; + start_offset = end_offset + } else { + to_insert = clipboard_text.as_str(); + entire_line = all_selections_were_entire_line; + } + + selection.start = (selection.start as isize + delta) as usize; + selection.end = (selection.end as isize + delta) as usize; + + self.buffer.update(cx, |buffer, cx| { + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let range = if selection.is_empty() && entire_line { + let column = selection.start.to_point(&buffer.read(cx)).column as usize; + let line_start = selection.start - column; + line_start..line_start + } else { + selection.start..selection.end + }; + + delta += to_insert.len() as isize - range.len() as isize; + buffer.edit([range], to_insert, cx); + selection.start += to_insert.len(); + selection.end = selection.start; + }); + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } else { + self.insert(clipboard_text, cx); + } + } + } + + pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { + if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { + self.set_selections(selections, None, cx); + } + self.request_autoscroll(Autoscroll::Fit, cx); + } + } + + pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { + if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { + if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { + self.set_selections(selections, None, cx); + } + self.request_autoscroll(Autoscroll::Fit, cx); + } + } + + pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + + if start != end { + selection.end = selection.start.clone(); + } else { + let cursor = movement::left(&display_map, start) + .unwrap() + .to_point(&display_map); + selection.start = cursor.clone(); + selection.end = cursor; + } + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::left(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + + if start != end { + selection.start = selection.end.clone(); + } else { + let cursor = movement::right(&display_map, end) + .unwrap() + .to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + } + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::right(&display_map, head) + .unwrap() + .to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if self.take_rename(cx).is_some() { + return; + } + + if let Some(context_menu) = self.context_menu.as_mut() { + if context_menu.select_prev(cx) { + return; + } + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + if start != end { + selection.goal = SelectionGoal::None; + } + + let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap(); + let cursor = start.to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.goal = goal; + selection.reversed = false; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap(); + let cursor = head.to_point(&display_map); + selection.set_head(cursor); + selection.goal = goal; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + self.take_rename(cx); + + if let Some(context_menu) = self.context_menu.as_mut() { + if context_menu.select_next(cx) { + return; + } + } + + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); + if start != end { + selection.goal = SelectionGoal::None; + } + + let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap(); + let cursor = start.to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.goal = goal; + selection.reversed = false; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap(); + let cursor = head.to_point(&display_map); + selection.set_head(cursor); + selection.goal = goal; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_to_previous_word_boundary( + &mut self, + _: &MoveToPreviousWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); + selection.start = cursor.clone(); + selection.end = cursor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_previous_word_boundary( + &mut self, + _: &SelectToPreviousWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_previous_word_boundary( + &mut self, + _: &DeleteToPreviousWordBoundary, + cx: &mut ViewContext, + ) { + self.start_transaction(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = + movement::prev_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn move_to_next_word_boundary( + &mut self, + _: &MoveToNextWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_next_word_boundary( + &mut self, + _: &SelectToNextWordBoundary, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_next_word_boundary( + &mut self, + _: &DeleteToNextWordBoundary, + cx: &mut ViewContext, + ) { + self.start_transaction(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + if selection.is_empty() { + let head = selection.head().to_display_point(&display_map); + let cursor = + movement::next_word_boundary(&display_map, head).to_point(&display_map); + selection.set_head(cursor); + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.insert("", cx); + self.end_transaction(cx); + } + + pub fn move_to_beginning_of_line( + &mut self, + _: &MoveToBeginningOfLine, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_beginning(&display_map, head, true); + let cursor = new_head.to_point(&display_map); + selection.start = cursor; + selection.end = cursor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_beginning_of_line( + &mut self, + SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_beginning(&display_map, head, *stop_at_soft_boundaries); + selection.set_head(new_head.to_point(&display_map)); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_beginning_of_line( + &mut self, + _: &DeleteToBeginningOfLine, + cx: &mut ViewContext, + ) { + self.start_transaction(cx); + self.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); + self.backspace(&Backspace, cx); + self.end_transaction(cx); + } + + pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + { + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_end(&display_map, head, true); + let anchor = new_head.to_point(&display_map); + selection.start = anchor.clone(); + selection.end = anchor; + selection.reversed = false; + selection.goal = SelectionGoal::None; + } + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn select_to_end_of_line( + &mut self, + SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine, + cx: &mut ViewContext, + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + for selection in &mut selections { + let head = selection.head().to_display_point(&display_map); + let new_head = movement::line_end(&display_map, head, *stop_at_soft_boundaries); + selection.set_head(new_head.to_point(&display_map)); + selection.goal = SelectionGoal::None; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { + self.start_transaction(cx); + self.select_to_end_of_line(&SelectToEndOfLine(false), cx); + self.delete(&Delete, cx); + self.end_transaction(cx); + } + + pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { + self.start_transaction(cx); + self.select_to_end_of_line(&SelectToEndOfLine(false), cx); + self.cut(&Cut, cx); + self.end_transaction(cx); + } + + pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start: 0, + end: 0, + reversed: false, + goal: SelectionGoal::None, + }; + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { + let mut selection = self.local_selections::(cx).last().unwrap().clone(); + selection.set_head(Point::zero()); + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext) { + if matches!(self.mode, EditorMode::SingleLine) { + cx.propagate_action(); + return; + } + + let cursor = self.buffer.read(cx).read(cx).len(); + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + }; + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn set_nav_history(&mut self, nav_history: Option) { + self.nav_history = nav_history; + } + + pub fn nav_history(&self) -> Option<&ItemNavHistory> { + self.nav_history.as_ref() + } + + fn push_to_nav_history( + &self, + position: Anchor, + new_position: Option, + cx: &mut ViewContext, + ) { + if let Some(nav_history) = &self.nav_history { + let buffer = self.buffer.read(cx).read(cx); + let offset = position.to_offset(&buffer); + let point = position.to_point(&buffer); + drop(buffer); + + if let Some(new_position) = new_position { + let row_delta = (new_position.row as i64 - point.row as i64).abs(); + if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + return; + } + } + + nav_history.push(Some(NavigationData { + anchor: position, + offset, + })); + } + } + + pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { + let mut selection = self.local_selections::(cx).first().unwrap().clone(); + selection.set_head(self.buffer.read(cx).read(cx).len()); + self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); + } + + pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { + let selection = Selection { + id: post_inc(&mut self.next_selection_id), + start: 0, + end: self.buffer.read(cx).read(cx).len(), + reversed: false, + goal: SelectionGoal::None, + }; + self.update_selections(vec![selection], None, cx); + } + + pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + let max_point = display_map.buffer_snapshot.max_point(); + for selection in &mut selections { + let rows = selection.spanned_rows(true, &display_map); + selection.start = Point::new(rows.start, 0); + selection.end = cmp::min(max_point, Point::new(rows.end, 0)); + selection.reversed = false; + } + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn split_selection_into_lines( + &mut self, + _: &SplitSelectionIntoLines, + cx: &mut ViewContext, + ) { + let mut to_unfold = Vec::new(); + let mut new_selections = Vec::new(); + { + let selections = self.local_selections::(cx); + let buffer = self.buffer.read(cx).read(cx); + for selection in selections { + for row in selection.start.row..selection.end.row { + let cursor = Point::new(row, buffer.line_len(row)); + new_selections.push(Selection { + id: post_inc(&mut self.next_selection_id), + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + }); + } + new_selections.push(Selection { + id: selection.id, + start: selection.end, + end: selection.end, + reversed: false, + goal: SelectionGoal::None, + }); + to_unfold.push(selection.start..selection.end); + } + } + self.unfold_ranges(to_unfold, cx); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + } + + pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext) { + self.add_selection(true, cx); + } + + pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext) { + self.add_selection(false, cx); + } + + fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = self.local_selections::(cx); + let mut state = self.add_selections_state.take().unwrap_or_else(|| { + let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); + let range = oldest_selection.display_range(&display_map).sorted(); + let columns = cmp::min(range.start.column(), range.end.column()) + ..cmp::max(range.start.column(), range.end.column()); + + selections.clear(); + let mut stack = Vec::new(); + for row in range.start.row()..=range.end.row() { + if let Some(selection) = self.build_columnar_selection( + &display_map, + row, + &columns, + oldest_selection.reversed, + ) { + stack.push(selection.id); + selections.push(selection); + } + } + + if above { + stack.reverse(); + } + + AddSelectionsState { above, stack } + }); + + let last_added_selection = *state.stack.last().unwrap(); + let mut new_selections = Vec::new(); + if above == state.above { + let end_row = if above { + 0 + } else { + display_map.max_point().row() + }; + + 'outer: for selection in selections { + if selection.id == last_added_selection { + let range = selection.display_range(&display_map).sorted(); + debug_assert_eq!(range.start.row(), range.end.row()); + let mut row = range.start.row(); + let columns = if let SelectionGoal::ColumnRange { start, end } = selection.goal + { + start..end + } else { + cmp::min(range.start.column(), range.end.column()) + ..cmp::max(range.start.column(), range.end.column()) + }; + + while row != end_row { + if above { + row -= 1; + } else { + row += 1; + } + + if let Some(new_selection) = self.build_columnar_selection( + &display_map, + row, + &columns, + selection.reversed, + ) { + state.stack.push(new_selection.id); + if above { + new_selections.push(new_selection); + new_selections.push(selection); + } else { + new_selections.push(selection); + new_selections.push(new_selection); + } + + continue 'outer; + } + } + } + + new_selections.push(selection); + } + } else { + new_selections = selections; + new_selections.retain(|s| s.id != last_added_selection); + state.stack.pop(); + } + + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + if state.stack.len() > 1 { + self.add_selections_state = Some(state); + } + } + + pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) { + let replace_newest = action.0; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let mut selections = self.local_selections::(cx); + if let Some(mut select_next_state) = self.select_next_state.take() { + let query = &select_next_state.query; + if !select_next_state.done { + let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); + let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); + let mut next_selected_range = None; + + let bytes_after_last_selection = + buffer.bytes_in_range(last_selection.end..buffer.len()); + let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); + let query_matches = query + .stream_find_iter(bytes_after_last_selection) + .map(|result| (last_selection.end, result)) + .chain( + query + .stream_find_iter(bytes_before_first_selection) + .map(|result| (0, result)), + ); + for (start_offset, query_match) in query_matches { + let query_match = query_match.unwrap(); // can only fail due to I/O + let offset_range = + start_offset + query_match.start()..start_offset + query_match.end(); + let display_range = offset_range.start.to_display_point(&display_map) + ..offset_range.end.to_display_point(&display_map); + + if !select_next_state.wordwise + || (!movement::is_inside_word(&display_map, display_range.start) + && !movement::is_inside_word(&display_map, display_range.end)) + { + next_selected_range = Some(offset_range); + break; + } + } + + if let Some(next_selected_range) = next_selected_range { + if replace_newest { + if let Some(newest_id) = + selections.iter().max_by_key(|s| s.id).map(|s| s.id) + { + selections.retain(|s| s.id != newest_id); + } + } + selections.push(Selection { + id: post_inc(&mut self.next_selection_id), + start: next_selected_range.start, + end: next_selected_range.end, + reversed: false, + goal: SelectionGoal::None, + }); + self.update_selections(selections, Some(Autoscroll::Newest), cx); + } else { + select_next_state.done = true; + } + } + + self.select_next_state = Some(select_next_state); + } else if selections.len() == 1 { + let selection = selections.last_mut().unwrap(); + if selection.start == selection.end { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + selection.start = word_range.start.to_offset(&display_map, Bias::Left); + selection.end = word_range.end.to_offset(&display_map, Bias::Left); + selection.goal = SelectionGoal::None; + selection.reversed = false; + + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + let select_state = SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: true, + done: false, + }; + self.update_selections(selections, Some(Autoscroll::Newest), cx); + self.select_next_state = Some(select_state); + } else { + let query = buffer + .text_for_range(selection.start..selection.end) + .collect::(); + self.select_next_state = Some(SelectNextState { + query: AhoCorasick::new_auto_configured(&[query]), + wordwise: false, + done: false, + }); + self.select_next(action, cx); + } + } + } + + pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext) { + // 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 = + if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) { + prefix.to_string() + } else { + return; + }; + let comment_prefix = full_comment_prefix.trim_end_matches(' '); + let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; + + self.start_transaction(cx); + let mut selections = self.local_selections::(cx); + let mut all_selection_lines_are_comments = true; + let mut edit_ranges = Vec::new(); + let mut last_toggled_row = None; + self.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + 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); + } + + if snapshot.is_line_blank(row) { + continue; + } + + let start = Point::new(row, snapshot.indent_column_for_line(row)); + 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 { + all_selection_lines_are_comments = false; + edit_ranges.push(start..start); + } + } + + if !edit_ranges.is_empty() { + if all_selection_lines_are_comments { + buffer.edit(edit_ranges.iter().cloned(), "", cx); + } else { + let min_column = edit_ranges.iter().map(|r| r.start.column).min().unwrap(); + let edit_ranges = edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + position..position + }); + buffer.edit(edit_ranges, &full_comment_prefix, cx); + } + } + } + }); + + self.update_selections( + self.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); + self.end_transaction(cx); + } + + pub fn select_larger_syntax_node( + &mut self, + _: &SelectLargerSyntaxNode, + cx: &mut ViewContext, + ) { + let old_selections = self.local_selections::(cx).into_boxed_slice(); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); + let mut selected_larger_node = false; + let new_selections = old_selections + .iter() + .map(|selection| { + let old_range = selection.start..selection.end; + let mut new_range = old_range.clone(); + while let Some(containing_range) = + buffer.range_for_syntax_ancestor(new_range.clone()) + { + new_range = containing_range; + if !display_map.intersects_fold(new_range.start) + && !display_map.intersects_fold(new_range.end) + { + break; + } + } + + selected_larger_node |= new_range != old_range; + Selection { + id: selection.id, + start: new_range.start, + end: new_range.end, + goal: SelectionGoal::None, + reversed: selection.reversed, + } + }) + .collect::>(); + + if selected_larger_node { + stack.push(old_selections); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + } + self.select_larger_syntax_node_stack = stack; + } + + pub fn select_smaller_syntax_node( + &mut self, + _: &SelectSmallerSyntaxNode, + cx: &mut ViewContext, + ) { + let mut stack = mem::take(&mut self.select_larger_syntax_node_stack); + if let Some(selections) = stack.pop() { + self.update_selections(selections.to_vec(), Some(Autoscroll::Fit), cx); + } + self.select_larger_syntax_node_stack = stack; + } + + pub fn move_to_enclosing_bracket( + &mut self, + _: &MoveToEnclosingBracket, + cx: &mut ViewContext, + ) { + let mut selections = self.local_selections::(cx); + let buffer = self.buffer.read(cx).snapshot(cx); + for selection in &mut selections { + if let Some((open_range, close_range)) = + buffer.enclosing_bracket_ranges(selection.start..selection.end) + { + let close_range = close_range.to_inclusive(); + let destination = if close_range.contains(&selection.start) + && close_range.contains(&selection.end) + { + open_range.end + } else { + *close_range.start() + }; + selection.start = destination; + selection.end = destination; + } + } + + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext) { + let buffer = self.buffer.read(cx).snapshot(cx); + let selection = self.newest_selection::(&buffer); + let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { + active_diagnostics + .primary_range + .to_offset(&buffer) + .to_inclusive() + }); + let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() { + if active_primary_range.contains(&selection.head()) { + *active_primary_range.end() + } else { + selection.head() + } + } else { + selection.head() + }; + + loop { + let next_group = buffer + .diagnostics_in_range::<_, usize>(search_start..buffer.len()) + .find_map(|entry| { + if entry.diagnostic.is_primary + && !entry.range.is_empty() + && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) + { + Some((entry.range, entry.diagnostic.group_id)) + } else { + None + } + }); + + if let Some((primary_range, group_id)) = next_group { + self.activate_diagnostics(group_id, cx); + self.update_selections( + vec![Selection { + id: selection.id, + start: primary_range.start, + end: primary_range.start, + reversed: false, + goal: SelectionGoal::None, + }], + Some(Autoscroll::Center), + cx, + ); + break; + } else if search_start == 0 { + break; + } else { + // Cycle around to the start of the buffer, potentially moving back to the start of + // the currently active diagnostic. + search_start = 0; + active_primary_range.take(); + } + } + } + + pub fn go_to_definition( + workspace: &mut Workspace, + _: &GoToDefinition, + cx: &mut ViewContext, + ) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(cx)) + { + editor + } else { + return; + }; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + let head = editor.newest_selection::(&buffer.read(cx)).head(); + let (buffer, head) = + if let Some(text_anchor) = editor.buffer.read(cx).text_anchor_for_position(head, cx) { + text_anchor + } else { + return; + }; + + let definitions = workspace + .project() + .update(cx, |project, cx| project.definition(&buffer, head, cx)); + cx.spawn(|workspace, mut cx| async move { + let definitions = definitions.await?; + workspace.update(&mut cx, |workspace, cx| { + let nav_history = workspace.active_pane().read(cx).nav_history().clone(); + for definition in definitions { + let range = definition.range.to_offset(definition.buffer.read(cx)); + let target_editor_handle = workspace + .open_item(BufferItemHandle(definition.buffer), cx) + .downcast::() + .unwrap(); + + target_editor_handle.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + if editor_handle != target_editor_handle { + nav_history.borrow_mut().disable(); + } + target_editor.select_ranges([range], Some(Autoscroll::Center), cx); + nav_history.borrow_mut().enable(); + }); + } + }); + + Ok::<(), anyhow::Error>(()) + }) + .detach_and_log_err(cx); + } + + pub fn find_all_references( + workspace: &mut Workspace, + _: &FindAllReferences, + cx: &mut ViewContext, + ) -> Option>> { + let active_item = workspace.active_item(cx)?; + let editor_handle = active_item.act_as::(cx)?; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + let head = editor.newest_selection::(&buffer.read(cx)).head(); + let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; + let replica_id = editor.replica_id(cx); + + let references = workspace + .project() + .update(cx, |project, cx| project.references(&buffer, head, cx)); + Some(cx.spawn(|workspace, mut cx| async move { + let mut locations = references.await?; + if locations.is_empty() { + return Ok(()); + } + + locations.sort_by_key(|location| location.buffer.id()); + let mut locations = locations.into_iter().peekable(); + let mut ranges_to_highlight = Vec::new(); + + let excerpt_buffer = cx.add_model(|cx| { + let mut symbol_name = None; + let mut multibuffer = MultiBuffer::new(replica_id); + while let Some(location) = locations.next() { + let buffer = location.buffer.read(cx); + let mut ranges_for_buffer = Vec::new(); + let range = location.range.to_offset(buffer); + ranges_for_buffer.push(range.clone()); + if symbol_name.is_none() { + symbol_name = Some(buffer.text_for_range(range).collect::()); + } + + while let Some(next_location) = locations.peek() { + if next_location.buffer == location.buffer { + ranges_for_buffer.push(next_location.range.to_offset(buffer)); + locations.next(); + } else { + break; + } + } + + ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); + ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines( + location.buffer.clone(), + ranges_for_buffer, + 1, + cx, + )); + } + multibuffer.with_title(format!("References to `{}`", symbol_name.unwrap())) + }); + + workspace.update(&mut cx, |workspace, cx| { + let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_ranges::(ranges_to_highlight, color, cx); + }); + } + }); + + Ok(()) + })) + } + + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; + + let project = self.project.clone()?; + let selection = self.newest_anchor_selection().clone(); + let (cursor_buffer, cursor_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, tail_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } + + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) + }); + + Some(cx.spawn(|this, mut cx| async move { + if let Some(rename_range) = prepare_rename.await? { + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + let tail_offset_in_rename_range = + tail_buffer_offset.saturating_sub(rename_buffer_range.start); + + this.update(&mut cx, |this, cx| { + this.take_rename(cx); + let style = this.style(cx); + let buffer = this.buffer.read(cx).read(cx); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let old_name = buffer + .text_for_range(rename_start..rename_end) + .collect::(); + drop(buffer); + + // Position the selection in the rename editor so that it matches the current selection. + let rename_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line(this.settings.clone(), None, cx); + editor + .buffer + .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx)); + editor.select_ranges( + [tail_offset_in_rename_range..cursor_offset_in_rename_range], + None, + cx, + ); + editor.highlight_ranges::( + vec![Anchor::min()..Anchor::max()], + style.diff_background_inserted, + cx, + ); + editor + }); + this.highlight_ranges::( + vec![range.clone()], + style.diff_background_deleted, + cx, + ); + this.update_selections( + vec![Selection { + id: selection.id, + start: rename_end, + end: rename_end, + reversed: false, + goal: SelectionGoal::None, + }], + None, + cx, + ); + cx.focus(&rename_editor); + let block_id = this.insert_blocks( + [BlockProperties { + position: range.start.clone(), + height: 1, + render: Arc::new({ + let editor = rename_editor.clone(); + move |cx: &BlockContext| { + ChildView::new(editor.clone()) + .contained() + .with_padding_left(cx.anchor_x) + .boxed() + } + }), + disposition: BlockDisposition::Below, + }], + cx, + )[0]; + this.pending_rename = Some(RenameState { + range, + old_name, + editor: rename_editor, + block_id, + }); + }); + } + + Ok(()) + })) + } + + pub fn confirm_rename( + workspace: &mut Workspace, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { + let rename = editor.take_rename(cx)?; + let buffer = editor.buffer.read(cx); + let (start_buffer, start) = + buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; + if start_buffer == end_buffer { + let new_name = rename.editor.read(cx).text(cx); + Some((start_buffer, start..end, rename.old_name, new_name)) + } else { + None + } + })?; + + let rename = workspace.project().clone().update(cx, |project, cx| { + project.perform_rename( + buffer.clone(), + range.start.clone(), + new_name.clone(), + true, + cx, + ) + }); + + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + editor, + workspace, + project_transaction, + format!("Rename: {} → {}", old_name, new_name), + cx, + ) + .await + })) + } + + fn take_rename(&mut self, cx: &mut ViewContext) -> Option { + let rename = self.pending_rename.take()?; + self.remove_blocks([rename.block_id].into_iter().collect(), cx); + self.clear_highlighted_ranges::(cx); + + let editor = rename.editor.read(cx); + let buffer = editor.buffer.read(cx).snapshot(cx); + let selection = editor.newest_selection::(&buffer); + + // Update the selection to match the position of the selection inside + // the rename editor. + let snapshot = self.buffer.read(cx).snapshot(cx); + let rename_range = rename.range.to_offset(&snapshot); + let start = snapshot + .clip_offset(rename_range.start + selection.start, Bias::Left) + .min(rename_range.end); + let end = snapshot + .clip_offset(rename_range.start + selection.end, Bias::Left) + .min(rename_range.end); + self.update_selections( + vec![Selection { + id: self.newest_anchor_selection().id, + start, + end, + reversed: selection.reversed, + goal: SelectionGoal::None, + }], + None, + cx, + ); + + Some(rename) + } + + fn invalidate_rename_range( + &mut self, + buffer: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) { + if let Some(rename) = self.pending_rename.as_ref() { + if self.selections.len() == 1 { + let head = self.selections[0].head().to_offset(buffer); + let range = rename.range.to_offset(buffer).to_inclusive(); + if range.contains(&head) { + return; + } + } + let rename = self.pending_rename.take().unwrap(); + self.remove_blocks([rename.block_id].into_iter().collect(), cx); + self.clear_highlighted_ranges::(cx); + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn pending_rename(&self) -> Option<&RenameState> { + self.pending_rename.as_ref() + } + + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { + let buffer = self.buffer.read(cx).snapshot(cx); + let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer); + let is_valid = buffer + .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) + .any(|entry| { + entry.diagnostic.is_primary + && !entry.range.is_empty() + && entry.range.start == primary_range_start + && entry.diagnostic.message == active_diagnostics.primary_message + }); + + if is_valid != active_diagnostics.is_valid { + active_diagnostics.is_valid = is_valid; + let mut new_styles = HashMap::default(); + for (block_id, diagnostic) in &active_diagnostics.blocks { + new_styles.insert( + *block_id, + diagnostic_block_renderer( + diagnostic.clone(), + is_valid, + self.settings.clone(), + ), + ); + } + self.display_map + .update(cx, |display_map, _| display_map.replace_blocks(new_styles)); + } + } + } + + fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { + self.dismiss_diagnostics(cx); + self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut primary_range = None; + let mut primary_message = None; + let mut group_end = Point::zero(); + let diagnostic_group = buffer + .diagnostic_group::(group_id) + .map(|entry| { + if entry.range.end > group_end { + group_end = entry.range.end; + } + if entry.diagnostic.is_primary { + primary_range = Some(entry.range.clone()); + primary_message = Some(entry.diagnostic.message.clone()); + } + entry + }) + .collect::>(); + let primary_range = primary_range.unwrap(); + let primary_message = primary_message.unwrap(); + let primary_range = + buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end); + + let blocks = display_map + .insert_blocks( + diagnostic_group.iter().map(|entry| { + let diagnostic = entry.diagnostic.clone(); + let message_height = diagnostic.message.lines().count() as u8; + BlockProperties { + position: buffer.anchor_after(entry.range.start), + height: message_height, + render: diagnostic_block_renderer( + diagnostic, + true, + self.settings.clone(), + ), + disposition: BlockDisposition::Below, + } + }), + cx, + ) + .into_iter() + .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic)) + .collect(); + + Some(ActiveDiagnosticGroup { + primary_range, + primary_message, + blocks, + is_valid: true, + }) + }); + } + + fn dismiss_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostic_group) = self.active_diagnostics.take() { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); + }); + cx.notify(); + } + } + + fn build_columnar_selection( + &mut self, + display_map: &DisplaySnapshot, + row: u32, + columns: &Range, + reversed: bool, + ) -> Option> { + let is_empty = columns.start == columns.end; + let line_len = display_map.line_len(row); + if columns.start < line_len || (is_empty && columns.start == line_len) { + let start = DisplayPoint::new(row, columns.start); + let end = DisplayPoint::new(row, cmp::min(columns.end, line_len)); + Some(Selection { + id: post_inc(&mut self.next_selection_id), + start: start.to_point(display_map), + end: end.to_point(display_map), + reversed, + goal: SelectionGoal::ColumnRange { + start: columns.start, + end: columns.end, + }, + }) + } else { + None + } + } + + pub fn local_selections_in_range( + &self, + range: Range, + display_map: &DisplaySnapshot, + ) -> Vec> { + let buffer = &display_map.buffer_snapshot; + + let start_ix = match self + .selections + .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer).unwrap()) + { + Ok(ix) | Err(ix) => ix, + }; + let end_ix = match self + .selections + .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer).unwrap()) + { + Ok(ix) => ix + 1, + Err(ix) => ix, + }; + + fn point_selection( + selection: &Selection, + buffer: &MultiBufferSnapshot, + ) -> Selection { + let start = selection.start.to_point(&buffer); + let end = selection.end.to_point(&buffer); + Selection { + id: selection.id, + start, + end, + reversed: selection.reversed, + goal: selection.goal, + } + } + + self.selections[start_ix..end_ix] + .iter() + .chain( + self.pending_selection + .as_ref() + .map(|pending| &pending.selection), + ) + .map(|s| point_selection(s, &buffer)) + .collect() + } + + pub fn local_selections<'a, D>(&self, cx: &'a AppContext) -> Vec> + where + D: 'a + TextDimension + Ord + Sub, + { + let buffer = self.buffer.read(cx).snapshot(cx); + let mut selections = self + .resolve_selections::(self.selections.iter(), &buffer) + .peekable(); + + let mut pending_selection = self.pending_selection::(&buffer); + + iter::from_fn(move || { + if let Some(pending) = pending_selection.as_mut() { + while let Some(next_selection) = selections.peek() { + if pending.start <= next_selection.end && pending.end >= next_selection.start { + let next_selection = selections.next().unwrap(); + if next_selection.start < pending.start { + pending.start = next_selection.start; + } + if next_selection.end > pending.end { + pending.end = next_selection.end; + } + } else if next_selection.end < pending.start { + return selections.next(); + } else { + break; + } + } + + pending_selection.take() + } else { + selections.next() + } + }) + .collect() + } + + fn resolve_selections<'a, D, I>( + &self, + selections: I, + snapshot: &MultiBufferSnapshot, + ) -> impl 'a + Iterator> + where + D: TextDimension + Ord + Sub, + I: 'a + IntoIterator>, + { + let (to_summarize, selections) = selections.into_iter().tee(); + let mut summaries = snapshot + .summaries_for_anchors::(to_summarize.flat_map(|s| [&s.start, &s.end])) + .into_iter(); + selections.map(move |s| Selection { + id: s.id, + start: summaries.next().unwrap(), + end: summaries.next().unwrap(), + reversed: s.reversed, + goal: s.goal, + }) + } + + fn pending_selection>( + &self, + snapshot: &MultiBufferSnapshot, + ) -> Option> { + self.pending_selection + .as_ref() + .map(|pending| self.resolve_selection(&pending.selection, &snapshot)) + } + + fn resolve_selection>( + &self, + selection: &Selection, + buffer: &MultiBufferSnapshot, + ) -> Selection { + Selection { + id: selection.id, + start: selection.start.summary::(&buffer), + end: selection.end.summary::(&buffer), + reversed: selection.reversed, + goal: selection.goal, + } + } + + fn selection_count<'a>(&self) -> usize { + let mut count = self.selections.len(); + if self.pending_selection.is_some() { + count += 1; + } + count + } + + pub fn oldest_selection>( + &self, + snapshot: &MultiBufferSnapshot, + ) -> Selection { + self.selections + .iter() + .min_by_key(|s| s.id) + .map(|selection| self.resolve_selection(selection, snapshot)) + .or_else(|| self.pending_selection(snapshot)) + .unwrap() + } + + pub fn newest_selection>( + &self, + snapshot: &MultiBufferSnapshot, + ) -> Selection { + self.resolve_selection(self.newest_anchor_selection(), snapshot) + } + + pub fn newest_anchor_selection(&self) -> &Selection { + self.pending_selection + .as_ref() + .map(|s| &s.selection) + .or_else(|| self.selections.iter().max_by_key(|s| s.id)) + .unwrap() + } + + pub fn update_selections( + &mut self, + mut selections: Vec>, + autoscroll: Option, + cx: &mut ViewContext, + ) where + T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug, + { + let buffer = self.buffer.read(cx).snapshot(cx); + selections.sort_unstable_by_key(|s| s.start); + + // Merge overlapping selections. + let mut i = 1; + while i < selections.len() { + if selections[i - 1].end >= selections[i].start { + let removed = selections.remove(i); + if removed.start < selections[i - 1].start { + selections[i - 1].start = removed.start; + } + if removed.end > selections[i - 1].end { + selections[i - 1].end = removed.end; + } + } else { + i += 1; + } + } + + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } + + self.set_selections( + Arc::from_iter(selections.into_iter().map(|selection| { + let end_bias = if selection.end > selection.start { + Bias::Left + } else { + Bias::Right + }; + Selection { + id: selection.id, + start: buffer.anchor_after(selection.start), + end: buffer.anchor_at(selection.end, end_bias), + reversed: selection.reversed, + goal: selection.goal, + } + })), + None, + cx, + ); + } + + /// Compute new ranges for any selections that were located in excerpts that have + /// since been removed. + /// + /// Returns a `HashMap` indicating which selections whose former head position + /// was no longer present. The keys of the map are selection ids. The values are + /// the id of the new excerpt where the head of the selection has been moved. + pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { + let snapshot = self.buffer.read(cx).read(cx); + let anchors_with_status = snapshot.refresh_anchors( + self.selections + .iter() + .flat_map(|selection| [&selection.start, &selection.end]), + ); + let offsets = + snapshot.summaries_for_anchors::(anchors_with_status.iter().map(|a| &a.1)); + let offsets = offsets.chunks(2); + let statuses = anchors_with_status + .chunks(2) + .map(|a| (a[0].0 / 2, a[0].2, a[1].2)); + + let mut selections_with_lost_position = HashMap::default(); + let new_selections = offsets + .zip(statuses) + .map(|(offsets, (selection_ix, kept_start, kept_end))| { + let selection = &self.selections[selection_ix]; + let kept_head = if selection.reversed { + kept_start + } else { + kept_end + }; + if !kept_head { + selections_with_lost_position + .insert(selection.id, selection.head().excerpt_id.clone()); + } + + Selection { + id: selection.id, + start: offsets[0], + end: offsets[1], + reversed: selection.reversed, + goal: selection.goal, + } + }) + .collect(); + drop(snapshot); + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + selections_with_lost_position + } + + fn set_selections( + &mut self, + selections: Arc<[Selection]>, + pending_selection: Option, + cx: &mut ViewContext, + ) { + let old_cursor_position = self.newest_anchor_selection().head(); + + self.selections = selections; + self.pending_selection = pending_selection; + if self.focused { + self.buffer.update(cx, |buffer, cx| { + buffer.set_active_selections(&self.selections, cx) + }); + } + + let display_map = self + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + self.add_selections_state = None; + self.select_next_state = None; + self.select_larger_syntax_node_stack.clear(); + self.autoclose_stack.invalidate(&self.selections, &buffer); + self.snippet_stack.invalidate(&self.selections, &buffer); + self.invalidate_rename_range(&buffer, cx); + + let new_cursor_position = self.newest_anchor_selection().head(); + + self.push_to_nav_history( + old_cursor_position.clone(), + Some(new_cursor_position.to_point(&buffer)), + cx, + ); + + let completion_menu = match self.context_menu.as_mut() { + Some(ContextMenu::Completions(menu)) => Some(menu), + _ => { + self.context_menu.take(); + None + } + }; + + if let Some(completion_menu) = completion_menu { + let cursor_position = new_cursor_position.to_offset(&buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_menu.initial_position.clone()); + if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) + { + let query = Self::completion_query(&buffer, cursor_position); + cx.background() + .block(completion_menu.filter(query.as_deref(), cx.background().clone())); + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_context_menu(cx); + } + } + + if old_cursor_position.to_display_point(&display_map).row() + != new_cursor_position.to_display_point(&display_map).row() + { + self.available_code_actions.take(); + } + self.refresh_code_actions(cx); + self.refresh_document_highlights(cx); + + self.pause_cursor_blinking(cx); + cx.emit(Event::SelectionsChanged); + } + + pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + self.autoscroll_request = Some(autoscroll); + cx.notify(); + } + + fn start_transaction(&mut self, cx: &mut ViewContext) { + self.start_transaction_at(Instant::now(), cx); + } + + fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + self.end_selection(cx); + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) + { + self.selection_history + .insert(tx_id, (self.selections.clone(), None)); + } + } + + fn end_transaction(&mut self, cx: &mut ViewContext) { + self.end_transaction_at(Instant::now(), cx); + } + + fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { + if let Some(tx_id) = self + .buffer + .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { + *end_selections = Some(self.selections.clone()); + } else { + log::error!("unexpectedly ended a transaction that wasn't started by this editor"); + } + } + } + + pub fn page_up(&mut self, _: &PageUp, _: &mut ViewContext) { + log::info!("Editor::page_up"); + } + + pub fn page_down(&mut self, _: &PageDown, _: &mut ViewContext) { + log::info!("Editor::page_down"); + } + + pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { + let mut fold_ranges = Vec::new(); + + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + for selection in selections { + let range = selection.display_range(&display_map).sorted(); + let buffer_start_row = range.start.to_point(&display_map).row; + + for row in (0..=range.end.row()).rev() { + if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) { + let fold_range = self.foldable_range_for_line(&display_map, row); + if fold_range.end.row >= buffer_start_row { + fold_ranges.push(fold_range); + if row <= range.start.row() { + break; + } + } + } + } + } + + self.fold_ranges(fold_ranges, cx); + } + + pub fn unfold(&mut self, _: &Unfold, cx: &mut ViewContext) { + let selections = self.local_selections::(cx); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = &display_map.buffer_snapshot; + let ranges = selections + .iter() + .map(|s| { + let range = s.display_range(&display_map).sorted(); + let mut start = range.start.to_point(&display_map); + let mut end = range.end.to_point(&display_map); + start.column = 0; + end.column = buffer.line_len(end.row); + start..end + }) + .collect::>(); + self.unfold_ranges(ranges, cx); + } + + fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool { + let max_point = display_map.max_point(); + if display_row >= max_point.row() { + false + } else { + let (start_indent, is_blank) = display_map.line_indent(display_row); + if is_blank { + false + } else { + for display_row in display_row + 1..=max_point.row() { + let (indent, is_blank) = display_map.line_indent(display_row); + if !is_blank { + return indent > start_indent; + } + } + false + } + } + } + + fn foldable_range_for_line( + &self, + display_map: &DisplaySnapshot, + start_row: u32, + ) -> Range { + let max_point = display_map.max_point(); + + let (start_indent, _) = display_map.line_indent(start_row); + let start = DisplayPoint::new(start_row, display_map.line_len(start_row)); + let mut end = None; + for row in start_row + 1..=max_point.row() { + let (indent, is_blank) = display_map.line_indent(row); + if !is_blank && indent <= start_indent { + end = Some(DisplayPoint::new(row - 1, display_map.line_len(row - 1))); + break; + } + } + + let end = end.unwrap_or(max_point); + return start.to_point(display_map)..end.to_point(display_map); + } + + pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { + let selections = self.local_selections::(cx); + let ranges = selections.into_iter().map(|s| s.start..s.end); + self.fold_ranges(ranges, cx); + } + + fn fold_ranges( + &mut self, + ranges: impl IntoIterator>, + cx: &mut ViewContext, + ) { + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_some() { + self.display_map.update(cx, |map, cx| map.fold(ranges, cx)); + self.request_autoscroll(Autoscroll::Fit, cx); + cx.notify(); + } + } + + fn unfold_ranges(&mut self, ranges: Vec>, cx: &mut ViewContext) { + if !ranges.is_empty() { + self.display_map + .update(cx, |map, cx| map.unfold(ranges, cx)); + self.request_autoscroll(Autoscroll::Fit, cx); + cx.notify(); + } + } + + pub fn insert_blocks( + &mut self, + blocks: impl IntoIterator>, + cx: &mut ViewContext, + ) -> Vec { + let blocks = self + .display_map + .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); + self.request_autoscroll(Autoscroll::Fit, cx); + blocks + } + + pub fn replace_blocks( + &mut self, + blocks: HashMap, + cx: &mut ViewContext, + ) { + self.display_map + .update(cx, |display_map, _| display_map.replace_blocks(blocks)); + self.request_autoscroll(Autoscroll::Fit, cx); + } + + pub fn remove_blocks(&mut self, block_ids: HashSet, cx: &mut ViewContext) { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(block_ids, cx) + }); + } + + pub fn longest_row(&self, cx: &mut MutableAppContext) -> u32 { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .longest_row() + } + + pub fn max_point(&self, cx: &mut MutableAppContext) -> DisplayPoint { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .max_point() + } + + pub fn text(&self, cx: &AppContext) -> String { + self.buffer.read(cx).read(cx).text() + } + + pub fn set_text(&mut self, text: impl Into, cx: &mut ViewContext) { + self.buffer + .read(cx) + .as_singleton() + .expect("you can only call set_text on editors for singleton buffers") + .update(cx, |buffer, cx| buffer.set_text(text, cx)); + } + + pub fn display_text(&self, cx: &mut MutableAppContext) -> String { + self.display_map + .update(cx, |map, cx| map.snapshot(cx)) + .text() + } + + pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { + let language = self.language(cx); + let settings = self.settings.borrow(); + let mode = self + .soft_wrap_mode_override + .unwrap_or_else(|| settings.soft_wrap(language)); + match mode { + settings::SoftWrap::None => SoftWrap::None, + settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length(language)) + } + } + } + + pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext) { + self.soft_wrap_mode_override = Some(mode); + cx.notify(); + } + + pub fn set_wrap_width(&self, width: Option, cx: &mut MutableAppContext) -> bool { + self.display_map + .update(cx, |map, cx| map.set_wrap_width(width, cx)) + } + + pub fn set_highlighted_rows(&mut self, rows: Option>) { + self.highlighted_rows = rows; + } + + pub fn highlighted_rows(&self) -> Option> { + self.highlighted_rows.clone() + } + + pub fn highlight_ranges( + &mut self, + ranges: Vec>, + color: Color, + cx: &mut ViewContext, + ) { + self.highlighted_ranges + .insert(TypeId::of::(), (color, ranges)); + cx.notify(); + } + + pub fn clear_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Color, Vec>)> { + cx.notify(); + self.highlighted_ranges.remove(&TypeId::of::()) + } + + #[cfg(feature = "test-support")] + pub fn all_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Vec<(Range, Color)> { + let snapshot = self.snapshot(cx); + let buffer = &snapshot.buffer_snapshot; + let start = buffer.anchor_before(0); + let end = buffer.anchor_after(buffer.len()); + self.highlighted_ranges_in_range(start..end, &snapshot) + } + + pub fn highlighted_ranges_for_type(&self) -> Option<(Color, &[Range])> { + self.highlighted_ranges + .get(&TypeId::of::()) + .map(|(color, ranges)| (*color, ranges.as_slice())) + } + + pub fn highlighted_ranges_in_range( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + ) -> Vec<(Range, Color)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + for (color, ranges) in self.highlighted_ranges.values() { + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((start..end, *color)) + } + } + results + } + + fn next_blink_epoch(&mut self) -> usize { + self.blink_epoch += 1; + self.blink_epoch + } + + fn pause_cursor_blinking(&mut self, cx: &mut ViewContext) { + if !self.focused { + return; + } + + self.show_local_cursors = true; + cx.notify(); + + let epoch = self.next_blink_epoch(); + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) + } + } + }) + .detach(); + } + + fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext) { + if epoch == self.blink_epoch { + self.blinking_paused = false; + self.blink_cursors(epoch, cx); + } + } + + fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext) { + if epoch == self.blink_epoch && self.focused && !self.blinking_paused { + self.show_local_cursors = !self.show_local_cursors; + cx.notify(); + + let epoch = self.next_blink_epoch(); + cx.spawn(|this, mut cx| { + let this = this.downgrade(); + async move { + Timer::after(CURSOR_BLINK_INTERVAL).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx)); + } + } + }) + .detach(); + } + } + + pub fn show_local_cursors(&self) -> bool { + self.show_local_cursors + } + + fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + cx.notify(); + } + + fn on_buffer_event( + &mut self, + _: ModelHandle, + event: &language::Event, + cx: &mut ViewContext, + ) { + match event { + language::Event::Edited => { + self.refresh_active_diagnostics(cx); + self.refresh_code_actions(cx); + cx.emit(Event::Edited); + } + language::Event::Dirtied => cx.emit(Event::Dirtied), + language::Event::Saved => cx.emit(Event::Saved), + language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), + language::Event::Reloaded => cx.emit(Event::TitleChanged), + language::Event::Closed => cx.emit(Event::Closed), + language::Event::DiagnosticsUpdated => { + self.refresh_active_diagnostics(cx); + } + _ => {} + } + } + + fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + cx.notify(); + } + + pub fn set_searchable(&mut self, searchable: bool) { + self.searchable = searchable; + } + + pub fn searchable(&self) -> bool { + self.searchable + } + + fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(cx)) + { + editor + } else { + cx.propagate_action(); + return; + }; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + if buffer.is_singleton() { + cx.propagate_action(); + return; + } + + let mut new_selections_by_buffer = HashMap::default(); + for selection in editor.local_selections::(cx) { + for (buffer, mut range) in + buffer.range_to_buffer_ranges(selection.start..selection.end, cx) + { + if selection.reversed { + mem::swap(&mut range.start, &mut range.end); + } + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push(range) + } + } + + editor_handle.update(cx, |editor, cx| { + editor.push_to_nav_history(editor.newest_anchor_selection().head(), None, cx); + }); + let nav_history = workspace.active_pane().read(cx).nav_history().clone(); + nav_history.borrow_mut().disable(); + + // We defer the pane interaction because we ourselves are a workspace item + // and activating a new item causes the pane to call a method on us reentrantly, + // which panics if we're on the stack. + cx.defer(move |workspace, cx| { + for (ix, (buffer, ranges)) in new_selections_by_buffer.into_iter().enumerate() { + let buffer = BufferItemHandle(buffer); + if ix == 0 && !workspace.activate_pane_for_item(&buffer, cx) { + workspace.activate_next_pane(cx); + } + + let editor = workspace + .open_item(buffer, cx) + .downcast::() + .unwrap(); + + editor.update(cx, |editor, cx| { + editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); + }); + } + + nav_history.borrow_mut().enable(); + }); + } +} + +impl EditorSnapshot { + pub fn is_focused(&self) -> bool { + self.is_focused + } + + pub fn placeholder_text(&self) -> Option<&Arc> { + self.placeholder_text.as_ref() + } + + pub fn scroll_position(&self) -> Vector2F { + compute_scroll_position( + &self.display_snapshot, + self.scroll_position, + &self.scroll_top_anchor, + ) + } +} + +impl Deref for EditorSnapshot { + type Target = DisplaySnapshot; + + fn deref(&self) -> &Self::Target { + &self.display_snapshot + } +} + +fn compute_scroll_position( + snapshot: &DisplaySnapshot, + mut scroll_position: Vector2F, + scroll_top_anchor: &Option, +) -> Vector2F { + if let Some(anchor) = scroll_top_anchor { + let scroll_top = anchor.to_display_point(snapshot).row() as f32; + scroll_position.set_y(scroll_top + scroll_position.y()); + } else { + scroll_position.set_y(0.); + } + scroll_position +} + +#[derive(Copy, Clone)] +pub enum Event { + Activate, + Edited, + Blurred, + Dirtied, + Saved, + TitleChanged, + SelectionsChanged, + Closed, +} + +impl Entity for Editor { + type Event = Event; +} + +impl View for Editor { + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let style = self.style(cx); + self.display_map.update(cx, |map, cx| { + map.set_font(style.text.font_id, style.text.font_size, cx) + }); + EditorElement::new(self.handle.clone(), style.clone()).boxed() + } + + fn ui_name() -> &'static str { + "Editor" + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + self.focused = true; + self.blink_cursors(self.blink_epoch, cx); + self.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx); + buffer.set_active_selections(&self.selections, cx) + }); + } + + fn on_blur(&mut self, cx: &mut ViewContext) { + self.focused = false; + self.show_local_cursors = false; + self.buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + self.hide_context_menu(cx); + cx.emit(Event::Blurred); + cx.notify(); + } + + fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { + let mut cx = Self::default_keymap_context(); + let mode = match self.mode { + EditorMode::SingleLine => "single_line", + EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Full => "full", + }; + cx.map.insert("mode".into(), mode.into()); + if self.pending_rename.is_some() { + cx.set.insert("renaming".into()); + } + match self.context_menu.as_ref() { + Some(ContextMenu::Completions(_)) => { + cx.set.insert("showing_completions".into()); + } + Some(ContextMenu::CodeActions(_)) => { + cx.set.insert("showing_code_actions".into()); + } + None => {} + } + cx + } +} + +fn build_style( + settings: &Settings, + get_field_editor_theme: Option, + cx: &AppContext, +) -> EditorStyle { + let mut theme = settings.theme.editor.clone(); + if let Some(get_field_editor_theme) = get_field_editor_theme { + let field_editor_theme = get_field_editor_theme(&settings.theme); + if let Some(background) = field_editor_theme.container.background_color { + theme.background = background; + } + theme.text_color = field_editor_theme.text.color; + theme.selection = field_editor_theme.selection; + EditorStyle { + text: field_editor_theme.text, + placeholder_text: field_editor_theme.placeholder_text, + theme, + } + } else { + let font_cache = cx.font_cache(); + let font_family_id = settings.buffer_font_family; + let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); + let font_properties = Default::default(); + let font_id = font_cache + .select_font(font_family_id, &font_properties) + .unwrap(); + let font_size = settings.buffer_font_size; + EditorStyle { + text: TextStyle { + color: settings.theme.editor.text_color, + font_family_name, + font_family_id, + font_id, + font_size, + font_properties, + underline: None, + }, + placeholder_text: None, + theme, + } + } +} + +impl SelectionExt for Selection { + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range { + let start = self.start.to_point(buffer); + let end = self.end.to_point(buffer); + if self.reversed { + end..start + } else { + start..end + } + } + + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range { + let start = self.start.to_offset(buffer); + let end = self.end.to_offset(buffer); + if self.reversed { + end..start + } else { + start..end + } + } + + fn display_range(&self, map: &DisplaySnapshot) -> Range { + let start = self + .start + .to_point(&map.buffer_snapshot) + .to_display_point(map); + let end = self + .end + .to_point(&map.buffer_snapshot) + .to_display_point(map); + if self.reversed { + end..start + } else { + start..end + } + } + + fn spanned_rows( + &self, + include_end_if_at_line_start: bool, + map: &DisplaySnapshot, + ) -> Range { + let start = self.start.to_point(&map.buffer_snapshot); + let mut end = self.end.to_point(&map.buffer_snapshot); + if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { + end.row -= 1; + } + + let buffer_start = map.prev_line_boundary(start).0; + let buffer_end = map.next_line_boundary(end).0; + buffer_start.row..buffer_end.row + 1 + } +} + +impl InvalidationStack { + fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) + where + S: Clone + ToOffset, + { + while let Some(region) = self.last() { + let all_selections_inside_invalidation_ranges = + if selections.len() == region.ranges().len() { + selections + .iter() + .zip(region.ranges().iter().map(|r| r.to_offset(&buffer))) + .all(|(selection, invalidation_range)| { + let head = selection.head().to_offset(&buffer); + invalidation_range.start <= head && invalidation_range.end >= head + }) + } else { + false + }; + + if all_selections_inside_invalidation_ranges { + break; + } else { + self.pop(); + } + } + } +} + +impl Default for InvalidationStack { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Deref for InvalidationStack { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for InvalidationStack { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl InvalidationRegion for BracketPairState { + fn ranges(&self) -> &[Range] { + &self.ranges + } +} + +impl InvalidationRegion for SnippetState { + fn ranges(&self) -> &[Range] { + &self.ranges[self.active_index] + } +} + +impl Deref for EditorStyle { + type Target = theme::Editor; + + fn deref(&self) -> &Self::Target { + &self.theme + } +} + +pub fn diagnostic_block_renderer( + diagnostic: Diagnostic, + is_valid: bool, + settings: watch::Receiver, +) -> RenderBlock { + let mut highlighted_lines = Vec::new(); + for line in diagnostic.message.lines() { + highlighted_lines.push(highlight_diagnostic_message(line)); + } + + Arc::new(move |cx: &BlockContext| { + let settings = settings.borrow(); + let theme = &settings.theme.editor; + let style = diagnostic_style(diagnostic.severity, is_valid, theme); + let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); + Flex::column() + .with_children(highlighted_lines.iter().map(|(line, highlights)| { + Label::new( + line.clone(), + style.message.clone().with_font_size(font_size), + ) + .with_highlights(highlights.clone()) + .contained() + .with_margin_left(cx.anchor_x) + .boxed() + })) + .aligned() + .left() + .boxed() + }) +} + +pub fn highlight_diagnostic_message(message: &str) -> (String, Vec) { + let mut message_without_backticks = String::new(); + let mut prev_offset = 0; + let mut inside_block = false; + let mut highlights = Vec::new(); + for (match_ix, (offset, _)) in message + .match_indices('`') + .chain([(message.len(), "")]) + .enumerate() + { + message_without_backticks.push_str(&message[prev_offset..offset]); + if inside_block { + highlights.extend(prev_offset - match_ix..offset - match_ix); + } + + inside_block = !inside_block; + prev_offset = offset + 1; + } + + (message_without_backticks, highlights) +} + +pub fn diagnostic_style( + severity: DiagnosticSeverity, + valid: bool, + theme: &theme::Editor, +) -> DiagnosticStyle { + match (severity, valid) { + (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(), + (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(), + (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(), + (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(), + (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(), + (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(), + _ => theme.invalid_hint_diagnostic.clone(), + } +} + +pub fn combine_syntax_and_fuzzy_match_highlights( + text: &str, + default_style: HighlightStyle, + syntax_ranges: impl Iterator, HighlightStyle)>, + match_indices: &[usize], +) -> Vec<(Range, HighlightStyle)> { + let mut result = Vec::new(); + let mut match_indices = match_indices.iter().copied().peekable(); + + for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) + { + syntax_highlight.font_properties.weight(Default::default()); + + // Add highlights for any fuzzy match characters before the next + // syntax highlight range. + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.start { + break; + } + match_indices.next(); + let end_index = char_ix_after(match_index, text); + let mut match_style = default_style; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + } + + if range.start == usize::MAX { + break; + } + + // Add highlights for any fuzzy match characters within the + // syntax highlight range. + let mut offset = range.start; + while let Some(&match_index) = match_indices.peek() { + if match_index >= range.end { + break; + } + + match_indices.next(); + if match_index > offset { + result.push((offset..match_index, syntax_highlight)); + } + + let mut end_index = char_ix_after(match_index, text); + while let Some(&next_match_index) = match_indices.peek() { + if next_match_index == end_index && next_match_index < range.end { + end_index = char_ix_after(next_match_index, text); + match_indices.next(); + } else { + break; + } + } + + let mut match_style = syntax_highlight; + match_style.font_properties.weight(fonts::Weight::BOLD); + result.push((match_index..end_index, match_style)); + offset = end_index; + } + + if offset < range.end { + result.push((offset..range.end, syntax_highlight)); + } + } + + fn char_ix_after(ix: usize, text: &str) -> usize { + ix + text[ix..].chars().next().unwrap().len_utf8() + } + + result +} + +pub fn styled_runs_for_code_label<'a>( + label: &'a CodeLabel, + default_color: Color, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + const MUTED_OPACITY: usize = 165; + + let mut muted_default_style = HighlightStyle { + color: default_color, + ..Default::default() + }; + muted_default_style.color.a = ((default_color.a as usize * MUTED_OPACITY) / 255) as u8; + + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style.clone(); + muted_style.color.a = ((style.color.a as usize * MUTED_OPACITY) / 255) as u8; + + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, muted_default_style)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); + + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), muted_default_style)); + } + + runs + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use language::LanguageConfig; + use lsp::FakeLanguageServer; + use project::{FakeFs, ProjectPath}; + use smol::stream::StreamExt; + use std::{cell::RefCell, rc::Rc, time::Instant}; + use text::Point; + use unindent::Unindent; + use util::test::sample_text; + + #[gpui::test] + fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { + let mut now = Instant::now(); + let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + let group_interval = buffer.read(cx).transaction_group_interval(); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let settings = Settings::test(cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + editor.update(cx, |editor, cx| { + editor.start_transaction_at(now, cx); + editor.select_ranges([2..4], None, cx); + editor.insert("cd", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cd56"); + assert_eq!(editor.selected_ranges(cx), vec![4..4]); + + editor.start_transaction_at(now, cx); + editor.select_ranges([4..5], None, cx); + editor.insert("e", cx); + editor.end_transaction_at(now, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selected_ranges(cx), vec![5..5]); + + now += group_interval + Duration::from_millis(1); + editor.select_ranges([2..2], None, cx); + + // Simulate an edit in another editor + buffer.update(cx, |buffer, cx| { + buffer.start_transaction_at(now, cx); + buffer.edit([0..1], "a", cx); + buffer.edit([1..1], "b", cx); + buffer.end_transaction_at(now, cx); + }); + + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selected_ranges(cx), vec![3..3]); + + // Last transaction happened past the group interval in a different editor. + // Undo it individually and don't restore selections. + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selected_ranges(cx), vec![2..2]); + + // First two transactions happened within the group interval in this editor. + // Undo them together and restore selections. + editor.undo(&Undo, cx); + editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op. + assert_eq!(editor.text(cx), "123456"); + assert_eq!(editor.selected_ranges(cx), vec![0..0]); + + // Redo the first two transactions together. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "12cde6"); + assert_eq!(editor.selected_ranges(cx), vec![5..5]); + + // Redo the last transaction on its own. + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "ab2cde6"); + assert_eq!(editor.selected_ranges(cx), vec![6..6]); + + // Test empty transactions. + editor.start_transaction_at(now, cx); + editor.end_transaction_at(now, cx); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "12cde6"); + }); + } + + #[gpui::test] + fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + let settings = Settings::test(cx); + let (_, editor) = + cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + + editor.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)] + ); + + editor.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [ + DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0) + ] + ); + + editor.update(cx, |view, cx| { + view.end_selection(cx); + }); + + assert_eq!( + editor.update(cx, |view, cx| view.selected_display_ranges(cx)), + [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] + ); + } + + #[gpui::test] + fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + let settings = Settings::test(cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] + ); + }); + + view.update(cx, |view, cx| { + view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] + ); + }); + } + + #[gpui::test] + fn test_navigation_history(cx: &mut gpui::MutableAppContext) { + cx.add_window(Default::default(), |cx| { + use workspace::ItemView; + let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); + let mut editor = build_editor(buffer.clone(), settings, cx); + editor.nav_history = Some(ItemNavHistory::new(nav_history.clone(), &cx.handle())); + + // Move the cursor a small distance. + // Nothing is added to the navigation history. + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + editor.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); + assert!(nav_history.borrow_mut().pop_backward().is_none()); + + // Move the cursor a large distance. + // The history can jump back to the previous position. + editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); + let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] + ); + + // Move the cursor a small distance via the mouse. + // Nothing is added to the navigation history. + editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + assert!(nav_history.borrow_mut().pop_backward().is_none()); + + // Move the cursor a large distance via the mouse. + // The history can jump back to the previous position. + editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx); + editor.end_selection(cx); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] + ); + let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); + editor.navigate(nav_entry.data.unwrap(), cx); + assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!( + editor.selected_display_ranges(cx), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] + ); + + editor + }); + } + + #[gpui::test] + fn test_cancel(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + let settings = Settings::test(cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); + view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx); + view.end_selection(cx); + + view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); + view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx); + view.end_selection(cx); + assert_eq!( + view.selected_display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] + ); + }); + + view.update(cx, |view, cx| { + view.cancel(&Cancel, cx); + assert_eq!( + view.selected_display_ranges(cx), + [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] + ); + }); + } + + #[gpui::test] + fn test_fold(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple( + &" + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() { + 2 + } + + fn c() { + 3 + } + } + " + .unindent(), + cx, + ); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], cx); + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {… + } + + fn c() {… + } + } + " + .unindent(), + ); + + view.fold(&Fold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo {… + } + " + .unindent(), + ); + + view.unfold(&Unfold, cx); + assert_eq!( + view.display_text(cx), + " + impl Foo { + // Hello! + + fn a() { + 1 + } + + fn b() {… + } + + fn c() {… + } + } + " + .unindent(), + ); + + view.unfold(&Unfold, cx); + assert_eq!(view.display_text(cx), buffer.read(cx).read(cx).text()); + }); + } + + #[gpui::test] + fn test_move_cursor(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + buffer.update(cx, |buffer, cx| { + buffer.edit( + vec![ + Point::new(1, 0)..Point::new(1, 0), + Point::new(1, 1)..Point::new(1, 1), + ], + "\t", + cx, + ); + }); + + view.update(cx, |view, cx| { + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + ); + + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.move_to_end(&MoveToEnd, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] + ); + + view.move_to_beginning(&MoveToBeginning, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] + ); + + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], cx); + view.select_to_beginning(&SelectToBeginning, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] + ); + + view.select_to_end(&SelectToEnd, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] + ); + }); + } + + #[gpui::test] + fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + assert_eq!('ⓐ'.len_utf8(), 3); + assert_eq!('α'.len_utf8(), 2); + + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 6)..Point::new(0, 12), + Point::new(1, 2)..Point::new(1, 4), + Point::new(2, 4)..Point::new(2, 8), + ], + cx, + ); + assert_eq!(view.display_text(cx), "ⓐⓑ…ⓔ\nab…e\nαβ…ε\n"); + + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ…".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "ab…".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "ab".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "a".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "α".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβ".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβ…".len())] + ); + view.move_right(&MoveRight, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβ…ε".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "ab…e".len())] + ); + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ…ⓔ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ…".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐⓑ".len())] + ); + view.move_left(&MoveLeft, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(0, "ⓐ".len())] + ); + }); + } + + #[gpui::test] + fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + view.update(cx, |view, cx| { + view.select_display_ranges(&[empty_range(0, "ⓐⓑⓒⓓⓔ".len())], cx); + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(1, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_down(&MoveDown, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(3, "abcd".len())] + ); + + view.move_up(&MoveUp, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[empty_range(2, "αβγ".len())] + ); + }); + } + + #[gpui::test] + fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ], + cx, + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + // Moving to the end of line again is a no-op. + view.update(cx, |view, cx| { + view.move_to_end_of_line(&MoveToEndOfLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_left(&MoveLeft, cx); + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_beginning_of_line(&SelectToBeginningOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_end_of_line(&SelectToEndOfLine(true), cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_end_of_line(&DeleteToEndOfLine, cx); + assert_eq!(view.display_text(cx), "ab\n de"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); + assert_eq!(view.display_text(cx), "\n"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + } + + #[gpui::test] + fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ], + cx, + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_right(&MoveRight, cx); + view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), + ] + ); + }); + } + + #[gpui::test] + fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.set_wrap_width(Some(140.), cx); + assert_eq!( + view.display_text(cx), + "use one::{\n two::three::\n four::five\n};" + ); + + view.select_display_ranges(&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], cx); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] + ); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] + ); + + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] + ); + + view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] + ); + }); + } + + #[gpui::test] + fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("one two three four", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the preceding word fragment is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12), + ], + cx, + ); + view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); + }); + + assert_eq!(buffer.read(cx).read(cx).text(), "e two te four"); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the following word fragment is deleted + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + // characters selected - they are deleted + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10), + ], + cx, + ); + view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); + }); + + assert_eq!(buffer.read(cx).read(cx).text(), "e t te our"); + } + + #[gpui::test] + fn test_newline(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6), + ], + cx, + ); + + view.newline(&Newline, cx); + assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n"); + }); + } + + #[gpui::test] + fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple( + " + a + b( + X + ) + c( + X + ) + " + .unindent() + .as_str(), + cx, + ); + + let settings = Settings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(buffer.clone(), settings, cx); + editor.select_ranges( + [ + Point::new(2, 4)..Point::new(2, 5), + Point::new(5, 4)..Point::new(5, 5), + ], + None, + cx, + ); + editor + }); + + // Edit the buffer directly, deleting ranges surrounding the editor's selections + buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + Point::new(1, 2)..Point::new(3, 0), + Point::new(4, 2)..Point::new(6, 0), + ], + "", + cx, + ); + assert_eq!( + buffer.read(cx).text(), + " + a + b() + c() + " + .unindent() + ); + }); + + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selected_ranges(cx), + &[ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 2)..Point::new(2, 2), + ], + ); + + editor.newline(&Newline, cx); + assert_eq!( + editor.text(cx), + " + a + b( + ) + c( + ) + " + .unindent() + ); + + // The selections are moved after the inserted newlines + assert_eq!( + editor.selected_ranges(cx), + &[ + Point::new(2, 0)..Point::new(2, 0), + Point::new(4, 0)..Point::new(4, 0), + ], + ); + }); + } + + #[gpui::test] + fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); + + let settings = Settings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(buffer.clone(), settings, cx); + editor.select_ranges([3..4, 11..12, 19..20], None, cx); + editor + }); + + // Edit the buffer directly, deleting ranges surrounding the editor's selections + buffer.update(cx, |buffer, cx| { + buffer.edit([2..5, 10..13, 18..21], "", cx); + assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); + }); + + editor.update(cx, |editor, cx| { + assert_eq!(editor.selected_ranges(cx), &[2..2, 7..7, 12..12],); + + editor.insert("Z", cx); + assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); + + // The selections are moved after the inserted characters + assert_eq!(editor.selected_ranges(cx), &[3..3, 9..9, 15..15],); + }); + } + + #[gpui::test] + fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(" one two\nthree\n four", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + // two selections on the same line + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5), + DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9), + ], + cx, + ); + + // indent from mid-tabstop to full tabstop + view.tab(&Tab, cx); + assert_eq!(view.text(cx), " one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), + DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11), + ] + ); + + // outdent from 1 tabstop to 0 tabstops + view.outdent(&Outdent, cx); + assert_eq!(view.text(cx), "one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3), + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), + ] + ); + + // select across line ending + view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); + + // indent and outdent affect only the preceding line + view.tab(&Tab, cx); + assert_eq!(view.text(cx), "one two\n three\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] + ); + view.outdent(&Outdent, cx); + assert_eq!(view.text(cx), "one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)] + ); + + // Ensure that indenting/outdenting works when the cursor is at column 0. + view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + view.tab(&Tab, cx); + assert_eq!(view.text(cx), "one two\n three\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] + ); + + view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); + view.outdent(&Outdent, cx); + assert_eq!(view.text(cx), "one two\nthree\n four"); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] + ); + }); + } + + #[gpui::test] + fn test_backspace(cx: &mut gpui::MutableAppContext) { + let buffer = + MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the preceding character is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.backspace(&Backspace, cx); + }); + + assert_eq!( + buffer.read(cx).read(cx).text(), + "oe two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete(cx: &mut gpui::MutableAppContext) { + let buffer = + MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + // an empty selection - the following character is deleted + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + // one character selected - it is deleted + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + // a line suffix selected - it is deleted + DisplayPoint::new(2, 6)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.delete(&Delete, cx); + }); + + assert_eq!( + buffer.read(cx).read(cx).text(), + "on two three\nfou five six\nseven ten\n" + ); + } + + #[gpui::test] + fn test_delete_line(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi"); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) + ] + ); + }); + + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx); + view.delete_line(&DeleteLine, cx); + assert_eq!(view.display_text(cx), "ghi\n"); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] + ); + }); + } + + #[gpui::test] + fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0), + ] + ); + }); + + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), + ], + cx, + ); + view.duplicate_line(&DuplicateLine, cx); + assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1), + DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1), + ] + ); + }); + } + + #[gpui::test] + fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + cx, + ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2), + ], + cx, + ); + assert_eq!( + view.display_text(cx), + "aa…bbb\nccc…eeee\nfffff\nggggg\n…i\njjjjj" + ); + + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "aa…bbb\nccc…eeee\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc…eeee\naa…bbb\nfffff\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_down(&MoveLineDown, cx); + assert_eq!( + view.display_text(cx), + "ccc…eeee\nfffff\naa…bbb\nggggg\n…i\njjjjj" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3), + DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2) + ] + ); + }); + + view.update(cx, |view, cx| { + view.move_line_up(&MoveLineUp, cx); + assert_eq!( + view.display_text(cx), + "ccc…eeee\naa…bbb\nggggg\n…i\njjjjj\nfffff" + ); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3), + DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2) + ] + ); + }); + } + + #[gpui::test] + fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + let snapshot = buffer.read(cx).snapshot(cx); + let (_, editor) = + cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + editor.update(cx, |editor, cx| { + editor.insert_blocks( + [BlockProperties { + position: snapshot.anchor_after(Point::new(2, 0)), + disposition: BlockDisposition::Below, + height: 1, + render: Arc::new(|_| Empty::new().boxed()), + }], + cx, + ); + editor.select_ranges([Point::new(2, 0)..Point::new(2, 0)], None, cx); + editor.move_line_down(&MoveLineDown, cx); + }); + } + + #[gpui::test] + fn test_clipboard(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); + let settings = Settings::test(&cx); + let view = cx + .add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }) + .1; + + // Cut with three selections. Clipboard text is divided into three slices. + view.update(cx, |view, cx| { + view.select_ranges(vec![0..7, 11..17, 22..27], None, cx); + view.cut(&Cut, cx); + assert_eq!(view.display_text(cx), "two four six "); + }); + + // Paste with three cursors. Each cursor pastes one slice of the clipboard text. + view.update(cx, |view, cx| { + view.select_ranges(vec![4..4, 9..9, 13..13], None, cx); + view.paste(&Paste, cx); + assert_eq!(view.display_text(cx), "two one✅ four three six five "); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(0, 22)..DisplayPoint::new(0, 22), + DisplayPoint::new(0, 31)..DisplayPoint::new(0, 31) + ] + ); + }); + + // Paste again but with only two cursors. Since the number of cursors doesn't + // match the number of slices in the clipboard, the entire clipboard text + // is pasted at each cursor. + view.update(cx, |view, cx| { + view.select_ranges(vec![0..0, 31..31], None, cx); + view.handle_input(&Input("( ".into()), cx); + view.paste(&Paste, cx); + view.handle_input(&Input(") ".into()), cx); + assert_eq!( + view.display_text(cx), + "( one✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + }); + + view.update(cx, |view, cx| { + view.select_ranges(vec![0..0], None, cx); + view.handle_input(&Input("123\n4567\n89\n".into()), cx); + assert_eq!( + view.display_text(cx), + "123\n4567\n89\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + }); + + // Cut with three selections, one of which is full-line. + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), + ], + cx, + ); + view.cut(&Cut, cx); + assert_eq!( + view.display_text(cx), + "13\n9\n( one✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + }); + + // Paste with three selections, noticing how the copied selection that was full-line + // gets inserted before the second cursor. + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 3), + ], + cx, + ); + view.paste(&Paste, cx); + assert_eq!( + view.display_text(cx), + "123\n4567\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 3)..DisplayPoint::new(3, 3), + ] + ); + }); + + // Copy with a single cursor only, which writes the whole line into the clipboard. + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], cx); + view.copy(&Copy, cx); + }); + + // Paste with three selections, noticing how the copied full-line selection is inserted + // before the empty selections but replaces the selection that is non-empty. + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 2), + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + ], + cx, + ); + view.paste(&Paste, cx); + assert_eq!( + view.display_text(cx), + "123\n123\n123\n67\n123\n9\n( 8ne✅ three five ) two one✅ four three six five ( one✅ three five ) " + ); + assert_eq!( + view.selected_display_ranges(cx), + &[ + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1), + ] + ); + }); + } + + #[gpui::test] + fn test_select_all(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); + let settings = Settings::test(&cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_all(&SelectAll, cx); + assert_eq!( + view.selected_display_ranges(cx), + &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] + ); + }); + } + + #[gpui::test] + fn test_select_line(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), + ], + cx, + ); + view.select_line(&SelectLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0), + DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_line(&SelectLine, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] + ); + }); + } + + #[gpui::test] + fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + view.update(cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 2)..Point::new(1, 2), + Point::new(2, 3)..Point::new(4, 1), + Point::new(7, 0)..Point::new(8, 4), + ], + cx, + ); + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ], + cx, + ); + assert_eq!(view.display_text(cx), "aa…bbb\nccc…eeee\nfffff\nggggg\n…i"); + }); + + view.update(cx, |view, cx| { + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccc…eeee\nfffff\nggggg\n…i" + ); + assert_eq!( + view.selected_display_ranges(cx), + [ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4) + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)], cx); + view.split_selection_into_lines(&SplitSelectionIntoLines, cx); + assert_eq!( + view.display_text(cx), + "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii" + ); + assert_eq!( + view.selected_display_ranges(cx), + [ + DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5), + DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5), + DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5), + DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5), + DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5), + DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0) + ] + ); + }); + } + + #[gpui::test] + fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(&cx); + let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], cx); + }); + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3) + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], cx); + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2), + ] + ); + }); + + view.update(cx, |view, cx| { + view.select_display_ranges(&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], cx); + }); + view.update(cx, |view, cx| { + view.add_selection_above(&AddSelectionAbove, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); + + view.update(cx, |view, cx| { + view.add_selection_below(&AddSelectionBelow, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1), + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1), + DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1), + ] + ); + }); + } + + #[gpui::test] + async fn test_select_larger_smaller_syntax_node(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig::default(), + Some(tree_sitter_rust::language()), + )); + + let text = r#" + use mod1::mod2::{mod3, mod4}; + + fn fn_1(param1: bool, param2: &str) { + let var1 = "text"; + } + "# + .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, settings, cx)); + view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(&mut cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ], + cx, + ); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + // Trying to expand the selected syntax node one more time has no effect. + view.update(&mut cx, |view, cx| { + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21), + ] + ); + + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Trying to shrink the selected syntax node one more time has no effect. + view.update(&mut cx, |view, cx| { + view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), + DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), + DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), + ] + ); + + // Ensure that we keep expanding the selection if the larger selection starts or ends within + // a fold. + view.update(&mut cx, |view, cx| { + view.fold_ranges( + vec![ + Point::new(0, 21)..Point::new(0, 24), + Point::new(3, 20)..Point::new(3, 22), + ], + cx, + ); + view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); + }); + assert_eq!( + view.update(&mut cx, |view, cx| view.selected_display_ranges(cx)), + &[ + DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), + DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7), + DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23), + ] + ); + } + + #[gpui::test] + async fn test_autoindent_selections(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new( + Language::new( + LanguageConfig { + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: false, + newline: true, + }, + BracketPair { + start: "(".to_string(), + end: ")".to_string(), + close: false, + newline: true, + }, + ], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap(), + ); + + let text = "fn a() {}"; + + 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 (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + editor + .condition(&cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .await; + + editor.update(&mut cx, |editor, cx| { + editor.select_ranges([5..5, 8..8, 9..9], None, cx); + editor.newline(&Newline, cx); + assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); + assert_eq!( + editor.selected_ranges(cx), + &[ + Point::new(1, 4)..Point::new(1, 4), + Point::new(3, 4)..Point::new(3, 4), + Point::new(5, 0)..Point::new(5, 0) + ] + ); + }); + } + + #[gpui::test] + async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig { + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/*".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = r#" + a + + / + + "# + .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, settings, cx)); + view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(&mut cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ], + cx, + ); + view.handle_input(&Input("{".to_string()), cx); + view.handle_input(&Input("{".to_string()), cx); + view.handle_input(&Input("{".to_string()), cx); + assert_eq!( + view.text(cx), + " + {{{}}} + {{{}}} + / + + " + .unindent() + ); + + view.move_right(&MoveRight, cx); + view.handle_input(&Input("}".to_string()), cx); + view.handle_input(&Input("}".to_string()), cx); + view.handle_input(&Input("}".to_string()), cx); + assert_eq!( + view.text(cx), + " + {{{}}}} + {{{}}}} + / + + " + .unindent() + ); + + view.undo(&Undo, cx); + view.handle_input(&Input("/".to_string()), cx); + view.handle_input(&Input("*".to_string()), cx); + assert_eq!( + view.text(cx), + " + /* */ + /* */ + / + + " + .unindent() + ); + + view.undo(&Undo, cx); + view.select_display_ranges( + &[ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + cx, + ); + view.handle_input(&Input("*".to_string()), cx); + assert_eq!( + view.text(cx), + " + a + + /* + * + " + .unindent() + ); + }); + } + + #[gpui::test] + async fn test_snippets(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + + let text = " + a. b + a. b + a. b + " + .unindent(); + let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); + let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + editor.update(&mut cx, |editor, cx| { + let buffer = &editor.snapshot(cx).buffer_snapshot; + let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); + let insertion_ranges = [ + Point::new(0, 2).to_offset(buffer)..Point::new(0, 2).to_offset(buffer), + Point::new(1, 2).to_offset(buffer)..Point::new(1, 2).to_offset(buffer), + Point::new(2, 2).to_offset(buffer)..Point::new(2, 2).to_offset(buffer), + ]; + + editor + .insert_snippet(&insertion_ranges, snippet, cx) + .unwrap(); + assert_eq!( + editor.text(cx), + " + a.f(one, two, three) b + a.f(one, two, three) b + a.f(one, two, three) b + " + .unindent() + ); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); + + // Can't move earlier than the first tab stop + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 9)..Point::new(0, 12), + Point::new(1, 9)..Point::new(1, 12), + Point::new(2, 9)..Point::new(2, 12) + ] + ); + + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 4)..Point::new(0, 7), + Point::new(0, 14)..Point::new(0, 19), + Point::new(1, 4)..Point::new(1, 7), + Point::new(1, 14)..Point::new(1, 19), + Point::new(2, 4)..Point::new(2, 7), + Point::new(2, 14)..Point::new(2, 19), + ] + ); + + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert!(editor.move_to_next_snippet_tabstop(cx)); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 20)..Point::new(0, 20), + Point::new(1, 20)..Point::new(1, 20), + Point::new(2, 20)..Point::new(2, 20) + ] + ); + + // As soon as the last tab stop is reached, snippet state is gone + editor.move_to_prev_snippet_tabstop(cx); + assert_eq!( + editor.selected_ranges::(cx), + &[ + Point::new(0, 20)..Point::new(0, 20), + Point::new(1, 20)..Point::new(1, 20), + Point::new(2, 20)..Point::new(2, 20) + ] + ); + }); + } + + #[gpui::test] + async fn test_completion(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let (language_server, mut fake) = cx.update(|cx| { + lsp::LanguageServer::fake_with_capabilities( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + }); + + let text = " + one + two + three + " + .unindent(); + + let fs = FakeFs::new(cx.background().clone()); + fs.insert_file("/file", text).await; + + let project = Project::test(fs, &mut cx); + + let (worktree, relative_path) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/file", false, cx) + }) + .await + .unwrap(); + let project_path = ProjectPath { + worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), + path: relative_path.into(), + }; + let buffer = project + .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) + .await + .unwrap(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language_server(Some(language_server), cx); + }); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + buffer.next_notification(&cx).await; + + let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx)); + + editor.update(&mut cx, |editor, cx| { + editor.project = Some(project); + editor.select_ranges([Point::new(0, 3)..Point::new(0, 3)], None, cx); + editor.handle_input(&Input(".".to_string()), cx); + }); + + handle_completion_request( + &mut fake, + "/file", + Point::new(0, 4), + vec![ + (Point::new(0, 4)..Point::new(0, 4), "first_completion"), + (Point::new(0, 4)..Point::new(0, 4), "second_completion"), + ], + ) + .await; + editor + .condition(&cx, |editor, _| editor.context_menu_visible()) + .await; + + let apply_additional_edits = editor.update(&mut cx, |editor, cx| { + editor.move_down(&MoveDown, cx); + let apply_additional_edits = editor + .confirm_completion(&ConfirmCompletion(None), cx) + .unwrap(); + assert_eq!( + editor.text(cx), + " + one.second_completion + two + three + " + .unindent() + ); + apply_additional_edits + }); + + handle_resolve_completion_request( + &mut fake, + Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")), + ) + .await; + apply_additional_edits.await.unwrap(); + assert_eq!( + editor.read_with(&cx, |editor, cx| editor.text(cx)), + " + one.second_completion + two + three + additional edit + " + .unindent() + ); + + editor.update(&mut cx, |editor, cx| { + editor.select_ranges( + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 5)..Point::new(2, 5), + ], + None, + cx, + ); + + editor.handle_input(&Input(" ".to_string()), cx); + assert!(editor.context_menu.is_none()); + editor.handle_input(&Input("s".to_string()), cx); + assert!(editor.context_menu.is_none()); + }); + + handle_completion_request( + &mut fake, + "/file", + Point::new(2, 7), + vec![ + (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"), + (Point::new(2, 6)..Point::new(2, 7), "fifth_completion"), + (Point::new(2, 6)..Point::new(2, 7), "sixth_completion"), + ], + ) + .await; + editor + .condition(&cx, |editor, _| editor.context_menu_visible()) + .await; + + editor.update(&mut cx, |editor, cx| { + editor.handle_input(&Input("i".to_string()), cx); + }); + + handle_completion_request( + &mut fake, + "/file", + Point::new(2, 8), + vec![ + (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"), + (Point::new(2, 6)..Point::new(2, 8), "fifth_completion"), + (Point::new(2, 6)..Point::new(2, 8), "sixth_completion"), + ], + ) + .await; + editor + .condition(&cx, |editor, _| editor.context_menu_visible()) + .await; + + let apply_additional_edits = editor.update(&mut cx, |editor, cx| { + let apply_additional_edits = editor + .confirm_completion(&ConfirmCompletion(None), cx) + .unwrap(); + assert_eq!( + editor.text(cx), + " + one.second_completion + two sixth_completion + three sixth_completion + additional edit + " + .unindent() + ); + apply_additional_edits + }); + handle_resolve_completion_request(&mut fake, None).await; + apply_additional_edits.await.unwrap(); + + async fn handle_completion_request( + fake: &mut FakeLanguageServer, + path: &'static str, + position: Point, + completions: Vec<(Range, &'static str)>, + ) { + fake.handle_request::(move |params, _| { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path(path).unwrap() + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(position.row, position.column) + ); + Some(lsp::CompletionResponse::Array( + completions + .iter() + .map(|(range, new_text)| lsp::CompletionItem { + label: new_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.start.row, range.start.column), + ), + new_text: new_text.to_string(), + })), + ..Default::default() + }) + .collect(), + )) + }) + .next() + .await; + } + + async fn handle_resolve_completion_request( + fake: &mut FakeLanguageServer, + edit: Option<(Range, &'static str)>, + ) { + fake.handle_request::(move |_, _| { + lsp::CompletionItem { + additional_text_edits: edit.clone().map(|(range, new_text)| { + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.end.row, range.end.column), + ), + new_text.to_string(), + )] + }), + ..Default::default() + } + }) + .next() + .await; + } + } + + #[gpui::test] + async fn test_toggle_comment(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig { + line_comment: Some("// ".to_string()), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = " + fn a() { + //b(); + // c(); + // d(); + } + " + .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, settings, cx)); + + view.update(&mut cx, |editor, cx| { + // If multiple selections intersect a line, the line is only + // toggled once. + editor.select_display_ranges( + &[ + DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3), + DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6), + ], + cx, + ); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + b(); + c(); + d(); + } + " + .unindent() + ); + + // The comment prefix is inserted at the same column for every line + // in a selection. + editor.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)], cx); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + // b(); + // c(); + // d(); + } + " + .unindent() + ); + + // If a selection ends at the beginning of a line, that line is not toggled. + editor.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)], cx); + editor.toggle_comments(&ToggleComments, cx); + assert_eq!( + editor.text(cx), + " + fn a() { + // b(); + c(); + // d(); + } + " + .unindent() + ); + }); + } + + #[gpui::test] + fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer.clone(), + [ + Point::new(0, 0)..Point::new(0, 4), + Point::new(1, 0)..Point::new(1, 4), + ], + cx, + ); + multibuffer + }); + + assert_eq!(multibuffer.read(cx).read(cx).text(), "aaaa\nbbbb"); + + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(multibuffer, settings, cx) + }); + view.update(cx, |view, cx| { + assert_eq!(view.text(cx), "aaaa\nbbbb"); + view.select_ranges( + [ + Point::new(0, 0)..Point::new(0, 0), + Point::new(1, 0)..Point::new(1, 0), + ], + None, + cx, + ); + + view.handle_input(&Input("X".to_string()), cx); + assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); + assert_eq!( + view.selected_ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(1, 1)..Point::new(1, 1), + ] + ) + }); + } + + #[gpui::test] + fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer, + [ + Point::new(0, 0)..Point::new(1, 4), + Point::new(1, 0)..Point::new(2, 4), + ], + cx, + ); + multibuffer + }); + + assert_eq!( + multibuffer.read(cx).read(cx).text(), + "aaaa\nbbbb\nbbbb\ncccc" + ); + + let (_, view) = cx.add_window(Default::default(), |cx| { + build_editor(multibuffer, settings, cx) + }); + view.update(cx, |view, cx| { + view.select_ranges( + [ + Point::new(1, 1)..Point::new(1, 1), + Point::new(2, 3)..Point::new(2, 3), + ], + None, + cx, + ); + + view.handle_input(&Input("X".to_string()), cx); + assert_eq!(view.text(cx), "aaaa\nbXbbXb\nbXbbXb\ncccc"); + assert_eq!( + view.selected_ranges(cx), + [ + Point::new(1, 2)..Point::new(1, 2), + Point::new(2, 5)..Point::new(2, 5), + ] + ); + + view.newline(&Newline, cx); + assert_eq!(view.text(cx), "aaaa\nbX\nbbX\nb\nbX\nbbX\nb\ncccc"); + assert_eq!( + view.selected_ranges(cx), + [ + Point::new(2, 0)..Point::new(2, 0), + Point::new(6, 0)..Point::new(6, 0), + ] + ); + }); + } + + #[gpui::test] + fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { + let settings = Settings::test(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + Point::new(0, 0)..Point::new(1, 4), + Point::new(1, 0)..Point::new(2, 4), + ], + cx, + ) + .into_iter() + .next(); + multibuffer + }); + assert_eq!( + multibuffer.read(cx).read(cx).text(), + "aaaa\nbbbb\nbbbb\ncccc" + ); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(multibuffer.clone(), settings, cx); + editor.select_ranges( + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ], + None, + cx, + ); + editor + }); + + // Refreshing selections is a no-op when excerpts haven't changed. + editor.update(cx, |editor, cx| { + editor.refresh_selections(cx); + assert_eq!( + editor.selected_ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + // Removing an excerpt causes the first selection to become degenerate. + assert_eq!( + editor.selected_ranges(cx), + [ + Point::new(0, 0)..Point::new(0, 0), + Point::new(0, 1)..Point::new(0, 1) + ] + ); + + // Refreshing selections will relocate the first selection to the original buffer + // location. + editor.refresh_selections(cx); + assert_eq!( + editor.selected_ranges(cx), + [ + Point::new(0, 1)..Point::new(0, 1), + Point::new(0, 3)..Point::new(0, 3) + ] + ); + }); + } + + #[gpui::test] + async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) { + let settings = cx.read(Settings::test); + let language = Arc::new(Language::new( + LanguageConfig { + brackets: vec![ + BracketPair { + start: "{".to_string(), + end: "}".to_string(), + close: true, + newline: true, + }, + BracketPair { + start: "/* ".to_string(), + end: " */".to_string(), + close: true, + newline: true, + }, + ], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let text = concat!( + "{ }\n", // Suppress rustfmt + " x\n", // + " /* */\n", // + "x\n", // + "{{} }\n", // + ); + + 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, settings, cx)); + view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .await; + + view.update(&mut cx, |view, cx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), + ], + cx, + ); + view.newline(&Newline, cx); + + assert_eq!( + view.buffer().read(cx).read(cx).text(), + concat!( + "{ \n", // Suppress rustfmt + "\n", // + "}\n", // + " x\n", // + " /* \n", // + " \n", // + " */\n", // + "x\n", // + "{{} \n", // + "}\n", // + ) + ); + }); + } + + #[gpui::test] + fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + let settings = Settings::test(&cx); + let (_, editor) = cx.add_window(Default::default(), |cx| { + build_editor(buffer.clone(), settings, cx) + }); + + editor.update(cx, |editor, cx| { + struct Type1; + struct Type2; + + let buffer = buffer.read(cx).snapshot(cx); + + let anchor_range = |range: Range| { + buffer.anchor_after(range.start)..buffer.anchor_after(range.end) + }; + + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(2, 1)..Point::new(2, 3)), + anchor_range(Point::new(4, 2)..Point::new(4, 4)), + anchor_range(Point::new(6, 3)..Point::new(6, 5)), + anchor_range(Point::new(8, 4)..Point::new(8, 6)), + ], + Color::red(), + cx, + ); + editor.highlight_ranges::( + vec![ + anchor_range(Point::new(3, 2)..Point::new(3, 5)), + anchor_range(Point::new(5, 3)..Point::new(5, 6)), + anchor_range(Point::new(7, 4)..Point::new(7, 7)), + anchor_range(Point::new(9, 5)..Point::new(9, 8)), + ], + Color::green(), + cx, + ); + + let snapshot = editor.snapshot(cx); + let mut highlighted_ranges = editor.highlighted_ranges_in_range( + anchor_range(Point::new(3, 4)..Point::new(7, 4)), + &snapshot, + ); + // Enforce a consistent ordering based on color without relying on the ordering of the + // highlight's `TypeId` which is non-deterministic. + highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); + assert_eq!( + highlighted_ranges, + &[ + ( + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), + Color::green(), + ), + ( + DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), + Color::green(), + ), + ( + DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), + Color::red(), + ), + ( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + ), + ] + ); + assert_eq!( + editor.highlighted_ranges_in_range( + anchor_range(Point::new(5, 6)..Point::new(6, 4)), + &snapshot, + ), + &[( + DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), + Color::red(), + )] + ); + }); + } + + #[test] + fn test_combine_syntax_and_fuzzy_match_highlights() { + let string = "abcdefghijklmnop"; + let default = HighlightStyle::default(); + let syntax_ranges = [ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..8, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ]; + let match_indices = [4, 6, 7, 8]; + assert_eq!( + combine_syntax_and_fuzzy_match_highlights( + &string, + default, + syntax_ranges.into_iter(), + &match_indices, + ), + &[ + ( + 0..3, + HighlightStyle { + color: Color::red(), + ..default + }, + ), + ( + 4..5, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 5..6, + HighlightStyle { + color: Color::green(), + ..default + }, + ), + ( + 6..8, + HighlightStyle { + color: Color::green(), + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ( + 8..9, + HighlightStyle { + font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD), + ..default + }, + ), + ] + ); + } + + fn empty_range(row: usize, column: usize) -> Range { + let point = DisplayPoint::new(row as u32, column as u32); + point..point + } + + fn build_editor( + buffer: ModelHandle, + settings: Settings, + cx: &mut ViewContext, + ) -> Editor { + let settings = watch::channel_with(settings); + Editor::new(EditorMode::Full, buffer, None, settings.1, None, cx) + } +} + +trait RangeExt { + fn sorted(&self) -> Range; + fn to_inclusive(&self) -> RangeInclusive; +} + +impl RangeExt for Range { + fn sorted(&self) -> Self { + cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() + } + + fn to_inclusive(&self) -> RangeInclusive { + self.start.clone()..=self.end.clone() + } +} diff --git a/crates/find/src/project_find.rs b/crates/find/src/project_find.rs index 2d0466a8b25974c601c57eda67d16f255b14ecf9..523767f71eef6c918da2db5c729de435e30b26b3 100644 --- a/crates/find/src/project_find.rs +++ b/crates/find/src/project_find.rs @@ -1,577 +1,577 @@ -use crate::SearchOption; -use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; -use gpui::{ - action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, - ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, - ViewHandle, -}; -use postage::watch; -use project::{search::SearchQuery, Project}; -use std::{ - any::{Any, TypeId}, - ops::Range, - path::PathBuf, -}; -use util::ResultExt as _; -use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; - -action!(Deploy); -action!(Search); -action!(SearchInNew); -action!(ToggleSearchOption, SearchOption); -action!(ToggleFocus); - -pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), - Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), - Binding::new("cmd-shift-F", Deploy, Some("Workspace")), - Binding::new("enter", Search, Some("ProjectFindView")), - Binding::new("cmd-enter", SearchInNew, Some("ProjectFindView")), - ]); - cx.add_action(ProjectFindView::deploy); - cx.add_action(ProjectFindView::search); - cx.add_action(ProjectFindView::search_in_new); - cx.add_action(ProjectFindView::toggle_search_option); - cx.add_action(ProjectFindView::toggle_focus); -} - -struct ProjectFind { - project: ModelHandle, - excerpts: ModelHandle, - pending_search: Option>>, - highlighted_ranges: Vec>, - active_query: Option, -} - -struct ProjectFindView { - model: ModelHandle, - query_editor: ViewHandle, - results_editor: ViewHandle, - case_sensitive: bool, - whole_word: bool, - regex: bool, - query_contains_error: bool, - settings: watch::Receiver, -} - -impl Entity for ProjectFind { - type Event = (); -} - -impl ProjectFind { - fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { - let replica_id = project.read(cx).replica_id(); - Self { - project, - excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), - pending_search: Default::default(), - highlighted_ranges: Default::default(), - active_query: None, - } - } - - fn clone(&self, new_cx: &mut ModelContext) -> Self { - Self { - project: self.project.clone(), - excerpts: self - .excerpts - .update(new_cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), - pending_search: Default::default(), - highlighted_ranges: self.highlighted_ranges.clone(), - active_query: self.active_query.clone(), - } - } - - fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { - let search = self - .project - .update(cx, |project, cx| project.search(query.clone(), cx)); - self.active_query = Some(query); - self.highlighted_ranges.clear(); - self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let matches = search.await.log_err()?; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.highlighted_ranges.clear(); - let mut matches = matches.into_iter().collect::>(); - matches - .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - for (buffer, buffer_matches) in matches { - let ranges_to_highlight = excerpts.push_excerpts_with_context_lines( - buffer, - buffer_matches.clone(), - 1, - cx, - ); - this.highlighted_ranges.extend(ranges_to_highlight); - } - }); - this.pending_search.take(); - cx.notify(); - }); - } - None - })); - cx.notify(); - } -} - -impl Item for ProjectFind { - type View = ProjectFindView; - - fn build_view( - model: ModelHandle, - workspace: &Workspace, - nav_history: ItemNavHistory, - cx: &mut gpui::ViewContext, - ) -> Self::View { - let settings = workspace.settings(); - let excerpts = model.read(cx).excerpts.clone(); - - let mut query_text = String::new(); - let mut regex = false; - let mut case_sensitive = false; - let mut whole_word = false; - if let Some(active_query) = model.read(cx).active_query.as_ref() { - query_text = active_query.as_str().to_string(); - regex = active_query.is_regex(); - case_sensitive = active_query.case_sensitive(); - whole_word = active_query.whole_word(); - } - - let query_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - settings.clone(), - Some(|theme| theme.find.editor.input.clone()), - cx, - ); - editor.set_text(query_text, cx); - editor - }); - let results_editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer( - excerpts, - Some(workspace.project().clone()), - settings.clone(), - cx, - ); - editor.set_searchable(false); - editor.set_nav_history(Some(nav_history)); - editor - }); - cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) - .detach(); - cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) - .detach(); - - ProjectFindView { - model, - query_editor, - results_editor, - case_sensitive, - whole_word, - regex, - query_contains_error: false, - settings, - } - } - - fn project_path(&self) -> Option { - None - } -} - -enum ViewEvent { - UpdateTab, -} - -impl Entity for ProjectFindView { - type Event = ViewEvent; -} - -impl View for ProjectFindView { - fn ui_name() -> &'static str { - "ProjectFindView" - } - - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let model = &self.model.read(cx); - let results = if model.highlighted_ranges.is_empty() { - let theme = &self.settings.borrow().theme; - let text = if self.query_editor.read(cx).text(cx).is_empty() { - "" - } else if model.pending_search.is_some() { - "Searching..." - } else { - "No results" - }; - Label::new(text.to_string(), theme.find.results_status.clone()) - .aligned() - .contained() - .with_background_color(theme.editor.background) - .flexible(1., true) - .boxed() - } else { - ChildView::new(&self.results_editor) - .flexible(1., true) - .boxed() - }; - - Flex::column() - .with_child(self.render_query_editor(cx)) - .with_child(results) - .boxed() - } - - fn on_focus(&mut self, cx: &mut ViewContext) { - if self.model.read(cx).highlighted_ranges.is_empty() { - cx.focus(&self.query_editor); - } else { - self.focus_results_editor(cx); - } - } -} - -impl ItemView for ProjectFindView { - fn act_as_type( - &self, - type_id: TypeId, - self_handle: &ViewHandle, - _: &gpui::AppContext, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.into()) - } else if type_id == TypeId::of::() { - Some((&self.results_editor).into()) - } else { - None - } - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - self.results_editor - .update(cx, |editor, cx| editor.deactivated(cx)); - } - - fn item(&self, _: &gpui::AppContext) -> Box { - Box::new(self.model.clone()) - } - - fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { - let settings = self.settings.borrow(); - let find_theme = &settings.theme.find; - Flex::row() - .with_child( - Svg::new("icons/magnifier.svg") - .with_color(style.label.text.color) - .constrained() - .with_width(find_theme.tab_icon_width) - .aligned() - .boxed(), - ) - .with_children(self.model.read(cx).active_query.as_ref().map(|query| { - Label::new(query.as_str().to_string(), style.label.clone()) - .aligned() - .contained() - .with_margin_left(find_theme.tab_icon_spacing) - .boxed() - })) - .boxed() - } - - fn project_path(&self, _: &gpui::AppContext) -> Option { - None - } - - fn can_save(&self, _: &gpui::AppContext) -> bool { - true - } - - fn is_dirty(&self, cx: &AppContext) -> bool { - self.results_editor.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &AppContext) -> bool { - self.results_editor.read(cx).has_conflict(cx) - } - - fn save( - &mut self, - project: ModelHandle, - cx: &mut ViewContext, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.save(project, cx)) - } - - fn can_save_as(&self, _: &gpui::AppContext) -> bool { - false - } - - fn save_as( - &mut self, - _: ModelHandle, - _: PathBuf, - _: &mut ViewContext, - ) -> Task> { - unreachable!("save_as should not have been called") - } - - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option - where - Self: Sized, - { - let query_editor = cx.add_view(|cx| { - let query = self.query_editor.read(cx).text(cx); - let editor = Editor::single_line( - self.settings.clone(), - Some(|theme| theme.find.editor.input.clone()), - cx, - ); - editor - .buffer() - .update(cx, |buffer, cx| buffer.edit([0..0], query, cx)); - editor - }); - let model = self - .model - .update(cx, |model, cx| cx.add_model(|cx| model.clone(cx))); - - cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) - .detach(); - let results_editor = cx.add_view(|cx| { - let model = model.read(cx); - let excerpts = model.excerpts.clone(); - let project = model.project.clone(); - let scroll_position = self - .results_editor - .update(cx, |editor, cx| editor.scroll_position(cx)); - - let mut editor = Editor::for_buffer(excerpts, Some(project), self.settings.clone(), cx); - editor.set_searchable(false); - editor.set_nav_history(Some(nav_history)); - editor.set_scroll_position(scroll_position, cx); - editor - }); - let mut view = Self { - model, - query_editor, - results_editor, - case_sensitive: self.case_sensitive, - whole_word: self.whole_word, - regex: self.regex, - query_contains_error: self.query_contains_error, - settings: self.settings.clone(), - }; - view.model_changed(false, cx); - Some(view) - } - - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { - self.results_editor - .update(cx, |editor, cx| editor.navigate(data, cx)); - } - - fn should_update_tab_on_event(event: &ViewEvent) -> bool { - matches!(event, ViewEvent::UpdateTab) - } -} - -impl ProjectFindView { - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - if let Some(existing) = workspace - .items_of_type::(cx) - .max_by_key(|existing| existing.id()) - { - workspace.activate_item(&existing, cx); - } else { - let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); - workspace.open_item(model, cx); - } - } - - fn search(&mut self, _: &Search, cx: &mut ViewContext) { - if let Some(query) = self.build_search_query(cx) { - self.model.update(cx, |model, cx| model.search(query, cx)); - } - } - - fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { - if let Some(find_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let new_query = find_view.update(cx, |find_view, cx| { - let new_query = find_view.build_search_query(cx); - if new_query.is_some() { - if let Some(old_query) = find_view.model.read(cx).active_query.clone() { - find_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), cx); - }); - find_view.regex = old_query.is_regex(); - find_view.whole_word = old_query.whole_word(); - find_view.case_sensitive = old_query.case_sensitive(); - } - } - new_query - }); - if let Some(new_query) = new_query { - let model = cx.add_model(|cx| { - let mut model = ProjectFind::new(workspace.project().clone(), cx); - model.search(new_query, cx); - model - }); - workspace.open_item(model, cx); - } - } - } - - fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { - let text = self.query_editor.read(cx).text(cx); - if self.regex { - match SearchQuery::regex(text, self.whole_word, self.case_sensitive) { - Ok(query) => Some(query), - Err(_) => { - self.query_contains_error = true; - cx.notify(); - None - } - } - } else { - Some(SearchQuery::text( - text, - self.whole_word, - self.case_sensitive, - )) - } - } - - fn toggle_search_option( - &mut self, - ToggleSearchOption(option): &ToggleSearchOption, - cx: &mut ViewContext, - ) { - let value = match option { - SearchOption::WholeWord => &mut self.whole_word, - SearchOption::CaseSensitive => &mut self.case_sensitive, - SearchOption::Regex => &mut self.regex, - }; - *value = !*value; - self.search(&Search, cx); - cx.notify(); - } - - fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { - if self.query_editor.is_focused(cx) { - if !self.model.read(cx).highlighted_ranges.is_empty() { - self.focus_results_editor(cx); - } - } else { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&SelectAll, cx); - }); - cx.focus(&self.query_editor); - } - } - - fn focus_results_editor(&self, cx: &mut ViewContext) { - self.query_editor.update(cx, |query_editor, cx| { - let head = query_editor.newest_anchor_selection().head(); - query_editor.select_ranges([head.clone()..head], None, cx); - }); - cx.focus(&self.results_editor); - } - - fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { - let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); - if !highlighted_ranges.is_empty() { - let theme = &self.settings.borrow().theme.find; - self.results_editor.update(cx, |editor, cx| { - editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); - if reset_selections { - editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); - } - }); - if self.query_editor.is_focused(cx) { - self.focus_results_editor(cx); - } - } - - cx.emit(ViewEvent::UpdateTab); - cx.notify(); - } - - fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &self.settings.borrow().theme; - let editor_container = if self.query_contains_error { - theme.find.invalid_editor - } else { - theme.find.editor.input.container - }; - Flex::row() - .with_child( - ChildView::new(&self.query_editor) - .contained() - .with_style(editor_container) - .aligned() - .constrained() - .with_max_width(theme.find.editor.max_width) - .boxed(), - ) - .with_child( - Flex::row() - .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) - .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) - .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) - .contained() - .with_style(theme.find.option_button_group) - .aligned() - .boxed(), - ) - .contained() - .with_style(theme.find.container) - .constrained() - .with_height(theme.workspace.toolbar.height) - .named("find bar") - } - - fn render_option_button( - &self, - icon: &str, - option: SearchOption, - cx: &mut RenderContext, - ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; - let is_active = self.is_option_enabled(option); - MouseEventHandler::new::(option as usize, cx, |state, _| { - let style = match (is_active, state.hovered) { - (false, false) => &theme.option_button, - (false, true) => &theme.hovered_option_button, - (true, false) => &theme.active_option_button, - (true, true) => &theme.active_hovered_option_button, - }; - Label::new(icon.to_string(), style.text.clone()) - .contained() - .with_style(style.container) - .boxed() - }) - .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) - .with_cursor_style(CursorStyle::PointingHand) - .boxed() - } - - fn is_option_enabled(&self, option: SearchOption) -> bool { - match option { - SearchOption::WholeWord => self.whole_word, - SearchOption::CaseSensitive => self.case_sensitive, - SearchOption::Regex => self.regex, - } - } -} +use crate::SearchOption; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; +use gpui::{ + action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, + ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, +}; +use postage::watch; +use project::{search::SearchQuery, Project}; +use std::{ + any::{Any, TypeId}, + ops::Range, + path::PathBuf, +}; +use util::ResultExt as _; +use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; + +action!(Deploy); +action!(Search); +action!(SearchInNew); +action!(ToggleSearchOption, SearchOption); +action!(ToggleFocus); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_bindings([ + Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), + Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), + Binding::new("cmd-shift-F", Deploy, Some("Workspace")), + Binding::new("enter", Search, Some("ProjectFindView")), + Binding::new("cmd-enter", SearchInNew, Some("ProjectFindView")), + ]); + cx.add_action(ProjectFindView::deploy); + cx.add_action(ProjectFindView::search); + cx.add_action(ProjectFindView::search_in_new); + cx.add_action(ProjectFindView::toggle_search_option); + cx.add_action(ProjectFindView::toggle_focus); +} + +struct ProjectFind { + project: ModelHandle, + excerpts: ModelHandle, + pending_search: Option>>, + highlighted_ranges: Vec>, + active_query: Option, +} + +struct ProjectFindView { + model: ModelHandle, + query_editor: ViewHandle, + results_editor: ViewHandle, + case_sensitive: bool, + whole_word: bool, + regex: bool, + query_contains_error: bool, + settings: watch::Receiver, +} + +impl Entity for ProjectFind { + type Event = (); +} + +impl ProjectFind { + fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + let replica_id = project.read(cx).replica_id(); + Self { + project, + excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), + pending_search: Default::default(), + highlighted_ranges: Default::default(), + active_query: None, + } + } + + fn clone(&self, new_cx: &mut ModelContext) -> Self { + Self { + project: self.project.clone(), + excerpts: self + .excerpts + .update(new_cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + pending_search: Default::default(), + highlighted_ranges: self.highlighted_ranges.clone(), + active_query: self.active_query.clone(), + } + } + + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + let search = self + .project + .update(cx, |project, cx| project.search(query.clone(), cx)); + self.active_query = Some(query); + self.highlighted_ranges.clear(); + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let matches = search.await.log_err()?; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.highlighted_ranges.clear(); + let mut matches = matches.into_iter().collect::>(); + matches + .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + for (buffer, buffer_matches) in matches { + let ranges_to_highlight = excerpts.push_excerpts_with_context_lines( + buffer, + buffer_matches.clone(), + 1, + cx, + ); + this.highlighted_ranges.extend(ranges_to_highlight); + } + }); + this.pending_search.take(); + cx.notify(); + }); + } + None + })); + cx.notify(); + } +} + +impl Item for ProjectFind { + type View = ProjectFindView; + + fn build_view( + model: ModelHandle, + workspace: &Workspace, + nav_history: ItemNavHistory, + cx: &mut gpui::ViewContext, + ) -> Self::View { + let settings = workspace.settings(); + let excerpts = model.read(cx).excerpts.clone(); + + let mut query_text = String::new(); + let mut regex = false; + let mut case_sensitive = false; + let mut whole_word = false; + if let Some(active_query) = model.read(cx).active_query.as_ref() { + query_text = active_query.as_str().to_string(); + regex = active_query.is_regex(); + case_sensitive = active_query.case_sensitive(); + whole_word = active_query.whole_word(); + } + + let query_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + settings.clone(), + Some(|theme| theme.find.editor.input.clone()), + cx, + ); + editor.set_text(query_text, cx); + editor + }); + let results_editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer( + excerpts, + Some(workspace.project().clone()), + settings.clone(), + cx, + ); + editor.set_searchable(false); + editor.set_nav_history(Some(nav_history)); + editor + }); + cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) + .detach(); + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) + .detach(); + + ProjectFindView { + model, + query_editor, + results_editor, + case_sensitive, + whole_word, + regex, + query_contains_error: false, + settings, + } + } + + fn project_path(&self) -> Option { + None + } +} + +enum ViewEvent { + UpdateTab, +} + +impl Entity for ProjectFindView { + type Event = ViewEvent; +} + +impl View for ProjectFindView { + fn ui_name() -> &'static str { + "ProjectFindView" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let model = &self.model.read(cx); + let results = if model.highlighted_ranges.is_empty() { + let theme = &self.settings.borrow().theme; + let text = if self.query_editor.read(cx).text(cx).is_empty() { + "" + } else if model.pending_search.is_some() { + "Searching..." + } else { + "No results" + }; + Label::new(text.to_string(), theme.find.results_status.clone()) + .aligned() + .contained() + .with_background_color(theme.editor.background) + .flexible(1., true) + .boxed() + } else { + ChildView::new(&self.results_editor) + .flexible(1., true) + .boxed() + }; + + Flex::column() + .with_child(self.render_query_editor(cx)) + .with_child(results) + .boxed() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + if self.model.read(cx).highlighted_ranges.is_empty() { + cx.focus(&self.query_editor); + } else { + self.focus_results_editor(cx); + } + } +} + +impl ItemView for ProjectFindView { + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &gpui::AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.into()) + } else if type_id == TypeId::of::() { + Some((&self.results_editor).into()) + } else { + None + } + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.deactivated(cx)); + } + + fn item(&self, _: &gpui::AppContext) -> Box { + Box::new(self.model.clone()) + } + + fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = self.settings.borrow(); + let find_theme = &settings.theme.find; + Flex::row() + .with_child( + Svg::new("icons/magnifier.svg") + .with_color(style.label.text.color) + .constrained() + .with_width(find_theme.tab_icon_width) + .aligned() + .boxed(), + ) + .with_children(self.model.read(cx).active_query.as_ref().map(|query| { + Label::new(query.as_str().to_string(), style.label.clone()) + .aligned() + .contained() + .with_margin_left(find_theme.tab_icon_spacing) + .boxed() + })) + .boxed() + } + + fn project_path(&self, _: &gpui::AppContext) -> Option { + None + } + + fn can_save(&self, _: &gpui::AppContext) -> bool { + true + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).has_conflict(cx) + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.save(project, cx)) + } + + fn can_save_as(&self, _: &gpui::AppContext) -> bool { + false + } + + fn save_as( + &mut self, + _: ModelHandle, + _: PathBuf, + _: &mut ViewContext, + ) -> Task> { + unreachable!("save_as should not have been called") + } + + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option + where + Self: Sized, + { + let query_editor = cx.add_view(|cx| { + let query = self.query_editor.read(cx).text(cx); + let editor = Editor::single_line( + self.settings.clone(), + Some(|theme| theme.find.editor.input.clone()), + cx, + ); + editor + .buffer() + .update(cx, |buffer, cx| buffer.edit([0..0], query, cx)); + editor + }); + let model = self + .model + .update(cx, |model, cx| cx.add_model(|cx| model.clone(cx))); + + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) + .detach(); + let results_editor = cx.add_view(|cx| { + let model = model.read(cx); + let excerpts = model.excerpts.clone(); + let project = model.project.clone(); + let scroll_position = self + .results_editor + .update(cx, |editor, cx| editor.scroll_position(cx)); + + let mut editor = Editor::for_buffer(excerpts, Some(project), self.settings.clone(), cx); + editor.set_searchable(false); + editor.set_nav_history(Some(nav_history)); + editor.set_scroll_position(scroll_position, cx); + editor + }); + let mut view = Self { + model, + query_editor, + results_editor, + case_sensitive: self.case_sensitive, + whole_word: self.whole_word, + regex: self.regex, + query_contains_error: self.query_contains_error, + settings: self.settings.clone(), + }; + view.model_changed(false, cx); + Some(view) + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.navigate(data, cx)); + } + + fn should_update_tab_on_event(event: &ViewEvent) -> bool { + matches!(event, ViewEvent::UpdateTab) + } +} + +impl ProjectFindView { + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + if let Some(existing) = workspace + .items_of_type::(cx) + .max_by_key(|existing| existing.id()) + { + workspace.activate_item(&existing, cx); + } else { + let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); + workspace.open_item(model, cx); + } + } + + fn search(&mut self, _: &Search, cx: &mut ViewContext) { + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); + } + } + + fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { + if let Some(find_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let new_query = find_view.update(cx, |find_view, cx| { + let new_query = find_view.build_search_query(cx); + if new_query.is_some() { + if let Some(old_query) = find_view.model.read(cx).active_query.clone() { + find_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), cx); + }); + find_view.regex = old_query.is_regex(); + find_view.whole_word = old_query.whole_word(); + find_view.case_sensitive = old_query.case_sensitive(); + } + } + new_query + }); + if let Some(new_query) = new_query { + let model = cx.add_model(|cx| { + let mut model = ProjectFind::new(workspace.project().clone(), cx); + model.search(new_query, cx); + model + }); + workspace.open_item(model, cx); + } + } + } + + fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { + let text = self.query_editor.read(cx).text(cx); + if self.regex { + match SearchQuery::regex(text, self.whole_word, self.case_sensitive) { + Ok(query) => Some(query), + Err(_) => { + self.query_contains_error = true; + cx.notify(); + None + } + } + } else { + Some(SearchQuery::text( + text, + self.whole_word, + self.case_sensitive, + )) + } + } + + fn toggle_search_option( + &mut self, + ToggleSearchOption(option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + let value = match option { + SearchOption::WholeWord => &mut self.whole_word, + SearchOption::CaseSensitive => &mut self.case_sensitive, + SearchOption::Regex => &mut self.regex, + }; + *value = !*value; + self.search(&Search, cx); + cx.notify(); + } + + fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { + if self.query_editor.is_focused(cx) { + if !self.model.read(cx).highlighted_ranges.is_empty() { + self.focus_results_editor(cx); + } + } else { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&SelectAll, cx); + }); + cx.focus(&self.query_editor); + } + } + + fn focus_results_editor(&self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + let head = query_editor.newest_anchor_selection().head(); + query_editor.select_ranges([head.clone()..head], None, cx); + }); + cx.focus(&self.results_editor); + } + + fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { + let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); + if !highlighted_ranges.is_empty() { + let theme = &self.settings.borrow().theme.find; + self.results_editor.update(cx, |editor, cx| { + editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); + if reset_selections { + editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + } + }); + if self.query_editor.is_focused(cx) { + self.focus_results_editor(cx); + } + } + + cx.emit(ViewEvent::UpdateTab); + cx.notify(); + } + + fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let editor_container = if self.query_contains_error { + theme.find.invalid_editor + } else { + theme.find.editor.input.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.find.editor.max_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.find.option_button_group) + .aligned() + .boxed(), + ) + .contained() + .with_style(theme.find.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("find bar") + } + + fn render_option_button( + &self, + icon: &str, + option: SearchOption, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.find; + let is_active = self.is_option_enabled(option); + MouseEventHandler::new::(option as usize, cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn is_option_enabled(&self, option: SearchOption) -> bool { + match option { + SearchOption::WholeWord => self.whole_word, + SearchOption::CaseSensitive => self.case_sensitive, + SearchOption::Regex => self.regex, + } + } +} diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index a82460dfa3be954266325b5a1f9113fe53d359f7..58fa04eeb2feabba45ae36c7d23b3630f22ca621 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -1,227 +1,227 @@ -use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; -use anyhow::Result; -use client::proto; -use language::{char_kind, Rope}; -use regex::{Regex, RegexBuilder}; -use smol::future::yield_now; -use std::{ - io::{BufRead, BufReader, Read}, - ops::Range, - sync::Arc, -}; - -#[derive(Clone)] -pub enum SearchQuery { - Text { - search: Arc>, - query: Arc, - whole_word: bool, - case_sensitive: bool, - }, - Regex { - regex: Regex, - query: Arc, - multiline: bool, - whole_word: bool, - case_sensitive: bool, - }, -} - -impl SearchQuery { - pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { - let query = query.to_string(); - let search = AhoCorasickBuilder::new() - .auto_configure(&[&query]) - .ascii_case_insensitive(!case_sensitive) - .build(&[&query]); - Self::Text { - search: Arc::new(search), - query: Arc::from(query), - whole_word, - case_sensitive, - } - } - - pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { - let mut query = query.to_string(); - let initial_query = Arc::from(query.as_str()); - if whole_word { - let mut word_query = String::new(); - word_query.push_str("\\b"); - word_query.push_str(&query); - word_query.push_str("\\b"); - query = word_query - } - - let multiline = query.contains("\n") || query.contains("\\n"); - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .multi_line(multiline) - .build()?; - Ok(Self::Regex { - regex, - query: initial_query, - multiline, - whole_word, - case_sensitive, - }) - } - - pub fn from_proto(message: proto::SearchProject) -> Result { - if message.regex { - Self::regex(message.query, message.whole_word, message.case_sensitive) - } else { - Ok(Self::text( - message.query, - message.whole_word, - message.case_sensitive, - )) - } - } - - pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { - proto::SearchProject { - project_id, - query: self.as_str().to_string(), - regex: self.is_regex(), - whole_word: self.whole_word(), - case_sensitive: self.case_sensitive(), - } - } - - pub fn detect(&self, stream: T) -> Result { - if self.as_str().is_empty() { - return Ok(false); - } - - match self { - Self::Text { search, .. } => { - let mat = search.stream_find_iter(stream).next(); - match mat { - Some(Ok(_)) => Ok(true), - Some(Err(err)) => Err(err.into()), - None => Ok(false), - } - } - Self::Regex { - regex, multiline, .. - } => { - let mut reader = BufReader::new(stream); - if *multiline { - let mut text = String::new(); - if let Err(err) = reader.read_to_string(&mut text) { - Err(err.into()) - } else { - Ok(regex.find(&text).is_some()) - } - } else { - for line in reader.lines() { - let line = line?; - if regex.find(&line).is_some() { - return Ok(true); - } - } - Ok(false) - } - } - } - } - - pub async fn search(&self, rope: &Rope) -> Vec> { - const YIELD_INTERVAL: usize = 20000; - - if self.as_str().is_empty() { - return Default::default(); - } - - let mut matches = Vec::new(); - match self { - Self::Text { - search, whole_word, .. - } => { - for (ix, mat) in search - .stream_find_iter(rope.bytes_in_range(0..rope.len())) - .enumerate() - { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - let mat = mat.unwrap(); - if *whole_word { - let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); - let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = rope.chars_at(mat.end()).next().map(char_kind); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { - continue; - } - } - matches.push(mat.start()..mat.end()) - } - } - Self::Regex { - regex, multiline, .. - } => { - if *multiline { - let text = rope.to_string(); - for (ix, mat) in regex.find_iter(&text).enumerate() { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - matches.push(mat.start()..mat.end()); - } - } else { - let mut line = String::new(); - let mut line_offset = 0; - for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { - if (chunk_ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - for mat in regex.find_iter(&line) { - let start = line_offset + mat.start(); - let end = line_offset + mat.end(); - matches.push(start..end); - } - - line_offset += line.len() + 1; - line.clear(); - } - line.push_str(text); - } - } - } - } - } - matches - } - - pub fn as_str(&self) -> &str { - match self { - Self::Text { query, .. } => query.as_ref(), - Self::Regex { query, .. } => query.as_ref(), - } - } - - pub fn whole_word(&self) -> bool { - match self { - Self::Text { whole_word, .. } => *whole_word, - Self::Regex { whole_word, .. } => *whole_word, - } - } - - pub fn case_sensitive(&self) -> bool { - match self { - Self::Text { case_sensitive, .. } => *case_sensitive, - Self::Regex { case_sensitive, .. } => *case_sensitive, - } - } - - pub fn is_regex(&self) -> bool { - matches!(self, Self::Regex { .. }) - } -} +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; +use anyhow::Result; +use client::proto; +use language::{char_kind, Rope}; +use regex::{Regex, RegexBuilder}; +use smol::future::yield_now; +use std::{ + io::{BufRead, BufReader, Read}, + ops::Range, + sync::Arc, +}; + +#[derive(Clone)] +pub enum SearchQuery { + Text { + search: Arc>, + query: Arc, + whole_word: bool, + case_sensitive: bool, + }, + Regex { + regex: Regex, + query: Arc, + multiline: bool, + whole_word: bool, + case_sensitive: bool, + }, +} + +impl SearchQuery { + pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { + let query = query.to_string(); + let search = AhoCorasickBuilder::new() + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); + Self::Text { + search: Arc::new(search), + query: Arc::from(query), + whole_word, + case_sensitive, + } + } + + pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { + let mut query = query.to_string(); + let initial_query = Arc::from(query.as_str()); + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = word_query + } + + let multiline = query.contains("\n") || query.contains("\\n"); + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(multiline) + .build()?; + Ok(Self::Regex { + regex, + query: initial_query, + multiline, + whole_word, + case_sensitive, + }) + } + + pub fn from_proto(message: proto::SearchProject) -> Result { + if message.regex { + Self::regex(message.query, message.whole_word, message.case_sensitive) + } else { + Ok(Self::text( + message.query, + message.whole_word, + message.case_sensitive, + )) + } + } + + pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { + proto::SearchProject { + project_id, + query: self.as_str().to_string(), + regex: self.is_regex(), + whole_word: self.whole_word(), + case_sensitive: self.case_sensitive(), + } + } + + pub fn detect(&self, stream: T) -> Result { + if self.as_str().is_empty() { + return Ok(false); + } + + match self { + Self::Text { search, .. } => { + let mat = search.stream_find_iter(stream).next(); + match mat { + Some(Ok(_)) => Ok(true), + Some(Err(err)) => Err(err.into()), + None => Ok(false), + } + } + Self::Regex { + regex, multiline, .. + } => { + let mut reader = BufReader::new(stream); + if *multiline { + let mut text = String::new(); + if let Err(err) = reader.read_to_string(&mut text) { + Err(err.into()) + } else { + Ok(regex.find(&text).is_some()) + } + } else { + for line in reader.lines() { + let line = line?; + if regex.find(&line).is_some() { + return Ok(true); + } + } + Ok(false) + } + } + } + } + + pub async fn search(&self, rope: &Rope) -> Vec> { + const YIELD_INTERVAL: usize = 20000; + + if self.as_str().is_empty() { + return Default::default(); + } + + let mut matches = Vec::new(); + match self { + Self::Text { + search, whole_word, .. + } => { + for (ix, mat) in search + .stream_find_iter(rope.bytes_in_range(0..rope.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + if *whole_word { + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } + matches.push(mat.start()..mat.end()) + } + } + Self::Regex { + regex, multiline, .. + } => { + if *multiline { + let text = rope.to_string(); + for (ix, mat) in regex.find_iter(&text).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + matches.push(mat.start()..mat.end()); + } + } else { + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + matches.push(start..end); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } + } + } + } + } + matches + } + + pub fn as_str(&self) -> &str { + match self { + Self::Text { query, .. } => query.as_ref(), + Self::Regex { query, .. } => query.as_ref(), + } + } + + pub fn whole_word(&self) -> bool { + match self { + Self::Text { whole_word, .. } => *whole_word, + Self::Regex { whole_word, .. } => *whole_word, + } + } + + pub fn case_sensitive(&self) -> bool { + match self { + Self::Text { case_sensitive, .. } => *case_sensitive, + Self::Regex { case_sensitive, .. } => *case_sensitive, + } + } + + pub fn is_regex(&self) -> bool { + matches!(self, Self::Regex { .. }) + } +} From ec317159d7ce04b203dd73b409dcb59b63aae3f5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 08:15:38 -0700 Subject: [PATCH 44/65] Rename "find" to "search" Search is both a verb and a noun, which makes it more natural to use in situations where we need to name a thing rather than a process. --- Cargo.lock | 36 +-- crates/{find => search}/Cargo.toml | 4 +- .../src/buffer_search.rs} | 210 +++++++++--------- .../src/project_search.rs} | 102 ++++----- .../src/find.rs => search/src/search.rs} | 8 +- crates/theme/src/theme.rs | 4 +- crates/zed/Cargo.toml | 2 +- crates/zed/assets/themes/_base.toml | 26 +-- crates/zed/src/main.rs | 2 +- 9 files changed, 197 insertions(+), 197 deletions(-) rename crates/{find => search}/Cargo.toml (94%) rename crates/{find/src/buffer_find.rs => search/src/buffer_search.rs} (82%) rename crates/{find/src/project_find.rs => search/src/project_search.rs} (85%) rename crates/{find/src/find.rs => search/src/search.rs} (63%) diff --git a/Cargo.lock b/Cargo.lock index 3862053f043861ca73f4137ceddf807d6fe43f71..bbfa3f52aa77a691520d5e90133c40eb19def78d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1772,23 +1772,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "find" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "editor", - "gpui", - "language", - "postage", - "project", - "theme", - "unindent", - "util", - "workspace", -] - [[package]] name = "fixedbitset" version = "0.2.0" @@ -4180,6 +4163,23 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "search" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "editor", + "gpui", + "language", + "postage", + "project", + "theme", + "unindent", + "util", + "workspace", +] + [[package]] name = "semver" version = "0.9.0" @@ -5860,7 +5860,6 @@ dependencies = [ "editor", "env_logger", "file_finder", - "find", "fsevent", "futures", "fuzzy", @@ -5889,6 +5888,7 @@ dependencies = [ "rpc", "rsa", "rust-embed", + "search", "serde", "serde_json", "serde_path_to_error", diff --git a/crates/find/Cargo.toml b/crates/search/Cargo.toml similarity index 94% rename from crates/find/Cargo.toml rename to crates/search/Cargo.toml index 5e644e40c44b69d60beae613129f31fcbac36b7f..38eec0cf8521095ebb9cd2d982015bf668f17f46 100644 --- a/crates/find/Cargo.toml +++ b/crates/search/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "find" +name = "search" version = "0.1.0" edition = "2021" [lib] -path = "src/find.rs" +path = "src/search.rs" [dependencies] collections = { path = "../collections" } diff --git a/crates/find/src/buffer_find.rs b/crates/search/src/buffer_search.rs similarity index 82% rename from crates/find/src/buffer_find.rs rename to crates/search/src/buffer_search.rs index 2926cb4066b49462cdf32dce164b941d27222d76..9514939203954ead1767fed599dc7aae41189532 100644 --- a/crates/find/src/buffer_find.rs +++ b/crates/search/src/buffer_search.rs @@ -30,22 +30,22 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), - Binding::new("escape", Dismiss, Some("FindBar")), - Binding::new("cmd-f", FocusEditor, Some("FindBar")), - Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), - Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), + Binding::new("escape", Dismiss, Some("SearchBar")), + Binding::new("cmd-f", FocusEditor, Some("SearchBar")), + Binding::new("enter", GoToMatch(Direction::Next), Some("SearchBar")), + Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("SearchBar")), Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), ]); - cx.add_action(FindBar::deploy); - cx.add_action(FindBar::dismiss); - cx.add_action(FindBar::focus_editor); - cx.add_action(FindBar::toggle_search_option); - cx.add_action(FindBar::go_to_match); - cx.add_action(FindBar::go_to_match_on_pane); + cx.add_action(SearchBar::deploy); + cx.add_action(SearchBar::dismiss); + cx.add_action(SearchBar::focus_editor); + cx.add_action(SearchBar::toggle_search_option); + cx.add_action(SearchBar::go_to_match); + cx.add_action(SearchBar::go_to_match_on_pane); } -struct FindBar { +struct SearchBar { settings: watch::Receiver, query_editor: ViewHandle, active_editor: Option>, @@ -60,13 +60,13 @@ struct FindBar { dismissed: bool, } -impl Entity for FindBar { +impl Entity for SearchBar { type Event = (); } -impl View for FindBar { +impl View for SearchBar { fn ui_name() -> &'static str { - "FindBar" + "SearchBar" } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -76,9 +76,9 @@ impl View for FindBar { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; let editor_container = if self.query_contains_error { - theme.find.invalid_editor + theme.search.invalid_editor } else { - theme.find.editor.input.container + theme.search.editor.input.container }; Flex::row() .with_child( @@ -87,7 +87,7 @@ impl View for FindBar { .with_style(editor_container) .aligned() .constrained() - .with_max_width(theme.find.editor.max_width) + .with_max_width(theme.search.editor.max_width) .boxed(), ) .with_child( @@ -96,7 +96,7 @@ impl View for FindBar { .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) .contained() - .with_style(theme.find.option_button_group) + .with_style(theme.search.option_button_group) .aligned() .boxed(), ) @@ -116,22 +116,22 @@ impl View for FindBar { }; Some( - Label::new(message, theme.find.match_index.text.clone()) + Label::new(message, theme.search.match_index.text.clone()) .contained() - .with_style(theme.find.match_index.container) + .with_style(theme.search.match_index.container) .aligned() .boxed(), ) })) .contained() - .with_style(theme.find.container) + .with_style(theme.search.container) .constrained() .with_height(theme.workspace.toolbar.height) - .named("find bar") + .named("search bar") } } -impl Toolbar for FindBar { +impl Toolbar for SearchBar { fn active_item_changed( &mut self, item: Option>, @@ -163,13 +163,13 @@ impl Toolbar for FindBar { } } -impl FindBar { +impl SearchBar { fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { let query_editor = cx.add_view(|cx| { Editor::auto_height( 2, settings.clone(), - Some(|theme| theme.find.editor.input.clone()), + Some(|theme| theme.search.editor.input.clone()), cx, ) }); @@ -207,7 +207,7 @@ impl FindBar { search_option: SearchOption, cx: &mut RenderContext, ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; + let theme = &self.settings.borrow().theme.search; let is_active = self.is_search_option_enabled(search_option); MouseEventHandler::new::(search_option as usize, cx, |state, _| { let style = match (is_active, state.hovered) { @@ -232,7 +232,7 @@ impl FindBar { direction: Direction, cx: &mut RenderContext, ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; + let theme = &self.settings.borrow().theme.search; enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, _| { let style = if state.hovered { @@ -253,13 +253,13 @@ impl FindBar { fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { - pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); + pane.show_toolbar(cx, |cx| SearchBar::new(settings, cx)); - if let Some(find_bar) = pane + if let Some(search_bar) = pane .active_toolbar() .and_then(|toolbar| toolbar.downcast::()) { - find_bar.update(cx, |find_bar, _| find_bar.dismissed = false); + search_bar.update(cx, |search_bar, _| search_bar.dismissed = false); let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); let display_map = editor .update(cx, |editor, cx| editor.snapshot(cx)) @@ -286,15 +286,15 @@ impl FindBar { } if !text.is_empty() { - find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx)); } if *focus { - let query_editor = find_bar.read(cx).query_editor.clone(); + let query_editor = search_bar.read(cx).query_editor.clone(); query_editor.update(cx, |query_editor, cx| { query_editor.select_all(&editor::SelectAll, cx); }); - cx.focus(&find_bar); + cx.focus(&search_bar); } } else { cx.propagate_action(); @@ -303,7 +303,7 @@ impl FindBar { } fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext) { - if pane.toolbar::().is_some() { + if pane.toolbar::().is_some() { pane.dismiss_toolbar(cx); } } @@ -381,8 +381,8 @@ impl FindBar { } fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { - if let Some(find_bar) = pane.toolbar::() { - find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); + if let Some(search_bar) = pane.toolbar::() { + search_bar.update(cx, |search_bar, cx| search_bar.go_to_match(action, cx)); } } @@ -494,7 +494,7 @@ impl FindBar { this.update_match_index(cx); if !this.dismissed { editor.update(cx, |editor, cx| { - let theme = &this.settings.borrow().theme.find; + let theme = &this.settings.borrow().theme.search; if select_closest_match { if let Some(match_ix) = this.active_match_index { @@ -558,10 +558,10 @@ mod tests { use unindent::Unindent as _; #[gpui::test] - async fn test_find_simple(mut cx: TestAppContext) { + async fn test_search_simple(mut cx: TestAppContext) { let fonts = cx.font_cache(); let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); - theme.find.match_background = Color::red(); + theme.search.match_background = Color::red(); let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); let settings = watch::channel_with(settings).1; @@ -581,16 +581,16 @@ mod tests { Editor::for_buffer(buffer.clone(), None, settings.clone(), cx) }); - let find_bar = cx.add_view(Default::default(), |cx| { - let mut find_bar = FindBar::new(settings, cx); - find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); - find_bar + let search_bar = cx.add_view(Default::default(), |cx| { + let mut search_bar = SearchBar::new(settings, cx); + search_bar.active_item_changed(Some(Box::new(editor.clone())), cx); + search_bar }); // Search for a string that appears with different casing. // By default, search is case-insensitive. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("us", cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.set_query("us", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -610,8 +610,8 @@ mod tests { }); // Switch to a case sensitive search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -626,8 +626,8 @@ mod tests { // Search for a string that appears both as a whole word and // within other words. By default, all results are found. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("or", cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.set_query("or", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -667,8 +667,8 @@ mod tests { }); // Switch to a whole word search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -694,82 +694,82 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.go_to_match(&GoToMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.go_to_match(&GoToMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.go_to_match(&GoToMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.go_to_match(&GoToMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); // Park the cursor in between matches and ensure that going to the previous match selects @@ -777,16 +777,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); // Park the cursor in between matches and ensure that going to the next match selects the @@ -794,16 +794,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.go_to_match(&GoToMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); }); // Park the cursor after the last match and ensure that going to the previous match selects @@ -811,16 +811,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); // Park the cursor after the last match and ensure that going to the next match selects the @@ -828,16 +828,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.go_to_match(&GoToMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); // Park the cursor before the first match and ensure that going to the previous match @@ -845,16 +845,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); } } diff --git a/crates/find/src/project_find.rs b/crates/search/src/project_search.rs similarity index 85% rename from crates/find/src/project_find.rs rename to crates/search/src/project_search.rs index 523767f71eef6c918da2db5c729de435e30b26b3..13abd5cde70140df4ef1d41b5da70e53c2329275 100644 --- a/crates/find/src/project_find.rs +++ b/crates/search/src/project_search.rs @@ -23,20 +23,20 @@ action!(ToggleFocus); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ - Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectFindView")), - Binding::new("cmd-f", ToggleFocus, Some("ProjectFindView")), + Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")), + Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")), Binding::new("cmd-shift-F", Deploy, Some("Workspace")), - Binding::new("enter", Search, Some("ProjectFindView")), - Binding::new("cmd-enter", SearchInNew, Some("ProjectFindView")), + Binding::new("enter", Search, Some("ProjectSearchView")), + Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")), ]); - cx.add_action(ProjectFindView::deploy); - cx.add_action(ProjectFindView::search); - cx.add_action(ProjectFindView::search_in_new); - cx.add_action(ProjectFindView::toggle_search_option); - cx.add_action(ProjectFindView::toggle_focus); + cx.add_action(ProjectSearchView::deploy); + cx.add_action(ProjectSearchView::search); + cx.add_action(ProjectSearchView::search_in_new); + cx.add_action(ProjectSearchView::toggle_search_option); + cx.add_action(ProjectSearchView::toggle_focus); } -struct ProjectFind { +struct ProjectSearch { project: ModelHandle, excerpts: ModelHandle, pending_search: Option>>, @@ -44,8 +44,8 @@ struct ProjectFind { active_query: Option, } -struct ProjectFindView { - model: ModelHandle, +struct ProjectSearchView { + model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, case_sensitive: bool, @@ -55,11 +55,11 @@ struct ProjectFindView { settings: watch::Receiver, } -impl Entity for ProjectFind { +impl Entity for ProjectSearch { type Event = (); } -impl ProjectFind { +impl ProjectSearch { fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { let replica_id = project.read(cx).replica_id(); Self { @@ -119,8 +119,8 @@ impl ProjectFind { } } -impl Item for ProjectFind { - type View = ProjectFindView; +impl Item for ProjectSearch { + type View = ProjectSearchView; fn build_view( model: ModelHandle, @@ -145,7 +145,7 @@ impl Item for ProjectFind { let query_editor = cx.add_view(|cx| { let mut editor = Editor::single_line( settings.clone(), - Some(|theme| theme.find.editor.input.clone()), + Some(|theme| theme.search.editor.input.clone()), cx, ); editor.set_text(query_text, cx); @@ -167,7 +167,7 @@ impl Item for ProjectFind { cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) .detach(); - ProjectFindView { + ProjectSearchView { model, query_editor, results_editor, @@ -188,13 +188,13 @@ enum ViewEvent { UpdateTab, } -impl Entity for ProjectFindView { +impl Entity for ProjectSearchView { type Event = ViewEvent; } -impl View for ProjectFindView { +impl View for ProjectSearchView { fn ui_name() -> &'static str { - "ProjectFindView" + "ProjectSearchView" } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { @@ -208,7 +208,7 @@ impl View for ProjectFindView { } else { "No results" }; - Label::new(text.to_string(), theme.find.results_status.clone()) + Label::new(text.to_string(), theme.search.results_status.clone()) .aligned() .contained() .with_background_color(theme.editor.background) @@ -235,7 +235,7 @@ impl View for ProjectFindView { } } -impl ItemView for ProjectFindView { +impl ItemView for ProjectSearchView { fn act_as_type( &self, type_id: TypeId, @@ -260,23 +260,23 @@ impl ItemView for ProjectFindView { Box::new(self.model.clone()) } - fn tab_content(&self, style: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { let settings = self.settings.borrow(); - let find_theme = &settings.theme.find; + let search_theme = &settings.theme.search; Flex::row() .with_child( Svg::new("icons/magnifier.svg") - .with_color(style.label.text.color) + .with_color(tab_theme.label.text.color) .constrained() - .with_width(find_theme.tab_icon_width) + .with_width(search_theme.tab_icon_width) .aligned() .boxed(), ) .with_children(self.model.read(cx).active_query.as_ref().map(|query| { - Label::new(query.as_str().to_string(), style.label.clone()) + Label::new(query.as_str().to_string(), tab_theme.label.clone()) .aligned() .contained() - .with_margin_left(find_theme.tab_icon_spacing) + .with_margin_left(search_theme.tab_icon_spacing) .boxed() })) .boxed() @@ -332,7 +332,7 @@ impl ItemView for ProjectFindView { let query = self.query_editor.read(cx).text(cx); let editor = Editor::single_line( self.settings.clone(), - Some(|theme| theme.find.editor.input.clone()), + Some(|theme| theme.search.editor.input.clone()), cx, ); editor @@ -384,15 +384,15 @@ impl ItemView for ProjectFindView { } } -impl ProjectFindView { +impl ProjectSearchView { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace - .items_of_type::(cx) + .items_of_type::(cx) .max_by_key(|existing| existing.id()) { workspace.activate_item(&existing, cx); } else { - let model = cx.add_model(|cx| ProjectFind::new(workspace.project().clone(), cx)); + let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); workspace.open_item(model, cx); } } @@ -404,27 +404,27 @@ impl ProjectFindView { } fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { - if let Some(find_view) = workspace + if let Some(search_view) = workspace .active_item(cx) - .and_then(|item| item.downcast::()) + .and_then(|item| item.downcast::()) { - let new_query = find_view.update(cx, |find_view, cx| { - let new_query = find_view.build_search_query(cx); + let new_query = search_view.update(cx, |search_view, cx| { + let new_query = search_view.build_search_query(cx); if new_query.is_some() { - if let Some(old_query) = find_view.model.read(cx).active_query.clone() { - find_view.query_editor.update(cx, |editor, cx| { + if let Some(old_query) = search_view.model.read(cx).active_query.clone() { + search_view.query_editor.update(cx, |editor, cx| { editor.set_text(old_query.as_str(), cx); }); - find_view.regex = old_query.is_regex(); - find_view.whole_word = old_query.whole_word(); - find_view.case_sensitive = old_query.case_sensitive(); + search_view.regex = old_query.is_regex(); + search_view.whole_word = old_query.whole_word(); + search_view.case_sensitive = old_query.case_sensitive(); } } new_query }); if let Some(new_query) = new_query { let model = cx.add_model(|cx| { - let mut model = ProjectFind::new(workspace.project().clone(), cx); + let mut model = ProjectSearch::new(workspace.project().clone(), cx); model.search(new_query, cx); model }); @@ -492,7 +492,7 @@ impl ProjectFindView { fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); if !highlighted_ranges.is_empty() { - let theme = &self.settings.borrow().theme.find; + let theme = &self.settings.borrow().theme.search; self.results_editor.update(cx, |editor, cx| { editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); if reset_selections { @@ -511,9 +511,9 @@ impl ProjectFindView { fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; let editor_container = if self.query_contains_error { - theme.find.invalid_editor + theme.search.invalid_editor } else { - theme.find.editor.input.container + theme.search.editor.input.container }; Flex::row() .with_child( @@ -522,7 +522,7 @@ impl ProjectFindView { .with_style(editor_container) .aligned() .constrained() - .with_max_width(theme.find.editor.max_width) + .with_max_width(theme.search.editor.max_width) .boxed(), ) .with_child( @@ -531,15 +531,15 @@ impl ProjectFindView { .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) .contained() - .with_style(theme.find.option_button_group) + .with_style(theme.search.option_button_group) .aligned() .boxed(), ) .contained() - .with_style(theme.find.container) + .with_style(theme.search.container) .constrained() .with_height(theme.workspace.toolbar.height) - .named("find bar") + .named("project search") } fn render_option_button( @@ -548,7 +548,7 @@ impl ProjectFindView { option: SearchOption, cx: &mut RenderContext, ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; + let theme = &self.settings.borrow().theme.search; let is_active = self.is_option_enabled(option); MouseEventHandler::new::(option as usize, cx, |state, _| { let style = match (is_active, state.hovered) { diff --git a/crates/find/src/find.rs b/crates/search/src/search.rs similarity index 63% rename from crates/find/src/find.rs rename to crates/search/src/search.rs index caf8b7a8436996904a8c4767af10c40d3b2b43bc..05049c328cd2c124bd46f9d4cd11da9b9a95be96 100644 --- a/crates/find/src/find.rs +++ b/crates/search/src/search.rs @@ -1,11 +1,11 @@ use gpui::MutableAppContext; -mod buffer_find; -mod project_find; +mod buffer_search; +mod project_search; pub fn init(cx: &mut MutableAppContext) { - buffer_find::init(cx); - project_find::init(cx); + buffer_search::init(cx); + project_search::init(cx); } #[derive(Clone, Copy)] diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 701047961947f7982b6058cab81ea1ce40b4ab79..1ca2e3a6040393f30ec424cdeef6ed88e7ce818e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -24,7 +24,7 @@ pub struct Theme { pub project_panel: ProjectPanel, pub selector: Selector, pub editor: Editor, - pub find: Find, + pub search: Search, pub project_diagnostics: ProjectDiagnostics, } @@ -95,7 +95,7 @@ pub struct Toolbar { } #[derive(Clone, Deserialize, Default)] -pub struct Find { +pub struct Search { #[serde(flatten)] pub container: ContainerStyle, pub editor: FindEditor, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ac25887aebe6b327b566f085504e34c5e748c4a7..12e83086cdb9344e299f8d97480842daaa384d57 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -36,7 +36,7 @@ contacts_panel = { path = "../contacts_panel" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } -find = { path = "../find" } +search = { path = "../search" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { path = "../go_to_line" } diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index b46f8efa5071dc738cab9f60bc897c96e93ca723..59a488c896b4791e66514a2f0b76f409e8483342 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -348,14 +348,14 @@ tab_icon_width = 13 tab_icon_spacing = 4 tab_summary_spacing = 10 -[find] +[search] match_background = "$state.highlighted_line" background = "$surface.1" results_status = { extends = "$text.0", size = 18 } tab_icon_width = 14 tab_icon_spacing = 4 -[find.option_button] +[search.option_button] extends = "$text.1" padding = { left = 6, right = 6, top = 1, bottom = 1 } corner_radius = 6 @@ -364,26 +364,26 @@ border = { width = 1, color = "$border.0" } margin.left = 1 margin.right = 1 -[find.option_button_group] +[search.option_button_group] padding = { left = 2, right = 2 } -[find.active_option_button] -extends = "$find.option_button" +[search.active_option_button] +extends = "$search.option_button" background = "$surface.2" -[find.hovered_option_button] -extends = "$find.option_button" +[search.hovered_option_button] +extends = "$search.option_button" background = "$surface.2" -[find.active_hovered_option_button] -extends = "$find.option_button" +[search.active_hovered_option_button] +extends = "$search.option_button" background = "$surface.2" -[find.match_index] +[search.match_index] extends = "$text.1" padding = 6 -[find.editor] +[search.editor] max_width = 400 background = "$surface.0" corner_radius = 6 @@ -394,6 +394,6 @@ placeholder_text = "$text.2" selection = "$selection.host" border = { width = 1, color = "$border.0" } -[find.invalid_editor] -extends = "$find.editor" +[search.invalid_editor] +extends = "$search.editor" border = { width = 1, color = "$status.bad" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a6dda2e27bf928e9557a233e3f0c3986ab5e35df..f855f4fb194a9b4a176c8a860a48fd6ccbfd6220 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -60,7 +60,7 @@ fn main() { project_symbols::init(cx); project_panel::init(cx); diagnostics::init(cx); - find::init(cx); + search::init(cx); cx.spawn({ let client = client.clone(); |cx| async move { From 039765b698600ff389028d45103fac5c16104b0d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 08:27:02 -0700 Subject: [PATCH 45/65] Limit project search tab label to 24 characters I'd love to fade the text out, but for now I just append an ellipsis. --- crates/search/src/project_search.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 13abd5cde70140df4ef1d41b5da70e53c2329275..ed604b7a7fb8b90ec68208dabced84598de794b4 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -21,6 +21,8 @@ action!(SearchInNew); action!(ToggleSearchOption, SearchOption); action!(ToggleFocus); +const MAX_TAB_TITLE_LEN: usize = 24; + pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")), @@ -273,7 +275,13 @@ impl ItemView for ProjectSearchView { .boxed(), ) .with_children(self.model.read(cx).active_query.as_ref().map(|query| { - Label::new(query.as_str().to_string(), tab_theme.label.clone()) + let query_text = if query.as_str().len() > MAX_TAB_TITLE_LEN { + query.as_str()[..MAX_TAB_TITLE_LEN].to_string() + "…" + } else { + query.as_str().to_string() + }; + + Label::new(query_text, tab_theme.label.clone()) .aligned() .contained() .with_margin_left(search_theme.tab_icon_spacing) From c7338ebe886a4523e56cb5401edd94f834b1a475 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 09:25:27 -0700 Subject: [PATCH 46/65] :lipstick: --- crates/search/src/buffer_search.rs | 50 ++++++++++++++++-------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 9514939203954ead1767fed599dc7aae41189532..792d2a866e18afccde8a8d244091b8c5cc9b2808 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -18,7 +18,7 @@ action!(Deploy, bool); action!(Dismiss); action!(FocusEditor); action!(ToggleSearchOption, SearchOption); -action!(GoToMatch, Direction); +action!(SelectMatch, Direction); #[derive(Clone, Copy, PartialEq, Eq)] pub enum Direction { @@ -32,17 +32,21 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), Binding::new("escape", Dismiss, Some("SearchBar")), Binding::new("cmd-f", FocusEditor, Some("SearchBar")), - Binding::new("enter", GoToMatch(Direction::Next), Some("SearchBar")), - Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("SearchBar")), - Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), - Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), + Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")), + Binding::new( + "shift-enter", + SelectMatch(Direction::Prev), + Some("SearchBar"), + ), + Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")), + Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")), ]); cx.add_action(SearchBar::deploy); cx.add_action(SearchBar::dismiss); cx.add_action(SearchBar::focus_editor); cx.add_action(SearchBar::toggle_search_option); - cx.add_action(SearchBar::go_to_match); - cx.add_action(SearchBar::go_to_match_on_pane); + cx.add_action(SearchBar::select_match); + cx.add_action(SearchBar::select_match_on_pane); } struct SearchBar { @@ -245,7 +249,7 @@ impl SearchBar { .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .on_click(move |cx| cx.dispatch_action(SelectMatch(direction))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -337,7 +341,7 @@ impl SearchBar { cx.notify(); } - fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { + fn select_match(&mut self, SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { if let Some(mut index) = self.active_match_index { if let Some(editor) = self.active_editor.as_ref() { editor.update(cx, |editor, cx| { @@ -380,9 +384,9 @@ impl SearchBar { } } - fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { + fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext) { if let Some(search_bar) = pane.toolbar::() { - search_bar.update(cx, |search_bar, cx| search_bar.go_to_match(action, cx)); + search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx)); } } @@ -696,7 +700,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] @@ -707,7 +711,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { - search_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] @@ -718,7 +722,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { - search_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] @@ -729,7 +733,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { - search_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] @@ -740,7 +744,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { - search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] @@ -751,7 +755,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { - search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] @@ -762,7 +766,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { - search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] @@ -779,7 +783,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] @@ -796,7 +800,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] @@ -813,7 +817,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] @@ -830,7 +834,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] @@ -847,7 +851,7 @@ mod tests { }); search_bar.update(&mut cx, |search_bar, cx| { assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] From 136699e7bdad34284456b9bc18a30a235c43b597 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 09:25:40 -0700 Subject: [PATCH 47/65] Add log dependency in search crate --- Cargo.lock | 1 + crates/search/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bbfa3f52aa77a691520d5e90133c40eb19def78d..dcd0896e4dbafe4a8535fd5ad00c0b7a62775b02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4172,6 +4172,7 @@ dependencies = [ "editor", "gpui", "language", + "log", "postage", "project", "theme", diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 38eec0cf8521095ebb9cd2d982015bf668f17f46..a6ed1e4cd9c8f73427c816cdbfbcc2cb9550b947 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -16,6 +16,7 @@ theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } anyhow = "1.0" +log = "0.4" postage = { version = "0.4.1", features = ["futures-traits"] } [dev-dependencies] From dabb17a2ef2360695e514d2fd998e9473aa32408 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 09:48:46 -0700 Subject: [PATCH 48/65] Clone editor's searchable state on split --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c6e5f2b026387202c72e7828bb38931b7cd88aee..f8f623485ac7e17e3b934e0a34f904de69db229a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -850,6 +850,7 @@ impl Editor { clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); clone.nav_history = Some(nav_history); + clone.searchable = self.searchable; clone } From 19b5de2181d74dbdf8bef75b96c44bf8b89be747 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 09:49:16 -0700 Subject: [PATCH 49/65] Introduce ProjectSearchView constructor We had some duplication when cloning on split, so this is to unify that before we add any more complexity to construction. --- crates/search/src/project_search.rs | 154 +++++++++++----------------- 1 file changed, 62 insertions(+), 92 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ed604b7a7fb8b90ec68208dabced84598de794b4..4e61df136892c883ba4b3636664fd3b99d36c5fa 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -73,16 +73,16 @@ impl ProjectSearch { } } - fn clone(&self, new_cx: &mut ModelContext) -> Self { - Self { + fn clone(&self, cx: &mut ModelContext) -> ModelHandle { + cx.add_model(|cx| Self { project: self.project.clone(), excerpts: self .excerpts - .update(new_cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), pending_search: Default::default(), highlighted_ranges: self.highlighted_ranges.clone(), active_query: self.active_query.clone(), - } + }) } fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { @@ -130,55 +130,7 @@ impl Item for ProjectSearch { nav_history: ItemNavHistory, cx: &mut gpui::ViewContext, ) -> Self::View { - let settings = workspace.settings(); - let excerpts = model.read(cx).excerpts.clone(); - - let mut query_text = String::new(); - let mut regex = false; - let mut case_sensitive = false; - let mut whole_word = false; - if let Some(active_query) = model.read(cx).active_query.as_ref() { - query_text = active_query.as_str().to_string(); - regex = active_query.is_regex(); - case_sensitive = active_query.case_sensitive(); - whole_word = active_query.whole_word(); - } - - let query_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - settings.clone(), - Some(|theme| theme.search.editor.input.clone()), - cx, - ); - editor.set_text(query_text, cx); - editor - }); - let results_editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer( - excerpts, - Some(workspace.project().clone()), - settings.clone(), - cx, - ); - editor.set_searchable(false); - editor.set_nav_history(Some(nav_history)); - editor - }); - cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) - .detach(); - cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) - .detach(); - - ProjectSearchView { - model, - query_editor, - results_editor, - case_sensitive, - whole_word, - regex, - query_contains_error: false, - settings, - } + ProjectSearchView::new(model, nav_history, workspace.settings(), cx) } fn project_path(&self) -> Option { @@ -336,63 +288,81 @@ impl ItemView for ProjectSearchView { where Self: Sized, { + let model = self.model.update(cx, |model, cx| model.clone(cx)); + Some(Self::new(model, nav_history, self.settings.clone(), cx)) + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.navigate(data, cx)); + } + + fn should_update_tab_on_event(event: &ViewEvent) -> bool { + matches!(event, ViewEvent::UpdateTab) + } +} + +impl ProjectSearchView { + fn new( + model: ModelHandle, + nav_history: ItemNavHistory, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + let project; + let excerpts; + let mut query_text = String::new(); + let mut regex = false; + let mut case_sensitive = false; + let mut whole_word = false; + + { + let model = model.read(cx); + project = model.project.clone(); + excerpts = model.excerpts.clone(); + if let Some(active_query) = model.active_query.as_ref() { + query_text = active_query.as_str().to_string(); + regex = active_query.is_regex(); + case_sensitive = active_query.case_sensitive(); + whole_word = active_query.whole_word(); + } + } + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) + .detach(); + let query_editor = cx.add_view(|cx| { - let query = self.query_editor.read(cx).text(cx); - let editor = Editor::single_line( - self.settings.clone(), + let mut editor = Editor::single_line( + settings.clone(), Some(|theme| theme.search.editor.input.clone()), cx, ); - editor - .buffer() - .update(cx, |buffer, cx| buffer.edit([0..0], query, cx)); + editor.set_text(query_text, cx); editor }); - let model = self - .model - .update(cx, |model, cx| cx.add_model(|cx| model.clone(cx))); - cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) - .detach(); let results_editor = cx.add_view(|cx| { - let model = model.read(cx); - let excerpts = model.excerpts.clone(); - let project = model.project.clone(); - let scroll_position = self - .results_editor - .update(cx, |editor, cx| editor.scroll_position(cx)); - - let mut editor = Editor::for_buffer(excerpts, Some(project), self.settings.clone(), cx); + let mut editor = Editor::for_buffer(excerpts, Some(project), settings.clone(), cx); editor.set_searchable(false); editor.set_nav_history(Some(nav_history)); - editor.set_scroll_position(scroll_position, cx); editor }); - let mut view = Self { + cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) + .detach(); + + let mut this = ProjectSearchView { model, query_editor, results_editor, - case_sensitive: self.case_sensitive, - whole_word: self.whole_word, - regex: self.regex, - query_contains_error: self.query_contains_error, - settings: self.settings.clone(), + case_sensitive, + whole_word, + regex, + query_contains_error: false, + settings, }; - view.model_changed(false, cx); - Some(view) + this.model_changed(false, cx); + this } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { - self.results_editor - .update(cx, |editor, cx| editor.navigate(data, cx)); - } - - fn should_update_tab_on_event(event: &ViewEvent) -> bool { - matches!(event, ViewEvent::UpdateTab) - } -} - -impl ProjectSearchView { fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace .items_of_type::(cx) From 7ef98fb9359518caa59ef9df1a298d4959376dd2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 13:02:09 -0700 Subject: [PATCH 50/65] Make versions oldest_selection and newest_selection that don't require snapshots I thought I needed this but actually didn't, but I still kinda think it's a good change for the public interface of Editor. --- crates/editor/src/editor.rs | 40 +++++++++++++++++------------ crates/editor/src/element.rs | 2 +- crates/editor/src/items.rs | 4 ++- crates/go_to_line/src/go_to_line.rs | 2 +- crates/outline/src/outline.rs | 4 ++- crates/search/src/buffer_search.rs | 2 +- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f8f623485ac7e17e3b934e0a34f904de69db229a..1dd4a514257e54f8733836533839771663b00199 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1061,7 +1061,8 @@ impl Editor { first_cursor_top = highlighted_rows.start as f32; last_cursor_bottom = first_cursor_top + 1.; } else if autoscroll == Autoscroll::Newest { - let newest_selection = self.newest_selection::(&display_map.buffer_snapshot); + let newest_selection = + self.newest_selection_with_snapshot::(&display_map.buffer_snapshot); first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; last_cursor_bottom = first_cursor_top + 1.; } else { @@ -1208,7 +1209,7 @@ impl Editor { ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let tail = self - .newest_selection::(&display_map.buffer_snapshot) + .newest_selection_with_snapshot::(&display_map.buffer_snapshot) .tail(); self.begin_selection(position, false, click_count, cx); @@ -1328,7 +1329,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let tail = self - .newest_selection::(&display_map.buffer_snapshot) + .newest_selection_with_snapshot::(&display_map.buffer_snapshot) .tail(); self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); @@ -1514,8 +1515,7 @@ impl Editor { self.set_selections(selections, None, cx); self.request_autoscroll(Autoscroll::Fit, cx); } else { - let buffer = self.buffer.read(cx).snapshot(cx); - let mut oldest_selection = self.oldest_selection::(&buffer); + let mut oldest_selection = self.oldest_selection::(&cx); if self.selection_count() == 1 { if oldest_selection.is_empty() { cx.propagate_action(); @@ -4086,7 +4086,7 @@ impl Editor { pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext) { let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self.newest_selection::(&buffer); + let selection = self.newest_selection_with_snapshot::(&buffer); let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { active_diagnostics .primary_range @@ -4158,8 +4158,7 @@ impl Editor { }; let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); + let head = editor.newest_selection::(cx).head(); let (buffer, head) = if let Some(text_anchor) = editor.buffer.read(cx).text_anchor_for_position(head, cx) { text_anchor @@ -4207,8 +4206,7 @@ impl Editor { let editor_handle = active_item.act_as::(cx)?; let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); + let head = editor.newest_selection::(cx).head(); let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; let replica_id = editor.replica_id(cx); @@ -4432,12 +4430,11 @@ impl Editor { self.clear_highlighted_ranges::(cx); let editor = rename.editor.read(cx); - let buffer = editor.buffer.read(cx).snapshot(cx); - let selection = editor.newest_selection::(&buffer); + let snapshot = self.buffer.read(cx).snapshot(cx); + let selection = editor.newest_selection_with_snapshot::(&snapshot); // Update the selection to match the position of the selection inside // the rename editor. - let snapshot = self.buffer.read(cx).snapshot(cx); let rename_range = rename.range.to_offset(&snapshot); let start = snapshot .clip_offset(rename_range.start + selection.start, Bias::Left) @@ -4748,17 +4745,28 @@ impl Editor { pub fn oldest_selection>( &self, - snapshot: &MultiBufferSnapshot, + cx: &AppContext, ) -> Selection { + let snapshot = self.buffer.read(cx).read(cx); self.selections .iter() .min_by_key(|s| s.id) - .map(|selection| self.resolve_selection(selection, snapshot)) - .or_else(|| self.pending_selection(snapshot)) + .map(|selection| self.resolve_selection(selection, &snapshot)) + .or_else(|| self.pending_selection(&snapshot)) .unwrap() } pub fn newest_selection>( + &self, + cx: &AppContext, + ) -> Selection { + self.resolve_selection( + self.newest_anchor_selection(), + &self.buffer.read(cx).read(cx), + ) + } + + pub fn newest_selection_with_snapshot>( &self, snapshot: &MultiBufferSnapshot, ) -> Selection { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f4277713b165decd679856e9315488762b72dd7c..dcf716e0bb91b6fc55aba4bf4cc7db55ac81a031 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -951,7 +951,7 @@ impl Element for EditorElement { } let newest_selection_head = view - .newest_selection::(&snapshot.buffer_snapshot) + .newest_selection_with_snapshot::(&snapshot.buffer_snapshot) .head() .to_display_point(&snapshot); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index dedd712867d650bde881733ad05cce76bb46234b..4e25a5a24b0dc3503056c60734319216d7d50970 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -382,7 +382,9 @@ impl DiagnosticMessage { fn update(&mut self, editor: ViewHandle, cx: &mut ViewContext) { let editor = editor.read(cx); let buffer = editor.buffer().read(cx); - let cursor_position = editor.newest_selection::(&buffer.read(cx)).head(); + let cursor_position = editor + .newest_selection_with_snapshot::(&buffer.read(cx)) + .head(); let new_diagnostic = buffer .read(cx) .diagnostics_in_range::<_, usize>(cursor_position..cursor_position) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 0b35ef7dbdbf3889b37b2995902fe18c067dd6b1..c3e9cdcbf257cc12aba8302d3865587bbdd167e6 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -54,7 +54,7 @@ impl GoToLine { let buffer = editor.buffer().read(cx).read(cx); ( Some(scroll_position), - editor.newest_selection(&buffer).head(), + editor.newest_selection_with_snapshot(&buffer).head(), buffer.max_point(), ) }); diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 4d2dc125c633b87e417c9ffb003ab972ec651d71..3607f1af87b4b95fb0222ecaad0c35d4a49c2010 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -259,7 +259,9 @@ impl OutlineView { let editor = self.active_editor.read(cx); let buffer = editor.buffer().read(cx).read(cx); - let cursor_offset = editor.newest_selection::(&buffer).head(); + let cursor_offset = editor + .newest_selection_with_snapshot::(&buffer) + .head(); selected_index = self .outline .items diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 792d2a866e18afccde8a8d244091b8c5cc9b2808..7a5e3d1514fe3e286dffc34da74ceafee7d9b74c 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -270,7 +270,7 @@ impl SearchBar { .display_snapshot; let selection = editor .read(cx) - .newest_selection::(&display_map.buffer_snapshot); + .newest_selection_with_snapshot::(&display_map.buffer_snapshot); let mut text: String; if selection.start == selection.end { From 64d22925c2feeb8db92a1f17f540c009381c249c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 14:18:04 -0700 Subject: [PATCH 51/65] Implement navigation between project search matches --- crates/search/src/buffer_search.rs | 9 +- crates/search/src/project_search.rs | 173 +++++++++++++++++++++++++--- crates/search/src/search.rs | 11 +- 3 files changed, 167 insertions(+), 26 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 7a5e3d1514fe3e286dffc34da74ceafee7d9b74c..c15f81ecf5ae8e62fbde672482c235f140810931 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,4 +1,4 @@ -use crate::SearchOption; +use crate::{Direction, SearchOption, SelectMatch}; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor}; use gpui::{ @@ -18,13 +18,6 @@ action!(Deploy, bool); action!(Dismiss); action!(FocusEditor); action!(ToggleSearchOption, SearchOption); -action!(SelectMatch, Direction); - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Direction { - Prev, - Next, -} pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4e61df136892c883ba4b3636664fd3b99d36c5fa..cf6a5653c8fd465f5d9142f32b4a8c0085831109 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,5 +1,5 @@ -use crate::SearchOption; -use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; +use crate::{Direction, SearchOption, SelectMatch, ToggleSearchOption}; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, SelectNext}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, @@ -9,6 +9,7 @@ use postage::watch; use project::{search::SearchQuery, Project}; use std::{ any::{Any, TypeId}, + cmp::{self, Ordering}, ops::Range, path::PathBuf, }; @@ -18,7 +19,6 @@ use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace} action!(Deploy); action!(Search); action!(SearchInNew); -action!(ToggleSearchOption, SearchOption); action!(ToggleFocus); const MAX_TAB_TITLE_LEN: usize = 24; @@ -30,19 +30,30 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-shift-F", Deploy, Some("Workspace")), Binding::new("enter", Search, Some("ProjectSearchView")), Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")), + Binding::new( + "cmd-g", + SelectMatch(Direction::Next), + Some("ProjectSearchView"), + ), + Binding::new( + "cmd-shift-G", + SelectMatch(Direction::Prev), + Some("ProjectSearchView"), + ), ]); cx.add_action(ProjectSearchView::deploy); cx.add_action(ProjectSearchView::search); cx.add_action(ProjectSearchView::search_in_new); cx.add_action(ProjectSearchView::toggle_search_option); cx.add_action(ProjectSearchView::toggle_focus); + cx.add_action(ProjectSearchView::select_match); } struct ProjectSearch { project: ModelHandle, excerpts: ModelHandle, pending_search: Option>>, - highlighted_ranges: Vec>, + match_ranges: Vec>, active_query: Option, } @@ -54,6 +65,7 @@ struct ProjectSearchView { whole_word: bool, regex: bool, query_contains_error: bool, + active_match_index: Option, settings: watch::Receiver, } @@ -68,7 +80,7 @@ impl ProjectSearch { project, excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), pending_search: Default::default(), - highlighted_ranges: Default::default(), + match_ranges: Default::default(), active_query: None, } } @@ -80,7 +92,7 @@ impl ProjectSearch { .excerpts .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), pending_search: Default::default(), - highlighted_ranges: self.highlighted_ranges.clone(), + match_ranges: self.match_ranges.clone(), active_query: self.active_query.clone(), }) } @@ -90,12 +102,12 @@ impl ProjectSearch { .project .update(cx, |project, cx| project.search(query.clone(), cx)); self.active_query = Some(query); - self.highlighted_ranges.clear(); + self.match_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { let matches = search.await.log_err()?; if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.highlighted_ranges.clear(); + this.match_ranges.clear(); let mut matches = matches.into_iter().collect::>(); matches .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); @@ -108,7 +120,7 @@ impl ProjectSearch { 1, cx, ); - this.highlighted_ranges.extend(ranges_to_highlight); + this.match_ranges.extend(ranges_to_highlight); } }); this.pending_search.take(); @@ -153,7 +165,7 @@ impl View for ProjectSearchView { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let model = &self.model.read(cx); - let results = if model.highlighted_ranges.is_empty() { + let results = if model.match_ranges.is_empty() { let theme = &self.settings.borrow().theme; let text = if self.query_editor.read(cx).text(cx).is_empty() { "" @@ -181,7 +193,7 @@ impl View for ProjectSearchView { } fn on_focus(&mut self, cx: &mut ViewContext) { - if self.model.read(cx).highlighted_ranges.is_empty() { + if self.model.read(cx).match_ranges.is_empty() { cx.focus(&self.query_editor); } else { self.focus_results_editor(cx); @@ -348,6 +360,12 @@ impl ProjectSearchView { }); cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) .detach(); + cx.subscribe(&results_editor, |this, _, event, cx| { + if matches!(event, editor::Event::SelectionsChanged) { + this.update_match_index(cx); + } + }) + .detach(); let mut this = ProjectSearchView { model, @@ -357,6 +375,7 @@ impl ProjectSearchView { whole_word, regex, query_contains_error: false, + active_match_index: None, settings, }; this.model_changed(false, cx); @@ -446,9 +465,52 @@ impl ProjectSearchView { cx.notify(); } + fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { + if let Some(mut index) = self.active_match_index { + let range_to_select = { + let model = self.model.read(cx); + let results_editor = self.results_editor.read(cx); + let buffer = results_editor.buffer().read(cx).read(cx); + let cursor = results_editor.newest_anchor_selection().head(); + let ranges = &model.match_ranges; + + if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() { + if direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + } else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() { + if direction == Direction::Next { + index = 0; + } + } else if direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } else if direction == Direction::Next { + if index == ranges.len() - 1 { + index = 0 + } else { + index += 1; + } + }; + ranges[index].clone() + }; + + self.results_editor.update(cx, |editor, cx| { + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); + }); + } + } + fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { if self.query_editor.is_focused(cx) { - if !self.model.read(cx).highlighted_ranges.is_empty() { + if !self.model.read(cx).match_ranges.is_empty() { self.focus_results_editor(cx); } } else { @@ -461,18 +523,20 @@ impl ProjectSearchView { fn focus_results_editor(&self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { - let head = query_editor.newest_anchor_selection().head(); - query_editor.select_ranges([head.clone()..head], None, cx); + let cursor = query_editor.newest_anchor_selection().head(); + query_editor.select_ranges([cursor.clone()..cursor], None, cx); }); cx.focus(&self.results_editor); } fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { - let highlighted_ranges = self.model.read(cx).highlighted_ranges.clone(); - if !highlighted_ranges.is_empty() { + let match_ranges = self.model.read(cx).match_ranges.clone(); + if match_ranges.is_empty() { + self.active_match_index = None; + } else { let theme = &self.settings.borrow().theme.search; self.results_editor.update(cx, |editor, cx| { - editor.highlight_ranges::(highlighted_ranges, theme.match_background, cx); + editor.highlight_ranges::(match_ranges, theme.match_background, cx); if reset_selections { editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); } @@ -486,6 +550,34 @@ impl ProjectSearchView { cx.notify(); } + fn update_match_index(&mut self, cx: &mut ViewContext) { + let match_ranges = self.model.read(cx).match_ranges.clone(); + if match_ranges.is_empty() { + self.active_match_index = None; + } else { + let results_editor = &self.results_editor.read(cx); + let cursor = results_editor.newest_anchor_selection().head(); + let new_index = { + let buffer = results_editor.buffer().read(cx).read(cx); + match match_ranges.binary_search_by(|probe| { + if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, match_ranges.len() - 1)), + } + }; + if self.active_match_index != new_index { + self.active_match_index = new_index; + cx.notify(); + } + } + } + fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; let editor_container = if self.query_contains_error { @@ -513,6 +605,29 @@ impl ProjectSearchView { .aligned() .boxed(), ) + .with_children({ + self.active_match_index.into_iter().flat_map(|match_ix| { + [ + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + Label::new( + format!( + "{}/{}", + match_ix + 1, + self.model.read(cx).match_ranges.len() + ), + theme.search.match_index.text.clone(), + ) + .contained() + .with_style(theme.search.match_index.container) + .aligned() + .boxed(), + ] + }) + }) .contained() .with_style(theme.search.container) .constrained() @@ -552,4 +667,28 @@ impl ProjectSearchView { SearchOption::Regex => self.regex, } } + + fn render_nav_button( + &self, + icon: &str, + direction: Direction, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.search; + enum NavButton {} + MouseEventHandler::new::(direction as usize, cx, |state, _| { + let style = if state.hovered { + &theme.hovered_option_button + } else { + &theme.option_button + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(SelectMatch(direction))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 05049c328cd2c124bd46f9d4cd11da9b9a95be96..6e8e7e00f3289bd0f68dbc22e047da462e40d11a 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,4 +1,4 @@ -use gpui::MutableAppContext; +use gpui::{action, MutableAppContext}; mod buffer_search; mod project_search; @@ -8,9 +8,18 @@ pub fn init(cx: &mut MutableAppContext) { project_search::init(cx); } +action!(ToggleSearchOption, SearchOption); +action!(SelectMatch, Direction); + #[derive(Clone, Copy)] pub enum SearchOption { WholeWord, CaseSensitive, Regex, } + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} From 7831979be096738c6ccf8a29e719a6936034516f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 14:21:28 -0700 Subject: [PATCH 52/65] Fix warning --- crates/search/src/project_search.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index cf6a5653c8fd465f5d9142f32b4a8c0085831109..eb8608a3ef8b2a4a24251e76d469512cd356c4b9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,5 +1,5 @@ use crate::{Direction, SearchOption, SelectMatch, ToggleSearchOption}; -use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll, SelectNext}; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, From 7d119dcd546648099f992b0e319f771df9c01d02 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 14:24:06 -0700 Subject: [PATCH 53/65] Select first match when results are ready --- crates/search/src/project_search.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index eb8608a3ef8b2a4a24251e76d469512cd356c4b9..b823d0fd2e43643001a2c6417bf12aae85d34057 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -536,10 +536,10 @@ impl ProjectSearchView { } else { let theme = &self.settings.borrow().theme.search; self.results_editor.update(cx, |editor, cx| { - editor.highlight_ranges::(match_ranges, theme.match_background, cx); if reset_selections { - editor.select_ranges([0..0], Some(Autoscroll::Fit), cx); + editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx); } + editor.highlight_ranges::(match_ranges, theme.match_background, cx); }); if self.query_editor.is_focused(cx) { self.focus_results_editor(cx); From 71241b1fb8ab6f78128beeb6ec2f7691d3f81282 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 16:14:40 -0700 Subject: [PATCH 54/65] Add capture phase for action dispatch Just like the DOM, we now bubble events down the tree during a capture phase before bubbling them back up. --- crates/gpui/src/app.rs | 158 ++++++++++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 40 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index dec73ab6c2a6396895c81bcb2edbba94deff2326..00b1ef2de57f4faeeda3852aabda848791cd116d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -722,6 +722,7 @@ pub struct MutableAppContext { foreground_platform: Rc, assets: Arc, cx: AppContext, + capture_actions: HashMap>>>, actions: HashMap>>>, global_actions: HashMap>, keystroke_matcher: keymap::Matcher, @@ -768,6 +769,7 @@ impl MutableAppContext { font_cache, platform, }, + capture_actions: HashMap::new(), actions: HashMap::new(), global_actions: HashMap::new(), keystroke_matcher: keymap::Matcher::default(), @@ -857,7 +859,25 @@ impl MutableAppContext { .map(|debug_elements| debug_elements(&self.cx)) } - pub fn add_action(&mut self, mut handler: F) + pub fn add_action(&mut self, handler: F) + where + A: Action, + V: View, + F: 'static + FnMut(&mut V, &A, &mut ViewContext), + { + self.add_action_internal(handler, false) + } + + pub fn capture_action(&mut self, handler: F) + where + A: Action, + V: View, + F: 'static + FnMut(&mut V, &A, &mut ViewContext), + { + self.add_action_internal(handler, true) + } + + fn add_action_internal(&mut self, mut handler: F, capture: bool) where A: Action, V: View, @@ -881,7 +901,13 @@ impl MutableAppContext { }, ); - self.actions + let actions = if capture { + &mut self.capture_actions + } else { + &mut self.actions + }; + + actions .entry(TypeId::of::()) .or_default() .entry(TypeId::of::()) @@ -1169,29 +1195,33 @@ impl MutableAppContext { ) -> bool { self.update(|this| { this.halt_action_dispatch = false; - for view_id in path.iter().rev() { - if let Some(mut view) = this.cx.views.remove(&(window_id, *view_id)) { + for (capture_phase, view_id) in path + .iter() + .map(|view_id| (true, *view_id)) + .chain(path.iter().rev().map(|view_id| (false, *view_id))) + { + if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) { let type_id = view.as_any().type_id(); if let Some((name, mut handlers)) = this - .actions + .actions_mut(capture_phase) .get_mut(&type_id) .and_then(|h| h.remove_entry(&action.id())) { for handler in handlers.iter_mut().rev() { this.halt_action_dispatch = true; - handler(view.as_mut(), action, this, window_id, *view_id); + handler(view.as_mut(), action, this, window_id, view_id); if this.halt_action_dispatch { break; } } - this.actions + this.actions_mut(capture_phase) .get_mut(&type_id) .unwrap() .insert(name, handlers); } - this.cx.views.insert((window_id, *view_id), view); + this.cx.views.insert((window_id, view_id), view); if this.halt_action_dispatch { break; @@ -1206,6 +1236,17 @@ impl MutableAppContext { }) } + fn actions_mut( + &mut self, + capture_phase: bool, + ) -> &mut HashMap>>> { + if capture_phase { + &mut self.capture_actions + } else { + &mut self.actions + } + } + pub fn dispatch_global_action(&mut self, action: A) { self.dispatch_global_action_any(&action); } @@ -4320,40 +4361,58 @@ mod tests { let actions = Rc::new(RefCell::new(Vec::new())); - let actions_clone = actions.clone(); - cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { - actions_clone.borrow_mut().push("global".to_string()); - }); + { + let actions = actions.clone(); + cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { + actions.borrow_mut().push("global".to_string()); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewA, action: &Action, cx| { - assert_eq!(action.0, "bar"); - cx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} a", view.id)); - }); + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewA, action: &Action, cx| { + assert_eq!(action.0, "bar"); + cx.propagate_action(); + actions.borrow_mut().push(format!("{} a", view.id)); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewA, _: &Action, cx| { - if view.id != 1 { - cx.add_view(|cx| { - cx.propagate_action(); // Still works on a nested ViewContext - ViewB { id: 5 } - }); - } - actions_clone.borrow_mut().push(format!("{} b", view.id)); - }); + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewA, _: &Action, cx| { + if view.id != 1 { + cx.add_view(|cx| { + cx.propagate_action(); // Still works on a nested ViewContext + ViewB { id: 5 } + }); + } + actions.borrow_mut().push(format!("{} b", view.id)); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewB, _: &Action, cx| { - cx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} c", view.id)); - }); + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} c", view.id)); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewB, _: &Action, cx| { - cx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} d", view.id)); - }); + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} d", view.id)); + }); + } + + { + let actions = actions.clone(); + cx.capture_action(move |view: &mut ViewA, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} capture", view.id)); + }); + } let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 }); let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 }); @@ -4368,7 +4427,17 @@ mod tests { assert_eq!( *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "1 b"] + vec![ + "1 capture", + "3 capture", + "4 d", + "4 c", + "3 b", + "3 a", + "2 d", + "2 c", + "1 b" + ] ); // Remove view_1, which doesn't propagate the action @@ -4381,7 +4450,16 @@ mod tests { assert_eq!( *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global"] + vec![ + "3 capture", + "4 d", + "4 c", + "3 b", + "3 a", + "2 d", + "2 c", + "global" + ] ); } From 1ddae2adfd43de7f8ac12531f26401b1d078be4e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 16:15:06 -0700 Subject: [PATCH 55/65] Focus the project find results editor on a tab in the query editor --- crates/search/src/project_search.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b823d0fd2e43643001a2c6417bf12aae85d34057..be2ac6a51a86f6cd9f2dc271e0b034df64d59c79 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -45,8 +45,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectSearchView::search); cx.add_action(ProjectSearchView::search_in_new); cx.add_action(ProjectSearchView::toggle_search_option); - cx.add_action(ProjectSearchView::toggle_focus); cx.add_action(ProjectSearchView::select_match); + cx.add_action(ProjectSearchView::toggle_focus); + cx.capture_action(ProjectSearchView::tab); } struct ProjectSearch { @@ -521,6 +522,16 @@ impl ProjectSearchView { } } + fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { + if self.query_editor.is_focused(cx) { + if !self.model.read(cx).match_ranges.is_empty() { + self.focus_results_editor(cx); + } + } else { + cx.propagate_action() + } + } + fn focus_results_editor(&self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { let cursor = query_editor.newest_anchor_selection().head(); From cb230ad57487ec4ef8d3cf1ddd9ff10e797c83f0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 18:07:46 -0700 Subject: [PATCH 56/65] Re-activate the most recently-activated project search on cmd-shift-F This commits adds the beginnings of an application state facility as a non-static place to store the most recently-activated search for each project. I also store workspace items by descending order of their entity id so that we always fetch the newest item of a given type when calling `Workspace::item_of_type`. --- crates/gpui/src/app.rs | 63 +++++++++++++++++++++++++++++ crates/gpui/src/presenter.rs | 4 ++ crates/search/src/project_search.rs | 55 ++++++++++++++++++++----- crates/workspace/src/workspace.rs | 17 ++++---- 4 files changed, 122 insertions(+), 17 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 00b1ef2de57f4faeeda3852aabda848791cd116d..dcfb00617d600a2db2feeb94c4aca99d1428b49f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -85,6 +85,8 @@ pub trait UpgradeModelHandle { handle: &WeakModelHandle, ) -> Option>; + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool; + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option; } @@ -608,6 +610,10 @@ impl UpgradeModelHandle for AsyncAppContext { self.0.borrow().upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.0.borrow().model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.0.borrow().upgrade_any_model_handle(handle) } @@ -763,6 +769,7 @@ impl MutableAppContext { models: Default::default(), views: Default::default(), windows: Default::default(), + app_states: Default::default(), element_states: Default::default(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background, @@ -1306,6 +1313,27 @@ impl MutableAppContext { Ok(pending) } + pub fn add_app_state(&mut self, state: T) { + self.cx + .app_states + .insert(TypeId::of::(), Box::new(state)); + } + + pub fn update_app_state(&mut self, update: F) -> U + where + F: FnOnce(&mut T, &mut MutableAppContext) -> U, + { + let type_id = TypeId::of::(); + let mut state = self + .cx + .app_states + .remove(&type_id) + .expect("no app state has been added for this type"); + let result = update(state.downcast_mut().unwrap(), self); + self.cx.app_states.insert(type_id, state); + result + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle where T: Entity, @@ -1828,6 +1856,10 @@ impl UpgradeModelHandle for MutableAppContext { self.cx.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.cx.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.cx.upgrade_any_model_handle(handle) } @@ -1898,6 +1930,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, + app_states: HashMap>, element_states: HashMap>, background: Arc, ref_counts: Arc>, @@ -1929,6 +1962,14 @@ impl AppContext { pub fn platform(&self) -> &Arc { &self.platform } + + pub fn app_state(&self) -> &T { + self.app_states + .get(&TypeId::of::()) + .expect("no app state has been added for this type") + .downcast_ref() + .unwrap() + } } impl ReadModel for AppContext { @@ -1956,6 +1997,10 @@ impl UpgradeModelHandle for AppContext { } } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.models.contains_key(&handle.model_id) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { if self.models.contains_key(&handle.model_id) { self.ref_counts.lock().inc_model(handle.model_id); @@ -2361,6 +2406,10 @@ impl UpgradeModelHandle for ModelContext<'_, M> { self.cx.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.cx.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.cx.upgrade_any_model_handle(handle) } @@ -2699,6 +2748,10 @@ impl UpgradeModelHandle for ViewContext<'_, V> { self.cx.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.cx.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.cx.upgrade_any_model_handle(handle) } @@ -2941,6 +2994,12 @@ impl PartialEq for ModelHandle { impl Eq for ModelHandle {} +impl PartialEq> for ModelHandle { + fn eq(&self, other: &WeakModelHandle) -> bool { + self.model_id == other.model_id + } +} + impl Hash for ModelHandle { fn hash(&self, state: &mut H) { self.model_id.hash(state); @@ -3013,6 +3072,10 @@ impl WeakModelHandle { self.model_id } + pub fn is_upgradable(&self, cx: &impl UpgradeModelHandle) -> bool { + cx.model_handle_is_upgradable(self) + } + pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option> { cx.upgrade_model_handle(self) } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index ba59e14a30b38fe4b551b96f3b119020752b30ec..8a41a76e714bc352593c20a87e18edb5758b8e0b 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -281,6 +281,10 @@ impl<'a> UpgradeModelHandle for LayoutContext<'a> { self.app.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.app.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.app.upgrade_any_model_handle(handle) } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index be2ac6a51a86f6cd9f2dc271e0b034df64d59c79..d1db3f0d2bd92060eea7370053fa0a46ed7cb12f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,9 +1,10 @@ use crate::{Direction, SearchOption, SelectMatch, ToggleSearchOption}; +use collections::HashMap; use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, - ViewHandle, + ViewHandle, WeakModelHandle, }; use postage::watch; use project::{search::SearchQuery, Project}; @@ -14,7 +15,9 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; +use workspace::{ + Item, ItemHandle, ItemNavHistory, ItemView, Settings, WeakItemViewHandle, Workspace, +}; action!(Deploy); action!(Search); @@ -23,7 +26,11 @@ action!(ToggleFocus); const MAX_TAB_TITLE_LEN: usize = 24; +#[derive(Default)] +struct ActiveSearches(HashMap, WeakModelHandle>); + pub fn init(cx: &mut MutableAppContext) { + cx.add_app_state(ActiveSearches::default()); cx.add_bindings([ Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")), Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")), @@ -194,6 +201,13 @@ impl View for ProjectSearchView { } fn on_focus(&mut self, cx: &mut ViewContext) { + cx.update_app_state(|state: &mut ActiveSearches, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.model.downgrade(), + ) + }); + if self.model.read(cx).match_ranges.is_empty() { cx.focus(&self.query_editor); } else { @@ -383,11 +397,28 @@ impl ProjectSearchView { this } + // Re-activate the most recently activated search or the most recent if it has been closed. + // If no search exists in the workspace, create a new one. fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - if let Some(existing) = workspace - .items_of_type::(cx) - .max_by_key(|existing| existing.id()) - { + // Clean up entries for dropped projects + cx.update_app_state(|state: &mut ActiveSearches, cx| { + state.0.retain(|project, _| project.is_upgradable(cx)) + }); + + let active_search = cx + .app_state::() + .0 + .get(&workspace.project().downgrade()); + + let existing = active_search + .and_then(|active_search| { + workspace + .items_of_type::(cx) + .find(|search| search == active_search) + }) + .or_else(|| workspace.item_of_type::(cx)); + + if let Some(existing) = existing { workspace.activate_item(&existing, cx); } else { let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); @@ -515,10 +546,7 @@ impl ProjectSearchView { self.focus_results_editor(cx); } } else { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&SelectAll, cx); - }); - cx.focus(&self.query_editor); + self.focus_query_editor(cx); } } @@ -532,6 +560,13 @@ impl ProjectSearchView { } } + fn focus_query_editor(&self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&SelectAll, cx); + }); + cx.focus(&self.query_editor); + } + fn focus_results_editor(&self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { let cursor = query_editor.newest_anchor_selection().head(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 585695c520c757b3dddc851571a13c519373598e..0ad3a1e5e35ad0cce16c2f6bf2c40f5cf03293db 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,7 +9,7 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use clock::ReplicaId; -use collections::HashSet; +use collections::BTreeMap; use gpui::{ action, color::Color, @@ -36,6 +36,7 @@ pub use status_bar::StatusItemView; use std::{ any::{Any, TypeId}, cell::RefCell, + cmp::Reverse, future::Future, hash::{Hash, Hasher}, path::{Path, PathBuf}, @@ -569,7 +570,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, path_openers: Arc<[Box]>, - items: HashSet>, + items: BTreeMap, Box>, _observe_current_user: Task<()>, } @@ -815,14 +816,14 @@ impl Workspace { fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option> { self.items - .iter() + .values() .filter_map(|i| i.upgrade(cx)) .find(|i| i.project_path(cx).as_ref() == Some(path)) } pub fn item_of_type(&self, cx: &AppContext) -> Option> { self.items - .iter() + .values() .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) } @@ -831,7 +832,7 @@ impl Workspace { cx: &'a AppContext, ) -> impl 'a + Iterator> { self.items - .iter() + .values() .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) } @@ -974,7 +975,8 @@ impl Workspace { where T: 'static + ItemHandle, { - self.items.insert(item_handle.downgrade()); + self.items + .insert(Reverse(item_handle.id()), item_handle.downgrade()); pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx)) } @@ -1068,7 +1070,8 @@ impl Workspace { if let Some(item) = pane.read(cx).active_item() { let nav_history = new_pane.read(cx).nav_history().clone(); if let Some(clone) = item.clone_on_split(nav_history, cx.as_mut()) { - self.items.insert(clone.item(cx).downgrade()); + let item = clone.item(cx).downgrade(); + self.items.insert(Reverse(item.id()), item); new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } From 8eba96424eb370266c37c91a9fb7690dda3876b8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 27 Feb 2022 18:17:15 -0700 Subject: [PATCH 57/65] Fix warning --- crates/search/src/project_search.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d1db3f0d2bd92060eea7370053fa0a46ed7cb12f..449d9d3537045c032fb68d2e40b6a000d44217be 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -15,9 +15,7 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{ - Item, ItemHandle, ItemNavHistory, ItemView, Settings, WeakItemViewHandle, Workspace, -}; +use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; action!(Deploy); action!(Search); From ed89475cf63358d5e1997b78856ba355b5941064 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Feb 2022 10:34:11 +0100 Subject: [PATCH 58/65] Extract a common `match_index_for_direction` and `active_match_index` --- crates/search/src/buffer_search.rs | 91 +++++++++-------------------- crates/search/src/project_search.rs | 86 ++++++++------------------- crates/search/src/search.rs | 63 ++++++++++++++++++++ 3 files changed, 115 insertions(+), 125 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c15f81ecf5ae8e62fbde672482c235f140810931..847345448a1dfad09424a403377e9a2802a0ef51 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,4 +1,4 @@ -use crate::{Direction, SearchOption, SelectMatch}; +use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch}; use collections::HashMap; use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor}; use gpui::{ @@ -8,10 +8,7 @@ use gpui::{ use language::AnchorRangeExt; use postage::watch; use project::search::SearchQuery; -use std::{ - cmp::{self, Ordering}, - ops::Range, -}; +use std::ops::Range; use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); @@ -334,43 +331,23 @@ impl SearchBar { cx.notify(); } - fn select_match(&mut self, SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { - if let Some(mut index) = self.active_match_index { + fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { + if let Some(index) = self.active_match_index { if let Some(editor) = self.active_editor.as_ref() { editor.update(cx, |editor, cx| { - let newest_selection = editor.newest_anchor_selection().clone(); if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) { - let position = newest_selection.head(); - let buffer = editor.buffer().read(cx).read(cx); - if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { - if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } - } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { - if *direction == Direction::Next { - index = 0; - } - } else if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } else if *direction == Direction::Next { - if index == ranges.len() - 1 { - index = 0 - } else { - index += 1; - } - } - - let range_to_select = ranges[index].clone(); - drop(buffer); - editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); + let new_index = match_index_for_direction( + ranges, + &editor.newest_anchor_selection().head(), + index, + direction, + &editor.buffer().read(cx).read(cx), + ); + editor.select_ranges( + [ranges[new_index].clone()], + Some(Autoscroll::Fit), + cx, + ); } }); } @@ -518,30 +495,18 @@ impl SearchBar { } fn update_match_index(&mut self, cx: &mut ViewContext) { - self.active_match_index = self.active_match_index(cx); - cx.notify(); - } - - fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { - let editor = self.active_editor.as_ref()?; - let ranges = self.editors_with_matches.get(&editor.downgrade())?; - let editor = editor.read(cx); - let position = editor.newest_anchor_selection().head(); - if ranges.is_empty() { - None - } else { - let buffer = editor.buffer().read(cx).read(cx); - match ranges.binary_search_by(|probe| { - if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { - Ordering::Less - } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { - Ordering::Greater - } else { - Ordering::Equal - } - }) { - Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), - } + let new_index = self.active_editor.as_ref().and_then(|editor| { + let ranges = self.editors_with_matches.get(&editor.downgrade())?; + let editor = editor.read(cx); + active_match_index( + &ranges, + &editor.newest_anchor_selection().head(), + &editor.buffer().read(cx).read(cx), + ) + }); + if new_index != self.active_match_index { + self.active_match_index = new_index; + cx.notify(); } } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 449d9d3537045c032fb68d2e40b6a000d44217be..2a211a005ae363601380e34172938fc95783f5e9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,4 +1,7 @@ -use crate::{Direction, SearchOption, SelectMatch, ToggleSearchOption}; +use crate::{ + active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch, + ToggleSearchOption, +}; use collections::HashMap; use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; use gpui::{ @@ -10,7 +13,6 @@ use postage::watch; use project::{search::SearchQuery, Project}; use std::{ any::{Any, TypeId}, - cmp::{self, Ordering}, ops::Range, path::PathBuf, }; @@ -496,42 +498,17 @@ impl ProjectSearchView { } fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { - if let Some(mut index) = self.active_match_index { - let range_to_select = { - let model = self.model.read(cx); - let results_editor = self.results_editor.read(cx); - let buffer = results_editor.buffer().read(cx).read(cx); - let cursor = results_editor.newest_anchor_selection().head(); - let ranges = &model.match_ranges; - - if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() { - if direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } - } else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() { - if direction == Direction::Next { - index = 0; - } - } else if direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } else if direction == Direction::Next { - if index == ranges.len() - 1 { - index = 0 - } else { - index += 1; - } - }; - ranges[index].clone() - }; - + if let Some(index) = self.active_match_index { + let model = self.model.read(cx); + let results_editor = self.results_editor.read(cx); + let new_index = match_index_for_direction( + &model.match_ranges, + &results_editor.newest_anchor_selection().head(), + index, + direction, + &results_editor.buffer().read(cx).read(cx), + ); + let range_to_select = model.match_ranges[new_index].clone(); self.results_editor.update(cx, |editor, cx| { editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); }); @@ -595,30 +572,15 @@ impl ProjectSearchView { } fn update_match_index(&mut self, cx: &mut ViewContext) { - let match_ranges = self.model.read(cx).match_ranges.clone(); - if match_ranges.is_empty() { - self.active_match_index = None; - } else { - let results_editor = &self.results_editor.read(cx); - let cursor = results_editor.newest_anchor_selection().head(); - let new_index = { - let buffer = results_editor.buffer().read(cx).read(cx); - match match_ranges.binary_search_by(|probe| { - if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() { - Ordering::Less - } else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() { - Ordering::Greater - } else { - Ordering::Equal - } - }) { - Ok(i) | Err(i) => Some(cmp::min(i, match_ranges.len() - 1)), - } - }; - if self.active_match_index != new_index { - self.active_match_index = new_index; - cx.notify(); - } + let results_editor = self.results_editor.read(cx); + let new_index = active_match_index( + &self.model.read(cx).match_ranges, + &results_editor.newest_anchor_selection().head(), + &results_editor.buffer().read(cx).read(cx), + ); + if self.active_match_index != new_index { + self.active_match_index = new_index; + cx.notify(); } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 6e8e7e00f3289bd0f68dbc22e047da462e40d11a..a1e335a0354ffdaaf903ba30c3d32f5d24c90093 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,3 +1,9 @@ +use std::{ + cmp::{self, Ordering}, + ops::Range, +}; + +use editor::{Anchor, MultiBufferSnapshot}; use gpui::{action, MutableAppContext}; mod buffer_search; @@ -23,3 +29,60 @@ pub enum Direction { Prev, Next, } + +pub(crate) fn active_match_index( + ranges: &[Range], + cursor: &Anchor, + buffer: &MultiBufferSnapshot, +) -> Option { + if ranges.is_empty() { + None + } else { + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), + } + } +} + +pub(crate) fn match_index_for_direction( + ranges: &[Range], + cursor: &Anchor, + mut index: usize, + direction: Direction, + buffer: &MultiBufferSnapshot, +) -> usize { + if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() { + if direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + } else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() { + if direction == Direction::Next { + index = 0; + } + } else if direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } else if direction == Direction::Next { + if index == ranges.len() - 1 { + index = 0 + } else { + index += 1; + } + }; + index +} From 720056d0db4e9a16e21620d98b158a94be941139 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Feb 2022 11:10:22 +0100 Subject: [PATCH 59/65] Add unit test for project search --- Cargo.lock | 1 + crates/project/src/project.rs | 3 + crates/search/Cargo.toml | 1 + crates/search/src/project_search.rs | 156 +++++++++++++++++++++++++++- 4 files changed, 157 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dcd0896e4dbafe4a8535fd5ad00c0b7a62775b02..e8cf0a12f076aff68c4d12fa8b5e9a1f9df5e70f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4175,6 +4175,7 @@ dependencies = [ "log", "postage", "project", + "serde_json", "theme", "unindent", "util", diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 30c75443f12f115090701ace1eda7f906dffd929..b8363bf9dfa09210f1411673e8e7fcb329cd7472 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2063,6 +2063,9 @@ impl Project { let background = cx.background().clone(); let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + if path_count == 0 { + return Task::ready(Ok(Default::default())); + } let workers = background.num_cpus().min(path_count); let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); cx.background() diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index a6ed1e4cd9c8f73427c816cdbfbcc2cb9550b947..cee9f156e040d469ee8e716b953b3181f1d56237 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -22,5 +22,6 @@ postage = { version = "0.4.1", features = ["futures-traits"] } [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } unindent = "0.1" diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 2a211a005ae363601380e34172938fc95783f5e9..78031dd951ccf17b9cd25a12793d546588cf8f74 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -150,7 +150,7 @@ impl Item for ProjectSearch { nav_history: ItemNavHistory, cx: &mut gpui::ViewContext, ) -> Self::View { - ProjectSearchView::new(model, nav_history, workspace.settings(), cx) + ProjectSearchView::new(model, Some(nav_history), workspace.settings(), cx) } fn project_path(&self) -> Option { @@ -316,7 +316,12 @@ impl ItemView for ProjectSearchView { Self: Sized, { let model = self.model.update(cx, |model, cx| model.clone(cx)); - Some(Self::new(model, nav_history, self.settings.clone(), cx)) + Some(Self::new( + model, + Some(nav_history), + self.settings.clone(), + cx, + )) } fn navigate(&mut self, data: Box, cx: &mut ViewContext) { @@ -332,7 +337,7 @@ impl ItemView for ProjectSearchView { impl ProjectSearchView { fn new( model: ModelHandle, - nav_history: ItemNavHistory, + nav_history: Option, settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { @@ -370,7 +375,7 @@ impl ProjectSearchView { let results_editor = cx.add_view(|cx| { let mut editor = Editor::for_buffer(excerpts, Some(project), settings.clone(), cx); editor.set_searchable(false); - editor.set_nav_history(Some(nav_history)); + editor.set_nav_history(nav_history); editor }); cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) @@ -698,3 +703,146 @@ impl ProjectSearchView { .boxed() } } + +#[cfg(test)] +mod tests { + use super::*; + use editor::DisplayPoint; + use gpui::{color::Color, TestAppContext}; + use project::FakeFs; + use serde_json::json; + use std::sync::Arc; + + #[gpui::test] + async fn test_project_search(mut cx: TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); + theme.search.match_background = Color::red(); + let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); + let settings = watch::channel_with(settings).1; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), &mut cx); + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); + let search_view = cx.add_view(Default::default(), |cx| { + ProjectSearchView::new(search.clone(), None, settings, cx) + }); + + search_view.update(&mut cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(&Search, cx); + }); + search_view.next_notification(&cx).await; + search_view.update(&mut cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" + ); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.all_highlighted_ranges(cx)), + &[ + ( + DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), + Color::red() + ), + ( + DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), + Color::red() + ), + ( + DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), + Color::red() + ) + ] + ); + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + + search_view.select_match(&SelectMatch(Direction::Next), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + search_view.select_match(&SelectMatch(Direction::Next), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(&SelectMatch(Direction::Next), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + search_view.select_match(&SelectMatch(Direction::Prev), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(&SelectMatch(Direction::Prev), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + }); + } +} From abdfdcdabfc0942d76fd18a9951bb534bdb406f0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Feb 2022 14:20:25 +0100 Subject: [PATCH 60/65] Include buffer's deferred ops when computing `has_buffered_operations` --- crates/project/src/project.rs | 9 +++++++-- crates/server/src/rpc.rs | 26 ++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b8363bf9dfa09210f1411673e8e7fcb329cd7472..84b52e94739ded616e0bc268c342c68a766281de 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -405,12 +405,17 @@ impl Project { } #[cfg(any(test, feature = "test-support"))] - pub fn has_buffered_operations(&self) -> bool { + pub fn has_buffered_operations(&self, cx: &AppContext) -> bool { self.buffers_state .borrow() .open_buffers .values() - .any(|buffer| matches!(buffer, OpenBuffer::Loading(_))) + .any(|buffer| match buffer { + OpenBuffer::Loaded(buffer) => buffer + .upgrade(cx) + .map_or(false, |buffer| buffer.read(cx).deferred_ops_len() > 0), + OpenBuffer::Loading(_) => true, + }) } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7887cb763e0b31307f6572a09a0782d0f8e75542..189dbe1b7c00af38bed1f39632c1424a043a5e93 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4368,9 +4368,9 @@ mod tests { .project .as_ref() .unwrap() - .read_with(guest_cx, |project, _| { + .read_with(guest_cx, |project, cx| { assert!( - !project.has_buffered_operations(), + !project.has_buffered_operations(cx), "guest {} has buffered operations ", guest_id, ); @@ -4382,7 +4382,7 @@ mod tests { project .shared_buffer(guest_client.peer_id, buffer_id) .expect(&format!( - "host doest not have buffer for guest:{}, peer:{}, id:{}", + "host does not have buffer for guest:{}, peer:{}, id:{}", guest_id, guest_client.peer_id, buffer_id )) }); @@ -4867,9 +4867,19 @@ mod tests { project_path.1 ); let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) + .update(&mut cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + }) .await .unwrap(); + log::info!( + "Guest {}: path in worktree {:?} {:?} {:?} opened with buffer id {:?}", + guest_id, + project_path.0, + worktree_root_name, + project_path.1, + buffer.read_with(&cx, |buffer, _| buffer.remote_id()) + ); self.buffers.insert(buffer.clone()); buffer } else { @@ -4958,7 +4968,7 @@ mod tests { save.await; } } - 40..=45 => { + 40..=44 => { let prepare_rename = project.update(&mut cx, |project, cx| { log::info!( "Guest {}: preparing rename for buffer {:?}", @@ -4978,10 +4988,10 @@ mod tests { prepare_rename.await; } } - 46..=49 => { + 45..=49 => { let definitions = project.update(&mut cx, |project, cx| { log::info!( - "Guest {}: requesting defintions for buffer {:?}", + "Guest {}: requesting definitions for buffer {:?}", guest_id, buffer.read(cx).file().unwrap().full_path(cx) ); @@ -4999,7 +5009,7 @@ mod tests { .extend(definitions.await.into_iter().map(|loc| loc.buffer)); } } - 50..=55 => { + 50..=54 => { let highlights = project.update(&mut cx, |project, cx| { log::info!( "Guest {}: requesting highlights for buffer {:?}", From 5f7a759870dcc4dfa69aa82ab02992ad5f9958e0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Feb 2022 14:22:31 +0100 Subject: [PATCH 61/65] Add project-wide search to randomized integration test --- crates/server/src/rpc.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 189dbe1b7c00af38bed1f39632c1424a043a5e93..0b1f081d170c0a1b3a7b41ee3e03f5ccddad842a 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -5029,6 +5029,22 @@ mod tests { highlights.await; } } + 55..=59 => { + let search = project.update(&mut cx, |project, cx| { + let query = rng.lock().gen_range('a'..='z'); + log::info!("Guest {}: project-wide search {:?}", guest_id, query); + project.search(SearchQuery::text(query, false, false), cx) + }); + let search = cx + .background() + .spawn(async move { search.await.expect("search request failed") }); + if rng.lock().gen_bool(0.3) { + log::info!("Guest {}: detaching search request", guest_id); + search.detach(); + } else { + self.buffers.extend(search.await.into_keys()); + } + } _ => { buffer.update(&mut cx, |buffer, cx| { log::info!( From 1313ca8415014d8df40ef8e629521ad6934838e0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Feb 2022 14:26:10 +0100 Subject: [PATCH 62/65] Don't delete buffer state when calling `get_open_buffer` ...as we might be in the process of completing a request that could open a buffer. This was causing a failure in the randomized integration test. --- crates/project/src/project.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 84b52e94739ded616e0bc268c342c68a766281de..04d83e63615a04a8e6f3b4342b6f9568a63549d0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -811,24 +811,20 @@ impl Project { path: &ProjectPath, cx: &mut ModelContext, ) -> Option> { - let mut result = None; let worktree = self.worktree_for_id(path.worktree_id, cx)?; self.buffers_state - .borrow_mut() + .borrow() .open_buffers - .retain(|_, buffer| { - if let Some(buffer) = buffer.upgrade(cx) { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - if file.worktree == worktree && file.path() == &path.path { - result = Some(buffer); - } - } - true + .values() + .find_map(|buffer| { + let buffer = buffer.upgrade(cx)?; + let file = File::from_dyn(buffer.read(cx).file())?; + if file.worktree == worktree && file.path() == &path.path { + Some(buffer) } else { - false + None } - }); - result + }) } fn register_buffer( From 400a2fce585827c713a89cee870c165e25c4243a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Feb 2022 15:26:10 +0100 Subject: [PATCH 63/65] Don't use a bounded channel for signaling that buffers have been opened Blocking the sender could halt deserialization for no reason if nobody is consuming the notifications. --- crates/project/src/project.rs | 24 +++++++++++++----------- crates/server/src/rpc.rs | 7 ++++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 04d83e63615a04a8e6f3b4342b6f9568a63549d0..0a8e0913c78292029d65e5da007fd4586550a183 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -22,7 +22,7 @@ use language::{ }; use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; -use postage::{broadcast, prelude::Stream, sink::Sink, watch}; +use postage::watch; use rand::prelude::*; use search::SearchQuery; use sha2::{Digest, Sha256}; @@ -58,7 +58,7 @@ pub struct Project { collaborators: HashMap, subscriptions: Vec, language_servers_with_diagnostics_running: isize, - opened_buffer: broadcast::Sender<()>, + opened_buffer: (Rc>>, watch::Receiver<()>), loading_buffers: HashMap< ProjectPath, postage::watch::Receiver, Arc>>>, @@ -248,7 +248,7 @@ impl Project { move |this, mut cx| { async move { let mut status = rpc.status(); - while let Some(status) = status.recv().await { + while let Some(status) = status.next().await { if let Some(this) = this.upgrade(&cx) { let remote_id = if let client::Status::Connected { .. } = status { let response = rpc.request(proto::RegisterProject {}).await?; @@ -283,6 +283,7 @@ impl Project { } }); + let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); Self { worktrees: Default::default(), collaborators: Default::default(), @@ -295,7 +296,7 @@ impl Project { remote_id_rx, _maintain_remote_id_task, }, - opened_buffer: broadcast::channel(1).0, + opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), subscriptions: Vec::new(), active_entry: None, languages, @@ -336,11 +337,12 @@ impl Project { load_task.detach(); } + let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); let this = cx.add_model(|cx| { let mut this = Self { worktrees: Vec::new(), loading_buffers: Default::default(), - opened_buffer: broadcast::channel(1).0, + opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), shared_buffers: Default::default(), active_entry: None, collaborators: Default::default(), @@ -464,7 +466,7 @@ impl Project { if let Some(id) = id { return id; } - watch.recv().await; + watch.next().await; } } } @@ -661,7 +663,7 @@ impl Project { Err(error) => return Err(anyhow!("{}", error)), } } - loading_watch.recv().await; + loading_watch.next().await; } }) } @@ -3228,8 +3230,8 @@ impl Project { ) -> Task>> { let replica_id = self.replica_id(); - let mut opened_buffer_tx = self.opened_buffer.clone(); - let mut opened_buffer_rx = self.opened_buffer.subscribe(); + let opened_buffer_tx = self.opened_buffer.0.clone(); + let mut opened_buffer_rx = self.opened_buffer.1.clone(); cx.spawn(|this, mut cx| async move { match buffer.variant.ok_or_else(|| anyhow!("missing buffer"))? { proto::buffer::Variant::Id(id) => { @@ -3245,7 +3247,7 @@ impl Project { break buffer; } opened_buffer_rx - .recv() + .next() .await .ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?; }; @@ -3278,7 +3280,7 @@ impl Project { this.register_buffer(&buffer, buffer_worktree.as_ref(), cx) })?; - let _ = opened_buffer_tx.send(()).await; + *opened_buffer_tx.borrow_mut().borrow_mut() = (); Ok(buffer) } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 0b1f081d170c0a1b3a7b41ee3e03f5ccddad842a..17c67f3195f769fe76a3465d226c8177d13ef33e 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4371,7 +4371,7 @@ mod tests { .read_with(guest_cx, |project, cx| { assert!( !project.has_buffered_operations(cx), - "guest {} has buffered operations ", + "guest {} has buffered operations", guest_id, ); }); @@ -4779,8 +4779,9 @@ mod tests { } else { buffer.update(&mut cx, |buffer, cx| { log::info!( - "Host: updating buffer {:?}", - buffer.file().unwrap().full_path(cx) + "Host: updating buffer {:?} ({})", + buffer.file().unwrap().full_path(cx), + buffer.remote_id() ); buffer.randomly_edit(&mut *rng.lock(), 5, cx) }); From 2111ec04c887a45d07230c230e524cc77393e495 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Feb 2022 16:19:34 +0100 Subject: [PATCH 64/65] Make `SearchProject` a `Foreground` message However, the randomized integration test is still failing: ``` ITERATIONS=100000 SEED=3027 OPERATIONS=200 cargo test --release test_random --package=zed-server -- --nocapture ``` Co-Authored-By: Nathan Sobo --- crates/rpc/src/proto.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index c5da067f17dd1403631371237cdfd4afb9c6e132..1bfb392db0b7c037b8e4c906b8e61b4b80e9b15b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -190,8 +190,8 @@ messages!( (RegisterWorktree, Foreground), (RemoveProjectCollaborator, Foreground), (SaveBuffer, Foreground), - (SearchProject, Background), - (SearchProjectResponse, Background), + (SearchProject, Foreground), + (SearchProjectResponse, Foreground), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), From d1d324e42bbbc2728fd65ac965399969341a7255 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Feb 2022 11:36:43 -0800 Subject: [PATCH 65/65] Never close buffers when sharing Co-Authored-By: Antonio Scandurra --- crates/project/src/lsp_command.rs | 23 +- crates/project/src/project.rs | 364 +++++++++++------------------- crates/server/src/rpc.rs | 32 +-- 3 files changed, 140 insertions(+), 279 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index b091fe0bc390bf95f36d51596e81696790c01fa0..3b502fc8fafc5accfc977eee572c853a68701b48 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,4 +1,4 @@ -use crate::{BufferRequestHandle, DocumentHighlight, Location, Project, ProjectTransaction}; +use crate::{DocumentHighlight, Location, Project, ProjectTransaction}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use client::{proto, PeerId}; @@ -48,7 +48,6 @@ pub(crate) trait LspCommand: 'static + Sized { message: ::Response, project: ModelHandle, buffer: ModelHandle, - request_handle: BufferRequestHandle, cx: AsyncAppContext, ) -> Result; fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; @@ -162,7 +161,6 @@ impl LspCommand for PrepareRename { message: proto::PrepareRenameResponse, _: ModelHandle, buffer: ModelHandle, - _: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result>> { if message.can_rename { @@ -279,7 +277,6 @@ impl LspCommand for PerformRename { message: proto::PerformRenameResponse, project: ModelHandle, _: ModelHandle, - request_handle: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result { let message = message @@ -287,12 +284,7 @@ impl LspCommand for PerformRename { .ok_or_else(|| anyhow!("missing transaction"))?; project .update(&mut cx, |project, cx| { - project.deserialize_project_transaction( - message, - self.push_to_history, - request_handle, - cx, - ) + project.deserialize_project_transaction(message, self.push_to_history, cx) }) .await } @@ -435,16 +427,13 @@ impl LspCommand for GetDefinition { message: proto::GetDefinitionResponse, project: ModelHandle, _: ModelHandle, - request_handle: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result> { let mut locations = Vec::new(); for location in message.locations { let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; let buffer = project - .update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle.clone(), cx) - }) + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .await?; let start = location .start @@ -586,16 +575,13 @@ impl LspCommand for GetReferences { message: proto::GetReferencesResponse, project: ModelHandle, _: ModelHandle, - request_handle: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result> { let mut locations = Vec::new(); for location in message.locations { let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; let target_buffer = project - .update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle.clone(), cx) - }) + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .await?; let start = location .start @@ -720,7 +706,6 @@ impl LspCommand for GetDocumentHighlights { message: proto::GetDocumentHighlightsResponse, _: ModelHandle, _: ModelHandle, - _: BufferRequestHandle, _: AsyncAppContext, ) -> Result> { Ok(message diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0a8e0913c78292029d65e5da007fd4586550a183..507870341a1c92a727d8dc18f3e0f19b3d0c8060 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -59,24 +59,18 @@ pub struct Project { subscriptions: Vec, language_servers_with_diagnostics_running: isize, opened_buffer: (Rc>>, watch::Receiver<()>), + shared_buffers: HashMap>, loading_buffers: HashMap< ProjectPath, postage::watch::Receiver, Arc>>>, >, - buffers_state: Rc>, - shared_buffers: HashMap>>, + opened_buffers: HashMap, nonce: u128, } -#[derive(Default)] -struct ProjectBuffers { - buffer_request_count: usize, - preserved_buffers: Vec>, - open_buffers: HashMap, -} - enum OpenBuffer { - Loaded(WeakModelHandle), + Strong(ModelHandle), + Weak(WeakModelHandle), Loading(Vec), } @@ -155,8 +149,6 @@ pub struct Symbol { pub signature: [u8; 32], } -pub struct BufferRequestHandle(Rc>); - #[derive(Default)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); @@ -287,9 +279,9 @@ impl Project { Self { worktrees: Default::default(), collaborators: Default::default(), - buffers_state: Default::default(), - loading_buffers: Default::default(), + opened_buffers: Default::default(), shared_buffers: Default::default(), + loading_buffers: Default::default(), client_state: ProjectClientState::Local { is_shared: false, remote_id_tx, @@ -359,7 +351,7 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), - buffers_state: Default::default(), + opened_buffers: Default::default(), nonce: StdRng::from_entropy().gen(), }; for worktree in worktrees { @@ -399,25 +391,21 @@ impl Project { } #[cfg(any(test, feature = "test-support"))] - pub fn shared_buffer(&self, peer_id: PeerId, remote_id: u64) -> Option> { - self.shared_buffers - .get(&peer_id) - .and_then(|buffers| buffers.get(&remote_id)) - .cloned() + pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { + self.opened_buffers + .get(&remote_id) + .and_then(|buffer| buffer.upgrade(cx)) } #[cfg(any(test, feature = "test-support"))] - pub fn has_buffered_operations(&self, cx: &AppContext) -> bool { - self.buffers_state - .borrow() - .open_buffers - .values() - .any(|buffer| match buffer { - OpenBuffer::Loaded(buffer) => buffer - .upgrade(cx) - .map_or(false, |buffer| buffer.read(cx).deferred_ops_len() > 0), - OpenBuffer::Loading(_) => true, - }) + pub fn has_deferred_operations(&self, cx: &AppContext) -> bool { + self.opened_buffers.values().any(|buffer| match buffer { + OpenBuffer::Strong(buffer) => buffer.read(cx).deferred_ops_len() > 0, + OpenBuffer::Weak(buffer) => buffer + .upgrade(cx) + .map_or(false, |buffer| buffer.read(cx).deferred_ops_len() > 0), + OpenBuffer::Loading(_) => false, + }) } #[cfg(any(test, feature = "test-support"))] @@ -518,7 +506,7 @@ impl Project { pub fn share(&self, cx: &mut ModelContext) -> Task> { let rpc = self.client.clone(); cx.spawn(|this, mut cx| async move { - let project_id = this.update(&mut cx, |this, _| { + let project_id = this.update(&mut cx, |this, cx| { if let ProjectClientState::Local { is_shared, remote_id_rx, @@ -526,6 +514,17 @@ impl Project { } = &mut this.client_state { *is_shared = true; + for open_buffer in this.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(_) => {} + OpenBuffer::Weak(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + *open_buffer = OpenBuffer::Strong(buffer); + } + } + OpenBuffer::Loading(_) => unreachable!(), + } + } remote_id_rx .borrow() .ok_or_else(|| anyhow!("no project id")) @@ -535,6 +534,7 @@ impl Project { })?; rpc.request(proto::ShareProject { project_id }).await?; + let mut tasks = Vec::new(); this.update(&mut cx, |this, cx| { for worktree in this.worktrees(cx).collect::>() { @@ -563,6 +563,15 @@ impl Project { } = &mut this.client_state { *is_shared = false; + for open_buffer in this.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(buffer) => { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } + OpenBuffer::Weak(_) => {} + OpenBuffer::Loading(_) => unreachable!(), + } + } remote_id_rx .borrow() .ok_or_else(|| anyhow!("no project id")) @@ -702,7 +711,6 @@ impl Project { let remote_worktree_id = worktree.read(cx).id(); let path = path.clone(); let path_string = path.to_string_lossy().to_string(); - let request_handle = self.start_buffer_request(cx); cx.spawn(|this, mut cx| async move { let response = rpc .request(proto::OpenBuffer { @@ -712,11 +720,8 @@ impl Project { }) .await?; let buffer = response.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - - this.update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle, cx) - }) - .await + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await }) } @@ -757,10 +762,6 @@ impl Project { }) } - fn start_buffer_request(&self, cx: &AppContext) -> BufferRequestHandle { - BufferRequestHandle::new(self.buffers_state.clone(), cx) - } - pub fn save_buffer_as( &self, buffer: ModelHandle, @@ -789,20 +790,16 @@ impl Project { pub fn has_open_buffer(&self, path: impl Into, cx: &AppContext) -> bool { let path = path.into(); if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) { - self.buffers_state - .borrow() - .open_buffers - .iter() - .any(|(_, buffer)| { - if let Some(buffer) = buffer.upgrade(cx) { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - if file.worktree == worktree && file.path() == &path.path { - return true; - } + self.opened_buffers.iter().any(|(_, buffer)| { + if let Some(buffer) = buffer.upgrade(cx) { + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if file.worktree == worktree && file.path() == &path.path { + return true; } } - false - }) + } + false + }) } else { false } @@ -814,19 +811,15 @@ impl Project { cx: &mut ModelContext, ) -> Option> { let worktree = self.worktree_for_id(path.worktree_id, cx)?; - self.buffers_state - .borrow() - .open_buffers - .values() - .find_map(|buffer| { - let buffer = buffer.upgrade(cx)?; - let file = File::from_dyn(buffer.read(cx).file())?; - if file.worktree == worktree && file.path() == &path.path { - Some(buffer) - } else { - None - } - }) + self.opened_buffers.values().find_map(|buffer| { + let buffer = buffer.upgrade(cx)?; + let file = File::from_dyn(buffer.read(cx).file())?; + if file.worktree == worktree && file.path() == &path.path { + Some(buffer) + } else { + None + } + }) } fn register_buffer( @@ -836,17 +829,18 @@ impl Project { cx: &mut ModelContext, ) -> Result<()> { let remote_id = buffer.read(cx).remote_id(); - match self - .buffers_state - .borrow_mut() - .open_buffers - .insert(remote_id, OpenBuffer::Loaded(buffer.downgrade())) - { + let open_buffer = if self.is_remote() || self.is_shared() { + OpenBuffer::Strong(buffer.clone()) + } else { + OpenBuffer::Weak(buffer.downgrade()) + }; + + match self.opened_buffers.insert(remote_id, open_buffer) { None => {} Some(OpenBuffer::Loading(operations)) => { buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))? } - Some(OpenBuffer::Loaded(existing_handle)) => { + Some(OpenBuffer::Weak(existing_handle)) => { if existing_handle.upgrade(cx).is_some() { Err(anyhow!( "already registered buffer with remote id {}", @@ -854,6 +848,10 @@ impl Project { ))? } } + Some(OpenBuffer::Strong(_)) => Err(anyhow!( + "already registered buffer with remote id {}", + remote_id + ))?, } self.assign_language_to_buffer(&buffer, worktree, cx); Ok(()) @@ -1173,7 +1171,7 @@ impl Project { path: relative_path.into(), }; - for buffer in self.buffers_state.borrow().open_buffers.values() { + for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { if buffer .read(cx) @@ -1236,7 +1234,6 @@ impl Project { let remote_buffers = self.remote_id().zip(remote_buffers); let client = self.client.clone(); - let request_handle = self.start_buffer_request(cx); cx.spawn(|this, mut cx| async move { let mut project_transaction = ProjectTransaction::default(); @@ -1255,12 +1252,7 @@ impl Project { .ok_or_else(|| anyhow!("missing transaction"))?; project_transaction = this .update(&mut cx, |this, cx| { - this.deserialize_project_transaction( - response, - push_to_history, - request_handle, - cx, - ) + this.deserialize_project_transaction(response, push_to_history, cx) }) .await?; } @@ -1477,7 +1469,6 @@ impl Project { cx, ) } else if let Some(project_id) = self.remote_id() { - let request_handle = self.start_buffer_request(cx); let request = self.client.request(proto::OpenBufferForSymbol { project_id, symbol: Some(serialize_symbol(symbol)), @@ -1485,10 +1476,8 @@ impl Project { cx.spawn(|this, mut cx| async move { let response = request.await?; let buffer = response.buffer.ok_or_else(|| anyhow!("invalid buffer"))?; - this.update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle, cx) - }) - .await + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await }) } else { Task::ready(Err(anyhow!("project does not have a remote id"))) @@ -1867,7 +1856,6 @@ impl Project { }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); - let request_handle = self.start_buffer_request(cx); let request = proto::ApplyCodeAction { project_id, buffer_id: buffer_handle.read(cx).remote_id(), @@ -1880,12 +1868,7 @@ impl Project { .transaction .ok_or_else(|| anyhow!("missing transaction"))?; this.update(&mut cx, |this, cx| { - this.deserialize_project_transaction( - response, - push_to_history, - request_handle, - cx, - ) + this.deserialize_project_transaction(response, push_to_history, cx) }) .await }) @@ -2150,9 +2133,7 @@ impl Project { let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); let open_buffers = self - .buffers_state - .borrow() - .open_buffers + .opened_buffers .values() .filter_map(|b| b.upgrade(cx)) .collect::>(); @@ -2227,16 +2208,13 @@ impl Project { }) } else if let Some(project_id) = self.remote_id() { let request = self.client.request(query.to_proto(project_id)); - let request_handle = self.start_buffer_request(cx); cx.spawn(|this, mut cx| async move { let response = request.await?; let mut result = HashMap::default(); for location in response.locations { let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; let target_buffer = this - .update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle.clone(), cx) - }) + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .await?; let start = location .start @@ -2284,12 +2262,11 @@ impl Project { } } else if let Some(project_id) = self.remote_id() { let rpc = self.client.clone(); - let request_handle = self.start_buffer_request(cx); let message = request.to_proto(project_id, buffer); return cx.spawn(|this, cx| async move { let response = rpc.request(message).await?; request - .response_from_proto(response, this, buffer_handle, request_handle, cx) + .response_from_proto(response, this, buffer_handle, cx) .await }); } @@ -2417,7 +2394,7 @@ impl Project { ) { let snapshot = worktree_handle.read(cx).snapshot(); let mut buffers_to_delete = Vec::new(); - for (buffer_id, buffer) in &self.buffers_state.borrow().open_buffers { + for (buffer_id, buffer) in &self.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| { if let Some(old_file) = File::from_dyn(buffer.file()) { @@ -2474,10 +2451,7 @@ impl Project { } for buffer_id in buffers_to_delete { - self.buffers_state - .borrow_mut() - .open_buffers - .remove(&buffer_id); + self.opened_buffers.remove(&buffer_id); } } @@ -2604,8 +2578,7 @@ impl Project { .remove(&peer_id) .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))? .replica_id; - this.shared_buffers.remove(&peer_id); - for (_, buffer) in &this.buffers_state.borrow().open_buffers { + for (_, buffer) in &this.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } @@ -2731,24 +2704,16 @@ impl Project { .into_iter() .map(|op| language::proto::deserialize_operation(op)) .collect::, _>>()?; - let is_remote = this.is_remote(); - let mut buffers_state = this.buffers_state.borrow_mut(); - let buffer_request_count = buffers_state.buffer_request_count; - match buffers_state.open_buffers.entry(buffer_id) { + match this.opened_buffers.entry(buffer_id) { hash_map::Entry::Occupied(mut e) => match e.get_mut() { - OpenBuffer::Loaded(buffer) => { - if let Some(buffer) = buffer.upgrade(cx) { - buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?; - } else if is_remote && buffer_request_count > 0 { - e.insert(OpenBuffer::Loading(ops)); - } + OpenBuffer::Strong(buffer) => { + buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?; } OpenBuffer::Loading(operations) => operations.extend_from_slice(&ops), + _ => unreachable!(), }, hash_map::Entry::Vacant(e) => { - if is_remote && buffer_request_count > 0 { - e.insert(OpenBuffer::Loading(ops)); - } + e.insert(OpenBuffer::Loading(ops)); } } Ok(()) @@ -2770,9 +2735,7 @@ impl Project { .ok_or_else(|| anyhow!("no such worktree"))?; let file = File::from_proto(file, worktree.clone(), cx)?; let buffer = this - .buffers_state - .borrow_mut() - .open_buffers + .opened_buffers .get_mut(&buffer_id) .and_then(|b| b.upgrade(cx)) .ok_or_else(|| anyhow!("no such buffer"))?; @@ -2790,15 +2753,14 @@ impl Project { mut cx: AsyncAppContext, ) -> Result { let buffer_id = envelope.payload.buffer_id; - let sender_id = envelope.original_sender_id()?; let requested_version = envelope.payload.version.try_into()?; - let (project_id, buffer) = this.update(&mut cx, |this, _| { + let (project_id, buffer) = this.update(&mut cx, |this, cx| { let project_id = this.remote_id().ok_or_else(|| anyhow!("not connected"))?; let buffer = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned()) + .opened_buffers + .get(&buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; Ok::<_, anyhow::Error>((project_id, buffer)) })?; @@ -2827,16 +2789,12 @@ impl Project { ) -> Result { let sender_id = envelope.original_sender_id()?; let format = this.update(&mut cx, |this, cx| { - let shared_buffers = this - .shared_buffers - .get(&sender_id) - .ok_or_else(|| anyhow!("peer has no buffers"))?; let mut buffers = HashSet::default(); for buffer_id in &envelope.payload.buffer_ids { buffers.insert( - shared_buffers + this.opened_buffers .get(buffer_id) - .cloned() + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, ); } @@ -2858,17 +2816,16 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let sender_id = envelope.original_sender_id()?; let position = envelope .payload .position .and_then(language::proto::deserialize_anchor) .ok_or_else(|| anyhow!("invalid position"))?; let version = clock::Global::from(envelope.payload.version); - let buffer = this.read_with(&cx, |this, _| { - this.shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + let buffer = this.read_with(&cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; if !buffer @@ -2897,12 +2854,11 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let sender_id = envelope.original_sender_id()?; let apply_additional_edits = this.update(&mut cx, |this, cx| { let buffer = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; let language = buffer.read(cx).language(); let completion = language::proto::deserialize_completion( @@ -2931,7 +2887,6 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let sender_id = envelope.original_sender_id()?; let start = envelope .payload .start @@ -2942,10 +2897,10 @@ impl Project { .end .and_then(language::proto::deserialize_anchor) .ok_or_else(|| anyhow!("invalid end"))?; - let buffer = this.update(&mut cx, |this, _| { - this.shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; let version = buffer.read_with(&cx, |buffer, _| buffer.version()); @@ -2981,9 +2936,9 @@ impl Project { )?; let apply_code_action = this.update(&mut cx, |this, cx| { let buffer = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; Ok::<_, anyhow::Error>(this.apply_code_action(buffer, action, false, cx)) })?; @@ -3010,9 +2965,9 @@ impl Project { let (request, buffer_version) = this.update(&mut cx, |this, cx| { let buffer_id = T::buffer_id_from_proto(&envelope.payload); let buffer_handle = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned()) + .opened_buffers + .get(&buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; let buffer = buffer_handle.read(cx); let buffer_version = buffer.version(); @@ -3168,16 +3123,13 @@ impl Project { &mut self, message: proto::ProjectTransaction, push_to_history: bool, - request_handle: BufferRequestHandle, cx: &mut ModelContext, ) -> Task> { cx.spawn(|this, mut cx| async move { let mut project_transaction = ProjectTransaction::default(); for (buffer, transaction) in message.buffers.into_iter().zip(message.transactions) { let buffer = this - .update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle.clone(), cx) - }) + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .await?; let transaction = language::proto::deserialize_transaction(transaction)?; project_transaction.0.insert(buffer, transaction); @@ -3209,15 +3161,13 @@ impl Project { ) -> proto::Buffer { let buffer_id = buffer.read(cx).remote_id(); let shared_buffers = self.shared_buffers.entry(peer_id).or_default(); - match shared_buffers.entry(buffer_id) { - hash_map::Entry::Occupied(_) => proto::Buffer { + if shared_buffers.insert(buffer_id) { + proto::Buffer { + variant: Some(proto::buffer::Variant::State(buffer.read(cx).to_proto())), + } + } else { + proto::Buffer { variant: Some(proto::buffer::Variant::Id(buffer_id)), - }, - hash_map::Entry::Vacant(entry) => { - entry.insert(buffer.clone()); - proto::Buffer { - variant: Some(proto::buffer::Variant::State(buffer.read(cx).to_proto())), - } } } } @@ -3225,7 +3175,6 @@ impl Project { fn deserialize_buffer( &mut self, buffer: proto::Buffer, - request_handle: BufferRequestHandle, cx: &mut ModelContext, ) -> Task>> { let replica_id = self.replica_id(); @@ -3237,9 +3186,7 @@ impl Project { proto::buffer::Variant::Id(id) => { let buffer = loop { let buffer = this.read_with(&cx, |this, cx| { - this.buffers_state - .borrow() - .open_buffers + this.opened_buffers .get(&id) .and_then(|buffer| buffer.upgrade(cx)) }); @@ -3275,7 +3222,6 @@ impl Project { Buffer::from_proto(replica_id, buffer, buffer_file, cx).unwrap() }); - request_handle.preserve_buffer(buffer.clone()); this.update(&mut cx, |this, cx| { this.register_buffer(&buffer, buffer_worktree.as_ref(), cx) })?; @@ -3317,20 +3263,13 @@ impl Project { } async fn handle_close_buffer( - this: ModelHandle, - envelope: TypedEnvelope, + _: ModelHandle, + _: TypedEnvelope, _: Arc, - mut cx: AsyncAppContext, + _: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - if let Some(shared_buffers) = - this.shared_buffers.get_mut(&envelope.original_sender_id()?) - { - shared_buffers.remove(&envelope.payload.buffer_id); - cx.notify(); - } - Ok(()) - }) + // TODO: use this for following + Ok(()) } async fn handle_buffer_saved( @@ -3348,9 +3287,7 @@ impl Project { this.update(&mut cx, |this, cx| { let buffer = this - .buffers_state - .borrow() - .open_buffers + .opened_buffers .get(&envelope.payload.buffer_id) .and_then(|buffer| buffer.upgrade(cx)); if let Some(buffer) = buffer { @@ -3376,9 +3313,7 @@ impl Project { .into(); this.update(&mut cx, |this, cx| { let buffer = this - .buffers_state - .borrow() - .open_buffers + .opened_buffers .get(&payload.buffer_id) .and_then(|buffer| buffer.upgrade(cx)); if let Some(buffer) = buffer { @@ -3428,48 +3363,6 @@ impl Project { } } -impl BufferRequestHandle { - fn new(state: Rc>, cx: &AppContext) -> Self { - { - let state = &mut *state.borrow_mut(); - state.buffer_request_count += 1; - if state.buffer_request_count == 1 { - state.preserved_buffers.extend( - state - .open_buffers - .values() - .filter_map(|buffer| buffer.upgrade(cx)), - ) - } - } - Self(state) - } - - fn preserve_buffer(&self, buffer: ModelHandle) { - self.0.borrow_mut().preserved_buffers.push(buffer); - } -} - -impl Clone for BufferRequestHandle { - fn clone(&self) -> Self { - self.0.borrow_mut().buffer_request_count += 1; - Self(self.0.clone()) - } -} - -impl Drop for BufferRequestHandle { - fn drop(&mut self) { - let mut state = self.0.borrow_mut(); - state.buffer_request_count -= 1; - if state.buffer_request_count == 0 { - state.preserved_buffers.clear(); - state - .open_buffers - .retain(|_, buffer| matches!(buffer, OpenBuffer::Loaded(_))) - } - } -} - impl WorktreeHandle { pub fn upgrade(&self, cx: &AppContext) -> Option> { match self { @@ -3482,7 +3375,8 @@ impl WorktreeHandle { impl OpenBuffer { pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option> { match self { - OpenBuffer::Loaded(handle) => handle.upgrade(cx), + OpenBuffer::Strong(handle) => Some(handle.clone()), + OpenBuffer::Weak(handle) => handle.upgrade(cx), OpenBuffer::Loading(_) => None, } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 17c67f3195f769fe76a3465d226c8177d13ef33e..a9ebdceca09f403a81f8f21cc9a5562faf152329 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1165,14 +1165,6 @@ mod tests { // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) // .await; - // Close the buffer as client A, see that the buffer is closed. - cx_a.update(move |_| drop(buffer_a)); - project_a - .condition(&cx_a, |project, cx| { - !project.has_open_buffer((worktree_id, "b.txt"), cx) - }) - .await; - // Dropping the client B's project removes client B from client A's collaborators. cx_b.update(move |_| drop(project_b)); project_a @@ -2535,14 +2527,6 @@ mod tests { ); }); assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer); - - cx_b.update(|_| { - drop(definitions_1); - drop(definitions_2); - }); - project_b - .condition(&cx_b, |proj, cx| proj.worktrees(cx).count() == 1) - .await; } #[gpui::test(iterations = 10)] @@ -4370,21 +4354,19 @@ mod tests { .unwrap() .read_with(guest_cx, |project, cx| { assert!( - !project.has_buffered_operations(cx), - "guest {} has buffered operations", + !project.has_deferred_operations(cx), + "guest {} has deferred operations", guest_id, ); }); for guest_buffer in &guest_client.buffers { let buffer_id = guest_buffer.read_with(guest_cx, |buffer, _| buffer.remote_id()); - let host_buffer = host_project.read_with(&host_cx, |project, _| { - project - .shared_buffer(guest_client.peer_id, buffer_id) - .expect(&format!( - "host does not have buffer for guest:{}, peer:{}, id:{}", - guest_id, guest_client.peer_id, buffer_id - )) + let host_buffer = host_project.read_with(&host_cx, |project, cx| { + project.buffer_for_id(buffer_id, cx).expect(&format!( + "host does not have buffer for guest:{}, peer:{}, id:{}", + guest_id, guest_client.peer_id, buffer_id + )) }); assert_eq!( guest_buffer.read_with(guest_cx, |buffer, _| buffer.text()),