From 492805af9c54a65200c0238c8e61526f9ad25c0a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 3 Jan 2024 11:01:58 -0800 Subject: [PATCH] Remove 2 suffix for multi_buffer, outline, copilot Co-authored-by: Mikayla --- Cargo.lock | 112 +- Cargo.toml | 3 - crates/assistant2/Cargo.toml | 2 +- crates/breadcrumbs/Cargo.toml | 2 +- crates/copilot/Cargo.toml | 23 +- crates/copilot/src/copilot.rs | 348 +- crates/copilot/src/sign_in.rs | 392 +- crates/copilot2/Cargo.toml | 51 - crates/copilot2/src/copilot2.rs | 1253 ---- crates/copilot2/src/request.rs | 225 - crates/copilot2/src/sign_in.rs | 211 - crates/copilot_button/Cargo.toml | 2 +- crates/editor/Cargo.toml | 8 +- crates/editor2/src/editor_tests.rs | 8268 --------------------- crates/multi_buffer/Cargo.toml | 32 +- crates/multi_buffer/src/multi_buffer.rs | 188 +- crates/multi_buffer2/Cargo.toml | 78 - crates/multi_buffer2/src/anchor.rs | 138 - crates/multi_buffer2/src/multi_buffer2.rs | 5390 -------------- crates/outline/Cargo.toml | 16 +- crates/outline/src/outline.rs | 208 +- crates/outline2/Cargo.toml | 29 - crates/outline2/src/outline.rs | 309 - crates/project2/Cargo.toml | 2 +- crates/zed/Cargo.toml | 4 +- 25 files changed, 603 insertions(+), 16691 deletions(-) delete mode 100644 crates/copilot2/Cargo.toml delete mode 100644 crates/copilot2/src/copilot2.rs delete mode 100644 crates/copilot2/src/request.rs delete mode 100644 crates/copilot2/src/sign_in.rs delete mode 100644 crates/editor2/src/editor_tests.rs delete mode 100644 crates/multi_buffer2/Cargo.toml delete mode 100644 crates/multi_buffer2/src/anchor.rs delete mode 100644 crates/multi_buffer2/src/multi_buffer2.rs delete mode 100644 crates/outline2/Cargo.toml delete mode 100644 crates/outline2/src/outline.rs diff --git a/Cargo.lock b/Cargo.lock index ee0ef32dfb..0104dcf118 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,7 +385,7 @@ dependencies = [ "language2", "log", "menu2", - "multi_buffer2", + "multi_buffer", "ordered-float 2.10.0", "parking_lot 0.11.2", "project2", @@ -1093,7 +1093,7 @@ dependencies = [ "gpui2", "itertools 0.10.5", "language2", - "outline2", + "outline", "project2", "search", "settings2", @@ -1947,33 +1947,6 @@ dependencies = [ [[package]] name = "copilot" version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-tar", - "clock", - "collections", - "context_menu", - "fs", - "futures 0.3.28", - "gpui", - "language", - "log", - "lsp", - "node_runtime", - "parking_lot 0.11.2", - "rpc", - "serde", - "serde_derive", - "settings", - "smol", - "theme", - "util", -] - -[[package]] -name = "copilot2" -version = "0.1.0" dependencies = [ "anyhow", "async-compression", @@ -2003,7 +1976,7 @@ name = "copilot_button" version = "0.1.0" dependencies = [ "anyhow", - "copilot2", + "copilot", "editor", "fs2", "futures 0.3.28", @@ -2692,7 +2665,7 @@ dependencies = [ "clock", "collections", "convert_case 0.6.0", - "copilot2", + "copilot", "ctor", "db2", "env_logger", @@ -2706,7 +2679,7 @@ dependencies = [ "lazy_static", "log", "lsp2", - "multi_buffer2", + "multi_buffer", "ordered-float 2.10.0", "parking_lot 0.11.2", "postage", @@ -5071,55 +5044,6 @@ dependencies = [ [[package]] name = "multi_buffer" version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "client", - "clock", - "collections", - "context_menu", - "convert_case 0.6.0", - "copilot", - "ctor", - "env_logger", - "futures 0.3.28", - "git", - "gpui", - "indoc", - "itertools 0.10.5", - "language", - "lazy_static", - "log", - "lsp", - "ordered-float 2.10.0", - "parking_lot 0.11.2", - "postage", - "project", - "pulldown-cmark", - "rand 0.8.5", - "rich_text", - "schemars", - "serde", - "serde_derive", - "settings", - "smallvec", - "smol", - "snippet", - "sum_tree", - "text", - "theme", - "tree-sitter", - "tree-sitter-html", - "tree-sitter-rust", - "tree-sitter-typescript", - "unindent", - "util", - "workspace", -] - -[[package]] -name = "multi_buffer2" -version = "0.1.0" dependencies = [ "aho-corasick", "anyhow", @@ -5127,7 +5051,7 @@ dependencies = [ "clock", "collections", "convert_case 0.6.0", - "copilot2", + "copilot", "ctor", "env_logger", "futures 0.3.28", @@ -5742,24 +5666,6 @@ dependencies = [ [[package]] name = "outline" version = "0.1.0" -dependencies = [ - "editor", - "fuzzy", - "gpui", - "language", - "ordered-float 2.10.0", - "picker", - "postage", - "settings", - "smol", - "text", - "theme", - "workspace", -] - -[[package]] -name = "outline2" -version = "0.1.0" dependencies = [ "editor", "fuzzy2", @@ -6353,7 +6259,7 @@ dependencies = [ "client2", "clock", "collections", - "copilot2", + "copilot", "ctor", "db2", "env_logger", @@ -11013,7 +10919,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", - "copilot2", + "copilot", "copilot_button", "ctor", "db2", @@ -11045,7 +10951,7 @@ dependencies = [ "node_runtime", "notifications2", "num_cpus", - "outline2", + "outline", "parking_lot 0.11.2", "postage", "project2", diff --git a/Cargo.toml b/Cargo.toml index 325999066f..495f0221b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ members = [ "crates/component_test", "crates/context_menu", "crates/copilot", - "crates/copilot2", "crates/copilot_button", "crates/db", "crates/db2", @@ -64,12 +63,10 @@ members = [ "crates/menu", "crates/menu2", "crates/multi_buffer", - "crates/multi_buffer2", "crates/node_runtime", "crates/notifications", "crates/notifications2", "crates/outline", - "crates/outline2", "crates/picker", "crates/plugin", "crates/plugin_macros", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 6e8132f724..2a46b157f7 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -17,7 +17,7 @@ fs = { package = "fs2", path = "../fs2" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } menu = { package = "menu2", path = "../menu2" } -multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } +multi_buffer = { path = "../multi_buffer" } project = { package = "project2", path = "../project2" } search = { path = "../search" } semantic_index = { package = "semantic_index2", path = "../semantic_index2" } diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 4a6c5240af..a3ef6b170e 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -19,7 +19,7 @@ search = { path = "../search" } settings = { package = "settings2", path = "../settings2" } theme = { package = "theme2", path = "../theme2" } workspace = { package = "workspace2", path = "../workspace2" } -outline = { package = "outline2", path = "../outline2" } +outline = { path = "../outline" } itertools = "0.10" [dev-dependencies] diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 985e784367..c3c8759baa 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -20,14 +20,15 @@ test-support = [ [dependencies] collections = { path = "../collections" } -context_menu = { path = "../context_menu" } -gpui = { path = "../gpui" } -language = { path = "../language" } -settings = { path = "../settings" } -theme = { path = "../theme" } -lsp = { path = "../lsp" } +# context_menu = { path = "../context_menu" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } +ui = { package = "ui2", path = "../ui2" } async-compression.workspace = true async-tar = "0.4.2" anyhow.workspace = true @@ -42,9 +43,9 @@ parking_lot.workspace = true clock = { path = "../clock" } collections = { path = "../collections", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } -language = { path = "../language", features = ["test-support"] } -lsp = { path = "../lsp", features = ["test-support"] } -rpc = { path = "../rpc", features = ["test-support"] } -settings = { path = "../settings", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 698e118fc3..658eb3451f 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,13 +1,14 @@ pub mod request; mod sign_in; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use collections::{HashMap, HashSet}; use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; use gpui::{ - actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, + actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Model, + ModelContext, Task, WeakModel, }; use language::{ language_settings::{all_language_settings, language_settings}, @@ -21,24 +22,27 @@ use request::StatusNotification; use settings::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ + any::TypeId, ffi::OsString, mem, ops::Range, path::{Path, PathBuf}, - pin::Pin, sync::Arc, }; use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; -const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; -actions!(copilot_auth, [SignIn, SignOut]); - -const COPILOT_NAMESPACE: &'static str = "copilot"; actions!( copilot, - [Suggest, NextSuggestion, PreviousSuggestion, Reinstall] + [ + Suggest, + NextSuggestion, + PreviousSuggestion, + Reinstall, + SignIn, + SignOut + ] ); pub fn init( @@ -47,50 +51,69 @@ pub fn init( node_runtime: Arc, cx: &mut AppContext, ) { - let copilot = cx.add_model({ + let copilot = cx.new_model({ let node_runtime = node_runtime.clone(); move |cx| Copilot::start(new_server_id, http, node_runtime, cx) }); cx.set_global(copilot.clone()); - cx.observe(&copilot, |handle, cx| { + let copilot_action_types = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + let copilot_auth_action_types = [TypeId::of::()]; + let copilot_no_auth_action_types = [TypeId::of::()]; let status = handle.read(cx).status(); - cx.update_default_global::(move |filter, _cx| { - match status { - Status::Disabled => { - filter.hidden_namespaces.insert(COPILOT_NAMESPACE); - filter.hidden_namespaces.insert(COPILOT_AUTH_NAMESPACE); - } - Status::Authorized => { - filter.hidden_namespaces.remove(COPILOT_NAMESPACE); - filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE); - } - _ => { - filter.hidden_namespaces.insert(COPILOT_NAMESPACE); - filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE); + let filter = cx.default_global::(); + + match status { + Status::Disabled => { + filter.hidden_action_types.extend(copilot_action_types); + filter.hidden_action_types.extend(copilot_auth_action_types); + filter + .hidden_action_types + .extend(copilot_no_auth_action_types); + } + Status::Authorized => { + filter + .hidden_action_types + .extend(copilot_no_auth_action_types); + for type_id in copilot_action_types + .iter() + .chain(&copilot_auth_action_types) + { + filter.hidden_action_types.remove(type_id); } } - }); + _ => { + filter.hidden_action_types.extend(copilot_action_types); + filter.hidden_action_types.extend(copilot_auth_action_types); + for type_id in &copilot_no_auth_action_types { + filter.hidden_action_types.remove(type_id); + } + } + } }) .detach(); sign_in::init(cx); - cx.add_global_action(|_: &SignIn, cx| { + cx.on_action(|_: &SignIn, cx| { if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.sign_in(cx)) .detach_and_log_err(cx); } }); - cx.add_global_action(|_: &SignOut, cx| { + cx.on_action(|_: &SignOut, cx| { if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) .detach_and_log_err(cx); } }); - - cx.add_global_action(|_: &Reinstall, cx| { + cx.on_action(|_: &Reinstall, cx| { if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.reinstall(cx)) @@ -133,7 +156,7 @@ struct RunningCopilotServer { name: LanguageServerName, lsp: Arc, sign_in_status: SignInStatus, - registered_buffers: HashMap, + registered_buffers: HashMap, } #[derive(Clone, Debug)] @@ -180,7 +203,7 @@ struct RegisteredBuffer { impl RegisteredBuffer { fn report_changes( &mut self, - buffer: &ModelHandle, + buffer: &Model, cx: &mut ModelContext, ) -> oneshot::Receiver<(i32, BufferSnapshot)> { let (done_tx, done_rx) = oneshot::channel(); @@ -189,23 +212,23 @@ impl RegisteredBuffer { let _ = done_tx.send((self.snapshot_version, self.snapshot.clone())); } else { let buffer = buffer.downgrade(); - let id = buffer.id(); + let id = buffer.entity_id(); let prev_pending_change = mem::replace(&mut self.pending_buffer_change, Task::ready(None)); - self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move { + self.pending_buffer_change = cx.spawn(move |copilot, mut cx| async move { prev_pending_change.await; - let old_version = copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| { - let server = copilot.server.as_authenticated().log_err()?; - let buffer = server.registered_buffers.get_mut(&id)?; - Some(buffer.snapshot.version.clone()) - })?; - let new_snapshot = buffer - .upgrade(&cx)? - .read_with(&cx, |buffer, _| buffer.snapshot()); + let old_version = copilot + .update(&mut cx, |copilot, _| { + let server = copilot.server.as_authenticated().log_err()?; + let buffer = server.registered_buffers.get_mut(&id)?; + Some(buffer.snapshot.version.clone()) + }) + .ok()??; + let new_snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot()).ok()?; let content_changes = cx - .background() + .background_executor() .spawn({ let new_snapshot = new_snapshot.clone(); async move { @@ -231,28 +254,30 @@ impl RegisteredBuffer { }) .await; - copilot.upgrade(&cx)?.update(&mut cx, |copilot, _| { - let server = copilot.server.as_authenticated().log_err()?; - let buffer = server.registered_buffers.get_mut(&id)?; - if !content_changes.is_empty() { - buffer.snapshot_version += 1; - buffer.snapshot = new_snapshot; - server - .lsp - .notify::( - lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - buffer.uri.clone(), - buffer.snapshot_version, - ), - content_changes, - }, - ) - .log_err(); - } - let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone())); - Some(()) - })?; + copilot + .update(&mut cx, |copilot, _| { + let server = copilot.server.as_authenticated().log_err()?; + let buffer = server.registered_buffers.get_mut(&id)?; + if !content_changes.is_empty() { + buffer.snapshot_version += 1; + buffer.snapshot = new_snapshot; + server + .lsp + .notify::( + lsp::DidChangeTextDocumentParams { + text_document: lsp::VersionedTextDocumentIdentifier::new( + buffer.uri.clone(), + buffer.snapshot_version, + ), + content_changes, + }, + ) + .log_err(); + } + let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone())); + Some(()) + }) + .ok()?; Some(()) }); @@ -273,36 +298,21 @@ pub struct Copilot { http: Arc, node_runtime: Arc, server: CopilotServer, - buffers: HashSet>, + buffers: HashSet>, server_id: LanguageServerId, + _subscription: gpui::Subscription, } pub enum Event { CopilotLanguageServerStarted, } -impl Entity for Copilot { - type Event = Event; - - fn app_will_quit( - &mut self, - _: &mut AppContext, - ) -> Option>>> { - match mem::replace(&mut self.server, CopilotServer::Disabled) { - CopilotServer::Running(server) => Some(Box::pin(async move { - if let Some(shutdown) = server.lsp.shutdown() { - shutdown.await; - } - })), - _ => None, - } - } -} +impl EventEmitter for Copilot {} impl Copilot { - pub fn global(cx: &AppContext) -> Option> { - if cx.has_global::>() { - Some(cx.global::>().clone()) + pub fn global(cx: &AppContext) -> Option> { + if cx.has_global::>() { + Some(cx.global::>().clone()) } else { None } @@ -320,24 +330,39 @@ impl Copilot { node_runtime, server: CopilotServer::Disabled, buffers: Default::default(), + _subscription: cx.on_app_quit(Self::shutdown_language_server), }; this.enable_or_disable_copilot(cx); - cx.observe_global::(move |this, cx| this.enable_or_disable_copilot(cx)) + cx.observe_global::(move |this, cx| this.enable_or_disable_copilot(cx)) .detach(); this } - fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { + fn shutdown_language_server( + &mut self, + _cx: &mut ModelContext, + ) -> impl Future { + let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) { + CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })), + _ => None, + }; + + async move { + if let Some(shutdown) = shutdown { + shutdown.await; + } + } + } + + fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { let server_id = self.server_id; let http = self.http.clone(); let node_runtime = self.node_runtime.clone(); if all_language_settings(None, cx).copilot_enabled(None, None) { if matches!(self.server, CopilotServer::Disabled) { let start_task = cx - .spawn({ - move |this, cx| { - Self::start_language_server(server_id, http, node_runtime, this, cx) - } + .spawn(move |this, cx| { + Self::start_language_server(server_id, http, node_runtime, this, cx) }) .shared(); self.server = CopilotServer::Starting { task: start_task }; @@ -350,14 +375,14 @@ impl Copilot { } #[cfg(any(test, feature = "test-support"))] - pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle, lsp::FakeLanguageServer) { + pub fn fake(cx: &mut gpui::TestAppContext) -> (Model, lsp::FakeLanguageServer) { use node_runtime::FakeNodeRuntime; let (server, fake_server) = LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let node_runtime = FakeNodeRuntime::new(); - let this = cx.add_model(|_| Self { + let this = cx.new_model(|cx| Self { server_id: LanguageServerId(0), http: http.clone(), node_runtime, @@ -367,6 +392,7 @@ impl Copilot { sign_in_status: SignInStatus::Authorized, registered_buffers: Default::default(), }), + _subscription: cx.on_app_quit(Self::shutdown_language_server), buffers: Default::default(), }); (this, fake_server) @@ -376,7 +402,7 @@ impl Copilot { new_server_id: LanguageServerId, http: Arc, node_runtime: Arc, - this: ModelHandle, + this: WeakModel, mut cx: AsyncAppContext, ) -> impl Future { async move { @@ -448,6 +474,7 @@ impl Copilot { } } }) + .ok(); } } @@ -489,7 +516,7 @@ impl Copilot { cx.notify(); } } - }); + })?; let response = lsp .request::( request::SignInConfirmParams { @@ -515,7 +542,7 @@ impl Copilot { ); Err(Arc::new(error)) } - }) + })? }) .shared(); server.sign_in_status = SignInStatus::SigningIn { @@ -527,7 +554,7 @@ impl Copilot { } }; - cx.foreground() + cx.background_executor() .spawn(task.map_err(|err| anyhow!("{:?}", err))) } else { // If we're downloading, wait until download is finished @@ -540,7 +567,7 @@ impl Copilot { self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx); if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server { let server = server.clone(); - cx.background().spawn(async move { + cx.background_executor().spawn(async move { server .request::(request::SignOutParams {}) .await?; @@ -570,7 +597,7 @@ impl Copilot { cx.notify(); - cx.foreground().spawn(start_task) + cx.background_executor().spawn(start_task) } pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc)> { @@ -581,7 +608,7 @@ impl Copilot { } } - pub fn register_buffer(&mut self, buffer: &ModelHandle, cx: &mut ModelContext) { + pub fn register_buffer(&mut self, buffer: &Model, cx: &mut ModelContext) { let weak_buffer = buffer.downgrade(); self.buffers.insert(weak_buffer.clone()); @@ -596,51 +623,54 @@ impl Copilot { return; } - registered_buffers.entry(buffer.id()).or_insert_with(|| { - let uri: lsp::Url = uri_for_buffer(buffer, cx); - let language_id = id_for_language(buffer.read(cx).language()); - let snapshot = buffer.read(cx).snapshot(); - server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem { - uri: uri.clone(), - language_id: language_id.clone(), - version: 0, - text: snapshot.text(), + registered_buffers + .entry(buffer.entity_id()) + .or_insert_with(|| { + let uri: lsp::Url = uri_for_buffer(buffer, cx); + let language_id = id_for_language(buffer.read(cx).language()); + let snapshot = buffer.read(cx).snapshot(); + server + .notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem { + uri: uri.clone(), + language_id: language_id.clone(), + version: 0, + text: snapshot.text(), + }, }, - }, - ) - .log_err(); + ) + .log_err(); - RegisteredBuffer { - uri, - language_id, - snapshot, - snapshot_version: 0, - pending_buffer_change: Task::ready(Some(())), - _subscriptions: [ - cx.subscribe(buffer, |this, buffer, event, cx| { - this.handle_buffer_event(buffer, event, cx).log_err(); - }), - cx.observe_release(buffer, move |this, _buffer, _cx| { - this.buffers.remove(&weak_buffer); - this.unregister_buffer(&weak_buffer); - }), - ], - } - }); + RegisteredBuffer { + uri, + language_id, + snapshot, + snapshot_version: 0, + pending_buffer_change: Task::ready(Some(())), + _subscriptions: [ + cx.subscribe(buffer, |this, buffer, event, cx| { + this.handle_buffer_event(buffer, event, cx).log_err(); + }), + cx.observe_release(buffer, move |this, _buffer, _cx| { + this.buffers.remove(&weak_buffer); + this.unregister_buffer(&weak_buffer); + }), + ], + } + }); } } fn handle_buffer_event( &mut self, - buffer: ModelHandle, + buffer: Model, event: &language::Event, cx: &mut ModelContext, ) -> Result<()> { if let Ok(server) = self.server.as_running() { - if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) { + if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) + { match event { language::Event::Edited => { let _ = registered_buffer.report_changes(&buffer, cx); @@ -694,9 +724,9 @@ impl Copilot { Ok(()) } - fn unregister_buffer(&mut self, buffer: &WeakModelHandle) { + fn unregister_buffer(&mut self, buffer: &WeakModel) { if let Ok(server) = self.server.as_running() { - if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) { + if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) { server .lsp .notify::( @@ -711,7 +741,7 @@ impl Copilot { pub fn completions( &mut self, - buffer: &ModelHandle, + buffer: &Model, position: T, cx: &mut ModelContext, ) -> Task>> @@ -723,7 +753,7 @@ impl Copilot { pub fn completions_cycling( &mut self, - buffer: &ModelHandle, + buffer: &Model, position: T, cx: &mut ModelContext, ) -> Task>> @@ -748,7 +778,7 @@ impl Copilot { .request::(request::NotifyAcceptedParams { uuid: completion.uuid.clone(), }); - cx.background().spawn(async move { + cx.background_executor().spawn(async move { request.await?; Ok(()) }) @@ -772,7 +802,7 @@ impl Copilot { .map(|completion| completion.uuid.clone()) .collect(), }); - cx.background().spawn(async move { + cx.background_executor().spawn(async move { request.await?; Ok(()) }) @@ -780,7 +810,7 @@ impl Copilot { fn request_completions( &mut self, - buffer: &ModelHandle, + buffer: &Model, position: T, cx: &mut ModelContext, ) -> Task>> @@ -799,7 +829,10 @@ impl Copilot { Err(error) => return Task::ready(Err(error)), }; let lsp = server.lsp.clone(); - let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap(); + let registered_buffer = server + .registered_buffers + .get_mut(&buffer.entity_id()) + .unwrap(); let snapshot = registered_buffer.report_changes(buffer, cx); let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); @@ -812,7 +845,7 @@ impl Copilot { .map(|file| file.path().to_path_buf()) .unwrap_or_default(); - cx.foreground().spawn(async move { + cx.background_executor().spawn(async move { let (version, snapshot) = snapshot.await?; let result = lsp .request::(request::GetCompletionsParams { @@ -869,7 +902,7 @@ impl Copilot { lsp_status: request::SignInStatus, cx: &mut ModelContext, ) { - self.buffers.retain(|buffer| buffer.is_upgradable(cx)); + self.buffers.retain(|buffer| buffer.is_upgradable()); if let Ok(server) = self.server.as_running() { match lsp_status { @@ -878,20 +911,20 @@ impl Copilot { | request::SignInStatus::AlreadySignedIn { .. } => { server.sign_in_status = SignInStatus::Authorized; for buffer in self.buffers.iter().cloned().collect::>() { - if let Some(buffer) = buffer.upgrade(cx) { + if let Some(buffer) = buffer.upgrade() { self.register_buffer(&buffer, cx); } } } request::SignInStatus::NotAuthorized { .. } => { server.sign_in_status = SignInStatus::Unauthorized; - for buffer in self.buffers.iter().copied().collect::>() { + for buffer in self.buffers.iter().cloned().collect::>() { self.unregister_buffer(&buffer); } } request::SignInStatus::NotSignedIn => { server.sign_in_status = SignInStatus::SignedOut; - for buffer in self.buffers.iter().copied().collect::>() { + for buffer in self.buffers.iter().cloned().collect::>() { self.unregister_buffer(&buffer); } } @@ -911,11 +944,11 @@ fn id_for_language(language: Option<&Arc>) -> String { } } -fn uri_for_buffer(buffer: &ModelHandle, cx: &AppContext) -> lsp::Url { +fn uri_for_buffer(buffer: &Model, cx: &AppContext) -> lsp::Url { if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { lsp::Url::from_file_path(file.abs_path(cx)).unwrap() } else { - format!("buffer://{}", buffer.id()).parse().unwrap() + format!("buffer://{}", buffer.entity_id()).parse().unwrap() } } @@ -994,15 +1027,16 @@ async fn get_copilot_lsp(http: Arc) -> anyhow::Result { #[cfg(test)] mod tests { use super::*; - use gpui::{executor::Deterministic, TestAppContext}; + use gpui::TestAppContext; #[gpui::test(iterations = 10)] - async fn test_buffer_management(deterministic: Arc, cx: &mut TestAppContext) { - deterministic.forbid_parking(); + async fn test_buffer_management(cx: &mut TestAppContext) { let (copilot, mut lsp) = Copilot::fake(cx); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello")); - let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap(); + let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello")); + let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64()) + .parse() + .unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); assert_eq!( lsp.receive_notification::() @@ -1017,8 +1051,10 @@ mod tests { } ); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye")); - let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap(); + let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye")); + let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64()) + .parse() + .unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); assert_eq!( lsp.receive_notification::() @@ -1114,6 +1150,7 @@ mod tests { .update(cx, |copilot, cx| copilot.sign_in(cx)) .await .unwrap(); + assert_eq!( lsp.receive_notification::() .await, @@ -1138,7 +1175,6 @@ mod tests { ), } ); - // Dropping a buffer causes it to be closed on the LSP side as well. cx.update(|_| drop(buffer_2)); assert_eq!( diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index ac3b81f0c6..ba5dbe0e31 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,18 +1,11 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - elements::*, - geometry::rect::RectF, - platform::{WindowBounds, WindowKind, WindowOptions}, - AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, - WindowHandle, + div, size, AppContext, Bounds, ClipboardItem, Element, GlobalPixels, InteractiveElement, + IntoElement, ParentElement, Point, Render, Styled, ViewContext, VisualContext, WindowBounds, + WindowHandle, WindowKind, WindowOptions, }; -use theme::ui::modal; - -#[derive(PartialEq, Eq, Debug, Clone)] -struct CopyUserCode; - -#[derive(PartialEq, Eq, Debug, Clone)] -struct OpenGithub; +use theme::ActiveTheme; +use ui::{prelude::*, Button, Icon, IconElement, Label}; const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; @@ -26,14 +19,11 @@ pub fn init(cx: &mut AppContext) { crate::Status::SigningIn { prompt } => { if let Some(window) = verification_window.as_mut() { let updated = window - .root(cx) - .map(|root| { - root.update(cx, |verification, cx| { - verification.set_status(status.clone(), cx); - cx.activate_window(); - }) + .update(cx, |verification, cx| { + verification.set_status(status.clone(), cx); + cx.activate_window(); }) - .is_some(); + .is_ok(); if !updated { verification_window = Some(create_copilot_auth_window(cx, &status)); } @@ -43,18 +33,20 @@ pub fn init(cx: &mut AppContext) { } Status::Authorized | Status::Unauthorized => { if let Some(window) = verification_window.as_ref() { - if let Some(verification) = window.root(cx) { - verification.update(cx, |verification, cx| { + window + .update(cx, |verification, cx| { verification.set_status(status, cx); - cx.platform().activate(true); + cx.activate(true); cx.activate_window(); - }); - } + }) + .ok(); } } _ => { if let Some(code_verification) = verification_window.take() { - code_verification.update(cx, |cx| cx.remove_window()); + code_verification + .update(cx, |_, cx| cx.remove_window()) + .ok(); } } } @@ -67,20 +59,21 @@ fn create_copilot_auth_window( cx: &mut AppContext, status: &Status, ) -> WindowHandle { - let window_size = theme::current(cx).copilot.modal.dimensions(); + let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.)); let window_options = WindowOptions { - bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)), + bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)), titlebar: None, center: true, focus: true, show: true, - kind: WindowKind::Normal, + kind: WindowKind::PopUp, is_movable: true, - screen: None, + display_id: None, }; - cx.add_window(window_options, |_cx| { - CopilotCodeVerification::new(status.clone()) - }) + let window = cx.open_window(window_options, |cx| { + cx.new_view(|_| CopilotCodeVerification::new(status.clone())) + }); + window } pub struct CopilotCodeVerification { @@ -103,273 +96,116 @@ impl CopilotCodeVerification { fn render_device_code( data: &PromptUserDeviceFlow, - style: &theme::Copilot, cx: &mut ViewContext, - ) -> impl Element { + ) -> impl IntoElement { let copied = cx .read_from_clipboard() .map(|item| item.text() == &data.user_code) .unwrap_or(false); - - let device_code_style = &style.auth.prompting.device_code; - - MouseEventHandler::new::(0, cx, |state, _cx| { - Flex::row() - .with_child( - Label::new(data.user_code.clone(), device_code_style.text.clone()) - .aligned() - .contained() - .with_style(device_code_style.left_container) - .constrained() - .with_width(device_code_style.left), - ) - .with_child( - Label::new( - if copied { "Copied!" } else { "Copy" }, - device_code_style.cta.style_for(state).text.clone(), - ) - .aligned() - .contained() - .with_style(*device_code_style.right_container.style_for(state)) - .constrained() - .with_width(device_code_style.right), - ) - .contained() - .with_style(device_code_style.cta.style_for(state).container) - }) - .on_click(gpui::platform::MouseButton::Left, { - let user_code = data.user_code.clone(); - move |_, _, cx| { - cx.platform() - .write_to_clipboard(ClipboardItem::new(user_code.clone())); - cx.notify(); - } - }) - .with_cursor_style(gpui::platform::CursorStyle::PointingHand) + h_stack() + .cursor_pointer() + .justify_between() + .on_mouse_down(gpui::MouseButton::Left, { + let user_code = data.user_code.clone(); + move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .child(Label::new(data.user_code.clone())) + .child(div()) + .child(Label::new(if copied { "Copied!" } else { "Copy" })) } fn render_prompting_modal( connect_clicked: bool, data: &PromptUserDeviceFlow, - style: &theme::Copilot, cx: &mut ViewContext, - ) -> AnyElement { - enum ConnectButton {} - - Flex::column() - .with_child( - Flex::column() - .with_children([ - Label::new( - "Enable Copilot by connecting", - style.auth.prompting.subheading.text.clone(), - ) - .aligned(), - Label::new( - "your existing license.", - style.auth.prompting.subheading.text.clone(), - ) - .aligned(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.prompting.subheading.container), + ) -> impl Element { + let connect_button_label = if connect_clicked { + "Waiting for connection..." + } else { + "Connect to Github" + }; + v_stack() + .flex_1() + .items_center() + .justify_between() + .w_full() + .child(Label::new( + "Enable Copilot by connecting your existing license", + )) + .child(Self::render_device_code(data, cx)) + .child( + Label::new("Paste this code into GitHub after clicking the button below.") + .size(ui::LabelSize::Small), ) - .with_child(Self::render_device_code(data, &style, cx)) - .with_child( - Flex::column() - .with_children([ - Label::new( - "Paste this code into GitHub after", - style.auth.prompting.hint.text.clone(), - ) - .aligned(), - Label::new( - "clicking the button below.", - style.auth.prompting.hint.text.clone(), - ) - .aligned(), - ]) - .align_children_center() - .contained() - .with_style(style.auth.prompting.hint.container.clone()), - ) - .with_child(theme::ui::cta_button::( - if connect_clicked { - "Waiting for connection..." - } else { - "Connect to GitHub" - }, - style.auth.content_width, - &style.auth.cta_button, - cx, - { + .child( + Button::new("connect-button", connect_button_label).on_click({ let verification_uri = data.verification_uri.clone(); - move |_, verification, cx| { - cx.platform().open_url(&verification_uri); - verification.connect_clicked = true; - } - }, + cx.listener(move |this, _, cx| { + cx.open_url(&verification_uri); + this.connect_clicked = true; + }) + }), + ) + } + fn render_enabled_modal() -> impl Element { + v_stack() + .child(Label::new("Copilot Enabled!")) + .child(Label::new( + "You can update your settings or sign out from the Copilot menu in the status bar.", )) - .align_children_center() - .into_any() + .child( + Button::new("copilot-enabled-done-button", "Done") + .on_click(|_, cx| cx.remove_window()), + ) } - fn render_enabled_modal( - style: &theme::Copilot, - cx: &mut ViewContext, - ) -> AnyElement { - enum DoneButton {} - - let enabled_style = &style.auth.authorized; - Flex::column() - .with_child( - Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) - .contained() - .with_style(enabled_style.subheading.container) - .aligned(), - ) - .with_child( - Flex::column() - .with_children([ - Label::new( - "You can update your settings or", - enabled_style.hint.text.clone(), - ) - .aligned(), - Label::new( - "sign out from the Copilot menu in", - enabled_style.hint.text.clone(), - ) - .aligned(), - Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(), - ]) - .align_children_center() - .contained() - .with_style(enabled_style.hint.container), - ) - .with_child(theme::ui::cta_button::( - "Done", - style.auth.content_width, - &style.auth.cta_button, - cx, - |_, _, cx| cx.remove_window(), + fn render_unauthorized_modal() -> impl Element { + v_stack() + .child(Label::new( + "Enable Copilot by connecting your existing license.", )) - .align_children_center() - .into_any() - } - - fn render_unauthorized_modal( - style: &theme::Copilot, - cx: &mut ViewContext, - ) -> AnyElement { - let unauthorized_style = &style.auth.not_authorized; - - Flex::column() - .with_child( - Flex::column() - .with_children([ - Label::new( - "Enable Copilot by connecting", - unauthorized_style.subheading.text.clone(), - ) - .aligned(), - Label::new( - "your existing license.", - unauthorized_style.subheading.text.clone(), - ) - .aligned(), - ]) - .align_children_center() - .contained() - .with_style(unauthorized_style.subheading.container), + .child( + Label::new("You must have an active Copilot license to use it in Zed.") + .color(Color::Warning), ) - .with_child( - Flex::column() - .with_children([ - Label::new( - "You must have an active copilot", - unauthorized_style.warning.text.clone(), - ) - .aligned(), - Label::new( - "license to use it in Zed.", - unauthorized_style.warning.text.clone(), - ) - .aligned(), - ]) - .align_children_center() - .contained() - .with_style(unauthorized_style.warning.container), - ) - .with_child(theme::ui::cta_button::( - "Subscribe on GitHub", - style.auth.content_width, - &style.auth.cta_button, - cx, - |_, _, cx| { + .child( + Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| { cx.remove_window(); - cx.platform().open_url(COPILOT_SIGN_UP_URL) - }, - )) - .align_children_center() - .into_any() + cx.open_url(COPILOT_SIGN_UP_URL) + }), + ) } } -impl Entity for CopilotCodeVerification { - type Event = (); -} - -impl View for CopilotCodeVerification { - fn ui_name() -> &'static str { - "CopilotCodeVerification" - } - - fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - cx.notify() - } - - fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { - cx.notify() - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum ConnectModal {} - - let style = theme::current(cx).clone(); - - modal::( - "Connect Copilot to Zed", - &style.copilot.modal, - cx, - |cx| { - Flex::column() - .with_children([ - theme::ui::icon(&style.copilot.auth.header).into_any(), - match &self.status { - Status::SigningIn { - prompt: Some(prompt), - } => Self::render_prompting_modal( - self.connect_clicked, - &prompt, - &style.copilot, - cx, - ), - Status::Unauthorized => { - self.connect_clicked = false; - Self::render_unauthorized_modal(&style.copilot, cx) - } - Status::Authorized => { - self.connect_clicked = false; - Self::render_enabled_modal(&style.copilot, cx) - } - _ => Empty::new().into_any(), - }, - ]) - .align_children_center() - }, - ) - .into_any() +impl Render for CopilotCodeVerification { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let prompt = match &self.status { + Status::SigningIn { + prompt: Some(prompt), + } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(), + Status::Unauthorized => { + self.connect_clicked = false; + Self::render_unauthorized_modal().into_any_element() + } + Status::Authorized => { + self.connect_clicked = false; + Self::render_enabled_modal().into_any_element() + } + _ => div().into_any_element(), + }; + div() + .id("copilot code verification") + .flex() + .flex_col() + .size_full() + .items_center() + .p_10() + .bg(cx.theme().colors().element_background) + .child(ui::Label::new("Connect Copilot to Zed")) + .child(IconElement::new(Icon::ZedXCopilot)) + .child(prompt) } } diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml deleted file mode 100644 index ce169f3319..0000000000 --- a/crates/copilot2/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "copilot2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/copilot2.rs" -doctest = false - -[features] -test-support = [ - "collections/test-support", - "gpui/test-support", - "language/test-support", - "lsp/test-support", - "settings/test-support", - "util/test-support", -] - -[dependencies] -collections = { path = "../collections" } -# context_menu = { path = "../context_menu" } -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -lsp = { package = "lsp2", path = "../lsp2" } -node_runtime = { path = "../node_runtime"} -util = { path = "../util" } -ui = { package = "ui2", path = "../ui2" } -async-compression.workspace = true -async-tar = "0.4.2" -anyhow.workspace = true -log.workspace = true -serde.workspace = true -serde_derive.workspace = true -smol.workspace = true -futures.workspace = true -parking_lot.workspace = true - -[dev-dependencies] -clock = { path = "../clock" } -collections = { path = "../collections", features = ["test-support"] } -fs = { path = "../fs", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -language = { package = "language2", path = "../language2", features = ["test-support"] } -lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } -rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] } -settings = { package = "settings2", path = "../settings2", features = ["test-support"] } -util = { path = "../util", features = ["test-support"] } diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs deleted file mode 100644 index 658eb3451f..0000000000 --- a/crates/copilot2/src/copilot2.rs +++ /dev/null @@ -1,1253 +0,0 @@ -pub mod request; -mod sign_in; - -use anyhow::{anyhow, Context as _, Result}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use collections::{HashMap, HashSet}; -use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt}; -use gpui::{ - actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Model, - ModelContext, Task, WeakModel, -}; -use language::{ - language_settings::{all_language_settings, language_settings}, - point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, - LanguageServerName, PointUtf16, ToPointUtf16, -}; -use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId}; -use node_runtime::NodeRuntime; -use parking_lot::Mutex; -use request::StatusNotification; -use settings::SettingsStore; -use smol::{fs, io::BufReader, stream::StreamExt}; -use std::{ - any::TypeId, - ffi::OsString, - mem, - ops::Range, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::{ - fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, -}; - -actions!( - copilot, - [ - Suggest, - NextSuggestion, - PreviousSuggestion, - Reinstall, - SignIn, - SignOut - ] -); - -pub fn init( - new_server_id: LanguageServerId, - http: Arc, - node_runtime: Arc, - cx: &mut AppContext, -) { - let copilot = cx.new_model({ - let node_runtime = node_runtime.clone(); - move |cx| Copilot::start(new_server_id, http, node_runtime, cx) - }); - cx.set_global(copilot.clone()); - cx.observe(&copilot, |handle, cx| { - let copilot_action_types = [ - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - TypeId::of::(), - ]; - let copilot_auth_action_types = [TypeId::of::()]; - let copilot_no_auth_action_types = [TypeId::of::()]; - let status = handle.read(cx).status(); - let filter = cx.default_global::(); - - match status { - Status::Disabled => { - filter.hidden_action_types.extend(copilot_action_types); - filter.hidden_action_types.extend(copilot_auth_action_types); - filter - .hidden_action_types - .extend(copilot_no_auth_action_types); - } - Status::Authorized => { - filter - .hidden_action_types - .extend(copilot_no_auth_action_types); - for type_id in copilot_action_types - .iter() - .chain(&copilot_auth_action_types) - { - filter.hidden_action_types.remove(type_id); - } - } - _ => { - filter.hidden_action_types.extend(copilot_action_types); - filter.hidden_action_types.extend(copilot_auth_action_types); - for type_id in &copilot_no_auth_action_types { - filter.hidden_action_types.remove(type_id); - } - } - } - }) - .detach(); - - sign_in::init(cx); - cx.on_action(|_: &SignIn, cx| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - } - }); - cx.on_action(|_: &SignOut, cx| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| copilot.sign_out(cx)) - .detach_and_log_err(cx); - } - }); - cx.on_action(|_: &Reinstall, cx| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| copilot.reinstall(cx)) - .detach(); - } - }); -} - -enum CopilotServer { - Disabled, - Starting { task: Shared> }, - Error(Arc), - Running(RunningCopilotServer), -} - -impl CopilotServer { - fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> { - let server = self.as_running()?; - if matches!(server.sign_in_status, SignInStatus::Authorized { .. }) { - Ok(server) - } else { - Err(anyhow!("must sign in before using copilot")) - } - } - - fn as_running(&mut self) -> Result<&mut RunningCopilotServer> { - match self { - CopilotServer::Starting { .. } => Err(anyhow!("copilot is still starting")), - CopilotServer::Disabled => Err(anyhow!("copilot is disabled")), - CopilotServer::Error(error) => Err(anyhow!( - "copilot was not started because of an error: {}", - error - )), - CopilotServer::Running(server) => Ok(server), - } - } -} - -struct RunningCopilotServer { - name: LanguageServerName, - lsp: Arc, - sign_in_status: SignInStatus, - registered_buffers: HashMap, -} - -#[derive(Clone, Debug)] -enum SignInStatus { - Authorized, - Unauthorized, - SigningIn { - prompt: Option, - task: Shared>>>, - }, - SignedOut, -} - -#[derive(Debug, Clone)] -pub enum Status { - Starting { - task: Shared>, - }, - Error(Arc), - Disabled, - SignedOut, - SigningIn { - prompt: Option, - }, - Unauthorized, - Authorized, -} - -impl Status { - pub fn is_authorized(&self) -> bool { - matches!(self, Status::Authorized) - } -} - -struct RegisteredBuffer { - uri: lsp::Url, - language_id: String, - snapshot: BufferSnapshot, - snapshot_version: i32, - _subscriptions: [gpui::Subscription; 2], - pending_buffer_change: Task>, -} - -impl RegisteredBuffer { - fn report_changes( - &mut self, - buffer: &Model, - cx: &mut ModelContext, - ) -> oneshot::Receiver<(i32, BufferSnapshot)> { - let (done_tx, done_rx) = oneshot::channel(); - - if buffer.read(cx).version() == self.snapshot.version { - let _ = done_tx.send((self.snapshot_version, self.snapshot.clone())); - } else { - let buffer = buffer.downgrade(); - let id = buffer.entity_id(); - let prev_pending_change = - mem::replace(&mut self.pending_buffer_change, Task::ready(None)); - self.pending_buffer_change = cx.spawn(move |copilot, mut cx| async move { - prev_pending_change.await; - - let old_version = copilot - .update(&mut cx, |copilot, _| { - let server = copilot.server.as_authenticated().log_err()?; - let buffer = server.registered_buffers.get_mut(&id)?; - Some(buffer.snapshot.version.clone()) - }) - .ok()??; - let new_snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot()).ok()?; - - let content_changes = cx - .background_executor() - .spawn({ - let new_snapshot = new_snapshot.clone(); - async move { - new_snapshot - .edits_since::<(PointUtf16, usize)>(&old_version) - .map(|edit| { - let edit_start = edit.new.start.0; - let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); - let new_text = new_snapshot - .text_for_range(edit.new.start.1..edit.new.end.1) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - point_to_lsp(edit_start), - point_to_lsp(edit_end), - )), - range_length: None, - text: new_text, - } - }) - .collect::>() - } - }) - .await; - - copilot - .update(&mut cx, |copilot, _| { - let server = copilot.server.as_authenticated().log_err()?; - let buffer = server.registered_buffers.get_mut(&id)?; - if !content_changes.is_empty() { - buffer.snapshot_version += 1; - buffer.snapshot = new_snapshot; - server - .lsp - .notify::( - lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - buffer.uri.clone(), - buffer.snapshot_version, - ), - content_changes, - }, - ) - .log_err(); - } - let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone())); - Some(()) - }) - .ok()?; - - Some(()) - }); - } - - done_rx - } -} - -#[derive(Debug)] -pub struct Completion { - pub uuid: String, - pub range: Range, - pub text: String, -} - -pub struct Copilot { - http: Arc, - node_runtime: Arc, - server: CopilotServer, - buffers: HashSet>, - server_id: LanguageServerId, - _subscription: gpui::Subscription, -} - -pub enum Event { - CopilotLanguageServerStarted, -} - -impl EventEmitter for Copilot {} - -impl Copilot { - pub fn global(cx: &AppContext) -> Option> { - if cx.has_global::>() { - Some(cx.global::>().clone()) - } else { - None - } - } - - fn start( - new_server_id: LanguageServerId, - http: Arc, - node_runtime: Arc, - cx: &mut ModelContext, - ) -> Self { - let mut this = Self { - server_id: new_server_id, - http, - node_runtime, - server: CopilotServer::Disabled, - buffers: Default::default(), - _subscription: cx.on_app_quit(Self::shutdown_language_server), - }; - this.enable_or_disable_copilot(cx); - cx.observe_global::(move |this, cx| this.enable_or_disable_copilot(cx)) - .detach(); - this - } - - fn shutdown_language_server( - &mut self, - _cx: &mut ModelContext, - ) -> impl Future { - let shutdown = match mem::replace(&mut self.server, CopilotServer::Disabled) { - CopilotServer::Running(server) => Some(Box::pin(async move { server.lsp.shutdown() })), - _ => None, - }; - - async move { - if let Some(shutdown) = shutdown { - shutdown.await; - } - } - } - - fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { - let server_id = self.server_id; - let http = self.http.clone(); - let node_runtime = self.node_runtime.clone(); - if all_language_settings(None, cx).copilot_enabled(None, None) { - if matches!(self.server, CopilotServer::Disabled) { - let start_task = cx - .spawn(move |this, cx| { - Self::start_language_server(server_id, http, node_runtime, this, cx) - }) - .shared(); - self.server = CopilotServer::Starting { task: start_task }; - cx.notify(); - } - } else { - self.server = CopilotServer::Disabled; - cx.notify(); - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn fake(cx: &mut gpui::TestAppContext) -> (Model, lsp::FakeLanguageServer) { - use node_runtime::FakeNodeRuntime; - - let (server, fake_server) = - LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); - let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); - let node_runtime = FakeNodeRuntime::new(); - let this = cx.new_model(|cx| Self { - server_id: LanguageServerId(0), - http: http.clone(), - node_runtime, - server: CopilotServer::Running(RunningCopilotServer { - name: LanguageServerName(Arc::from("copilot")), - lsp: Arc::new(server), - sign_in_status: SignInStatus::Authorized, - registered_buffers: Default::default(), - }), - _subscription: cx.on_app_quit(Self::shutdown_language_server), - buffers: Default::default(), - }); - (this, fake_server) - } - - fn start_language_server( - new_server_id: LanguageServerId, - http: Arc, - node_runtime: Arc, - this: WeakModel, - mut cx: AsyncAppContext, - ) -> impl Future { - async move { - let start_language_server = async { - let server_path = get_copilot_lsp(http).await?; - let node_path = node_runtime.binary_path().await?; - let arguments: Vec = vec![server_path.into(), "--stdio".into()]; - let binary = LanguageServerBinary { - path: node_path, - arguments, - }; - - let server = LanguageServer::new( - Arc::new(Mutex::new(None)), - new_server_id, - binary, - Path::new("/"), - None, - cx.clone(), - )?; - - server - .on_notification::( - |_, _| { /* Silence the notification */ }, - ) - .detach(); - - let server = server.initialize(Default::default()).await?; - - let status = server - .request::(request::CheckStatusParams { - local_checks_only: false, - }) - .await?; - - server - .request::(request::SetEditorInfoParams { - editor_info: request::EditorInfo { - name: "zed".into(), - version: env!("CARGO_PKG_VERSION").into(), - }, - editor_plugin_info: request::EditorPluginInfo { - name: "zed-copilot".into(), - version: "0.0.1".into(), - }, - }) - .await?; - - anyhow::Ok((server, status)) - }; - - let server = start_language_server.await; - this.update(&mut cx, |this, cx| { - cx.notify(); - match server { - Ok((server, status)) => { - this.server = CopilotServer::Running(RunningCopilotServer { - name: LanguageServerName(Arc::from("copilot")), - lsp: server, - sign_in_status: SignInStatus::SignedOut, - registered_buffers: Default::default(), - }); - cx.emit(Event::CopilotLanguageServerStarted); - this.update_sign_in_status(status, cx); - } - Err(error) => { - this.server = CopilotServer::Error(error.to_string().into()); - cx.notify() - } - } - }) - .ok(); - } - } - - pub fn sign_in(&mut self, cx: &mut ModelContext) -> Task> { - if let CopilotServer::Running(server) = &mut self.server { - let task = match &server.sign_in_status { - SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(), - SignInStatus::SigningIn { task, .. } => { - cx.notify(); - task.clone() - } - SignInStatus::SignedOut | SignInStatus::Unauthorized { .. } => { - let lsp = server.lsp.clone(); - let task = cx - .spawn(|this, mut cx| async move { - let sign_in = async { - let sign_in = lsp - .request::( - request::SignInInitiateParams {}, - ) - .await?; - match sign_in { - request::SignInInitiateResult::AlreadySignedIn { user } => { - Ok(request::SignInStatus::Ok { user }) - } - request::SignInInitiateResult::PromptUserDeviceFlow(flow) => { - this.update(&mut cx, |this, cx| { - if let CopilotServer::Running(RunningCopilotServer { - sign_in_status: status, - .. - }) = &mut this.server - { - if let SignInStatus::SigningIn { - prompt: prompt_flow, - .. - } = status - { - *prompt_flow = Some(flow.clone()); - cx.notify(); - } - } - })?; - let response = lsp - .request::( - request::SignInConfirmParams { - user_code: flow.user_code, - }, - ) - .await?; - Ok(response) - } - } - }; - - let sign_in = sign_in.await; - this.update(&mut cx, |this, cx| match sign_in { - Ok(status) => { - this.update_sign_in_status(status, cx); - Ok(()) - } - Err(error) => { - this.update_sign_in_status( - request::SignInStatus::NotSignedIn, - cx, - ); - Err(Arc::new(error)) - } - })? - }) - .shared(); - server.sign_in_status = SignInStatus::SigningIn { - prompt: None, - task: task.clone(), - }; - cx.notify(); - task - } - }; - - cx.background_executor() - .spawn(task.map_err(|err| anyhow!("{:?}", err))) - } else { - // If we're downloading, wait until download is finished - // If we're in a stuck state, display to the user - Task::ready(Err(anyhow!("copilot hasn't started yet"))) - } - } - - fn sign_out(&mut self, cx: &mut ModelContext) -> Task> { - self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx); - if let CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) = &self.server { - let server = server.clone(); - cx.background_executor().spawn(async move { - server - .request::(request::SignOutParams {}) - .await?; - anyhow::Ok(()) - }) - } else { - Task::ready(Err(anyhow!("copilot hasn't started yet"))) - } - } - - pub fn reinstall(&mut self, cx: &mut ModelContext) -> Task<()> { - let start_task = cx - .spawn({ - let http = self.http.clone(); - let node_runtime = self.node_runtime.clone(); - let server_id = self.server_id; - move |this, cx| async move { - clear_copilot_dir().await; - Self::start_language_server(server_id, http, node_runtime, this, cx).await - } - }) - .shared(); - - self.server = CopilotServer::Starting { - task: start_task.clone(), - }; - - cx.notify(); - - cx.background_executor().spawn(start_task) - } - - pub fn language_server(&self) -> Option<(&LanguageServerName, &Arc)> { - if let CopilotServer::Running(server) = &self.server { - Some((&server.name, &server.lsp)) - } else { - None - } - } - - pub fn register_buffer(&mut self, buffer: &Model, cx: &mut ModelContext) { - let weak_buffer = buffer.downgrade(); - self.buffers.insert(weak_buffer.clone()); - - if let CopilotServer::Running(RunningCopilotServer { - lsp: server, - sign_in_status: status, - registered_buffers, - .. - }) = &mut self.server - { - if !matches!(status, SignInStatus::Authorized { .. }) { - return; - } - - registered_buffers - .entry(buffer.entity_id()) - .or_insert_with(|| { - let uri: lsp::Url = uri_for_buffer(buffer, cx); - let language_id = id_for_language(buffer.read(cx).language()); - let snapshot = buffer.read(cx).snapshot(); - server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem { - uri: uri.clone(), - language_id: language_id.clone(), - version: 0, - text: snapshot.text(), - }, - }, - ) - .log_err(); - - RegisteredBuffer { - uri, - language_id, - snapshot, - snapshot_version: 0, - pending_buffer_change: Task::ready(Some(())), - _subscriptions: [ - cx.subscribe(buffer, |this, buffer, event, cx| { - this.handle_buffer_event(buffer, event, cx).log_err(); - }), - cx.observe_release(buffer, move |this, _buffer, _cx| { - this.buffers.remove(&weak_buffer); - this.unregister_buffer(&weak_buffer); - }), - ], - } - }); - } - } - - fn handle_buffer_event( - &mut self, - buffer: Model, - event: &language::Event, - cx: &mut ModelContext, - ) -> Result<()> { - if let Ok(server) = self.server.as_running() { - if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id()) - { - match event { - language::Event::Edited => { - let _ = registered_buffer.report_changes(&buffer, cx); - } - language::Event::Saved => { - server - .lsp - .notify::( - lsp::DidSaveTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new( - registered_buffer.uri.clone(), - ), - text: None, - }, - )?; - } - language::Event::FileHandleChanged | language::Event::LanguageChanged => { - let new_language_id = id_for_language(buffer.read(cx).language()); - let new_uri = uri_for_buffer(&buffer, cx); - if new_uri != registered_buffer.uri - || new_language_id != registered_buffer.language_id - { - let old_uri = mem::replace(&mut registered_buffer.uri, new_uri); - registered_buffer.language_id = new_language_id; - server - .lsp - .notify::( - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(old_uri), - }, - )?; - server - .lsp - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - registered_buffer.uri.clone(), - registered_buffer.language_id.clone(), - registered_buffer.snapshot_version, - registered_buffer.snapshot.text(), - ), - }, - )?; - } - } - _ => {} - } - } - } - - Ok(()) - } - - fn unregister_buffer(&mut self, buffer: &WeakModel) { - if let Ok(server) = self.server.as_running() { - if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) { - server - .lsp - .notify::( - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer.uri), - }, - ) - .log_err(); - } - } - } - - pub fn completions( - &mut self, - buffer: &Model, - position: T, - cx: &mut ModelContext, - ) -> Task>> - where - T: ToPointUtf16, - { - self.request_completions::(buffer, position, cx) - } - - pub fn completions_cycling( - &mut self, - buffer: &Model, - position: T, - cx: &mut ModelContext, - ) -> Task>> - where - T: ToPointUtf16, - { - self.request_completions::(buffer, position, cx) - } - - pub fn accept_completion( - &mut self, - completion: &Completion, - cx: &mut ModelContext, - ) -> Task> { - let server = match self.server.as_authenticated() { - Ok(server) => server, - Err(error) => return Task::ready(Err(error)), - }; - let request = - server - .lsp - .request::(request::NotifyAcceptedParams { - uuid: completion.uuid.clone(), - }); - cx.background_executor().spawn(async move { - request.await?; - Ok(()) - }) - } - - pub fn discard_completions( - &mut self, - completions: &[Completion], - cx: &mut ModelContext, - ) -> Task> { - let server = match self.server.as_authenticated() { - Ok(server) => server, - Err(error) => return Task::ready(Err(error)), - }; - let request = - server - .lsp - .request::(request::NotifyRejectedParams { - uuids: completions - .iter() - .map(|completion| completion.uuid.clone()) - .collect(), - }); - cx.background_executor().spawn(async move { - request.await?; - Ok(()) - }) - } - - fn request_completions( - &mut self, - buffer: &Model, - position: T, - cx: &mut ModelContext, - ) -> Task>> - where - R: 'static - + lsp::request::Request< - Params = request::GetCompletionsParams, - Result = request::GetCompletionsResult, - >, - T: ToPointUtf16, - { - self.register_buffer(buffer, cx); - - let server = match self.server.as_authenticated() { - Ok(server) => server, - Err(error) => return Task::ready(Err(error)), - }; - let lsp = server.lsp.clone(); - let registered_buffer = server - .registered_buffers - .get_mut(&buffer.entity_id()) - .unwrap(); - let snapshot = registered_buffer.report_changes(buffer, cx); - let buffer = buffer.read(cx); - let uri = registered_buffer.uri.clone(); - let position = position.to_point_utf16(buffer); - let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx); - let tab_size = settings.tab_size; - let hard_tabs = settings.hard_tabs; - let relative_path = buffer - .file() - .map(|file| file.path().to_path_buf()) - .unwrap_or_default(); - - cx.background_executor().spawn(async move { - let (version, snapshot) = snapshot.await?; - let result = lsp - .request::(request::GetCompletionsParams { - doc: request::GetCompletionsDocument { - uri, - tab_size: tab_size.into(), - indent_size: 1, - insert_spaces: !hard_tabs, - relative_path: relative_path.to_string_lossy().into(), - position: point_to_lsp(position), - version: version.try_into().unwrap(), - }, - }) - .await?; - let completions = result - .completions - .into_iter() - .map(|completion| { - let start = snapshot - .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left); - let end = - snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left); - Completion { - uuid: completion.uuid, - range: snapshot.anchor_before(start)..snapshot.anchor_after(end), - text: completion.text, - } - }) - .collect(); - anyhow::Ok(completions) - }) - } - - pub fn status(&self) -> Status { - match &self.server { - CopilotServer::Starting { task } => Status::Starting { task: task.clone() }, - CopilotServer::Disabled => Status::Disabled, - CopilotServer::Error(error) => Status::Error(error.clone()), - CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => { - match sign_in_status { - SignInStatus::Authorized { .. } => Status::Authorized, - SignInStatus::Unauthorized { .. } => Status::Unauthorized, - SignInStatus::SigningIn { prompt, .. } => Status::SigningIn { - prompt: prompt.clone(), - }, - SignInStatus::SignedOut => Status::SignedOut, - } - } - } - } - - fn update_sign_in_status( - &mut self, - lsp_status: request::SignInStatus, - cx: &mut ModelContext, - ) { - self.buffers.retain(|buffer| buffer.is_upgradable()); - - if let Ok(server) = self.server.as_running() { - match lsp_status { - request::SignInStatus::Ok { .. } - | request::SignInStatus::MaybeOk { .. } - | request::SignInStatus::AlreadySignedIn { .. } => { - server.sign_in_status = SignInStatus::Authorized; - for buffer in self.buffers.iter().cloned().collect::>() { - if let Some(buffer) = buffer.upgrade() { - self.register_buffer(&buffer, cx); - } - } - } - request::SignInStatus::NotAuthorized { .. } => { - server.sign_in_status = SignInStatus::Unauthorized; - for buffer in self.buffers.iter().cloned().collect::>() { - self.unregister_buffer(&buffer); - } - } - request::SignInStatus::NotSignedIn => { - server.sign_in_status = SignInStatus::SignedOut; - for buffer in self.buffers.iter().cloned().collect::>() { - self.unregister_buffer(&buffer); - } - } - } - - cx.notify(); - } - } -} - -fn id_for_language(language: Option<&Arc>) -> String { - let language_name = language.map(|language| language.name()); - match language_name.as_deref() { - Some("Plain Text") => "plaintext".to_string(), - Some(language_name) => language_name.to_lowercase(), - None => "plaintext".to_string(), - } -} - -fn uri_for_buffer(buffer: &Model, cx: &AppContext) -> lsp::Url { - if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) { - lsp::Url::from_file_path(file.abs_path(cx)).unwrap() - } else { - format!("buffer://{}", buffer.entity_id()).parse().unwrap() - } -} - -async fn clear_copilot_dir() { - remove_matching(&paths::COPILOT_DIR, |_| true).await -} - -async fn get_copilot_lsp(http: Arc) -> anyhow::Result { - const SERVER_PATH: &'static str = "dist/agent.js"; - - ///Check for the latest copilot language server and download it if we haven't already - async fn fetch_latest(http: Arc) -> anyhow::Result { - let release = latest_github_release("zed-industries/copilot", false, http.clone()).await?; - - let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name)); - - fs::create_dir_all(version_dir).await?; - let server_path = version_dir.join(SERVER_PATH); - - if fs::metadata(&server_path).await.is_err() { - // Copilot LSP looks for this dist dir specifcially, so lets add it in. - let dist_dir = version_dir.join("dist"); - fs::create_dir_all(dist_dir.as_path()).await?; - - let url = &release - .assets - .get(0) - .context("Github release for copilot contained no assets")? - .browser_download_url; - - let mut response = http - .get(&url, Default::default(), true) - .await - .map_err(|err| anyhow!("error downloading copilot release: {}", err))?; - let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(dist_dir).await?; - - remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await; - } - - Ok(server_path) - } - - match fetch_latest(http).await { - ok @ Result::Ok(..) => ok, - e @ Err(..) => { - e.log_err(); - // Fetch a cached binary, if it exists - (|| async move { - let mut last_version_dir = None; - let mut entries = fs::read_dir(paths::COPILOT_DIR.as_path()).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_version_dir = Some(entry.path()); - } - } - let last_version_dir = - last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let server_path = last_version_dir.join(SERVER_PATH); - if server_path.exists() { - Ok(server_path) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - })() - .await - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - - #[gpui::test(iterations = 10)] - async fn test_buffer_management(cx: &mut TestAppContext) { - let (copilot, mut lsp) = Copilot::fake(cx); - - let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello")); - let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64()) - .parse() - .unwrap(); - copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - buffer_1_uri.clone(), - "plaintext".into(), - 0, - "Hello".into() - ), - } - ); - - let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye")); - let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64()) - .parse() - .unwrap(); - copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - buffer_2_uri.clone(), - "plaintext".into(), - 0, - "Goodbye".into() - ), - } - ); - - buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx)); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1), - content_changes: vec![lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - lsp::Position::new(0, 5), - lsp::Position::new(0, 5) - )), - range_length: None, - text: " world".into(), - }], - } - ); - - // Ensure updates to the file are reflected in the LSP. - buffer_1.update(cx, |buffer, cx| { - buffer.file_updated( - Arc::new(File { - abs_path: "/root/child/buffer-1".into(), - path: Path::new("child/buffer-1").into(), - }), - cx, - ) - }); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri), - } - ); - let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap(); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - buffer_1_uri.clone(), - "plaintext".into(), - 1, - "Hello world".into() - ), - } - ); - - // Ensure all previously-registered buffers are closed when signing out. - lsp.handle_request::(|_, _| async { - Ok(request::SignOutResult {}) - }); - copilot - .update(cx, |copilot, cx| copilot.sign_out(cx)) - .await - .unwrap(); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), - } - ); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), - } - ); - - // Ensure all previously-registered buffers are re-opened when signing in. - lsp.handle_request::(|_, _| async { - Ok(request::SignInInitiateResult::AlreadySignedIn { - user: "user-1".into(), - }) - }); - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .await - .unwrap(); - - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - buffer_1_uri.clone(), - "plaintext".into(), - 0, - "Hello world".into() - ), - } - ); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - buffer_2_uri.clone(), - "plaintext".into(), - 0, - "Goodbye".into() - ), - } - ); - // Dropping a buffer causes it to be closed on the LSP side as well. - cx.update(|_| drop(buffer_2)); - assert_eq!( - lsp.receive_notification::() - .await, - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri), - } - ); - } - - struct File { - abs_path: PathBuf, - path: Arc, - } - - impl language::File for File { - fn as_local(&self) -> Option<&dyn language::LocalFile> { - Some(self) - } - - fn mtime(&self) -> std::time::SystemTime { - unimplemented!() - } - - fn path(&self) -> &Arc { - &self.path - } - - fn full_path(&self, _: &AppContext) -> PathBuf { - unimplemented!() - } - - fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr { - unimplemented!() - } - - fn is_deleted(&self) -> bool { - unimplemented!() - } - - fn as_any(&self) -> &dyn std::any::Any { - unimplemented!() - } - - fn to_proto(&self) -> rpc::proto::File { - unimplemented!() - } - - fn worktree_id(&self) -> usize { - 0 - } - } - - impl language::LocalFile for File { - fn abs_path(&self, _: &AppContext) -> PathBuf { - self.abs_path.clone() - } - - fn load(&self, _: &AppContext) -> Task> { - unimplemented!() - } - - fn buffer_reloaded( - &self, - _: u64, - _: &clock::Global, - _: language::RopeFingerprint, - _: language::LineEnding, - _: std::time::SystemTime, - _: &mut AppContext, - ) { - unimplemented!() - } - } -} diff --git a/crates/copilot2/src/request.rs b/crates/copilot2/src/request.rs deleted file mode 100644 index 0f9a478b91..0000000000 --- a/crates/copilot2/src/request.rs +++ /dev/null @@ -1,225 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub enum CheckStatus {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CheckStatusParams { - pub local_checks_only: bool, -} - -impl lsp::request::Request for CheckStatus { - type Params = CheckStatusParams; - type Result = SignInStatus; - const METHOD: &'static str = "checkStatus"; -} - -pub enum SignInInitiate {} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SignInInitiateParams {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "status")] -pub enum SignInInitiateResult { - AlreadySignedIn { user: String }, - PromptUserDeviceFlow(PromptUserDeviceFlow), -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PromptUserDeviceFlow { - pub user_code: String, - pub verification_uri: String, -} - -impl lsp::request::Request for SignInInitiate { - type Params = SignInInitiateParams; - type Result = SignInInitiateResult; - const METHOD: &'static str = "signInInitiate"; -} - -pub enum SignInConfirm {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SignInConfirmParams { - pub user_code: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "status")] -pub enum SignInStatus { - #[serde(rename = "OK")] - Ok { - user: String, - }, - MaybeOk { - user: String, - }, - AlreadySignedIn { - user: String, - }, - NotAuthorized { - user: String, - }, - NotSignedIn, -} - -impl lsp::request::Request for SignInConfirm { - type Params = SignInConfirmParams; - type Result = SignInStatus; - const METHOD: &'static str = "signInConfirm"; -} - -pub enum SignOut {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SignOutParams {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SignOutResult {} - -impl lsp::request::Request for SignOut { - type Params = SignOutParams; - type Result = SignOutResult; - const METHOD: &'static str = "signOut"; -} - -pub enum GetCompletions {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsParams { - pub doc: GetCompletionsDocument, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsDocument { - pub tab_size: u32, - pub indent_size: u32, - pub insert_spaces: bool, - pub uri: lsp::Url, - pub relative_path: String, - pub position: lsp::Position, - pub version: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetCompletionsResult { - pub completions: Vec, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Completion { - pub text: String, - pub position: lsp::Position, - pub uuid: String, - pub range: lsp::Range, - pub display_text: String, -} - -impl lsp::request::Request for GetCompletions { - type Params = GetCompletionsParams; - type Result = GetCompletionsResult; - const METHOD: &'static str = "getCompletions"; -} - -pub enum GetCompletionsCycling {} - -impl lsp::request::Request for GetCompletionsCycling { - type Params = GetCompletionsParams; - type Result = GetCompletionsResult; - const METHOD: &'static str = "getCompletionsCycling"; -} - -pub enum LogMessage {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LogMessageParams { - pub level: u8, - pub message: String, - pub metadata_str: String, - pub extra: Vec, -} - -impl lsp::notification::Notification for LogMessage { - type Params = LogMessageParams; - const METHOD: &'static str = "LogMessage"; -} - -pub enum StatusNotification {} - -#[derive(Debug, Serialize, Deserialize)] -pub struct StatusNotificationParams { - pub message: String, - pub status: String, // One of Normal/InProgress -} - -impl lsp::notification::Notification for StatusNotification { - type Params = StatusNotificationParams; - const METHOD: &'static str = "statusNotification"; -} - -pub enum SetEditorInfo {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SetEditorInfoParams { - pub editor_info: EditorInfo, - pub editor_plugin_info: EditorPluginInfo, -} - -impl lsp::request::Request for SetEditorInfo { - type Params = SetEditorInfoParams; - type Result = String; - const METHOD: &'static str = "setEditorInfo"; -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EditorInfo { - pub name: String, - pub version: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EditorPluginInfo { - pub name: String, - pub version: String, -} - -pub enum NotifyAccepted {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NotifyAcceptedParams { - pub uuid: String, -} - -impl lsp::request::Request for NotifyAccepted { - type Params = NotifyAcceptedParams; - type Result = String; - const METHOD: &'static str = "notifyAccepted"; -} - -pub enum NotifyRejected {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NotifyRejectedParams { - pub uuids: Vec, -} - -impl lsp::request::Request for NotifyRejected { - type Params = NotifyRejectedParams; - type Result = String; - const METHOD: &'static str = "notifyRejected"; -} diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs deleted file mode 100644 index ba5dbe0e31..0000000000 --- a/crates/copilot2/src/sign_in.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::{request::PromptUserDeviceFlow, Copilot, Status}; -use gpui::{ - div, size, AppContext, Bounds, ClipboardItem, Element, GlobalPixels, InteractiveElement, - IntoElement, ParentElement, Point, Render, Styled, ViewContext, VisualContext, WindowBounds, - WindowHandle, WindowKind, WindowOptions, -}; -use theme::ActiveTheme; -use ui::{prelude::*, Button, Icon, IconElement, Label}; - -const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; - -pub fn init(cx: &mut AppContext) { - if let Some(copilot) = Copilot::global(cx) { - let mut verification_window: Option> = None; - cx.observe(&copilot, move |copilot, cx| { - let status = copilot.read(cx).status(); - - match &status { - crate::Status::SigningIn { prompt } => { - if let Some(window) = verification_window.as_mut() { - let updated = window - .update(cx, |verification, cx| { - verification.set_status(status.clone(), cx); - cx.activate_window(); - }) - .is_ok(); - if !updated { - verification_window = Some(create_copilot_auth_window(cx, &status)); - } - } else if let Some(_prompt) = prompt { - verification_window = Some(create_copilot_auth_window(cx, &status)); - } - } - Status::Authorized | Status::Unauthorized => { - if let Some(window) = verification_window.as_ref() { - window - .update(cx, |verification, cx| { - verification.set_status(status, cx); - cx.activate(true); - cx.activate_window(); - }) - .ok(); - } - } - _ => { - if let Some(code_verification) = verification_window.take() { - code_verification - .update(cx, |_, cx| cx.remove_window()) - .ok(); - } - } - } - }) - .detach(); - } -} - -fn create_copilot_auth_window( - cx: &mut AppContext, - status: &Status, -) -> WindowHandle { - let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.)); - let window_options = WindowOptions { - bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)), - titlebar: None, - center: true, - focus: true, - show: true, - kind: WindowKind::PopUp, - is_movable: true, - display_id: None, - }; - let window = cx.open_window(window_options, |cx| { - cx.new_view(|_| CopilotCodeVerification::new(status.clone())) - }); - window -} - -pub struct CopilotCodeVerification { - status: Status, - connect_clicked: bool, -} - -impl CopilotCodeVerification { - pub fn new(status: Status) -> Self { - Self { - status, - connect_clicked: false, - } - } - - pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { - self.status = status; - cx.notify(); - } - - fn render_device_code( - data: &PromptUserDeviceFlow, - cx: &mut ViewContext, - ) -> impl IntoElement { - let copied = cx - .read_from_clipboard() - .map(|item| item.text() == &data.user_code) - .unwrap_or(false); - h_stack() - .cursor_pointer() - .justify_between() - .on_mouse_down(gpui::MouseButton::Left, { - let user_code = data.user_code.clone(); - move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); - cx.notify(); - } - }) - .child(Label::new(data.user_code.clone())) - .child(div()) - .child(Label::new(if copied { "Copied!" } else { "Copy" })) - } - - fn render_prompting_modal( - connect_clicked: bool, - data: &PromptUserDeviceFlow, - cx: &mut ViewContext, - ) -> impl Element { - let connect_button_label = if connect_clicked { - "Waiting for connection..." - } else { - "Connect to Github" - }; - v_stack() - .flex_1() - .items_center() - .justify_between() - .w_full() - .child(Label::new( - "Enable Copilot by connecting your existing license", - )) - .child(Self::render_device_code(data, cx)) - .child( - Label::new("Paste this code into GitHub after clicking the button below.") - .size(ui::LabelSize::Small), - ) - .child( - Button::new("connect-button", connect_button_label).on_click({ - let verification_uri = data.verification_uri.clone(); - cx.listener(move |this, _, cx| { - cx.open_url(&verification_uri); - this.connect_clicked = true; - }) - }), - ) - } - fn render_enabled_modal() -> impl Element { - v_stack() - .child(Label::new("Copilot Enabled!")) - .child(Label::new( - "You can update your settings or sign out from the Copilot menu in the status bar.", - )) - .child( - Button::new("copilot-enabled-done-button", "Done") - .on_click(|_, cx| cx.remove_window()), - ) - } - - fn render_unauthorized_modal() -> impl Element { - v_stack() - .child(Label::new( - "Enable Copilot by connecting your existing license.", - )) - .child( - Label::new("You must have an active Copilot license to use it in Zed.") - .color(Color::Warning), - ) - .child( - Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| { - cx.remove_window(); - cx.open_url(COPILOT_SIGN_UP_URL) - }), - ) - } -} - -impl Render for CopilotCodeVerification { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let prompt = match &self.status { - Status::SigningIn { - prompt: Some(prompt), - } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(), - Status::Unauthorized => { - self.connect_clicked = false; - Self::render_unauthorized_modal().into_any_element() - } - Status::Authorized => { - self.connect_clicked = false; - Self::render_enabled_modal().into_any_element() - } - _ => div().into_any_element(), - }; - div() - .id("copilot code verification") - .flex() - .flex_col() - .size_full() - .items_center() - .p_10() - .bg(cx.theme().colors().element_background) - .child(ui::Label::new("Connect Copilot to Zed")) - .child(IconElement::new(Icon::ZedXCopilot)) - .child(prompt) - } -} diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 4ed9fcc653..2602a17626 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -9,7 +9,7 @@ path = "src/copilot_button.rs" doctest = false [dependencies] -copilot = { package = "copilot2", path = "../copilot2" } +copilot = { path = "../copilot" } editor = { path = "../editor" } fs = { package = "fs2", path = "../fs2" } zed-actions = { package="zed_actions2", path = "../zed_actions2"} diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 2c48f06a25..fbbf58d67b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -25,7 +25,7 @@ test-support = [ [dependencies] client = { package = "client2", path = "../client2" } clock = { path = "../clock" } -copilot = { package="copilot2", path = "../copilot2" } +copilot = { path = "../copilot" } db = { package="db2", path = "../db2" } collections = { path = "../collections" } # context_menu = { path = "../context_menu" } @@ -34,7 +34,7 @@ git = { package = "git3", path = "../git3" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } lsp = { package = "lsp2", path = "../lsp2" } -multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } +multi_buffer = { path = "../multi_buffer" } project = { package = "project2", path = "../project2" } rpc = { package = "rpc2", path = "../rpc2" } rich_text = { package = "rich_text2", path = "../rich_text2" } @@ -72,7 +72,7 @@ tree-sitter-html = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } [dev-dependencies] -copilot = { package="copilot2", path = "../copilot2", features = ["test-support"] } +copilot = { path = "../copilot", features = ["test-support"] } text = { package="text2", path = "../text2", features = ["test-support"] } language = { package="language2", path = "../language2", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } @@ -81,7 +81,7 @@ util = { path = "../util", features = ["test-support"] } project = { package = "project2", path = "../project2", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } -multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2", features = ["test-support"] } +multi_buffer = { path = "../multi_buffer", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs deleted file mode 100644 index 4d507e0d37..0000000000 --- a/crates/editor2/src/editor_tests.rs +++ /dev/null @@ -1,8268 +0,0 @@ -use super::*; -use crate::{ - scroll::scroll_amount::ScrollAmount, - test::{ - assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, - editor_test_context::EditorTestContext, select_ranges, - }, - JoinLines, -}; - -use futures::StreamExt; -use gpui::{ - div, - serde_json::{self, json}, - TestAppContext, VisualTestContext, WindowBounds, WindowOptions, -}; -use indoc::indoc; -use language::{ - language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, - BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, - Override, Point, -}; -use parking_lot::Mutex; -use project::project_settings::{LspSettings, ProjectSettings}; -use project::FakeFs; -use std::sync::atomic; -use std::sync::atomic::AtomicUsize; -use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; -use unindent::Unindent; -use util::{ - assert_set_eq, - test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, -}; -use workspace::{ - item::{FollowEvent, FollowableItem, Item, ItemHandle}, - NavigationEntry, ViewId, -}; - -#[gpui::test] -fn test_edit_events(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.new_model(|cx| { - let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456"); - buffer.set_group_interval(Duration::from_secs(1)); - buffer - }); - - let events = Rc::new(RefCell::new(Vec::new())); - let editor1 = cx.add_window({ - let events = events.clone(); - |cx| { - let view = cx.view().clone(); - cx.subscribe(&view, move |_, _, event: &EditorEvent, _| { - if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { - events.borrow_mut().push(("editor1", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }); - - let editor2 = cx.add_window({ - let events = events.clone(); - |cx| { - cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| { - if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) { - events.borrow_mut().push(("editor2", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }); - - assert_eq!(mem::take(&mut *events.borrow_mut()), []); - - // Mutating editor 1 will emit an `Edited` event only for that editor. - _ = editor1.update(cx, |editor, cx| editor.insert("X", cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor1", EditorEvent::Edited), - ("editor1", EditorEvent::BufferEdited), - ("editor2", EditorEvent::BufferEdited), - ] - ); - - // Mutating editor 2 will emit an `Edited` event only for that editor. - _ = editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor2", EditorEvent::Edited), - ("editor1", EditorEvent::BufferEdited), - ("editor2", EditorEvent::BufferEdited), - ] - ); - - // Undoing on editor 1 will emit an `Edited` event only for that editor. - _ = editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor1", EditorEvent::Edited), - ("editor1", EditorEvent::BufferEdited), - ("editor2", EditorEvent::BufferEdited), - ] - ); - - // Redoing on editor 1 will emit an `Edited` event only for that editor. - _ = editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor1", EditorEvent::Edited), - ("editor1", EditorEvent::BufferEdited), - ("editor2", EditorEvent::BufferEdited), - ] - ); - - // Undoing on editor 2 will emit an `Edited` event only for that editor. - _ = editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor2", EditorEvent::Edited), - ("editor1", EditorEvent::BufferEdited), - ("editor2", EditorEvent::BufferEdited), - ] - ); - - // Redoing on editor 2 will emit an `Edited` event only for that editor. - _ = editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); - assert_eq!( - mem::take(&mut *events.borrow_mut()), - [ - ("editor2", EditorEvent::Edited), - ("editor1", EditorEvent::BufferEdited), - ("editor2", EditorEvent::BufferEdited), - ] - ); - - // No event is emitted when the mutation is a no-op. - _ = editor2.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([0..0])); - - editor.backspace(&Backspace, cx); - }); - assert_eq!(mem::take(&mut *events.borrow_mut()), []); -} - -#[gpui::test] -fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut now = Instant::now(); - let buffer = cx.new_model(|cx| language::Buffer::new(0, cx.entity_id().as_u64(), "123456")); - let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval()); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx)); - - _ = editor.update(cx, |editor, cx| { - editor.start_transaction_at(now, cx); - editor.change_selections(None, cx, |s| s.select_ranges([2..4])); - - editor.insert("cd", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cd56"); - assert_eq!(editor.selections.ranges(cx), vec![4..4]); - - editor.start_transaction_at(now, cx); - editor.change_selections(None, cx, |s| s.select_ranges([4..5])); - editor.insert("e", cx); - editor.end_transaction_at(now, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.ranges(cx), vec![5..5]); - - now += group_interval + Duration::from_millis(1); - editor.change_selections(None, cx, |s| s.select_ranges([2..2])); - - // Simulate an edit in another editor - _ = buffer.update(cx, |buffer, cx| { - buffer.start_transaction_at(now, cx); - buffer.edit([(0..1, "a")], None, cx); - buffer.edit([(1..1, "b")], None, cx); - buffer.end_transaction_at(now, cx); - }); - - assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selections.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.selections.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.selections.ranges(cx), vec![0..0]); - - // Redo the first two transactions together. - editor.redo(&Redo, cx); - assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.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.selections.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_ime_composition(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.new_model(|cx| { - let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "abcde"); - // Ensure automatic grouping doesn't occur. - buffer.set_group_interval(Duration::ZERO); - buffer - }); - - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - cx.add_window(|cx| { - let mut editor = build_editor(buffer.clone(), cx); - - // Start a new IME composition. - editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); - editor.replace_and_mark_text_in_range(Some(0..1), "á", None, cx); - editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, cx); - assert_eq!(editor.text(cx), "äbcde"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) - ); - - // Finalize IME composition. - editor.replace_text_in_range(None, "ā", cx); - assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // IME composition edits are grouped and are undone/redone at once. - editor.undo(&Default::default(), cx); - assert_eq!(editor.text(cx), "abcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - editor.redo(&Default::default(), cx); - assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // Start a new IME composition. - editor.replace_and_mark_text_in_range(Some(0..1), "à", None, cx); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![OffsetUtf16(0)..OffsetUtf16(1)]) - ); - - // Undoing during an IME composition cancels it. - editor.undo(&Default::default(), cx); - assert_eq!(editor.text(cx), "ābcde"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // Start a new IME composition with an invalid marked range, ensuring it gets clipped. - editor.replace_and_mark_text_in_range(Some(4..999), "è", None, cx); - assert_eq!(editor.text(cx), "ābcdè"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![OffsetUtf16(4)..OffsetUtf16(5)]) - ); - - // Finalize IME composition with an invalid replacement range, ensuring it gets clipped. - editor.replace_text_in_range(Some(4..999), "ę", cx); - assert_eq!(editor.text(cx), "ābcdę"); - assert_eq!(editor.marked_text_ranges(cx), None); - - // Start a new IME composition with multiple cursors. - editor.change_selections(None, cx, |s| { - s.select_ranges([ - OffsetUtf16(1)..OffsetUtf16(1), - OffsetUtf16(3)..OffsetUtf16(3), - OffsetUtf16(5)..OffsetUtf16(5), - ]) - }); - editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx); - assert_eq!(editor.text(cx), "XYZbXYZdXYZ"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![ - OffsetUtf16(0)..OffsetUtf16(3), - OffsetUtf16(4)..OffsetUtf16(7), - OffsetUtf16(8)..OffsetUtf16(11) - ]) - ); - - // Ensure the newly-marked range gets treated as relative to the previously-marked ranges. - editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx); - assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z"); - assert_eq!( - editor.marked_text_ranges(cx), - Some(vec![ - OffsetUtf16(1)..OffsetUtf16(2), - OffsetUtf16(5)..OffsetUtf16(6), - OffsetUtf16(9)..OffsetUtf16(10) - ]) - ); - - // Finalize IME composition with multiple cursors. - editor.replace_text_in_range(Some(9..10), "2", cx); - assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z"); - assert_eq!(editor.marked_text_ranges(cx), None); - - editor - }); -} - -#[gpui::test] -fn test_selection_with_mouse(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); - build_editor(buffer, cx) - }); - - _ = editor.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - }); - assert_eq!( - editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - - _ = editor.update(cx, |view, cx| { - view.update_selection( - DisplayPoint::new(3, 3), - 0, - gpui::Point::::default(), - cx, - ); - }); - - assert_eq!( - editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - - _ = editor.update(cx, |view, cx| { - view.update_selection( - DisplayPoint::new(1, 1), - 0, - gpui::Point::::default(), - cx, - ); - }); - - assert_eq!( - editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), - [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, - gpui::Point::::default(), - cx, - ); - }); - - assert_eq!( - editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), - [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, - gpui::Point::::default(), - cx, - ); - }); - - assert_eq!( - editor - .update(cx, |view, cx| view.selections.display_ranges(cx)) - .unwrap(), - [ - 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.selections.display_ranges(cx)) - .unwrap(), - [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)] - ); -} - -#[gpui::test] -fn test_canceling_pending_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) - }); - - _ = view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] - ); - }); - - _ = view.update(cx, |view, cx| { - view.update_selection( - DisplayPoint::new(3, 3), - 0, - gpui::Point::::default(), - cx, - ); - assert_eq!( - view.selections.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, - gpui::Point::::default(), - cx, - ); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] - ); - }); -} - -#[gpui::test] -fn test_clone(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let (text, selection_ranges) = marked_text_ranges( - indoc! {" - one - two - threeˇ - four - fiveˇ - "}, - true, - ); - - let editor = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&text, cx); - build_editor(buffer, cx) - }); - - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); - editor.fold_ranges( - [ - Point::new(1, 0)..Point::new(2, 0), - Point::new(3, 0)..Point::new(4, 0), - ], - true, - cx, - ); - }); - - let cloned_editor = editor - .update(cx, |editor, cx| { - cx.open_window(Default::default(), |cx| cx.new_view(|cx| editor.clone(cx))) - }) - .unwrap(); - - let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); - let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)).unwrap(); - - assert_eq!( - cloned_editor - .update(cx, |e, cx| e.display_text(cx)) - .unwrap(), - editor.update(cx, |e, cx| e.display_text(cx)).unwrap() - ); - assert_eq!( - cloned_snapshot - .folds_in_range(0..text.len()) - .collect::>(), - snapshot.folds_in_range(0..text.len()).collect::>(), - ); - assert_set_eq!( - cloned_editor - .update(cx, |editor, cx| editor.selections.ranges::(cx)) - .unwrap(), - editor - .update(cx, |editor, cx| editor.selections.ranges(cx)) - .unwrap() - ); - assert_set_eq!( - cloned_editor - .update(cx, |e, cx| e.selections.display_ranges(cx)) - .unwrap(), - editor - .update(cx, |e, cx| e.selections.display_ranges(cx)) - .unwrap() - ); -} - -//todo!(editor navigate) -#[gpui::test] -async fn test_navigation_history(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - use workspace::item::Item; - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project, cx)); - let pane = workspace - .update(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - - _ = workspace.update(cx, |_v, cx| { - cx.new_view(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); - let mut editor = build_editor(buffer.clone(), cx); - let handle = cx.view(); - editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle))); - - fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option { - editor.nav_history.as_mut().unwrap().pop_backward(cx) - } - - // Move the cursor a small distance. - // Nothing is added to the navigation history. - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) - }); - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]) - }); - assert!(pop_history(&mut editor, cx).is_none()); - - // Move the cursor a large distance. - // The history can jump back to the previous position. - editor.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)]) - }); - let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item.id(), cx.entity_id()); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] - ); - assert!(pop_history(&mut editor, cx).is_none()); - - // 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.selections.display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - assert!(pop_history(&mut editor, cx).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.selections.display_ranges(cx), - &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)] - ); - let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item.id(), cx.entity_id()); - assert_eq!( - editor.selections.display_ranges(cx), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] - ); - assert!(pop_history(&mut editor, cx).is_none()); - - // Set scroll position to check later - editor.set_scroll_position(gpui::Point::::new(5.5, 5.5), cx); - let original_scroll_position = editor.scroll_manager.anchor(); - - // Jump to the end of the document and adjust scroll - editor.move_to_end(&MoveToEnd, cx); - editor.set_scroll_position(gpui::Point::::new(-2.5, -0.5), cx); - assert_ne!(editor.scroll_manager.anchor(), original_scroll_position); - - let nav_entry = pop_history(&mut editor, cx).unwrap(); - editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(editor.scroll_manager.anchor(), original_scroll_position); - - // Ensure we don't panic when navigation data contains invalid anchors *and* points. - let mut invalid_anchor = editor.scroll_manager.anchor().anchor; - invalid_anchor.text_anchor.buffer_id = Some(999); - let invalid_point = Point::new(9999, 0); - editor.navigate( - Box::new(NavigationData { - cursor_anchor: invalid_anchor, - cursor_position: invalid_point, - scroll_anchor: ScrollAnchor { - anchor: invalid_anchor, - offset: Default::default(), - }, - scroll_top_row: invalid_point.row, - }), - cx, - ); - assert_eq!( - editor.selections.display_ranges(cx), - &[editor.max_point(cx)..editor.max_point(cx)] - ); - assert_eq!( - editor.scroll_position(cx), - gpui::Point::new(0., editor.max_point(cx).row() as f32) - ); - - editor - }) - }); -} - -#[gpui::test] -fn test_cancel(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) - }); - - _ = view.update(cx, |view, cx| { - view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection( - DisplayPoint::new(1, 1), - 0, - gpui::Point::::default(), - cx, - ); - view.end_selection(cx); - - view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection( - DisplayPoint::new(0, 3), - 0, - gpui::Point::::default(), - cx, - ); - view.end_selection(cx); - assert_eq!( - view.selections.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.selections.display_ranges(cx), - [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)] - ); - }); - - _ = view.update(cx, |view, cx| { - view.cancel(&Cancel, cx); - assert_eq!( - view.selections.display_ranges(cx), - [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)] - ); - }); -} - -#[gpui::test] -fn test_fold_action(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple( - &" - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() { - 2 - } - - fn c() { - 3 - } - } - " - .unindent(), - cx, - ); - build_editor(buffer.clone(), cx) - }); - - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]); - }); - 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_lines(&UnfoldLines, cx); - assert_eq!( - view.display_text(cx), - " - impl Foo { - // Hello! - - fn a() { - 1 - } - - fn b() {⋯ - } - - fn c() {⋯ - } - } - " - .unindent(), - ); - - view.unfold_lines(&UnfoldLines, cx); - assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text()); - }); -} - -#[gpui::test] -fn test_move_cursor(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); - let view = cx.add_window(|cx| build_editor(buffer.clone(), cx)); - - _ = buffer.update(cx, |buffer, cx| { - buffer.edit( - vec![ - (Point::new(1, 0)..Point::new(1, 0), "\t"), - (Point::new(1, 1)..Point::new(1, 1), "\t"), - ], - None, - cx, - ); - }); - _ = view.update(cx, |view, cx| { - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] - ); - - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.move_to_end(&MoveToEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)] - ); - - view.move_to_beginning(&MoveToBeginning, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)] - ); - - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]); - }); - view.select_to_beginning(&SelectToBeginning, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)] - ); - - view.select_to_end(&SelectToEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)] - ); - }); -} - -#[gpui::test] -fn test_move_cursor_multibyte(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε", cx); - build_editor(buffer.clone(), 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), - ], - true, - cx, - ); - assert_eq!(view.display_text(cx), "ⓐⓑ⋯ⓔ\nab⋯e\nαβ⋯ε"); - - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ⋯".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab⋯e".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab⋯".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "a".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "α".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβ".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβ⋯".len())] - ); - view.move_right(&MoveRight, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβ⋯ε".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab⋯e".len())] - ); - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβ⋯ε".len())] - ); - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "ab⋯e".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐⓑ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "ⓐ".len())] - ); - view.move_left(&MoveLeft, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(0, "".len())] - ); - }); -} - -//todo!(finish editor tests) -#[gpui::test] -fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); - }); - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(1, "abcd".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβγ".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(3, "abcd".len())] - ); - - view.move_down(&MoveDown, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(3, "abcd".len())] - ); - - view.move_up(&MoveUp, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[empty_range(2, "αβγ".len())] - ); - }); -} - -#[gpui::test] -fn test_beginning_end_of_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\n def", cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4), - ]); - }); - }); - - _ = view.update(cx, |view, cx| { - view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx); - assert_eq!( - view.selections.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.selections.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.selections.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.selections.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.selections.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 { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.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 { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.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 { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.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 { - stop_at_soft_wraps: true, - }, - cx, - ); - assert_eq!( - view.selections.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.selections.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.selections.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 TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), - ]) - }); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", view, cx); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", view, cx); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", view, cx); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", view, cx); - - view.move_right(&MoveRight, cx); - view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); - assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); - - view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); - assert_selection_ranges("use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}", view, cx); - - view.select_to_next_word_end(&SelectToNextWordEnd, cx); - assert_selection_ranges("use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}", view, cx); - }); -} - -//todo!(finish editor tests) -#[gpui::test] -fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); - build_editor(buffer, cx) - }); - - _ = view.update(cx, |view, cx| { - view.set_wrap_width(Some(140.0.into()), cx); - assert_eq!( - view.display_text(cx), - "use one::{\n two::three::\n four::five\n};" - ); - - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]); - }); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] - ); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] - ); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] - ); - - view.move_to_next_word_end(&MoveToNextWordEnd, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] - ); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] - ); - - view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] - ); - }); -} - -//todo!(simulate_resize) -#[gpui::test] -async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - - let line_height = cx.editor(|editor, cx| { - editor - .style() - .unwrap() - .text - .line_height_in_pixels(cx.rem_size()) - }); - cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height)); - - cx.set_state( - &r#"ˇone - two - - three - fourˇ - five - - six"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - ˇ - three - four - five - ˇ - six"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - - three - four - five - ˇ - sixˇ"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - - three - four - five - - sixˇ"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - - three - four - five - ˇ - six"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"one - two - ˇ - three - four - five - - six"# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx)); - cx.assert_editor_state( - &r#"ˇone - two - - three - four - five - - six"# - .unindent(), - ); -} - -#[gpui::test] -async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - let line_height = cx.editor(|editor, cx| { - editor - .style() - .unwrap() - .text - .line_height_in_pixels(cx.rem_size()) - }); - let window = cx.window; - cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5))); - - cx.set_state( - &r#"ˇone - two - three - four - five - six - seven - eight - nine - ten - "#, - ); - - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 0.) - ); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.) - ); - editor.scroll_screen(&ScrollAmount::Page(1.), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 6.) - ); - editor.scroll_screen(&ScrollAmount::Page(-1.), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.) - ); - - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 1.) - ); - editor.scroll_screen(&ScrollAmount::Page(0.5), cx); - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.) - ); - }); -} - -#[gpui::test] -async fn test_autoscroll(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - - let line_height = cx.update_editor(|editor, cx| { - editor.set_vertical_scroll_margin(2, cx); - editor - .style() - .unwrap() - .text - .line_height_in_pixels(cx.rem_size()) - }); - let window = cx.window; - cx.simulate_window_resize(window, size(px(1000.), 6. * line_height)); - - cx.set_state( - &r#"ˇone - two - three - four - five - six - seven - eight - nine - ten - "#, - ); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 0.0) - ); - }); - - // Add a cursor below the visible area. Since both cursors cannot fit - // on screen, the editor autoscrolls to reveal the newest cursor, and - // allows the vertical scroll margin below that cursor. - cx.update_editor(|editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { - selections.select_ranges([ - Point::new(0, 0)..Point::new(0, 0), - Point::new(6, 0)..Point::new(6, 0), - ]); - }) - }); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 3.0) - ); - }); - - // Move down. The editor cursor scrolls down to track the newest cursor. - cx.update_editor(|editor, cx| { - editor.move_down(&Default::default(), cx); - }); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 4.0) - ); - }); - - // Add a cursor above the visible area. Since both cursors fit on screen, - // the editor scrolls to show both. - cx.update_editor(|editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { - selections.select_ranges([ - Point::new(1, 0)..Point::new(1, 0), - Point::new(6, 0)..Point::new(6, 0), - ]); - }) - }); - cx.update_editor(|editor, cx| { - assert_eq!( - editor.snapshot(cx).scroll_position(), - gpui::Point::new(0., 1.0) - ); - }); -} - -#[gpui::test] -async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - - let line_height = cx.editor(|editor, cx| { - editor - .style() - .unwrap() - .text - .line_height_in_pixels(cx.rem_size()) - }); - let window = cx.window; - cx.simulate_window_resize(window, size(px(100.), 4. * line_height)); - cx.set_state( - &r#" - ˇone - two - threeˇ - four - five - six - seven - eight - nine - ten - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); - cx.assert_editor_state( - &r#" - one - two - three - ˇfour - five - sixˇ - seven - eight - nine - ten - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx)); - cx.assert_editor_state( - &r#" - one - two - three - four - five - six - ˇseven - eight - nineˇ - ten - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); - cx.assert_editor_state( - &r#" - one - two - three - ˇfour - five - sixˇ - seven - eight - nine - ten - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx)); - cx.assert_editor_state( - &r#" - ˇone - two - threeˇ - four - five - six - seven - eight - nine - ten - "# - .unindent(), - ); - - // Test select collapsing - cx.update_editor(|editor, cx| { - editor.move_page_down(&MovePageDown::default(), cx); - editor.move_page_down(&MovePageDown::default(), cx); - editor.move_page_down(&MovePageDown::default(), cx); - }); - cx.assert_editor_state( - &r#" - one - two - three - four - five - six - seven - eight - nine - ˇten - ˇ"# - .unindent(), - ); -} - -#[gpui::test] -async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("one «two threeˇ» four"); - cx.update_editor(|editor, cx| { - editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx); - assert_eq!(editor.text(cx), " four"); - }); -} - -#[gpui::test] -fn test_delete_to_word_boundary(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), cx) - }); - - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.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), - ]) - }); - view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four"); - }); - - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.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), - ]) - }); - view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); - assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our"); - }); -} - -#[gpui::test] -fn test_newline(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), cx) - }); - - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.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), - ]) - }); - - 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 TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple( - " - a - b( - X - ) - c( - X - ) - " - .unindent() - .as_str(), - cx, - ); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(2, 4)..Point::new(2, 5), - Point::new(5, 4)..Point::new(5, 5), - ]) - }); - editor - }); - - _ = editor.update(cx, |editor, cx| { - // Edit the buffer directly, deleting ranges surrounding the editor's selections - editor.buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(1, 2)..Point::new(3, 0), ""), - (Point::new(4, 2)..Point::new(6, 0), ""), - ], - None, - cx, - ); - assert_eq!( - buffer.read(cx).text(), - " - a - b() - c() - " - .unindent() - ); - }); - assert_eq!( - editor.selections.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.selections.ranges(cx), - &[ - Point::new(2, 0)..Point::new(2, 0), - Point::new(4, 0)..Point::new(4, 0), - ], - ); - }); -} - -#[gpui::test] -async fn test_newline_above(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - ) - .with_indents_query(r#"(_ "(" ")" @end) @indent"#) - .unwrap(), - ); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - const a: ˇA = ( - (ˇ - «const_functionˇ»(ˇ), - so«mˇ»et«hˇ»ing_ˇelse,ˇ - )ˇ - ˇ);ˇ - "}); - - cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); - cx.assert_editor_state(indoc! {" - ˇ - const a: A = ( - ˇ - ( - ˇ - ˇ - const_function(), - ˇ - ˇ - ˇ - ˇ - something_else, - ˇ - ) - ˇ - ˇ - ); - "}); -} - -#[gpui::test] -async fn test_newline_below(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - ) - .with_indents_query(r#"(_ "(" ")" @end) @indent"#) - .unwrap(), - ); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - const a: ˇA = ( - (ˇ - «const_functionˇ»(ˇ), - so«mˇ»et«hˇ»ing_ˇelse,ˇ - )ˇ - ˇ);ˇ - "}); - - cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); - cx.assert_editor_state(indoc! {" - const a: A = ( - ˇ - ( - ˇ - const_function(), - ˇ - ˇ - something_else, - ˇ - ˇ - ˇ - ˇ - ) - ˇ - ); - ˇ - ˇ - "}); -} - -#[gpui::test] -async fn test_newline_comments(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("//".into()), - ..LanguageConfig::default() - }, - None, - )); - { - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - // Fooˇ - "}); - - cx.update_editor(|e, cx| e.newline(&Newline, cx)); - cx.assert_editor_state(indoc! {" - // Foo - //ˇ - "}); - // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. - cx.set_state(indoc! {" - ˇ// Foo - "}); - cx.update_editor(|e, cx| e.newline(&Newline, cx)); - cx.assert_editor_state(indoc! {" - - ˇ// Foo - "}); - } - // Ensure that comment continuations can be disabled. - update_test_language_settings(cx, |settings| { - settings.defaults.extend_comment_on_newline = Some(false); - }); - let mut cx = EditorTestContext::new(cx).await; - cx.set_state(indoc! {" - // Fooˇ - "}); - cx.update_editor(|e, cx| e.newline(&Newline, cx)); - cx.assert_editor_state(indoc! {" - // Foo - ˇ - "}); -} - -#[gpui::test] -fn test_insert_with_old_selections(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); - editor - }); - - _ = editor.update(cx, |editor, cx| { - // Edit the buffer directly, deleting ranges surrounding the editor's selections - editor.buffer.update(cx, |buffer, cx| { - buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); - assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); - }); - assert_eq!(editor.selections.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.selections.ranges(cx), &[3..3, 9..9, 15..15],); - }); -} - -#[gpui::test] -async fn test_tab(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(3) - }); - - let mut cx = EditorTestContext::new(cx).await; - cx.set_state(indoc! {" - ˇabˇc - ˇ🏀ˇ🏀ˇefg - dˇ - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - ˇab ˇc - ˇ🏀 ˇ🏀 ˇefg - d ˇ - "}); - - cx.set_state(indoc! {" - a - «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - a - «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ» - "}); -} - -#[gpui::test] -async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - ) - .with_indents_query(r#"(_ "(" ")" @end) @indent"#) - .unwrap(), - ); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - // cursors that are already at the suggested indent level insert - // a soft tab. cursors that are to the left of the suggested indent - // auto-indent their line. - cx.set_state(indoc! {" - ˇ - const a: B = ( - c( - d( - ˇ - ) - ˇ - ˇ ) - ); - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - ˇ - const a: B = ( - c( - d( - ˇ - ) - ˇ - ˇ) - ); - "}); - - // handle auto-indent when there are multiple cursors on the same line - cx.set_state(indoc! {" - const a: B = ( - c( - ˇ ˇ - ˇ ) - ); - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - const a: B = ( - c( - ˇ - ˇ) - ); - "}); -} - -#[gpui::test] -async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4) - }); - - let language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - ) - .with_indents_query(r#"(_ "{" "}" @end) @indent"#) - .unwrap(), - ); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - cx.set_state(indoc! {" - fn a() { - if b { - \t ˇc - } - } - "}); - - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - fn a() { - if b { - ˇc - } - } - "}); -} - -#[gpui::test] -async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.tab_size = NonZeroU32::new(4); - }); - - let mut cx = EditorTestContext::new(cx).await; - - cx.set_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - - // select across line ending - cx.set_state(indoc! {" - one two - t«hree - ˇ» four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - t«hree - ˇ» four - "}); - - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - t«hree - ˇ» four - "}); - - // Ensure that indenting/outdenting works when the cursor is at column 0. - cx.set_state(indoc! {" - one two - ˇthree - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); - - cx.set_state(indoc! {" - one two - ˇ three - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); -} - -#[gpui::test] -async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.hard_tabs = Some(true); - }); - - let mut cx = EditorTestContext::new(cx).await; - - // select two ranges on one line - cx.set_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - \t«oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - \t\t«oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - \t«oneˇ» «twoˇ» - three - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - «oneˇ» «twoˇ» - three - four - "}); - - // select across a line ending - cx.set_state(indoc! {" - one two - t«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - \tt«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - \t\tt«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - \tt«hree - ˇ»four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - t«hree - ˇ»four - "}); - - // Ensure that indenting/outdenting works when the cursor is at column 0. - cx.set_state(indoc! {" - one two - ˇthree - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); - cx.update_editor(|e, cx| e.tab(&Tab, cx)); - cx.assert_editor_state(indoc! {" - one two - \tˇthree - four - "}); - cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx)); - cx.assert_editor_state(indoc! {" - one two - ˇthree - four - "}); -} - -#[gpui::test] -fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { - init_test(cx, |settings| { - settings.languages.extend([ - ( - "TOML".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(2), - ..Default::default() - }, - ), - ( - "Rust".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(4), - ..Default::default() - }, - ), - ]); - }); - - let toml_language = Arc::new(Language::new( - LanguageConfig { - name: "TOML".into(), - ..Default::default() - }, - None, - )); - let rust_language = Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - ..Default::default() - }, - None, - )); - - let toml_buffer = cx.new_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n").with_language(toml_language, cx) - }); - let rust_buffer = cx.new_model(|cx| { - Buffer::new(0, cx.entity_id().as_u64(), "const c: usize = 3;\n") - .with_language(rust_language, cx) - }); - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - toml_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(2, 0), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - rust_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 0), - primary: None, - }], - cx, - ); - multibuffer - }); - - cx.add_window(|cx| { - let mut editor = build_editor(multibuffer, cx); - - assert_eq!( - editor.text(cx), - indoc! {" - a = 1 - b = 2 - - const c: usize = 3; - "} - ); - - select_ranges( - &mut editor, - indoc! {" - «aˇ» = 1 - b = 2 - - «const c:ˇ» usize = 3; - "}, - cx, - ); - - editor.tab(&Tab, cx); - assert_text_with_selections( - &mut editor, - indoc! {" - «aˇ» = 1 - b = 2 - - «const c:ˇ» usize = 3; - "}, - cx, - ); - editor.tab_prev(&TabPrev, cx); - assert_text_with_selections( - &mut editor, - indoc! {" - «aˇ» = 1 - b = 2 - - «const c:ˇ» usize = 3; - "}, - cx, - ); - - editor - }); -} - -#[gpui::test] -async fn test_backspace(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Basic backspace - cx.set_state(indoc! {" - onˇe two three - fou«rˇ» five six - seven «ˇeight nine - »ten - "}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state(indoc! {" - oˇe two three - fouˇ five six - seven ˇten - "}); - - // Test backspace inside and around indents - cx.set_state(indoc! {" - zero - ˇone - ˇtwo - ˇ ˇ ˇ three - ˇ ˇ four - "}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state(indoc! {" - zero - ˇone - ˇtwo - ˇ threeˇ four - "}); - - // Test backspace with line_mode set to true - cx.update_editor(|e, _| e.selections.line_mode = true); - cx.set_state(indoc! {" - The ˇquick ˇbrown - fox jumps over - the lazy dog - ˇThe qu«ick bˇ»rown"}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state(indoc! {" - ˇfox jumps over - the lazy dogˇ"}); -} - -#[gpui::test] -async fn test_delete(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - cx.set_state(indoc! {" - onˇe two three - fou«rˇ» five six - seven «ˇeight nine - »ten - "}); - cx.update_editor(|e, cx| e.delete(&Delete, cx)); - cx.assert_editor_state(indoc! {" - onˇ two three - fouˇ five six - seven ˇten - "}); - - // Test backspace with line_mode set to true - cx.update_editor(|e, _| e.selections.line_mode = true); - cx.set_state(indoc! {" - The ˇquick ˇbrown - fox «ˇjum»ps over - the lazy dog - ˇThe qu«ick bˇ»rown"}); - cx.update_editor(|e, cx| e.backspace(&Backspace, cx)); - cx.assert_editor_state("ˇthe lazy dogˇ"); -} - -#[gpui::test] -fn test_delete_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.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), - ]) - }); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi"); - assert_eq!( - view.selections.display_ranges(cx), - vec![ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1) - ] - ); - }); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) - }); - view.delete_line(&DeleteLine, cx); - assert_eq!(view.display_text(cx), "ghi\n"); - assert_eq!( - view.selections.display_ranges(cx), - vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)] - ); - }); -} - -//todo!(select_anchor_ranges) -#[gpui::test] -fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); - let mut editor = build_editor(buffer.clone(), cx); - let buffer = buffer.read(cx).as_singleton().unwrap(); - - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 0)..Point::new(0, 0)] - ); - - // When on single line, replace newline at end by space - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 3)..Point::new(0, 3)] - ); - - // When multiple lines are selected, remove newlines that are spanned by the selection - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) - }); - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 11)..Point::new(0, 11)] - ); - - // Undo should be transactional - editor.undo(&Undo, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 5)..Point::new(2, 2)] - ); - - // When joining an empty line don't insert a space - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) - }); - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); - assert_eq!( - editor.selections.ranges::(cx), - [Point::new(2, 3)..Point::new(2, 3)] - ); - - // We can remove trailing newlines - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); - assert_eq!( - editor.selections.ranges::(cx), - [Point::new(2, 3)..Point::new(2, 3)] - ); - - // We don't blow up on the last line - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); - assert_eq!( - editor.selections.ranges::(cx), - [Point::new(2, 3)..Point::new(2, 3)] - ); - - // reset to test indentation - editor.buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(1, 0)..Point::new(1, 2), " "), - (Point::new(2, 0)..Point::new(2, 3), " \n\td"), - ], - None, - cx, - ) - }); - - // We remove any leading spaces - assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) - }); - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); - - // We don't insert a space for a line containing only spaces - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); - - // We ignore any leading tabs - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); - - editor - }); -} - -#[gpui::test] -fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); - let mut editor = build_editor(buffer.clone(), cx); - let buffer = buffer.read(cx).as_singleton().unwrap(); - - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(0, 2)..Point::new(1, 1), - Point::new(1, 2)..Point::new(1, 2), - Point::new(3, 1)..Point::new(3, 2), - ]) - }); - - editor.join_lines(&JoinLines, cx); - assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); - - assert_eq!( - editor.selections.ranges::(cx), - [ - Point::new(0, 7)..Point::new(0, 7), - Point::new(1, 3)..Point::new(1, 3) - ] - ); - editor - }); -} - -#[gpui::test] -async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Test sort_lines_case_insensitive() - cx.set_state(indoc! {" - «z - y - x - Z - Y - Xˇ» - "}); - cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); - cx.assert_editor_state(indoc! {" - «x - X - y - Y - z - Zˇ» - "}); - - // Test reverse_lines() - cx.set_state(indoc! {" - «5 - 4 - 3 - 2 - 1ˇ» - "}); - cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); - cx.assert_editor_state(indoc! {" - «1 - 2 - 3 - 4 - 5ˇ» - "}); - - // Skip testing shuffle_line() - - // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() - // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) - - // Don't manipulate when cursor is on single line, but expand the selection - cx.set_state(indoc! {" - ddˇdd - ccc - bb - a - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «ddddˇ» - ccc - bb - a - "}); - - // Basic manipulate case - // Start selection moves to column 0 - // End of selection shrinks to fit shorter line - cx.set_state(indoc! {" - dd«d - ccc - bb - aaaaaˇ» - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «aaaaa - bb - ccc - dddˇ» - "}); - - // Manipulate case with newlines - cx.set_state(indoc! {" - dd«d - ccc - - bb - aaaaa - - ˇ» - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - « - - aaaaa - bb - ccc - dddˇ» - - "}); -} - -#[gpui::test] -async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Manipulate with multiple selections on a single line - cx.set_state(indoc! {" - dd«dd - cˇ»c«c - bb - aaaˇ»aa - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «aaaaa - bb - ccc - ddddˇ» - "}); - - // Manipulate with multiple disjoin selections - cx.set_state(indoc! {" - 5« - 4 - 3 - 2 - 1ˇ» - - dd«dd - ccc - bb - aaaˇ»aa - "}); - cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); - cx.assert_editor_state(indoc! {" - «1 - 2 - 3 - 4 - 5ˇ» - - «aaaaa - bb - ccc - ddddˇ» - "}); -} - -#[gpui::test] -async fn test_manipulate_text(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // Test convert_to_upper_case() - cx.set_state(indoc! {" - «hello worldˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «HELLO WORLDˇ» - "}); - - // Test convert_to_lower_case() - cx.set_state(indoc! {" - «HELLO WORLDˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); - cx.assert_editor_state(indoc! {" - «hello worldˇ» - "}); - - // Test multiple line, single selection case - // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary - cx.set_state(indoc! {" - «The quick brown - fox jumps over - the lazy dogˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx)); - cx.assert_editor_state(indoc! {" - «The Quick Brown - Fox Jumps Over - The Lazy Dogˇ» - "}); - - // Test multiple line, single selection case - // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary - cx.set_state(indoc! {" - «The quick brown - fox jumps over - the lazy dogˇ» - "}); - cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx)); - cx.assert_editor_state(indoc! {" - «TheQuickBrown - FoxJumpsOver - TheLazyDogˇ» - "}); - - // From here on out, test more complex cases of manipulate_text() - - // Test no selection case - should affect words cursors are in - // Cursor at beginning, middle, and end of word - cx.set_state(indoc! {" - ˇhello big beauˇtiful worldˇ - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» - "}); - - // Test multiple selections on a single line and across multiple lines - cx.set_state(indoc! {" - «Theˇ» quick «brown - foxˇ» jumps «overˇ» - the «lazyˇ» dog - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «THEˇ» quick «BROWN - FOXˇ» jumps «OVERˇ» - the «LAZYˇ» dog - "}); - - // Test case where text length grows - cx.set_state(indoc! {" - «tschüߡ» - "}); - cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); - cx.assert_editor_state(indoc! {" - «TSCHÜSSˇ» - "}); - - // Test to make sure we don't crash when text shrinks - cx.set_state(indoc! {" - aaa_bbbˇ - "}); - cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); - cx.assert_editor_state(indoc! {" - «aaaBbbˇ» - "}); - - // Test to make sure we all aware of the fact that each word can grow and shrink - // Final selections should be aware of this fact - cx.set_state(indoc! {" - aaa_bˇbb bbˇb_ccc ˇccc_ddd - "}); - cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); - cx.assert_editor_state(indoc! {" - «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» - "}); -} - -#[gpui::test] -fn test_duplicate_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ]) - }); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n"); - assert_eq!( - view.selections.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 view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1), - ]) - }); - view.duplicate_line(&DuplicateLine, cx); - assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n"); - assert_eq!( - view.selections.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 TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, 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), - ], - true, - cx, - ); - view.change_selections(None, cx, |s| { - s.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), - ]) - }); - 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.selections.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.selections.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.selections.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.selections.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 TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, cx) - }); - _ = editor.update(cx, |editor, cx| { - let snapshot = editor.buffer.read(cx).snapshot(cx); - editor.insert_blocks( - [BlockProperties { - style: BlockStyle::Fixed, - position: snapshot.anchor_after(Point::new(2, 0)), - disposition: BlockDisposition::Below, - height: 1, - render: Arc::new(|_| div().into_any()), - }], - Some(Autoscroll::fit()), - cx, - ); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) - }); - editor.move_line_down(&MoveLineDown, cx); - }); -} - -//todo!(test_transpose) -#[gpui::test] -fn test_transpose(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([1..1])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bac"); - assert_eq!(editor.selections.ranges(cx), [2..2]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bca"); - assert_eq!(editor.selections.ranges(cx), [3..3]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bac"); - assert_eq!(editor.selections.ranges(cx), [3..3]); - - editor - }); - - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([3..3])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acb\nde"); - assert_eq!(editor.selections.ranges(cx), [3..3]); - - editor.change_selections(None, cx, |s| s.select_ranges([4..4])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbd\ne"); - assert_eq!(editor.selections.ranges(cx), [5..5]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbde\n"); - assert_eq!(editor.selections.ranges(cx), [6..6]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbd\ne"); - assert_eq!(editor.selections.ranges(cx), [6..6]); - - editor - }); - - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bacd\ne"); - assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcade\n"); - assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcda\ne"); - assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcade\n"); - assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcaed\n"); - assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); - - editor - }); - - _ = cx.add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); - editor.set_style(EditorStyle::default(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([4..4])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "🏀🍐✋"); - assert_eq!(editor.selections.ranges(cx), [8..8]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "🏀✋🍐"); - assert_eq!(editor.selections.ranges(cx), [11..11]); - - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "🏀🍐✋"); - assert_eq!(editor.selections.ranges(cx), [11..11]); - - editor - }); -} - -#[gpui::test] -async fn test_clipboard(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); - cx.assert_editor_state("ˇtwo ˇfour ˇsix "); - - // Paste with three cursors. Each cursor pastes one slice of the clipboard text. - cx.set_state("two ˇfour ˇsix ˇ"); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ"); - - // 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. - cx.set_state("ˇtwo one✅ four three six five ˇ"); - cx.update_editor(|e, cx| { - e.handle_input("( ", cx); - e.paste(&Paste, cx); - e.handle_input(") ", cx); - }); - cx.assert_editor_state( - &([ - "( one✅ ", - "three ", - "five ) ˇtwo one✅ four three six five ( one✅ ", - "three ", - "five ) ˇ", - ] - .join("\n")), - ); - - // Cut with three selections, one of which is full-line. - cx.set_state(indoc! {" - 1«2ˇ»3 - 4ˇ567 - «8ˇ»9"}); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); - cx.assert_editor_state(indoc! {" - 1ˇ3 - ˇ9"}); - - // Paste with three selections, noticing how the copied selection that was full-line - // gets inserted before the second cursor. - cx.set_state(indoc! {" - 1ˇ3 - 9ˇ - «oˇ»ne"}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state(indoc! {" - 12ˇ3 - 4567 - 9ˇ - 8ˇne"}); - - // Copy with a single cursor only, which writes the whole line into the clipboard. - cx.set_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}); - cx.update_editor(|e, cx| e.copy(&Copy, cx)); - assert_eq!( - cx.read_from_clipboard().map(|item| item.text().to_owned()), - Some("fox jumps over\n".to_owned()) - ); - - // 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. - cx.set_state(indoc! {" - Tˇhe quick brown - «foˇ»x jumps over - tˇhe lazy dog"}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state(indoc! {" - fox jumps over - Tˇhe quick brown - fox jumps over - ˇx jumps over - fox jumps over - tˇhe lazy dog"}); -} - -#[gpui::test] -async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let language = Arc::new(Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::language()), - )); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - // Cut an indented block, without the leading whitespace. - cx.set_state(indoc! {" - const a: B = ( - c(), - «d( - e, - f - )ˇ» - ); - "}); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); - cx.assert_editor_state(indoc! {" - const a: B = ( - c(), - ˇ - ); - "}); - - // Paste it at the same position. - cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state(indoc! {" - const a: B = ( - c(), - d( - e, - f - )ˇ - ); - "}); - - // Paste it at a line with a lower indent level. - cx.set_state(indoc! {" - ˇ - const a: B = ( - c(), - ); - "}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state(indoc! {" - d( - e, - f - )ˇ - const a: B = ( - c(), - ); - "}); - - // Cut an indented block, with the leading whitespace. - cx.set_state(indoc! {" - const a: B = ( - c(), - « d( - e, - f - ) - ˇ»); - "}); - cx.update_editor(|e, cx| e.cut(&Cut, cx)); - cx.assert_editor_state(indoc! {" - const a: B = ( - c(), - ˇ); - "}); - - // Paste it at the same position. - cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state(indoc! {" - const a: B = ( - c(), - d( - e, - f - ) - ˇ); - "}); - - // Paste it at a line with a higher indent level. - cx.set_state(indoc! {" - const a: B = ( - c(), - d( - e, - fˇ - ) - ); - "}); - cx.update_editor(|e, cx| e.paste(&Paste, cx)); - cx.assert_editor_state(indoc! {" - const a: B = ( - c(), - d( - e, - f d( - e, - f - ) - ˇ - ) - ); - "}); -} - -#[gpui::test] -fn test_select_all(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.select_all(&SelectAll, cx); - assert_eq!( - view.selections.display_ranges(cx), - &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)] - ); - }); -} - -#[gpui::test] -fn test_select_line(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); - build_editor(buffer, cx) - }); - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2), - ]) - }); - view.select_line(&SelectLine, cx); - assert_eq!( - view.selections.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.selections.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.selections.display_ranges(cx), - vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)] - ); - }); -} - -#[gpui::test] -fn test_split_selection_into_lines(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let view = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); - build_editor(buffer, 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), - ], - true, - cx, - ); - view.change_selections(None, cx, |s| { - s.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), - ]) - }); - 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.selections.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.change_selections(None, cx, |s| { - s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)]) - }); - 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.selections.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] -async fn test_add_selection_above_below(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - // let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); - cx.set_state(indoc!( - r#"abc - defˇghi - - jk - nlmo - "# - )); - - cx.update_editor(|editor, cx| { - editor.add_selection_above(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abcˇ - defˇghi - - jk - nlmo - "# - )); - - cx.update_editor(|editor, cx| { - editor.add_selection_above(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abcˇ - defˇghi - - jk - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - defˇghi - - jk - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.undo_selection(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abcˇ - defˇghi - - jk - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.redo_selection(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - defˇghi - - jk - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - defˇghi - - jk - nlmˇo - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - defˇghi - - jk - nlmˇo - "# - )); - - // change selections - cx.set_state(indoc!( - r#"abc - def«ˇg»hi - - jk - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - def«ˇg»hi - - jk - nlm«ˇo» - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - def«ˇg»hi - - jk - nlm«ˇo» - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - def«ˇg»hi - - jk - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - def«ˇg»hi - - jk - nlmo - "# - )); - - // Change selections again - cx.set_state(indoc!( - r#"a«bc - defgˇ»hi - - jk - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"a«bcˇ» - d«efgˇ»hi - - j«kˇ» - nlmo - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - cx.assert_editor_state(indoc!( - r#"a«bcˇ» - d«efgˇ»hi - - j«kˇ» - n«lmoˇ» - "# - )); - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"a«bcˇ» - d«efgˇ»hi - - j«kˇ» - nlmo - "# - )); - - // Change selections again - cx.set_state(indoc!( - r#"abc - d«ˇefghi - - jk - nlm»o - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_above(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"a«ˇbc» - d«ˇef»ghi - - j«ˇk» - n«ˇlm»o - "# - )); - - cx.update_editor(|view, cx| { - view.add_selection_below(&Default::default(), cx); - }); - - cx.assert_editor_state(indoc!( - r#"abc - d«ˇef»ghi - - j«ˇk» - n«ˇlm»o - "# - )); -} - -#[gpui::test] -async fn test_select_next(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); -} - -#[gpui::test] -async fn test_select_previous(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - { - // `Select previous` without a selection (selects wordwise) - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\nˇabc abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»"); - } - { - // `Select previous` with a selection - let mut cx = EditorTestContext::new(cx).await; - cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - - cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc"); - - cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx)); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»"); - - cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx)) - .unwrap(); - cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»"); - } -} - -#[gpui::test] -async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - 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 - .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - - view.condition::(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25), - DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12), - DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18), - ]); - }); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| { view.selections.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(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - _ = view.update(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.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(cx, |view, cx| { - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)] - ); - - _ = view.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.display_ranges(cx)), - &[ - DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28), - DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0), - ] - ); - - _ = view.update(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.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(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.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(cx, |view, cx| { - view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.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(cx, |view, cx| { - view.fold_ranges( - vec![ - Point::new(0, 21)..Point::new(0, 24), - Point::new(3, 20)..Point::new(3, 22), - ], - true, - cx, - ); - view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx); - }); - assert_eq!( - view.update(cx, |view, cx| view.selections.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(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new( - Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: 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() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_indents_query( - r#" - (_ "(" ")" @end) @indent - (_ "{" "}" @end) @indent - "#, - ) - .unwrap(), - ); - - let text = "fn a() {}"; - - let buffer = cx - .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - editor - .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) - .await; - - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); - editor.newline(&Newline, cx); - assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); - assert_eq!( - editor.selections.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(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let language = Arc::new(Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/*".to_string(), - end: " */".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "[".to_string(), - end: "]".to_string(), - close: false, - newline: true, - }, - BracketPair { - start: "\"".to_string(), - end: "\"".to_string(), - close: true, - newline: false, - }, - ], - ..Default::default() - }, - autoclose_before: "})]".to_string(), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(language.clone()); - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(language), cx); - }); - - cx.set_state( - &r#" - 🏀ˇ - εˇ - ❤️ˇ - "# - .unindent(), - ); - - // autoclose multiple nested brackets at multiple cursors - cx.update_editor(|view, cx| { - view.handle_input("{", cx); - view.handle_input("{", cx); - view.handle_input("{", cx); - }); - cx.assert_editor_state( - &" - 🏀{{{ˇ}}} - ε{{{ˇ}}} - ❤️{{{ˇ}}} - " - .unindent(), - ); - - // insert a different closing bracket - cx.update_editor(|view, cx| { - view.handle_input(")", cx); - }); - cx.assert_editor_state( - &" - 🏀{{{)ˇ}}} - ε{{{)ˇ}}} - ❤️{{{)ˇ}}} - " - .unindent(), - ); - - // skip over the auto-closed brackets when typing a closing bracket - cx.update_editor(|view, cx| { - view.move_right(&MoveRight, cx); - view.handle_input("}", cx); - view.handle_input("}", cx); - view.handle_input("}", cx); - }); - cx.assert_editor_state( - &" - 🏀{{{)}}}}ˇ - ε{{{)}}}}ˇ - ❤️{{{)}}}}ˇ - " - .unindent(), - ); - - // autoclose multi-character pairs - cx.set_state( - &" - ˇ - ˇ - " - .unindent(), - ); - cx.update_editor(|view, cx| { - view.handle_input("/", cx); - view.handle_input("*", cx); - }); - cx.assert_editor_state( - &" - /*ˇ */ - /*ˇ */ - " - .unindent(), - ); - - // one cursor autocloses a multi-character pair, one cursor - // does not autoclose. - cx.set_state( - &" - /ˇ - ˇ - " - .unindent(), - ); - cx.update_editor(|view, cx| view.handle_input("*", cx)); - cx.assert_editor_state( - &" - /*ˇ */ - *ˇ - " - .unindent(), - ); - - // Don't autoclose if the next character isn't whitespace and isn't - // listed in the language's "autoclose_before" section. - cx.set_state("ˇa b"); - cx.update_editor(|view, cx| view.handle_input("{", cx)); - cx.assert_editor_state("{ˇa b"); - - // Don't autoclose if `close` is false for the bracket pair - cx.set_state("ˇ"); - cx.update_editor(|view, cx| view.handle_input("[", cx)); - cx.assert_editor_state("[ˇ"); - - // Surround with brackets if text is selected - cx.set_state("«aˇ» b"); - cx.update_editor(|view, cx| view.handle_input("{", cx)); - cx.assert_editor_state("{«aˇ»} b"); - - // Autclose pair where the start and end characters are the same - cx.set_state("aˇ"); - cx.update_editor(|view, cx| view.handle_input("\"", cx)); - cx.assert_editor_state("a\"ˇ\""); - cx.update_editor(|view, cx| view.handle_input("\"", cx)); - cx.assert_editor_state("a\"\"ˇ"); -} - -#[gpui::test] -async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let html_language = Arc::new( - Language::new( - LanguageConfig { - name: "HTML".into(), - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "<".into(), - end: ">".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "(".into(), - end: ")".into(), - close: true, - ..Default::default() - }, - ], - ..Default::default() - }, - autoclose_before: "})]>".into(), - ..Default::default() - }, - Some(tree_sitter_html::language()), - ) - .with_injection_query( - r#" - (script_element - (raw_text) @content - (#set! "language" "javascript")) - "#, - ) - .unwrap(), - ); - - let javascript_language = Arc::new(Language::new( - LanguageConfig { - name: "JavaScript".into(), - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "/*".into(), - end: " */".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - ..Default::default() - }, - BracketPair { - start: "(".into(), - end: ")".into(), - close: true, - ..Default::default() - }, - ], - ..Default::default() - }, - autoclose_before: "})]>".into(), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(html_language.clone()); - registry.add(javascript_language.clone()); - - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(html_language), cx); - }); - - cx.set_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - // Precondition: different languages are active at different locations. - cx.update_editor(|editor, cx| { - let snapshot = editor.snapshot(cx); - let cursors = editor.selections.ranges::(cx); - let languages = cursors - .iter() - .map(|c| snapshot.language_at(c.start).unwrap().name()) - .collect::>(); - assert_eq!( - languages, - &["HTML".into(), "JavaScript".into(), "HTML".into()] - ); - }); - - // Angle brackets autoclose in HTML, but not JavaScript. - cx.update_editor(|editor, cx| { - editor.handle_input("<", cx); - editor.handle_input("a", cx); - }); - cx.assert_editor_state( - &r#" - - - - "# - .unindent(), - ); - - // Curly braces and parens autoclose in both HTML and JavaScript. - cx.update_editor(|editor, cx| { - editor.handle_input(" b=", cx); - editor.handle_input("{", cx); - editor.handle_input("c", cx); - editor.handle_input("(", cx); - }); - cx.assert_editor_state( - &r#" - - - - "# - .unindent(), - ); - - // Brackets that were already autoclosed are skipped. - cx.update_editor(|editor, cx| { - editor.handle_input(")", cx); - editor.handle_input("d", cx); - editor.handle_input("}", cx); - }); - cx.assert_editor_state( - &r#" - - - - "# - .unindent(), - ); - cx.update_editor(|editor, cx| { - editor.handle_input(">", cx); - }); - cx.assert_editor_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - // Reset - cx.set_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.handle_input("<", cx); - }); - cx.assert_editor_state( - &r#" - <ˇ> - - <ˇ> - "# - .unindent(), - ); - - // When backspacing, the closing angle brackets are removed. - cx.update_editor(|editor, cx| { - editor.backspace(&Backspace, cx); - }); - cx.assert_editor_state( - &r#" - ˇ - - ˇ - "# - .unindent(), - ); - - // Block comments autoclose in JavaScript, but not HTML. - cx.update_editor(|editor, cx| { - editor.handle_input("/", cx); - editor.handle_input("*", cx); - }); - cx.assert_editor_state( - &r#" - /*ˇ - - /*ˇ - "# - .unindent(), - ); -} - -#[gpui::test] -async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let rust_language = Arc::new( - Language::new( - LanguageConfig { - name: "Rust".into(), - brackets: serde_json::from_value(json!([ - { "start": "{", "end": "}", "close": true, "newline": true }, - { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] }, - ])) - .unwrap(), - autoclose_before: "})]>".into(), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_override_query("(string_literal) @string") - .unwrap(), - ); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(rust_language.clone()); - - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(rust_language), cx); - }); - - cx.set_state( - &r#" - let x = ˇ - "# - .unindent(), - ); - - // Inserting a quotation mark. A closing quotation mark is automatically inserted. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = "ˇ" - "# - .unindent(), - ); - - // Inserting another quotation mark. The cursor moves across the existing - // automatically-inserted quotation mark. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = ""ˇ - "# - .unindent(), - ); - - // Reset - cx.set_state( - &r#" - let x = ˇ - "# - .unindent(), - ); - - // Inserting a quotation mark inside of a string. A second quotation mark is not inserted. - cx.update_editor(|editor, cx| { - editor.handle_input("\"", cx); - editor.handle_input(" ", cx); - editor.move_left(&Default::default(), cx); - editor.handle_input("\\", cx); - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = "\"ˇ " - "# - .unindent(), - ); - - // Inserting a closing quotation mark at the position of an automatically-inserted quotation - // mark. Nothing is inserted. - cx.update_editor(|editor, cx| { - editor.move_right(&Default::default(), cx); - editor.handle_input("\"", cx); - }); - cx.assert_editor_state( - &r#" - let x = "\" "ˇ - "# - .unindent(), - ); -} - -#[gpui::test] -async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new(Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![ - BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }, - BracketPair { - start: "/* ".to_string(), - end: "*/".to_string(), - close: true, - ..Default::default() - }, - ], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = r#" - a - b - c - "# - .unindent(); - - let buffer = cx - .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1), - ]) - }); - - view.handle_input("{", cx); - view.handle_input("{", cx); - view.handle_input("{", cx); - assert_eq!( - view.text(cx), - " - {{{a}}} - {{{b}}} - {{{c}}} - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4), - DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4) - ] - ); - - view.undo(&Undo, cx); - view.undo(&Undo, cx); - view.undo(&Undo, cx); - assert_eq!( - view.text(cx), - " - a - b - c - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) - ] - ); - - // Ensure inserting the first character of a multi-byte bracket pair - // doesn't surround the selections with the bracket. - view.handle_input("/", cx); - assert_eq!( - view.text(cx), - " - / - / - / - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) - ] - ); - - view.undo(&Undo, cx); - assert_eq!( - view.text(cx), - " - a - b - c - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1) - ] - ); - - // Ensure inserting the last character of a multi-byte bracket pair - // doesn't surround the selections with the bracket. - view.handle_input("*", cx); - assert_eq!( - view.text(cx), - " - * - * - * - " - .unindent() - ); - assert_eq!( - view.selections.display_ranges(cx), - [ - DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1), - DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1), - DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1) - ] - ); - }); -} - -#[gpui::test] -async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new(Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: vec![BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }], - ..Default::default() - }, - autoclose_before: "}".to_string(), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let text = r#" - a - b - c - "# - .unindent(); - - let buffer = cx - .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - editor - .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 1)..Point::new(2, 1), - ]) - }); - - editor.handle_input("{", cx); - editor.handle_input("{", cx); - editor.handle_input("_", cx); - assert_eq!( - editor.text(cx), - " - a{{_}} - b{{_}} - c{{_}} - " - .unindent() - ); - assert_eq!( - editor.selections.ranges::(cx), - [ - Point::new(0, 4)..Point::new(0, 4), - Point::new(1, 4)..Point::new(1, 4), - Point::new(2, 4)..Point::new(2, 4) - ] - ); - - editor.backspace(&Default::default(), cx); - editor.backspace(&Default::default(), cx); - assert_eq!( - editor.text(cx), - " - a{} - b{} - c{} - " - .unindent() - ); - assert_eq!( - editor.selections.ranges::(cx), - [ - Point::new(0, 2)..Point::new(0, 2), - Point::new(1, 2)..Point::new(1, 2), - Point::new(2, 2)..Point::new(2, 2) - ] - ); - - editor.delete_to_previous_word_start(&Default::default(), cx); - assert_eq!( - editor.text(cx), - " - a - b - c - " - .unindent() - ); - assert_eq!( - editor.selections.ranges::(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - Point::new(2, 1)..Point::new(2, 1) - ] - ); - }); -} - -// todo!(select_anchor_ranges) -#[gpui::test] -async fn test_snippets(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let (text, insertion_ranges) = marked_text_ranges( - indoc! {" - a.ˇ b - a.ˇ b - a.ˇ b - "}, - false, - ); - - let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - - _ = editor.update(cx, |editor, cx| { - let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); - - editor - .insert_snippet(&insertion_ranges, snippet, cx) - .unwrap(); - - fn assert(editor: &mut Editor, cx: &mut ViewContext, marked_text: &str) { - let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); - assert_eq!(editor.text(cx), expected_text); - assert_eq!(editor.selections.ranges::(cx), selection_ranges); - } - - assert( - editor, - cx, - indoc! {" - a.f(«one», two, «three») b - a.f(«one», two, «three») b - a.f(«one», two, «three») b - "}, - ); - - // Can't move earlier than the first tab stop - assert!(!editor.move_to_prev_snippet_tabstop(cx)); - assert( - editor, - cx, - indoc! {" - a.f(«one», two, «three») b - a.f(«one», two, «three») b - a.f(«one», two, «three») b - "}, - ); - - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert( - editor, - cx, - indoc! {" - a.f(one, «two», three) b - a.f(one, «two», three) b - a.f(one, «two», three) b - "}, - ); - - editor.move_to_prev_snippet_tabstop(cx); - assert( - editor, - cx, - indoc! {" - a.f(«one», two, «three») b - a.f(«one», two, «three») b - a.f(«one», two, «three») b - "}, - ); - - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert( - editor, - cx, - indoc! {" - a.f(one, «two», three) b - a.f(one, «two», three) b - a.f(one, «two», three) b - "}, - ); - assert!(editor.move_to_next_snippet_tabstop(cx)); - assert( - editor, - cx, - indoc! {" - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - "}, - ); - - // As soon as the last tab stop is reached, snippet state is gone - editor.move_to_prev_snippet_tabstop(cx); - assert( - editor, - cx, - indoc! {" - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - a.f(one, two, three)ˇ b - "}, - ); - }); -} - -#[gpui::test] -async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) - .unwrap(); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 4); - Ok(Some(vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), - ", ".to_string(), - )])) - }) - .next() - .await; - cx.executor().start_waiting(); - let _x = save.await; - - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one, two\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - // Ensure we can still save even if formatting hangs. - fake_server.handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - futures::future::pending::<()>().await; - unreachable!() - }); - let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) - .unwrap(); - cx.executor().advance_clock(super::FORMAT_TIMEOUT); - cx.executor().start_waiting(); - save.await; - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one\ntwo\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - // Set rust language override and assert overridden tabsize is sent to language server - update_test_language_settings(cx, |settings| { - settings.languages.insert( - "Rust".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(8), - ..Default::default() - }, - ); - }); - - let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) - .unwrap(); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 8); - Ok(Some(vec![])) - }) - .next() - .await; - cx.executor().start_waiting(); - save.await; -} - -#[gpui::test] -async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_range_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) - .unwrap(); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 4); - Ok(Some(vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), - ", ".to_string(), - )])) - }) - .next() - .await; - cx.executor().start_waiting(); - save.await; - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one, two\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - assert!(cx.read(|cx| editor.is_dirty(cx))); - - // Ensure we can still save even if formatting hangs. - fake_server.handle_request::( - move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - futures::future::pending::<()>().await; - unreachable!() - }, - ); - let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) - .unwrap(); - cx.executor().advance_clock(super::FORMAT_TIMEOUT); - cx.executor().start_waiting(); - save.await; - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one\ntwo\nthree\n" - ); - assert!(!cx.read(|cx| editor.is_dirty(cx))); - - // Set rust language override and assert overridden tabsize is sent to language server - update_test_language_settings(cx, |settings| { - settings.languages.insert( - "Rust".into(), - LanguageSettingsContent { - tab_size: NonZeroU32::new(8), - ..Default::default() - }, - ); - }); - - let save = editor - .update(cx, |editor, cx| editor.save(project.clone(), cx)) - .unwrap(); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 8); - Ok(Some(vec![])) - }) - .next() - .await; - cx.executor().start_waiting(); - save.await; -} - -#[gpui::test] -async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer) - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - // Enable Prettier formatting for the same buffer, and ensure - // LSP is called instead of Prettier. - prettier_parser_name: Some("test_parser".to_string()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - _ = project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - - let format = editor - .update(cx, |editor, cx| { - editor.perform_format(project.clone(), FormatTrigger::Manual, cx) - }) - .unwrap(); - fake_server - .handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - assert_eq!(params.options.tab_size, 4); - Ok(Some(vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), - ", ".to_string(), - )])) - }) - .next() - .await; - cx.executor().start_waiting(); - format.await; - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one, two\nthree\n" - ); - - _ = editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); - // Ensure we don't lock if formatting hangs. - fake_server.handle_request::(move |params, _| async move { - assert_eq!( - params.text_document.uri, - lsp::Url::from_file_path("/file.rs").unwrap() - ); - futures::future::pending::<()>().await; - unreachable!() - }); - let format = editor - .update(cx, |editor, cx| { - editor.perform_format(project, FormatTrigger::Manual, cx) - }) - .unwrap(); - cx.executor().advance_clock(super::FORMAT_TIMEOUT); - cx.executor().start_waiting(); - format.await; - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - "one\ntwo\nthree\n" - ); -} - -#[gpui::test] -async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {" - one.twoˇ - "}); - - // The format request takes a long time. When it completes, it inserts - // a newline and an indent before the `.` - cx.lsp - .handle_request::(move |_, cx| { - let executor = cx.background_executor().clone(); - async move { - executor.timer(Duration::from_millis(100)).await; - Ok(Some(vec![lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)), - new_text: "\n ".into(), - }])) - } - }); - - // Submit a format request. - let format_1 = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) - .unwrap(); - cx.executor().run_until_parked(); - - // Submit a second format request. - let format_2 = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) - .unwrap(); - cx.executor().run_until_parked(); - - // Wait for both format requests to complete - cx.executor().advance_clock(Duration::from_millis(200)); - cx.executor().start_waiting(); - format_1.await.unwrap(); - cx.executor().start_waiting(); - format_2.await.unwrap(); - - // The formatting edits only happens once. - cx.assert_editor_state(indoc! {" - one - .twoˇ - "}); -} - -#[gpui::test] -async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::Auto) - }); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - cx, - ) - .await; - - // Set up a buffer white some trailing whitespace and no trailing newline. - cx.set_state( - &[ - "one ", // - "twoˇ", // - "three ", // - "four", // - ] - .join("\n"), - ); - - // Submit a format request. - let format = cx - .update_editor(|editor, cx| editor.format(&Format, cx)) - .unwrap(); - - // Record which buffer changes have been sent to the language server - let buffer_changes = Arc::new(Mutex::new(Vec::new())); - cx.lsp - .handle_notification::({ - let buffer_changes = buffer_changes.clone(); - move |params, _| { - buffer_changes.lock().extend( - params - .content_changes - .into_iter() - .map(|e| (e.range.unwrap(), e.text)), - ); - } - }); - - // Handle formatting requests to the language server. - cx.lsp.handle_request::({ - let buffer_changes = buffer_changes.clone(); - move |_, _| { - // When formatting is requested, trailing whitespace has already been stripped, - // and the trailing newline has already been added. - assert_eq!( - &buffer_changes.lock()[1..], - &[ - ( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)), - "".into() - ), - ( - lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)), - "".into() - ), - ( - lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)), - "\n".into() - ), - ] - ); - - // Insert blank lines between each line of the buffer. - async move { - Ok(Some(vec![ - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), - new_text: "\n".into(), - }, - lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)), - new_text: "\n".into(), - }, - ])) - } - } - }); - - // After formatting the buffer, the trailing whitespace is stripped, - // a newline is appended, and the edits provided by the language server - // have been applied. - format.await.unwrap(); - cx.assert_editor_state( - &[ - "one", // - "", // - "twoˇ", // - "", // - "three", // - "four", // - "", // - ] - .join("\n"), - ); - - // Undoing the formatting undoes the trailing whitespace removal, the - // trailing newline, and the LSP edits. - cx.update_buffer(|buffer, cx| buffer.undo(cx)); - cx.assert_editor_state( - &[ - "one ", // - "twoˇ", // - "three ", // - "four", // - ] - .join("\n"), - ); -} - -#[gpui::test] -async fn test_completion(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {" - oneˇ - two - three - "}); - cx.simulate_keystroke("."); - handle_completion_request( - &mut cx, - indoc! {" - one.|<> - two - three - "}, - vec!["first_completion", "second_completion"], - ) - .await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor.context_menu_next(&Default::default(), cx); - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state(indoc! {" - one.second_completionˇ - two - three - "}); - - handle_resolve_completion_request( - &mut cx, - Some(vec![ - ( - //This overlaps with the primary completion edit which is - //misbehavior from the LSP spec, test that we filter it out - indoc! {" - one.second_ˇcompletion - two - threeˇ - "}, - "overlapping additional edit", - ), - ( - indoc! {" - one.second_completion - two - threeˇ - "}, - "\nadditional edit", - ), - ]), - ) - .await; - apply_additional_edits.await.unwrap(); - cx.assert_editor_state(indoc! {" - one.second_completionˇ - two - three - additional edit - "}); - - cx.set_state(indoc! {" - one.second_completion - twoˇ - threeˇ - additional edit - "}); - cx.simulate_keystroke(" "); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - cx.simulate_keystroke("s"); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - - cx.assert_editor_state(indoc! {" - one.second_completion - two sˇ - three sˇ - additional edit - "}); - handle_completion_request( - &mut cx, - indoc! {" - one.second_completion - two s - three - additional edit - "}, - vec!["fourth_completion", "fifth_completion", "sixth_completion"], - ) - .await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - - cx.simulate_keystroke("i"); - - handle_completion_request( - &mut cx, - indoc! {" - one.second_completion - two si - three - additional edit - "}, - vec!["fourth_completion", "fifth_completion", "sixth_completion"], - ) - .await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state(indoc! {" - one.second_completion - two sixth_completionˇ - three sixth_completionˇ - additional edit - "}); - - handle_resolve_completion_request(&mut cx, None).await; - apply_additional_edits.await.unwrap(); - - _ = cx.update(|cx| { - cx.update_global::(|settings, cx| { - settings.update_user_settings::(cx, |settings| { - settings.show_completions_on_input = Some(false); - }); - }) - }); - cx.set_state("editorˇ"); - cx.simulate_keystroke("."); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - cx.simulate_keystroke("c"); - cx.simulate_keystroke("l"); - cx.simulate_keystroke("o"); - cx.assert_editor_state("editor.cloˇ"); - assert!(cx.editor(|e, _| e.context_menu.read().is_none())); - cx.update_editor(|editor, cx| { - editor.show_completions(&ShowCompletions, cx); - }); - handle_completion_request(&mut cx, "editor.", vec!["close", "clobber"]).await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state("editor.closeˇ"); - handle_resolve_completion_request(&mut cx, None).await; - apply_additional_edits.await.unwrap(); -} - -#[gpui::test] -async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("// ".into()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - // If multiple selections intersect a line, the line is only toggled once. - cx.set_state(indoc! {" - fn a() { - «//b(); - ˇ»// «c(); - //ˇ» d(); - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - «b(); - c(); - ˇ» d(); - } - "}); - - // The comment prefix is inserted at the same column for every line in a - // selection. - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - // «b(); - // c(); - ˇ»// d(); - } - "}); - - // If a selection ends at the beginning of a line, that line is not toggled. - cx.set_selections_state(indoc! {" - fn a() { - // b(); - «// c(); - ˇ» // d(); - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - // b(); - «c(); - ˇ» // d(); - } - "}); - - // If a selection span a single line and is empty, the line is toggled. - cx.set_state(indoc! {" - fn a() { - a(); - b(); - ˇ - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - a(); - b(); - //•ˇ - } - "}); - - // If a selection span multiple lines, empty lines are not toggled. - cx.set_state(indoc! {" - fn a() { - «a(); - - c();ˇ» - } - "}); - - cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx)); - - cx.assert_editor_state(indoc! {" - fn a() { - // «a(); - - // c();ˇ» - } - "}); -} - -#[gpui::test] -async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new(Language::new( - LanguageConfig { - line_comment: Some("// ".into()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(language.clone()); - - let mut cx = EditorTestContext::new(cx).await; - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(language), cx); - }); - - let toggle_comments = &ToggleComments { - advance_downwards: true, - }; - - // Single cursor on one line -> advance - // Cursor moves horizontally 3 characters as well on non-blank line - cx.set_state(indoc!( - "fn a() { - ˇdog(); - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - catˇ(); - }" - )); - - // Single selection on one line -> don't advance - cx.set_state(indoc!( - "fn a() { - «dog()ˇ»; - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // «dog()ˇ»; - cat(); - }" - )); - - // Multiple cursors on one line -> advance - cx.set_state(indoc!( - "fn a() { - ˇdˇog(); - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - catˇ(ˇ); - }" - )); - - // Multiple cursors on one line, with selection -> don't advance - cx.set_state(indoc!( - "fn a() { - ˇdˇog«()ˇ»; - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // ˇdˇog«()ˇ»; - cat(); - }" - )); - - // Single cursor on one line -> advance - // Cursor moves to column 0 on blank line - cx.set_state(indoc!( - "fn a() { - ˇdog(); - - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - ˇ - cat(); - }" - )); - - // Single cursor on one line -> advance - // Cursor starts and ends at column 0 - cx.set_state(indoc!( - "fn a() { - ˇ dog(); - cat(); - }" - )); - cx.update_editor(|editor, cx| { - editor.toggle_comments(toggle_comments, cx); - }); - cx.assert_editor_state(indoc!( - "fn a() { - // dog(); - ˇ cat(); - }" - )); -} - -#[gpui::test] -async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let html_language = Arc::new( - Language::new( - LanguageConfig { - name: "HTML".into(), - block_comment: Some(("".into())), - ..Default::default() - }, - Some(tree_sitter_html::language()), - ) - .with_injection_query( - r#" - (script_element - (raw_text) @content - (#set! "language" "javascript")) - "#, - ) - .unwrap(), - ); - - let javascript_language = Arc::new(Language::new( - LanguageConfig { - name: "JavaScript".into(), - line_comment: Some("// ".into()), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - )); - - let registry = Arc::new(LanguageRegistry::test()); - registry.add(html_language.clone()); - registry.add(javascript_language.clone()); - - cx.update_buffer(|buffer, cx| { - buffer.set_language_registry(registry); - buffer.set_language(Some(html_language), cx); - }); - - // Toggle comments for empty selections - cx.set_state( - &r#" -

A

ˇ -

B

ˇ -

C

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

A

ˇ -

B

ˇ -

C

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

-

ˇ»B

ˇ -

-

ˇ»D

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

-

ˇ»B

ˇ -

-

ˇ»D

ˇ - "# - .unindent(), - ); - - // Toggle comments when different languages are active for different - // selections. - cx.set_state( - &r#" - ˇ - "# - .unindent(), - ); - cx.executor().run_until_parked(); - cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx)); - cx.assert_editor_state( - &r#" - - // ˇvar x = new Y(); - - "# - .unindent(), - ); -} - -#[gpui::test] -fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(0, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(1, 0)..Point::new(1, 4), - primary: None, - }, - ], - cx, - ); - assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb"); - multibuffer - }); - - let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); - _ = view.update(cx, |view, cx| { - assert_eq!(view.text(cx), "aaaa\nbbbb"); - view.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(0, 0)..Point::new(0, 0), - Point::new(1, 0)..Point::new(1, 0), - ]) - }); - - view.handle_input("X", cx); - assert_eq!(view.text(cx), "Xaaaa\nXbbbb"); - assert_eq!( - view.selections.ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(1, 1)..Point::new(1, 1), - ] - ); - - // Ensure the cursor's head is respected when deleting across an excerpt boundary. - view.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 2)..Point::new(1, 2)]) - }); - view.backspace(&Default::default(), cx); - assert_eq!(view.text(cx), "Xa\nbbb"); - assert_eq!( - view.selections.ranges(cx), - [Point::new(1, 0)..Point::new(1, 0)] - ); - - view.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) - }); - view.backspace(&Default::default(), cx); - assert_eq!(view.text(cx), "X\nbb"); - assert_eq!( - view.selections.ranges(cx), - [Point::new(0, 1)..Point::new(0, 1)] - ); - }); -} - -#[gpui::test] -fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let markers = vec![('[', ']').into(), ('(', ')').into()]; - let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( - indoc! {" - [aaaa - (bbbb] - cccc)", - }, - markers.clone(), - ); - let excerpt_ranges = markers.into_iter().map(|marker| { - let context = excerpt_ranges.remove(&marker).unwrap()[0].clone(); - ExcerptRange { - context, - primary: None, - } - }); - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text)); - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts(buffer, excerpt_ranges, cx); - multibuffer - }); - - let (view, cx) = cx.add_window_view(|cx| build_editor(multibuffer, cx)); - _ = view.update(cx, |view, cx| { - let (expected_text, selection_ranges) = marked_text_ranges( - indoc! {" - aaaa - bˇbbb - bˇbbˇb - cccc" - }, - true, - ); - assert_eq!(view.text(cx), expected_text); - view.change_selections(None, cx, |s| s.select_ranges(selection_ranges)); - - view.handle_input("X", cx); - - let (expected_text, expected_selections) = marked_text_ranges( - indoc! {" - aaaa - bXˇbbXb - bXˇbbXˇb - cccc" - }, - false, - ); - assert_eq!(view.text(cx), expected_text); - assert_eq!(view.selections.ranges(cx), expected_selections); - - view.newline(&Newline, cx); - let (expected_text, expected_selections) = marked_text_ranges( - indoc! {" - aaaa - bX - ˇbbX - b - bX - ˇbbX - ˇb - cccc" - }, - false, - ); - assert_eq!(view.text(cx), expected_text); - assert_eq!(view.selections.ranges(cx), expected_selections); - }); -} - -#[gpui::test] -fn test_refresh_selections(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); - let mut excerpt1_id = None; - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - excerpt1_id = multibuffer - .push_excerpts( - buffer.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(1, 0)..Point::new(2, 4), - primary: None, - }, - ], - cx, - ) - .into_iter() - .next(); - assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); - multibuffer - }); - - let editor = cx.add_window(|cx| { - let mut editor = build_editor(multibuffer.clone(), cx); - let snapshot = editor.snapshot(cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) - }); - editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); - assert_eq!( - editor.selections.ranges(cx), - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ] - ); - editor - }); - - // Refreshing selections is a no-op when excerpts haven't changed. - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| s.refresh()); - assert_eq!( - editor.selections.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.selections.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.change_selections(None, cx, |s| s.refresh()); - assert_eq!( - editor.selections.ranges(cx), - [ - Point::new(0, 1)..Point::new(0, 1), - Point::new(0, 3)..Point::new(0, 3) - ] - ); - assert!(editor.selections.pending_anchor().is_some()); - }); -} - -#[gpui::test] -fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a'))); - let mut excerpt1_id = None; - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - excerpt1_id = multibuffer - .push_excerpts( - buffer.clone(), - [ - ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 4), - primary: None, - }, - ExcerptRange { - context: Point::new(1, 0)..Point::new(2, 4), - primary: None, - }, - ], - cx, - ) - .into_iter() - .next(); - assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc"); - multibuffer - }); - - let editor = cx.add_window(|cx| { - let mut editor = build_editor(multibuffer.clone(), cx); - let snapshot = editor.snapshot(cx); - editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); - assert_eq!( - editor.selections.ranges(cx), - [Point::new(1, 3)..Point::new(1, 3)] - ); - editor - }); - - _ = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); - }); - _ = editor.update(cx, |editor, cx| { - assert_eq!( - editor.selections.ranges(cx), - [Point::new(0, 0)..Point::new(0, 0)] - ); - - // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. - editor.change_selections(None, cx, |s| s.refresh()); - assert_eq!( - editor.selections.ranges(cx), - [Point::new(0, 3)..Point::new(0, 3)] - ); - assert!(editor.selections.pending_anchor().is_some()); - }); -} - -#[gpui::test] -async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language = Arc::new( - Language::new( - LanguageConfig { - brackets: BracketPairConfig { - pairs: 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() - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ) - .with_indents_query("") - .unwrap(), - ); - - let text = concat!( - "{ }\n", // - " x\n", // - " /* */\n", // - "x\n", // - "{{} }\n", // - ); - - let buffer = cx - .new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)); - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) - .await; - - _ = view.update(cx, |view, cx| { - view.change_selections(None, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3), - DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), - DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4), - ]) - }); - 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 TestAppContext) { - init_test(cx, |_| {}); - - let editor = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), cx) - }); - - _ = editor.update(cx, |editor, cx| { - struct Type1; - struct Type2; - - let buffer = editor.buffer.read(cx).snapshot(cx); - - let anchor_range = - |range: Range| buffer.anchor_after(range.start)..buffer.anchor_after(range.end); - - editor.highlight_background::( - 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)), - ], - |_| Hsla::red(), - cx, - ); - editor.highlight_background::( - 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)), - ], - |_| Hsla::green(), - cx, - ); - - let snapshot = editor.snapshot(cx); - let mut highlighted_ranges = editor.background_highlights_in_range( - anchor_range(Point::new(3, 4)..Point::new(7, 4)), - &snapshot, - cx.theme().colors(), - ); - // Enforce a consistent ordering based on color without relying on the ordering of the - // highlight's `TypeId` which is non-executor. - highlighted_ranges.sort_unstable_by_key(|(_, color)| *color); - assert_eq!( - highlighted_ranges, - &[ - ( - DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4), - Hsla::red(), - ), - ( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Hsla::red(), - ), - ( - DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5), - Hsla::green(), - ), - ( - DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6), - Hsla::green(), - ), - ] - ); - assert_eq!( - editor.background_highlights_in_range( - anchor_range(Point::new(5, 6)..Point::new(6, 4)), - &snapshot, - cx.theme().colors(), - ), - &[( - DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), - Hsla::red(), - )] - ); - }); -} - -// todo!(following) -#[gpui::test] -async fn test_following(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - - let buffer = project.update(cx, |project, cx| { - let buffer = project - .create_buffer(&sample_text(16, 8, 'a'), None, cx) - .unwrap(); - cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)) - }); - let leader = cx.add_window(|cx| build_editor(buffer.clone(), cx)); - let follower = cx.update(|cx| { - cx.open_window( - WindowOptions { - bounds: WindowBounds::Fixed(Bounds::from_corners( - gpui::Point::new((0. as f64).into(), (0. as f64).into()), - gpui::Point::new((10. as f64).into(), (80. as f64).into()), - )), - ..Default::default() - }, - |cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)), - ) - }); - - let is_still_following = Rc::new(RefCell::new(true)); - let follower_edit_event_count = Rc::new(RefCell::new(0)); - let pending_update = Rc::new(RefCell::new(None)); - _ = follower.update(cx, { - let update = pending_update.clone(); - let is_still_following = is_still_following.clone(); - let follower_edit_event_count = follower_edit_event_count.clone(); - |_, cx| { - cx.subscribe( - &leader.root_view(cx).unwrap(), - move |_, leader, event, cx| { - leader - .read(cx) - .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); - }, - ) - .detach(); - - cx.subscribe( - &follower.root_view(cx).unwrap(), - move |_, _, event: &EditorEvent, _cx| { - if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) { - *is_still_following.borrow_mut() = false; - } - - if let EditorEvent::BufferEdited = event { - *follower_edit_event_count.borrow_mut() += 1; - } - }, - ) - .detach(); - } - }); - - // Update the selections only - _ = leader.update(cx, |leader, cx| { - leader.change_selections(None, cx, |s| s.select_ranges([1..1])); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .unwrap() - .await - .unwrap(); - _ = follower.update(cx, |follower, cx| { - assert_eq!(follower.selections.ranges(cx), vec![1..1]); - }); - assert_eq!(*is_still_following.borrow(), true); - assert_eq!(*follower_edit_event_count.borrow(), 0); - - // Update the scroll position only - _ = leader.update(cx, |leader, cx| { - leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - follower - .update(cx, |follower, cx| follower.scroll_position(cx)) - .unwrap(), - gpui::Point::new(1.5, 3.5) - ); - assert_eq!(*is_still_following.borrow(), true); - assert_eq!(*follower_edit_event_count.borrow(), 0); - - // Update the selections and scroll position. The follower's scroll position is updated - // via autoscroll, not via the leader's exact scroll position. - _ = leader.update(cx, |leader, cx| { - leader.change_selections(None, cx, |s| s.select_ranges([0..0])); - leader.request_autoscroll(Autoscroll::newest(), cx); - leader.set_scroll_position(gpui::Point::new(1.5, 3.5), cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .unwrap() - .await - .unwrap(); - _ = follower.update(cx, |follower, cx| { - assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0)); - assert_eq!(follower.selections.ranges(cx), vec![0..0]); - }); - assert_eq!(*is_still_following.borrow(), true); - - // Creating a pending selection that precedes another selection - _ = leader.update(cx, |leader, cx| { - leader.change_selections(None, cx, |s| s.select_ranges([1..1])); - leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .unwrap() - .await - .unwrap(); - _ = follower.update(cx, |follower, cx| { - assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); - }); - assert_eq!(*is_still_following.borrow(), true); - - // Extend the pending selection so that it surrounds another selection - _ = leader.update(cx, |leader, cx| { - leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); - }); - follower - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx) - }) - .unwrap() - .await - .unwrap(); - _ = follower.update(cx, |follower, cx| { - assert_eq!(follower.selections.ranges(cx), vec![0..2]); - }); - - // Scrolling locally breaks the follow - _ = follower.update(cx, |follower, cx| { - let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0); - follower.set_scroll_anchor( - ScrollAnchor { - anchor: top_anchor, - offset: gpui::Point::new(0.0, 0.5), - }, - cx, - ); - }); - assert_eq!(*is_still_following.borrow(), false); -} - -#[gpui::test] -async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let pane = workspace - .update(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - - let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx); - - let leader = pane.update(cx, |_, cx| { - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - cx.new_view(|cx| build_editor(multibuffer.clone(), cx)) - }); - - // Start following the editor when it has no excerpts. - let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); - let follower_1 = cx - .update_window(*workspace.deref(), |_, cx| { - Editor::from_state_proto( - pane.clone(), - workspace.root_view(cx).unwrap(), - ViewId { - creator: Default::default(), - id: 0, - }, - &mut state_message, - cx, - ) - }) - .unwrap() - .unwrap() - .await - .unwrap(); - - let update_message = Rc::new(RefCell::new(None)); - follower_1.update(cx, { - let update = update_message.clone(); - |_, cx| { - cx.subscribe(&leader, move |_, leader, event, cx| { - leader - .read(cx) - .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); - }) - .detach(); - } - }); - - let (buffer_1, buffer_2) = project.update(cx, |project, cx| { - ( - project - .create_buffer("abc\ndef\nghi\njkl\n", None, cx) - .unwrap(), - project - .create_buffer("mno\npqr\nstu\nvwx\n", None, cx) - .unwrap(), - ) - }); - - // Insert some excerpts. - _ = leader.update(cx, |leader, cx| { - leader.buffer.update(cx, |multibuffer, cx| { - let excerpt_ids = multibuffer.push_excerpts( - buffer_1.clone(), - [ - ExcerptRange { - context: 1..6, - primary: None, - }, - ExcerptRange { - context: 12..15, - primary: None, - }, - ExcerptRange { - context: 0..3, - primary: None, - }, - ], - cx, - ); - multibuffer.insert_excerpts_after( - excerpt_ids[0], - buffer_2.clone(), - [ - ExcerptRange { - context: 8..12, - primary: None, - }, - ExcerptRange { - context: 0..6, - primary: None, - }, - ], - cx, - ); - }); - }); - - // Apply the update of adding the excerpts. - follower_1 - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) - }) - .await - .unwrap(); - assert_eq!( - follower_1.update(cx, |editor, cx| editor.text(cx)), - leader.update(cx, |editor, cx| editor.text(cx)) - ); - update_message.borrow_mut().take(); - - // Start following separately after it already has excerpts. - let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx)); - let follower_2 = cx - .update_window(*workspace.deref(), |_, cx| { - Editor::from_state_proto( - pane.clone(), - workspace.root_view(cx).unwrap().clone(), - ViewId { - creator: Default::default(), - id: 0, - }, - &mut state_message, - cx, - ) - }) - .unwrap() - .unwrap() - .await - .unwrap(); - assert_eq!( - follower_2.update(cx, |editor, cx| editor.text(cx)), - leader.update(cx, |editor, cx| editor.text(cx)) - ); - - // Remove some excerpts. - _ = leader.update(cx, |leader, cx| { - leader.buffer.update(cx, |multibuffer, cx| { - let excerpt_ids = multibuffer.excerpt_ids(); - multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx); - multibuffer.remove_excerpts([excerpt_ids[0]], cx); - }); - }); - - // Apply the update of removing the excerpts. - follower_1 - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) - }) - .await - .unwrap(); - follower_2 - .update(cx, |follower, cx| { - follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx) - }) - .await - .unwrap(); - update_message.borrow_mut().take(); - assert_eq!( - follower_1.update(cx, |editor, cx| editor.text(cx)), - leader.update(cx, |editor, cx| editor.text(cx)) - ); -} - -#[gpui::test] -async fn go_to_prev_overlapping_diagnostic( - executor: BackgroundExecutor, - cx: &mut gpui::TestAppContext, -) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - let project = cx.update_editor(|editor, _| editor.project.clone().unwrap()); - - cx.set_state(indoc! {" - ˇfn func(abc def: i32) -> u32 { - } - "}); - - _ = cx.update(|cx| { - _ = project.update(cx, |project, cx| { - project - .update_diagnostics( - LanguageServerId(0), - lsp::PublishDiagnosticsParams { - uri: lsp::Url::from_file_path("/root/file").unwrap(), - version: None, - diagnostics: vec![ - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 11), - lsp::Position::new(0, 12), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 12), - lsp::Position::new(0, 15), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - lsp::Diagnostic { - range: lsp::Range::new( - lsp::Position::new(0, 25), - lsp::Position::new(0, 28), - ), - severity: Some(lsp::DiagnosticSeverity::ERROR), - ..Default::default() - }, - ], - }, - &[], - cx, - ) - .unwrap() - }); - }); - - executor.run_until_parked(); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abc ˇdef: i32) -> u32 { - } - "}); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abcˇ def: i32) -> u32 { - } - "}); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx); - }); - - cx.assert_editor_state(indoc! {" - fn func(abc def: i32) -> ˇu32 { - } - "}); -} - -#[gpui::test] -async fn go_to_hunk(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorTestContext::new(cx).await; - - let diff_base = r#" - use some::mod; - - const A: u32 = 42; - - fn main() { - println!("hello"); - - println!("world"); - } - "# - .unindent(); - - // Edits are modified, removed, modified, added - cx.set_state( - &r#" - use some::modified; - - ˇ - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.set_diff_base(Some(&diff_base)); - executor.run_until_parked(); - - cx.update_editor(|editor, cx| { - //Wrap around the bottom of the buffer - for _ in 0..3 { - editor.go_to_hunk(&GoToHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - ˇuse some::modified; - - - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - //Wrap around the top of the buffer - for _ in 0..2 { - editor.go_to_prev_hunk(&GoToPrevHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - use some::modified; - - - fn main() { - ˇ println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.go_to_prev_hunk(&GoToPrevHunk, cx); - }); - - cx.assert_editor_state( - &r#" - use some::modified; - - ˇ - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - for _ in 0..3 { - editor.go_to_prev_hunk(&GoToPrevHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - use some::modified; - - - fn main() { - ˇ println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); - - cx.update_editor(|editor, cx| { - editor.fold(&Fold, cx); - - //Make sure that the fold only gets one hunk - for _ in 0..4 { - editor.go_to_hunk(&GoToHunk, cx); - } - }); - - cx.assert_editor_state( - &r#" - ˇuse some::modified; - - - fn main() { - println!("hello there"); - - println!("around the"); - println!("world"); - } - "# - .unindent(), - ); -} - -#[test] -fn test_split_words() { - fn split<'a>(text: &'a str) -> Vec<&'a str> { - split_words(text).collect() - } - - assert_eq!(split("HelloWorld"), &["Hello", "World"]); - assert_eq!(split("hello_world"), &["hello_", "world"]); - assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]); - assert_eq!(split("Hello_World"), &["Hello_", "World"]); - assert_eq!(split("helloWOrld"), &["hello", "WOrld"]); - assert_eq!(split("helloworld"), &["helloworld"]); -} - -#[gpui::test] -async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; - let mut assert = |before, after| { - let _state_context = cx.set_state(before); - cx.update_editor(|editor, cx| { - editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx) - }); - cx.assert_editor_state(after); - }; - - // Outside bracket jumps to outside of matching bracket - assert("console.logˇ(var);", "console.log(var)ˇ;"); - assert("console.log(var)ˇ;", "console.logˇ(var);"); - - // Inside bracket jumps to inside of matching bracket - assert("console.log(ˇvar);", "console.log(varˇ);"); - assert("console.log(varˇ);", "console.log(ˇvar);"); - - // When outside a bracket and inside, favor jumping to the inside bracket - assert( - "console.log('foo', [1, 2, 3]ˇ);", - "console.log(ˇ'foo', [1, 2, 3]);", - ); - assert( - "console.log(ˇ'foo', [1, 2, 3]);", - "console.log('foo', [1, 2, 3]ˇ);", - ); - - // Bias forward if two options are equally likely - assert( - "let result = curried_fun()ˇ();", - "let result = curried_fun()()ˇ;", - ); - - // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller - assert( - indoc! {" - function test() { - console.log('test')ˇ - }"}, - indoc! {" - function test() { - console.logˇ('test') - }"}, - ); -} - -// todo!(completions) -#[gpui::test(iterations = 10)] -async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { - // flaky - init_test(cx, |_| {}); - - let (copilot, copilot_lsp) = Copilot::fake(cx); - _ = cx.update(|cx| cx.set_global(copilot)); - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - // When inserting, ensure autocompletion is favored over Copilot suggestions. - cx.set_state(indoc! {" - oneˇ - two - three - "}); - cx.simulate_keystroke("."); - let _ = handle_completion_request( - &mut cx, - indoc! {" - one.|<> - two - three - "}, - vec!["completion_a", "completion_b"], - ); - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "one.copilot1".into(), - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), - ..Default::default() - }], - vec![], - ); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(editor.context_menu_visible()); - assert!(!editor.has_active_copilot_suggestion(cx)); - - // Confirming a completion inserts it and hides the context menu, without showing - // the copilot suggestion afterwards. - editor - .confirm_completion(&Default::default(), cx) - .unwrap() - .detach(); - assert!(!editor.context_menu_visible()); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n"); - assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n"); - }); - - // Ensure Copilot suggestions are shown right away if no autocompletion is available. - cx.set_state(indoc! {" - oneˇ - two - three - "}); - cx.simulate_keystroke("."); - let _ = handle_completion_request( - &mut cx, - indoc! {" - one.|<> - two - three - "}, - vec![], - ); - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "one.copilot1".into(), - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), - ..Default::default() - }], - vec![], - ); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(!editor.context_menu_visible()); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); - }); - - // Reset editor, and ensure autocompletion is still favored over Copilot suggestions. - cx.set_state(indoc! {" - oneˇ - two - three - "}); - cx.simulate_keystroke("."); - let _ = handle_completion_request( - &mut cx, - indoc! {" - one.|<> - two - three - "}, - vec!["completion_a", "completion_b"], - ); - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "one.copilot1".into(), - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)), - ..Default::default() - }], - vec![], - ); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(editor.context_menu_visible()); - assert!(!editor.has_active_copilot_suggestion(cx)); - - // When hiding the context menu, the Copilot suggestion becomes visible. - editor.hide_context_menu(cx); - assert!(!editor.context_menu_visible()); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.\ntwo\nthree\n"); - }); - - // Ensure existing completion is interpolated when inserting again. - cx.simulate_keystroke("c"); - executor.run_until_parked(); - cx.update_editor(|editor, cx| { - assert!(!editor.context_menu_visible()); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); - }); - - // After debouncing, new Copilot completions should be requested. - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "one.copilot2".into(), - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)), - ..Default::default() - }], - vec![], - ); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(!editor.context_menu_visible()); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); - - // Canceling should remove the active Copilot suggestion. - editor.cancel(&Default::default(), cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); - - // After canceling, tabbing shouldn't insert the previously shown suggestion. - editor.tab(&Default::default(), cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n"); - - // When undoing the previously active suggestion is shown again. - editor.undo(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n"); - }); - - // If an edit occurs outside of this editor, the suggestion is still correctly interpolated. - cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx)); - cx.update_editor(|editor, cx| { - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); - - // Tabbing when there is an active suggestion inserts it. - editor.tab(&Default::default(), cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n"); - - // When undoing the previously active suggestion is shown again. - editor.undo(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); - - // Hide suggestion. - editor.cancel(&Default::default(), cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n"); - }); - - // If an edit occurs outside of this editor but no suggestion is being shown, - // we won't make it visible. - cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx)); - cx.update_editor(|editor, cx| { - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n"); - assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n"); - }); - - // Reset the editor to verify how suggestions behave when tabbing on leading indentation. - cx.update_editor(|editor, cx| { - editor.set_text("fn foo() {\n \n}", cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 2)..Point::new(1, 2)]) - }); - }); - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: " let x = 4;".into(), - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), - ..Default::default() - }], - vec![], - ); - - cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - assert_eq!(editor.text(cx), "fn foo() {\n \n}"); - - // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion. - editor.tab(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.text(cx), "fn foo() {\n \n}"); - assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - - // Tabbing again accepts the suggestion. - editor.tab(&Default::default(), cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}"); - assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}"); - }); -} - -#[gpui::test] -async fn test_copilot_completion_invalidation( - executor: BackgroundExecutor, - cx: &mut gpui::TestAppContext, -) { - init_test(cx, |_| {}); - - let (copilot, copilot_lsp) = Copilot::fake(cx); - _ = cx.update(|cx| cx.set_global(copilot)); - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {" - one - twˇ - three - "}); - - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "two.foo()".into(), - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)), - ..Default::default() - }], - vec![], - ); - cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx)); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - cx.update_editor(|editor, cx| { - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\ntw\nthree\n"); - - editor.backspace(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\nt\nthree\n"); - - editor.backspace(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\n\nthree\n"); - - // Deleting across the original suggestion range invalidates it. - editor.backspace(&Default::default(), cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\nthree\n"); - assert_eq!(editor.text(cx), "one\nthree\n"); - - // Undoing the deletion restores the suggestion. - editor.undo(&Default::default(), cx); - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n"); - assert_eq!(editor.text(cx), "one\n\nthree\n"); - }); -} - -#[gpui::test] -async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let (copilot, copilot_lsp) = Copilot::fake(cx); - _ = cx.update(|cx| cx.set_global(copilot)); - - let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n")); - let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n")); - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(2, 0), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(2, 0), - primary: None, - }], - cx, - ); - multibuffer - }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); - - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "b = 2 + a".into(), - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)), - ..Default::default() - }], - vec![], - ); - _ = editor.update(cx, |editor, cx| { - // Ensure copilot suggestions are shown for the first excerpt. - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 5)..Point::new(1, 5)]) - }); - editor.next_copilot_suggestion(&Default::default(), cx); - }); - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - _ = editor.update(cx, |editor, cx| { - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!( - editor.display_text(cx), - "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n" - ); - assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); - }); - - handle_copilot_completion_request( - &copilot_lsp, - vec![copilot::request::Completion { - text: "d = 4 + c".into(), - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)), - ..Default::default() - }], - vec![], - ); - _ = editor.update(cx, |editor, cx| { - // Move to another excerpt, ensuring the suggestion gets cleared. - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(4, 5)..Point::new(4, 5)]) - }); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!( - editor.display_text(cx), - "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n" - ); - assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n"); - - // Type a character, ensuring we don't even try to interpolate the previous suggestion. - editor.handle_input(" ", cx); - assert!(!editor.has_active_copilot_suggestion(cx)); - assert_eq!( - editor.display_text(cx), - "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n" - ); - assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); - }); - - // Ensure the new suggestion is displayed when the debounce timeout expires. - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - _ = editor.update(cx, |editor, cx| { - assert!(editor.has_active_copilot_suggestion(cx)); - assert_eq!( - editor.display_text(cx), - "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n" - ); - assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n"); - }); -} - -#[gpui::test] -async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings - .copilot - .get_or_insert(Default::default()) - .disabled_globs = Some(vec![".env*".to_string()]); - }); - - let (copilot, copilot_lsp) = Copilot::fake(cx); - _ = cx.update(|cx| cx.set_global(copilot)); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - ".env": "SECRET=something\n", - "README.md": "hello\n" - }), - ) - .await; - let project = Project::test(fs, ["/test".as_ref()], cx).await; - - let private_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/test/.env", cx) - }) - .await - .unwrap(); - let public_buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/test/README.md", cx) - }) - .await - .unwrap(); - - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - private_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 0), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - public_buffer.clone(), - [ExcerptRange { - context: Point::new(0, 0)..Point::new(1, 0), - primary: None, - }], - cx, - ); - multibuffer - }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)); - - let mut copilot_requests = copilot_lsp - .handle_request::(move |_params, _cx| async move { - Ok(copilot::request::GetCompletionsResult { - completions: vec![copilot::request::Completion { - text: "next line".into(), - range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)), - ..Default::default() - }], - }) - }); - - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |selections| { - selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) - }); - editor.next_copilot_suggestion(&Default::default(), cx); - }); - - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - assert!(copilot_requests.try_next().is_err()); - - _ = editor.update(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(2, 0)..Point::new(2, 0)]) - }); - editor.next_copilot_suggestion(&Default::default(), cx); - }); - - executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT); - assert!(copilot_requests.try_next().is_ok()); -} - -#[gpui::test] -async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - brackets: BracketPairConfig { - pairs: vec![BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - newline: true, - }], - disabled_scopes_by_bracket_ix: Vec::new(), - }, - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions { - first_trigger_character: "{".to_string(), - more_trigger_character: None, - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { let a = 5; }", - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let worktree_id = workspace - .update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees().next().unwrap().read(cx).id() - }) - }) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let editor_handle = workspace - .update(cx, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs"), None, true, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - - fake_server.handle_request::(|params, _| async move { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path("/a/main.rs").unwrap(), - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(0, 21), - ); - - Ok(Some(vec![lsp::TextEdit { - new_text: "]".to_string(), - range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), - }])) - }); - - editor_handle.update(cx, |editor, cx| { - editor.focus(cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) - }); - editor.handle_input("{", cx); - }); - - cx.executor().run_until_parked(); - - _ = buffer.update(cx, |buffer, _| { - assert_eq!( - buffer.text(), - "fn main() { let a = {5}; }", - "No extra braces from on type formatting should appear in the buffer" - ) - }); -} - -#[gpui::test] -async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let language_name: Arc = "Rust".into(); - let mut language = Language::new( - LanguageConfig { - name: Arc::clone(&language_name), - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - - let server_restarts = Arc::new(AtomicUsize::new(0)); - let closure_restarts = Arc::clone(&server_restarts); - let language_server_name = "test language server"; - let mut fake_servers = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - name: language_server_name, - initialization_options: Some(json!({ - "testOptionValue": true - })), - initializer: Some(Box::new(move |fake_server| { - let task_restarts = Arc::clone(&closure_restarts); - fake_server.handle_request::(move |_, _| { - task_restarts.fetch_add(1, atomic::Ordering::Release); - futures::future::ready(Ok(())) - }); - })), - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/a", - json!({ - "main.rs": "fn main() { let a = 5; }", - "other.rs": "// Test file", - }), - ) - .await; - let project = Project::test(fs, ["/a".as_ref()], cx).await; - _ = project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer("/a/main.rs", cx) - }) - .await - .unwrap(); - let _fake_server = fake_servers.next().await.unwrap(); - update_test_language_settings(cx, |language_settings| { - language_settings.languages.insert( - Arc::clone(&language_name), - LanguageSettingsContent { - tab_size: NonZeroU32::new(8), - ..Default::default() - }, - ); - }); - cx.executor().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 0, - "Should not restart LSP server on an unrelated change" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - "Some other server name".into(), - LspSettings { - initialization_options: Some(json!({ - "some other init value": false - })), - }, - ); - }); - cx.executor().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 0, - "Should not restart LSP server on an unrelated LSP settings change" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - language_server_name.into(), - LspSettings { - initialization_options: Some(json!({ - "anotherInitValue": false - })), - }, - ); - }); - cx.executor().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 1, - "Should restart LSP server on a related LSP settings change" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - language_server_name.into(), - LspSettings { - initialization_options: Some(json!({ - "anotherInitValue": false - })), - }, - ); - }); - cx.executor().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 1, - "Should not restart LSP server on a related LSP settings change that is the same" - ); - - update_test_project_settings(cx, |project_settings| { - project_settings.lsp.insert( - language_server_name.into(), - LspSettings { - initialization_options: None, - }, - ); - }); - cx.executor().run_until_parked(); - assert_eq!( - server_restarts.load(atomic::Ordering::Acquire), - 2, - "Should restart LSP server on another related LSP settings change" - ); -} - -#[gpui::test] -async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); - cx.simulate_keystroke("."); - let completion_item = lsp::CompletionItem { - label: "some".into(), - kind: Some(lsp::CompletionItemKind::SNIPPET), - detail: Some("Wrap the expression in an `Option::Some`".to_string()), - documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: "```rust\nSome(2)\n```".to_string(), - })), - deprecated: Some(false), - sort_text: Some("fffffff2".to_string()), - filter_text: Some("some".to_string()), - insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 22, - }, - end: lsp::Position { - line: 0, - character: 22, - }, - }, - new_text: "Some(2)".to_string(), - })), - additional_text_edits: Some(vec![lsp::TextEdit { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 20, - }, - end: lsp::Position { - line: 0, - character: 22, - }, - }, - new_text: "".to_string(), - }]), - ..Default::default() - }; - - let closure_completion_item = completion_item.clone(); - let mut request = cx.handle_request::(move |_, _, _| { - let task_completion_item = closure_completion_item.clone(); - async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - task_completion_item, - ]))) - } - }); - - request.next().await; - - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - let apply_additional_edits = cx.update_editor(|editor, cx| { - editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap() - }); - cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"}); - - cx.handle_request::(move |_, _, _| { - let task_completion_item = completion_item.clone(); - async move { Ok(task_completion_item) } - }) - .next() - .await - .unwrap(); - apply_additional_edits.await.unwrap(); - cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"}); -} - -#[gpui::test] -async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) { - init_test(cx, |_| {}); - - let mut cx = EditorLspTestContext::new( - Language::new( - LanguageConfig { - path_suffixes: vec!["jsx".into()], - overrides: [( - "element".into(), - LanguageConfigOverride { - word_characters: Override::Set(['-'].into_iter().collect()), - ..Default::default() - }, - )] - .into_iter() - .collect(), - ..Default::default() - }, - Some(tree_sitter_typescript::language_tsx()), - ) - .with_override_query("(jsx_self_closing_element) @element") - .unwrap(), - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - - cx.lsp - .handle_request::(move |_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "bg-blue".into(), - ..Default::default() - }, - lsp::CompletionItem { - label: "bg-red".into(), - ..Default::default() - }, - lsp::CompletionItem { - label: "bg-yellow".into(), - ..Default::default() - }, - ]))) - }); - - cx.set_state(r#"

"#); - - // Trigger completion when typing a dash, because the dash is an extra - // word character in the 'element' scope, which contains the cursor. - cx.simulate_keystroke("-"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { - assert_eq!( - menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-red", "bg-blue", "bg-yellow"] - ); - } else { - panic!("expected completion menu to be open"); - } - }); - - cx.simulate_keystroke("l"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { - assert_eq!( - menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-blue", "bg-yellow"] - ); - } else { - panic!("expected completion menu to be open"); - } - }); - - // When filtering completions, consider the character after the '-' to - // be the start of a subword. - cx.set_state(r#"

"#); - cx.simulate_keystroke("l"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _| { - if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() { - assert_eq!( - menu.matches.iter().map(|m| &m.string).collect::>(), - &["bg-yellow"] - ); - } else { - panic!("expected completion menu to be open"); - } - }); -} - -#[gpui::test] -async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) { - init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::Prettier) - }); - - let mut language = Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - prettier_parser_name: Some("test_parser".to_string()), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ); - - let test_plugin = "test_plugin"; - let _ = language - .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { - prettier_plugins: vec![test_plugin], - ..Default::default() - })) - .await; - - let fs = FakeFs::new(cx.executor()); - fs.insert_file("/file.rs", Default::default()).await; - - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX; - _ = project.update(cx, |project, _| { - project.languages().add(Arc::new(language)); - }); - let buffer = project - .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx)) - .await - .unwrap(); - - let buffer_text = "one\ntwo\nthree\n"; - let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - _ = editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx)); - - editor - .update(cx, |editor, cx| { - editor.perform_format(project.clone(), FormatTrigger::Manual, cx) - }) - .unwrap() - .await; - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - buffer_text.to_string() + prettier_format_suffix, - "Test prettier formatting was not applied to the original buffer text", - ); - - update_test_language_settings(cx, |settings| { - settings.defaults.formatter = Some(language_settings::Formatter::Auto) - }); - let format = editor.update(cx, |editor, cx| { - editor.perform_format(project.clone(), FormatTrigger::Manual, cx) - }); - format.await.unwrap(); - assert_eq!( - editor.update(cx, |editor, cx| editor.text(cx)), - buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix, - "Autoformatting (via test prettier) was not applied to the original buffer text", - ); -} - -fn empty_range(row: usize, column: usize) -> Range { - let point = DisplayPoint::new(row as u32, column as u32); - point..point -} - -fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext) { - let (text, ranges) = marked_text_ranges(marked_text, true); - assert_eq!(view.text(cx), text); - assert_eq!( - view.selections.ranges(cx), - ranges, - "Assert selections are {}", - marked_text - ); -} - -/// Handle completion request passing a marked string specifying where the completion -/// should be triggered from using '|' character, what range should be replaced, and what completions -/// should be returned using '<' and '>' to delimit the range -pub fn handle_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, - marked_string: &str, - completions: Vec<&'static str>, -) -> impl Future { - let complete_from_marker: TextRangeMarker = '|'.into(); - let replace_range_marker: TextRangeMarker = ('<', '>').into(); - let (_, mut marked_ranges) = marked_text_ranges_by( - marked_string, - vec![complete_from_marker.clone(), replace_range_marker.clone()], - ); - - let complete_from_position = - cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start); - let replace_range = - cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone()); - - let mut request = cx.handle_request::(move |url, params, _| { - let completions = completions.clone(); - async move { - assert_eq!(params.text_document_position.text_document.uri, url.clone()); - assert_eq!( - params.text_document_position.position, - complete_from_position - ); - Ok(Some(lsp::CompletionResponse::Array( - completions - .iter() - .map(|completion_text| lsp::CompletionItem { - label: completion_text.to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: replace_range, - new_text: completion_text.to_string(), - })), - ..Default::default() - }) - .collect(), - ))) - } - }); - - async move { - request.next().await; - } -} - -fn handle_resolve_completion_request<'a>( - cx: &mut EditorLspTestContext<'a>, - edits: Option>, -) -> impl Future { - let edits = edits.map(|edits| { - edits - .iter() - .map(|(marked_string, new_text)| { - let (_, marked_ranges) = marked_text_ranges(marked_string, false); - let replace_range = cx.to_lsp_range(marked_ranges[0].clone()); - lsp::TextEdit::new(replace_range, new_text.to_string()) - }) - .collect::>() - }); - - let mut request = - cx.handle_request::(move |_, _, _| { - let edits = edits.clone(); - async move { - Ok(lsp::CompletionItem { - additional_text_edits: edits, - ..Default::default() - }) - } - }); - - async move { - request.next().await; - } -} - -fn handle_copilot_completion_request( - lsp: &lsp::FakeLanguageServer, - completions: Vec, - completions_cycling: Vec, -) { - lsp.handle_request::(move |_params, _cx| { - let completions = completions.clone(); - async move { - Ok(copilot::request::GetCompletionsResult { - completions: completions.clone(), - }) - } - }); - lsp.handle_request::(move |_params, _cx| { - let completions_cycling = completions_cycling.clone(); - async move { - Ok(copilot::request::GetCompletionsResult { - completions: completions_cycling.clone(), - }) - } - }); -} - -pub(crate) fn update_test_language_settings( - cx: &mut TestAppContext, - f: impl Fn(&mut AllLanguageSettingsContent), -) { - _ = cx.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, f); - }); - }); -} - -pub(crate) fn update_test_project_settings( - cx: &mut TestAppContext, - f: impl Fn(&mut ProjectSettings), -) { - _ = cx.update(|cx| { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings::(cx, f); - }); - }); -} - -pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { - _ = cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - crate::init(cx); - }); - - update_test_language_settings(cx, f); -} diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index 02c71c734a..dd381c7685 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -20,20 +20,19 @@ test-support = [ ] [dependencies] -client = { path = "../client" } +client = { package = "client2", path = "../client2" } clock = { path = "../clock" } collections = { path = "../collections" } -context_menu = { path = "../context_menu" } -git = { path = "../git" } -gpui = { path = "../gpui" } -language = { path = "../language" } -lsp = { path = "../lsp" } -rich_text = { path = "../rich_text" } -settings = { path = "../settings" } +git = { package = "git3", path = "../git3" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +lsp = { package = "lsp2", path = "../lsp2" } +rich_text = { package = "rich_text2", path = "../rich_text2" } +settings = { package = "settings2", path = "../settings2" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } -text = { path = "../text" } -theme = { path = "../theme" } +text = { package = "text2", path = "../text2" } +theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } aho-corasick = "1.1" @@ -61,14 +60,13 @@ tree-sitter-typescript = { workspace = true, optional = true } [dev-dependencies] copilot = { path = "../copilot", features = ["test-support"] } -text = { path = "../text", features = ["test-support"] } -language = { path = "../language", features = ["test-support"] } -lsp = { path = "../lsp", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } -project = { path = "../project", features = ["test-support"] } -settings = { path = "../settings", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } +settings = { package = "settings2", path = "../settings2", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index fc629c653f..49ec284a99 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -6,7 +6,7 @@ use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; use git::diff::DiffHunk; -use gpui::{AppContext, Entity, ModelContext, ModelHandle}; +use gpui::{AppContext, EventEmitter, Model, ModelContext}; pub use language::Completion; use language::{ char_kind, @@ -38,6 +38,9 @@ use text::{ use theme::SyntaxTheme; use util::post_inc; +#[cfg(any(test, feature = "test-support"))] +use gpui::Context; + const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] @@ -57,7 +60,7 @@ pub struct MultiBuffer { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { ExcerptsAdded { - buffer: ModelHandle, + buffer: Model, predecessor: ExcerptId, excerpts: Vec<(ExcerptId, ExcerptRange)>, }, @@ -119,7 +122,7 @@ pub trait ToPointUtf16: 'static + fmt::Debug { } struct BufferState { - buffer: ModelHandle, + buffer: Model, last_version: clock::Global, last_parse_count: usize, last_selections_update_count: usize, @@ -279,7 +282,7 @@ impl MultiBuffer { self } - pub fn singleton(buffer: ModelHandle, cx: &mut ModelContext) -> Self { + pub fn singleton(buffer: Model, cx: &mut ModelContext) -> Self { let mut this = Self::new(buffer.read(cx).replica_id()); this.singleton = true; this.push_excerpts( @@ -308,7 +311,7 @@ impl MultiBuffer { self.snapshot.borrow() } - pub fn as_singleton(&self) -> Option> { + pub fn as_singleton(&self) -> Option> { if self.singleton { return Some( self.buffers @@ -681,7 +684,7 @@ impl MultiBuffer { pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext) where - T: IntoIterator, &'a language::Transaction)>, + T: IntoIterator, &'a language::Transaction)>, { self.history .push_transaction(buffer_transactions, Instant::now(), cx); @@ -863,19 +866,19 @@ impl MultiBuffer { pub fn stream_excerpts_with_context_lines( &mut self, - buffer: ModelHandle, + buffer: Model, ranges: Vec>, context_line_count: u32, cx: &mut ModelContext, ) -> mpsc::Receiver> { - let (mut tx, rx) = mpsc::channel(256); - cx.spawn(|this, mut cx| async move { - let (buffer_id, buffer_snapshot) = - buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); + let (buffer_id, buffer_snapshot) = + buffer.update(cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); + let (mut tx, rx) = mpsc::channel(256); + cx.spawn(move |this, mut cx| async move { let mut excerpt_ranges = Vec::new(); let mut range_counts = Vec::new(); - cx.background() + cx.background_executor() .scoped(|scope| { scope.spawn(async { let (ranges, counts) = @@ -889,9 +892,12 @@ impl MultiBuffer { let mut ranges = ranges.into_iter(); let mut range_counts = range_counts.into_iter(); for excerpt_ranges in excerpt_ranges.chunks(100) { - let excerpt_ids = this.update(&mut cx, |this, cx| { + let excerpt_ids = match this.update(&mut cx, |this, cx| { this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) - }); + }) { + Ok(excerpt_ids) => excerpt_ids, + Err(_) => return, + }; for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref()) { @@ -920,7 +926,7 @@ impl MultiBuffer { pub fn push_excerpts( &mut self, - buffer: ModelHandle, + buffer: Model, ranges: impl IntoIterator>, cx: &mut ModelContext, ) -> Vec @@ -932,7 +938,7 @@ impl MultiBuffer { pub fn push_excerpts_with_context_lines( &mut self, - buffer: ModelHandle, + buffer: Model, ranges: Vec>, context_line_count: u32, cx: &mut ModelContext, @@ -970,7 +976,7 @@ impl MultiBuffer { pub fn insert_excerpts_after( &mut self, prev_excerpt_id: ExcerptId, - buffer: ModelHandle, + buffer: Model, ranges: impl IntoIterator>, cx: &mut ModelContext, ) -> Vec @@ -995,7 +1001,7 @@ impl MultiBuffer { pub fn insert_excerpts_with_ids_after( &mut self, prev_excerpt_id: ExcerptId, - buffer: ModelHandle, + buffer: Model, ranges: impl IntoIterator)>, cx: &mut ModelContext, ) where @@ -1132,7 +1138,7 @@ impl MultiBuffer { pub fn excerpts_for_buffer( &self, - buffer: &ModelHandle, + buffer: &Model, cx: &AppContext, ) -> Vec<(ExcerptId, ExcerptRange)> { let mut excerpts = Vec::new(); @@ -1169,7 +1175,7 @@ impl MultiBuffer { &self, position: impl ToOffset, cx: &AppContext, - ) -> Option<(ExcerptId, ModelHandle, Range)> { + ) -> Option<(ExcerptId, Model, Range)> { let snapshot = self.read(cx); let position = position.to_offset(&snapshot); @@ -1197,7 +1203,7 @@ impl MultiBuffer { &self, point: T, cx: &AppContext, - ) -> Option<(ModelHandle, usize, ExcerptId)> { + ) -> Option<(Model, usize, ExcerptId)> { let snapshot = self.read(cx); let offset = point.to_offset(&snapshot); let mut cursor = snapshot.excerpts.cursor::(); @@ -1219,7 +1225,7 @@ impl MultiBuffer { &self, range: Range, cx: &AppContext, - ) -> Vec<(ModelHandle, Range, ExcerptId)> { + ) -> Vec<(Model, Range, ExcerptId)> { let snapshot = self.read(cx); let start = range.start.to_offset(&snapshot); let end = range.end.to_offset(&snapshot); @@ -1377,7 +1383,7 @@ impl MultiBuffer { &self, position: T, cx: &AppContext, - ) -> Option<(ModelHandle, language::Anchor)> { + ) -> Option<(Model, language::Anchor)> { let snapshot = self.read(cx); let anchor = snapshot.anchor_before(position); let buffer = self @@ -1391,7 +1397,7 @@ impl MultiBuffer { fn on_buffer_event( &mut self, - _: ModelHandle, + _: Model, event: &language::Event, cx: &mut ModelContext, ) { @@ -1414,7 +1420,7 @@ impl MultiBuffer { }); } - pub fn all_buffers(&self) -> HashSet> { + pub fn all_buffers(&self) -> HashSet> { self.buffers .borrow() .values() @@ -1422,7 +1428,7 @@ impl MultiBuffer { .collect() } - pub fn buffer(&self, buffer_id: u64) -> Option> { + pub fn buffer(&self, buffer_id: u64) -> Option> { self.buffers .borrow() .get(&buffer_id) @@ -1487,7 +1493,7 @@ impl MultiBuffer { language_settings(language.as_ref(), file, cx) } - pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle)) { + pub fn for_each_buffer(&self, mut f: impl FnMut(&Model)) { self.buffers .borrow() .values() @@ -1642,18 +1648,18 @@ impl MultiBuffer { #[cfg(any(test, feature = "test-support"))] impl MultiBuffer { - pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> ModelHandle { - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); - cx.add_model(|cx| Self::singleton(buffer, cx)) + pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> Model { + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); + cx.new_model(|cx| Self::singleton(buffer, cx)) } pub fn build_multi( excerpts: [(&str, Vec>); COUNT], cx: &mut gpui::AppContext, - ) -> ModelHandle { - let multi = cx.add_model(|_| Self::new(0)); + ) -> Model { + let multi = cx.new_model(|_| Self::new(0)); for (text, ranges) in excerpts { - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { context: range, primary: None, @@ -1666,15 +1672,12 @@ impl MultiBuffer { multi } - pub fn build_from_buffer( - buffer: ModelHandle, - cx: &mut gpui::AppContext, - ) -> ModelHandle { - cx.add_model(|cx| Self::singleton(buffer, cx)) + pub fn build_from_buffer(buffer: Model, cx: &mut gpui::AppContext) -> Model { + cx.new_model(|cx| Self::singleton(buffer, cx)) } - pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> ModelHandle { - cx.add_model(|cx| { + pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model { + cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); let mutation_count = rng.gen_range(1..=5); multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); @@ -1745,7 +1748,7 @@ impl MultiBuffer { if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text))); + buffers.push(cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text))); let buffer = buffers.last().unwrap().read(cx); log::info!( "Creating new buffer {} with text: {:?}", @@ -1868,9 +1871,7 @@ impl MultiBuffer { } } -impl Entity for MultiBuffer { - type Event = Event; -} +impl EventEmitter for MultiBuffer {} impl MultiBufferSnapshot { pub fn text(&self) -> String { @@ -3405,7 +3406,7 @@ impl History { now: Instant, cx: &mut ModelContext, ) where - T: IntoIterator, &'a language::Transaction)>, + T: IntoIterator, &'a language::Transaction)>, { assert_eq!(self.transaction_depth, 0); let transaction = Transaction { @@ -4131,18 +4132,19 @@ where mod tests { use super::*; use futures::StreamExt; - use gpui::{AppContext, TestAppContext}; + use gpui::{AppContext, Context, TestAppContext}; use language::{Buffer, Rope}; + use parking_lot::RwLock; use rand::prelude::*; use settings::SettingsStore; - use std::{env, rc::Rc}; + use std::env; use util::test::sample_text; #[gpui::test] fn test_singleton(cx: &mut AppContext) { let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); - let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a'))); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), buffer.read(cx).text()); @@ -4168,11 +4170,11 @@ mod tests { #[gpui::test] fn test_remote(cx: &mut AppContext) { - let host_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a")); - let guest_buffer = cx.add_model(|cx| { + let host_buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a")); + let guest_buffer = cx.new_model(|cx| { let state = host_buffer.read(cx).to_proto(); let ops = cx - .background() + .background_executor() .block(host_buffer.read(cx).serialize_ops(None, cx)); let mut buffer = Buffer::from_proto(1, state, None).unwrap(); buffer @@ -4184,7 +4186,7 @@ mod tests { .unwrap(); buffer }); - let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), "a"); @@ -4200,17 +4202,17 @@ mod tests { #[gpui::test] fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { let buffer_1 = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a'))); let buffer_2 = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'g'))); - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'g'))); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let events = Rc::new(RefCell::new(Vec::::new())); + let events = Arc::new(RwLock::new(Vec::::new())); multibuffer.update(cx, |_, cx| { let events = events.clone(); cx.subscribe(&multibuffer, move |_, _, event, _| { if let Event::Edited { .. } = event { - events.borrow_mut().push(event.clone()) + events.write().push(event.clone()) } }) .detach(); @@ -4263,7 +4265,7 @@ mod tests { // Adding excerpts emits an edited event. assert_eq!( - events.borrow().as_slice(), + events.read().as_slice(), &[ Event::Edited { sigleton_buffer_edited: false @@ -4436,13 +4438,13 @@ mod tests { #[gpui::test] fn test_excerpt_events(cx: &mut AppContext) { let buffer_1 = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'a'))); + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'a'))); let buffer_2 = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'm'))); + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'm'))); - let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let follower_edit_event_count = Rc::new(RefCell::new(0)); + let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0)); + let follower_edit_event_count = Arc::new(RwLock::new(0)); follower_multibuffer.update(cx, |_, cx| { let follower_edit_event_count = follower_edit_event_count.clone(); @@ -4456,7 +4458,7 @@ mod tests { } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), Event::Edited { .. } => { - *follower_edit_event_count.borrow_mut() += 1; + *follower_edit_event_count.write() += 1; } _ => {} }, @@ -4499,7 +4501,7 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); - assert_eq!(*follower_edit_event_count.borrow(), 2); + assert_eq!(*follower_edit_event_count.read(), 2); leader_multibuffer.update(cx, |leader, cx| { let excerpt_ids = leader.excerpt_ids(); @@ -4509,7 +4511,7 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); - assert_eq!(*follower_edit_event_count.borrow(), 3); + assert_eq!(*follower_edit_event_count.read(), 3); // Removing an empty set of excerpts is a noop. leader_multibuffer.update(cx, |leader, cx| { @@ -4519,7 +4521,7 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); - assert_eq!(*follower_edit_event_count.borrow(), 3); + assert_eq!(*follower_edit_event_count.read(), 3); // Adding an empty set of excerpts is a noop. leader_multibuffer.update(cx, |leader, cx| { @@ -4529,7 +4531,7 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); - assert_eq!(*follower_edit_event_count.borrow(), 3); + assert_eq!(*follower_edit_event_count.read(), 3); leader_multibuffer.update(cx, |leader, cx| { leader.clear(cx); @@ -4538,14 +4540,14 @@ mod tests { leader_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(), ); - assert_eq!(*follower_edit_event_count.borrow(), 4); + assert_eq!(*follower_edit_event_count.read(), 4); } #[gpui::test] fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts_with_context_lines( buffer.clone(), @@ -4581,8 +4583,8 @@ mod tests { #[gpui::test] async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { let buffer = - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); let ranges = vec![ @@ -4596,7 +4598,7 @@ mod tests { let anchor_ranges = anchor_ranges.collect::>().await; - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); + let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); assert_eq!( snapshot.text(), "bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\nrrr\n" @@ -4617,7 +4619,7 @@ mod tests { #[gpui::test] fn test_empty_multibuffer(cx: &mut AppContext) { - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); let snapshot = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot.text(), ""); @@ -4627,8 +4629,8 @@ mod tests { #[gpui::test] fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); - let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); + let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { buffer.edit([(0..0, "X")], None, cx); @@ -4647,9 +4649,9 @@ mod tests { #[gpui::test] fn test_multibuffer_anchors(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "efghi")); - let multibuffer = cx.add_model(|cx| { + let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); + let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "efghi")); + let multibuffer = cx.new_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( buffer_1.clone(), @@ -4705,9 +4707,10 @@ mod tests { #[gpui::test] fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "ABCDEFGHIJKLMNOP")); - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); + let buffer_2 = + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "ABCDEFGHIJKLMNOP")); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); // Create an insertion id in buffer 1 that doesn't exist in buffer 2. // Add an excerpt from buffer 1 that spans this new insertion. @@ -4840,10 +4843,10 @@ mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let mut buffers: Vec> = Vec::new(); - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let mut buffers: Vec> = Vec::new(); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); let mut excerpt_ids = Vec::::new(); - let mut expected_excerpts = Vec::<(ModelHandle, Range)>::new(); + let mut expected_excerpts = Vec::<(Model, Range)>::new(); let mut anchors = Vec::new(); let mut old_versions = Vec::new(); @@ -4918,7 +4921,7 @@ mod tests { .take(10) .collect::(); buffers.push( - cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text)), + cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), base_text)), ); buffers.last().unwrap() } else { @@ -5258,11 +5261,12 @@ mod tests { #[gpui::test] fn test_history(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let test_settings = SettingsStore::test(cx); + cx.set_global(test_settings); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "1234")); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "5678")); - let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "1234")); + let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "5678")); + let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); let group_interval = multibuffer.read(cx).history.group_interval; multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts( diff --git a/crates/multi_buffer2/Cargo.toml b/crates/multi_buffer2/Cargo.toml deleted file mode 100644 index 98b96dfa1d..0000000000 --- a/crates/multi_buffer2/Cargo.toml +++ /dev/null @@ -1,78 +0,0 @@ -[package] -name = "multi_buffer2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/multi_buffer2.rs" -doctest = false - -[features] -test-support = [ - "copilot/test-support", - "text/test-support", - "language/test-support", - "gpui/test-support", - "util/test-support", - "tree-sitter-rust", - "tree-sitter-typescript" -] - -[dependencies] -client = { package = "client2", path = "../client2" } -clock = { path = "../clock" } -collections = { path = "../collections" } -git = { package = "git3", path = "../git3" } -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -lsp = { package = "lsp2", path = "../lsp2" } -rich_text = { package = "rich_text2", path = "../rich_text2" } -settings = { package = "settings2", path = "../settings2" } -snippet = { path = "../snippet" } -sum_tree = { path = "../sum_tree" } -text = { package = "text2", path = "../text2" } -theme = { package = "theme2", path = "../theme2" } -util = { path = "../util" } - -aho-corasick = "1.1" -anyhow.workspace = true -convert_case = "0.6.0" -futures.workspace = true -indoc = "1.0.4" -itertools = "0.10" -lazy_static.workspace = true -log.workspace = true -ordered-float.workspace = true -parking_lot.workspace = true -postage.workspace = true -pulldown-cmark = { version = "0.9.2", default-features = false } -rand.workspace = true -schemars.workspace = true -serde.workspace = true -serde_derive.workspace = true -smallvec.workspace = true -smol.workspace = true - -tree-sitter-rust = { workspace = true, optional = true } -tree-sitter-html = { workspace = true, optional = true } -tree-sitter-typescript = { workspace = true, optional = true } - -[dev-dependencies] -copilot = { package = "copilot2", path = "../copilot2", features = ["test-support"] } -text = { package = "text2", path = "../text2", features = ["test-support"] } -language = { package = "language2", path = "../language2", features = ["test-support"] } -lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -util = { path = "../util", features = ["test-support"] } -project = { package = "project2", path = "../project2", features = ["test-support"] } -settings = { package = "settings2", path = "../settings2", features = ["test-support"] } - -ctor.workspace = true -env_logger.workspace = true -rand.workspace = true -unindent.workspace = true -tree-sitter.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-html.workspace = true -tree-sitter-typescript.workspace = true diff --git a/crates/multi_buffer2/src/anchor.rs b/crates/multi_buffer2/src/anchor.rs deleted file mode 100644 index 39a8182da1..0000000000 --- a/crates/multi_buffer2/src/anchor.rs +++ /dev/null @@ -1,138 +0,0 @@ -use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint}; -use language::{OffsetUtf16, Point, TextDimension}; -use std::{ - cmp::Ordering, - ops::{Range, Sub}, -}; -use sum_tree::Bias; - -#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)] -pub struct Anchor { - pub buffer_id: Option, - pub excerpt_id: ExcerptId, - pub text_anchor: text::Anchor, -} - -impl Anchor { - pub fn min() -> Self { - Self { - buffer_id: None, - excerpt_id: ExcerptId::min(), - text_anchor: text::Anchor::MIN, - } - } - - pub fn max() -> Self { - Self { - buffer_id: None, - excerpt_id: ExcerptId::max(), - text_anchor: text::Anchor::MAX, - } - } - - pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { - let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id, snapshot); - if excerpt_id_cmp.is_eq() { - if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { - Ordering::Equal - } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer) - } else { - Ordering::Equal - } - } else { - excerpt_id_cmp - } - } - - pub fn bias(&self) -> Bias { - self.text_anchor.bias - } - - pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Left { - if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - return Self { - buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id.clone(), - text_anchor: self.text_anchor.bias_left(&excerpt.buffer), - }; - } - } - self.clone() - } - - pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Right { - if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - return Self { - buffer_id: self.buffer_id, - excerpt_id: self.excerpt_id.clone(), - text_anchor: self.text_anchor.bias_right(&excerpt.buffer), - }; - } - } - self.clone() - } - - pub fn summary(&self, snapshot: &MultiBufferSnapshot) -> D - where - D: TextDimension + Ord + Sub, - { - snapshot.summary_for_anchor(self) - } - - pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool { - if *self == Anchor::min() || *self == Anchor::max() { - true - } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - excerpt.contains(self) - && (self.text_anchor == excerpt.range.context.start - || self.text_anchor == excerpt.range.context.end - || self.text_anchor.is_valid(&excerpt.buffer)) - } else { - false - } - } -} - -impl ToOffset for Anchor { - fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize { - self.summary(snapshot) - } -} - -impl ToOffsetUtf16 for Anchor { - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { - self.summary(snapshot) - } -} - -impl ToPoint for Anchor { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - self.summary(snapshot) - } -} - -pub trait AnchorRangeExt { - fn cmp(&self, b: &Range, buffer: &MultiBufferSnapshot) -> Ordering; - fn to_offset(&self, content: &MultiBufferSnapshot) -> Range; - fn to_point(&self, content: &MultiBufferSnapshot) -> Range; -} - -impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering { - match self.start.cmp(&other.start, buffer) { - Ordering::Equal => other.end.cmp(&self.end, buffer), - ord => ord, - } - } - - fn to_offset(&self, content: &MultiBufferSnapshot) -> Range { - self.start.to_offset(content)..self.end.to_offset(content) - } - - fn to_point(&self, content: &MultiBufferSnapshot) -> Range { - self.start.to_point(content)..self.end.to_point(content) - } -} diff --git a/crates/multi_buffer2/src/multi_buffer2.rs b/crates/multi_buffer2/src/multi_buffer2.rs deleted file mode 100644 index 49ec284a99..0000000000 --- a/crates/multi_buffer2/src/multi_buffer2.rs +++ /dev/null @@ -1,5390 +0,0 @@ -mod anchor; - -pub use anchor::{Anchor, AnchorRangeExt}; -use anyhow::{anyhow, Result}; -use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; -use futures::{channel::mpsc, SinkExt}; -use git::diff::DiffHunk; -use gpui::{AppContext, EventEmitter, Model, ModelContext}; -pub use language::Completion; -use language::{ - char_kind, - language_settings::{language_settings, LanguageSettings}, - AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, - DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, - Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, - ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, -}; -use std::{ - borrow::Cow, - cell::{Ref, RefCell}, - cmp, fmt, - future::Future, - io, - iter::{self, FromIterator}, - mem, - ops::{Range, RangeBounds, Sub}, - str, - sync::Arc, - time::{Duration, Instant}, -}; -use sum_tree::{Bias, Cursor, SumTree}; -use text::{ - locator::Locator, - subscription::{Subscription, Topic}, - Edit, TextSummary, -}; -use theme::SyntaxTheme; -use util::post_inc; - -#[cfg(any(test, feature = "test-support"))] -use gpui::Context; - -const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; - -#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct ExcerptId(usize); - -pub struct MultiBuffer { - snapshot: RefCell, - buffers: RefCell>, - next_excerpt_id: usize, - subscriptions: Topic, - singleton: bool, - replica_id: ReplicaId, - history: History, - title: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - ExcerptsAdded { - buffer: Model, - predecessor: ExcerptId, - excerpts: Vec<(ExcerptId, ExcerptRange)>, - }, - ExcerptsRemoved { - ids: Vec, - }, - ExcerptsEdited { - ids: Vec, - }, - Edited { - sigleton_buffer_edited: bool, - }, - TransactionUndone { - transaction_id: TransactionId, - }, - Reloaded, - DiffBaseChanged, - LanguageChanged, - Reparsed, - Saved, - FileHandleChanged, - Closed, - DirtyChanged, - DiagnosticsUpdated, -} - -#[derive(Clone)] -struct History { - next_transaction_id: TransactionId, - undo_stack: Vec, - redo_stack: Vec, - transaction_depth: usize, - group_interval: Duration, -} - -#[derive(Clone)] -struct Transaction { - id: TransactionId, - buffer_transactions: HashMap, - first_edit_at: Instant, - last_edit_at: Instant, - suppress_grouping: bool, -} - -pub trait ToOffset: 'static + fmt::Debug { - fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize; -} - -pub trait ToOffsetUtf16: 'static + fmt::Debug { - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16; -} - -pub trait ToPoint: 'static + fmt::Debug { - fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point; -} - -pub trait ToPointUtf16: 'static + fmt::Debug { - fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16; -} - -struct BufferState { - buffer: Model, - last_version: clock::Global, - last_parse_count: usize, - last_selections_update_count: usize, - last_diagnostics_update_count: usize, - last_file_update_count: usize, - last_git_diff_update_count: usize, - excerpts: Vec, - _subscriptions: [gpui::Subscription; 2], -} - -#[derive(Clone, Default)] -pub struct MultiBufferSnapshot { - singleton: bool, - excerpts: SumTree, - excerpt_ids: SumTree, - parse_count: usize, - diagnostics_update_count: usize, - trailing_excerpt_update_count: usize, - git_diff_update_count: usize, - edit_count: usize, - is_dirty: bool, - has_conflict: bool, -} - -pub struct ExcerptBoundary { - pub id: ExcerptId, - pub row: u32, - pub buffer: BufferSnapshot, - pub range: ExcerptRange, - pub starts_new_buffer: bool, -} - -#[derive(Clone)] -struct Excerpt { - id: ExcerptId, - locator: Locator, - buffer_id: u64, - buffer: BufferSnapshot, - range: ExcerptRange, - max_buffer_row: u32, - text_summary: TextSummary, - has_trailing_newline: bool, -} - -#[derive(Clone, Debug)] -struct ExcerptIdMapping { - id: ExcerptId, - locator: Locator, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ExcerptRange { - pub context: Range, - pub primary: Option>, -} - -#[derive(Clone, Debug, Default)] -struct ExcerptSummary { - excerpt_id: ExcerptId, - excerpt_locator: Locator, - max_buffer_row: u32, - text: TextSummary, -} - -#[derive(Clone)] -pub struct MultiBufferRows<'a> { - buffer_row_range: Range, - excerpts: Cursor<'a, Excerpt, Point>, -} - -pub struct MultiBufferChunks<'a> { - range: Range, - excerpts: Cursor<'a, Excerpt, usize>, - excerpt_chunks: Option>, - language_aware: bool, -} - -pub struct MultiBufferBytes<'a> { - range: Range, - excerpts: Cursor<'a, Excerpt, usize>, - excerpt_bytes: Option>, - chunk: &'a [u8], -} - -pub struct ReversedMultiBufferBytes<'a> { - range: Range, - excerpts: Cursor<'a, Excerpt, usize>, - excerpt_bytes: Option>, - chunk: &'a [u8], -} - -struct ExcerptChunks<'a> { - content_chunks: BufferChunks<'a>, - footer_height: usize, -} - -struct ExcerptBytes<'a> { - content_bytes: text::Bytes<'a>, - footer_height: usize, -} - -impl MultiBuffer { - pub fn new(replica_id: ReplicaId) -> Self { - Self { - snapshot: Default::default(), - buffers: Default::default(), - next_excerpt_id: 1, - subscriptions: Default::default(), - singleton: false, - replica_id, - history: History { - next_transaction_id: Default::default(), - undo_stack: Default::default(), - redo_stack: Default::default(), - transaction_depth: 0, - group_interval: Duration::from_millis(300), - }, - title: Default::default(), - } - } - - 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, - last_git_diff_update_count: buffer_state.last_git_diff_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), - next_excerpt_id: 1, - 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 - } - - pub fn singleton(buffer: Model, cx: &mut ModelContext) -> Self { - let mut this = Self::new(buffer.read(cx).replica_id()); - this.singleton = true; - this.push_excerpts( - buffer, - [ExcerptRange { - context: text::Anchor::MIN..text::Anchor::MAX, - primary: None, - }], - cx, - ); - this.snapshot.borrow_mut().singleton = true; - this - } - - pub fn replica_id(&self) -> ReplicaId { - self.replica_id - } - - pub fn snapshot(&self, cx: &AppContext) -> MultiBufferSnapshot { - self.sync(cx); - self.snapshot.borrow().clone() - } - - pub fn read(&self, cx: &AppContext) -> Ref { - self.sync(cx); - self.snapshot.borrow() - } - - pub fn as_singleton(&self) -> Option> { - if self.singleton { - return Some( - self.buffers - .borrow() - .values() - .next() - .unwrap() - .buffer - .clone(), - ); - } else { - None - } - } - - pub fn is_singleton(&self) -> bool { - self.singleton - } - - pub fn subscribe(&mut self) -> Subscription { - self.subscriptions.subscribe() - } - - pub fn is_dirty(&self, cx: &AppContext) -> bool { - self.read(cx).is_dirty() - } - - pub fn has_conflict(&self, cx: &AppContext) -> bool { - self.read(cx).has_conflict() - } - - // The `is_empty` signature doesn't match what clippy expects - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, cx: &AppContext) -> usize { - self.read(cx).len() - } - - pub fn is_empty(&self, cx: &AppContext) -> bool { - self.len(cx) != 0 - } - - pub fn symbols_containing( - &self, - offset: T, - theme: Option<&SyntaxTheme>, - cx: &AppContext, - ) -> Option<(u64, Vec>)> { - self.read(cx).symbols_containing(offset, theme) - } - - pub fn edit( - &mut self, - edits: I, - mut autoindent_mode: Option, - cx: &mut ModelContext, - ) where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.buffers.borrow().is_empty() { - return; - } - - let snapshot = self.read(cx); - let edits = edits.into_iter().map(|(range, new_text)| { - let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); - if range.start > range.end { - mem::swap(&mut range.start, &mut range.end); - } - (range, new_text) - }); - - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| { - buffer.edit(edits, autoindent_mode, cx); - }); - } - - let original_indent_columns = match &mut autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) => mem::take(original_indent_columns), - _ => Default::default(), - }; - - struct BufferEdit { - range: Range, - new_text: Arc, - is_insertion: bool, - original_indent_column: u32, - } - let mut buffer_edits: HashMap> = Default::default(); - let mut edited_excerpt_ids = Vec::new(); - let mut cursor = snapshot.excerpts.cursor::(); - for (ix, (range, new_text)) in edits.enumerate() { - let new_text: Arc = new_text.into(); - let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0); - cursor.seek(&range.start, Bias::Right, &()); - if cursor.item().is_none() && range.start == *cursor.start() { - cursor.prev(&()); - } - let start_excerpt = cursor.item().expect("start offset out of bounds"); - let start_overshoot = range.start - cursor.start(); - let buffer_start = start_excerpt - .range - .context - .start - .to_offset(&start_excerpt.buffer) - + start_overshoot; - edited_excerpt_ids.push(start_excerpt.id); - - cursor.seek(&range.end, Bias::Right, &()); - if cursor.item().is_none() && range.end == *cursor.start() { - cursor.prev(&()); - } - let end_excerpt = cursor.item().expect("end offset out of bounds"); - let end_overshoot = range.end - cursor.start(); - let buffer_end = end_excerpt - .range - .context - .start - .to_offset(&end_excerpt.buffer) - + end_overshoot; - - if start_excerpt.id == end_excerpt.id { - buffer_edits - .entry(start_excerpt.buffer_id) - .or_insert(Vec::new()) - .push(BufferEdit { - range: buffer_start..buffer_end, - new_text, - is_insertion: true, - original_indent_column, - }); - } else { - edited_excerpt_ids.push(end_excerpt.id); - let start_excerpt_range = buffer_start - ..start_excerpt - .range - .context - .end - .to_offset(&start_excerpt.buffer); - let end_excerpt_range = end_excerpt - .range - .context - .start - .to_offset(&end_excerpt.buffer) - ..buffer_end; - buffer_edits - .entry(start_excerpt.buffer_id) - .or_insert(Vec::new()) - .push(BufferEdit { - range: start_excerpt_range, - new_text: new_text.clone(), - is_insertion: true, - original_indent_column, - }); - buffer_edits - .entry(end_excerpt.buffer_id) - .or_insert(Vec::new()) - .push(BufferEdit { - range: end_excerpt_range, - new_text: new_text.clone(), - is_insertion: false, - original_indent_column, - }); - - cursor.seek(&range.start, Bias::Right, &()); - cursor.next(&()); - while let Some(excerpt) = cursor.item() { - if excerpt.id == end_excerpt.id { - break; - } - buffer_edits - .entry(excerpt.buffer_id) - .or_insert(Vec::new()) - .push(BufferEdit { - range: excerpt.range.context.to_offset(&excerpt.buffer), - new_text: new_text.clone(), - is_insertion: false, - original_indent_column, - }); - edited_excerpt_ids.push(excerpt.id); - cursor.next(&()); - } - } - } - - drop(cursor); - drop(snapshot); - // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. - fn tail( - this: &mut MultiBuffer, - buffer_edits: HashMap>, - autoindent_mode: Option, - edited_excerpt_ids: Vec, - cx: &mut ModelContext, - ) { - for (buffer_id, mut edits) in buffer_edits { - edits.sort_unstable_by_key(|edit| edit.range.start); - this.buffers.borrow()[&buffer_id] - .buffer - .update(cx, |buffer, cx| { - let mut edits = edits.into_iter().peekable(); - let mut insertions = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut deletions = Vec::new(); - let empty_str: Arc = "".into(); - while let Some(BufferEdit { - mut range, - new_text, - mut is_insertion, - original_indent_column, - }) = edits.next() - { - while let Some(BufferEdit { - range: next_range, - is_insertion: next_is_insertion, - .. - }) = edits.peek() - { - if range.end >= next_range.start { - range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; - edits.next(); - } else { - break; - } - } - - if is_insertion { - original_indent_columns.push(original_indent_column); - insertions.push(( - buffer.anchor_before(range.start) - ..buffer.anchor_before(range.end), - new_text.clone(), - )); - } else if !range.is_empty() { - deletions.push(( - buffer.anchor_before(range.start) - ..buffer.anchor_before(range.end), - empty_str.clone(), - )); - } - } - - let deletion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns: Default::default(), - }) - } else { - None - }; - let insertion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - None - }; - - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); - }) - } - - cx.emit(Event::ExcerptsEdited { - ids: edited_excerpt_ids, - }); - } - tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx); - } - - pub fn start_transaction(&mut self, cx: &mut ModelContext) -> Option { - self.start_transaction_at(Instant::now(), cx) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - cx: &mut ModelContext, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - - for BufferState { buffer, .. } in self.buffers.borrow().values() { - buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - self.history.start_transaction(now) - } - - pub fn end_transaction(&mut self, cx: &mut ModelContext) -> Option { - self.end_transaction_at(Instant::now(), cx) - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut ModelContext, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); - } - - let mut buffer_transactions = HashMap::default(); - for BufferState { buffer, .. } in self.buffers.borrow().values() { - if let Some(transaction_id) = - buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); - } - } - - if self.history.end_transaction(now, buffer_transactions) { - let transaction_id = self.history.group().unwrap(); - Some(transaction_id) - } else { - None - } - } - - pub fn merge_transactions( - &mut self, - transaction: TransactionId, - destination: TransactionId, - cx: &mut ModelContext, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.merge_transactions(transaction, destination) - }); - } else { - if let Some(transaction) = self.history.forget(transaction) { - if let Some(destination) = self.history.transaction_mut(destination) { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(destination_buffer_transaction_id) = - destination.buffer_transactions.get(&buffer_id) - { - if let Some(state) = self.buffers.borrow().get(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.merge_transactions( - buffer_transaction_id, - *destination_buffer_transaction_id, - ) - }); - } - } else { - destination - .buffer_transactions - .insert(buffer_id, buffer_transaction_id); - } - } - } - } - } - } - - pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext) { - self.history.finalize_last_transaction(); - for BufferState { buffer, .. } in self.buffers.borrow().values() { - buffer.update(cx, |buffer, _| { - buffer.finalize_last_transaction(); - }); - } - } - - pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext) - where - T: IntoIterator, &'a language::Transaction)>, - { - self.history - .push_transaction(buffer_transactions, Instant::now(), cx); - self.history.finalize_last_transaction(); - } - - pub fn group_until_transaction( - &mut self, - transaction_id: TransactionId, - cx: &mut ModelContext, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.group_until_transaction(transaction_id) - }); - } else { - self.history.group_until(transaction_id); - } - } - - pub fn set_active_selections( - &mut self, - selections: &[Selection], - line_mode: bool, - cursor_shape: CursorShape, - cx: &mut ModelContext, - ) { - let mut selections_by_buffer: HashMap>> = - Default::default(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::>(); - for selection in selections { - let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id); - let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id); - - cursor.seek(&Some(start_locator), Bias::Left, &()); - while let Some(excerpt) = cursor.item() { - if excerpt.locator > *end_locator { - break; - } - - let mut start = excerpt.range.context.start; - let mut end = excerpt.range.context.end; - if excerpt.id == selection.start.excerpt_id { - start = selection.start.text_anchor; - } - if excerpt.id == selection.end.excerpt_id { - end = selection.end.text_anchor; - } - selections_by_buffer - .entry(excerpt.buffer_id) - .or_default() - .push(Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - }); - - cursor.next(&()); - } - } - - for (buffer_id, buffer_state) in self.buffers.borrow().iter() { - if !selections_by_buffer.contains_key(buffer_id) { - buffer_state - .buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - } - } - - for (buffer_id, mut selections) in selections_by_buffer { - self.buffers.borrow()[&buffer_id] - .buffer - .update(cx, |buffer, cx| { - selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); - let mut selections = selections.into_iter().peekable(); - let merged_selections = Arc::from_iter(iter::from_fn(|| { - let mut selection = selections.next()?; - while let Some(next_selection) = selections.peek() { - if selection.end.cmp(&next_selection.start, buffer).is_ge() { - let next_selection = selections.next().unwrap(); - if next_selection.end.cmp(&selection.end, buffer).is_ge() { - selection.end = next_selection.end; - } - } else { - break; - } - } - Some(selection) - })); - buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); - }); - } - } - - pub fn remove_active_selections(&mut self, cx: &mut ModelContext) { - for buffer in self.buffers.borrow().values() { - buffer - .buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - } - } - - pub fn undo(&mut self, cx: &mut ModelContext) -> Option { - let mut transaction_id = None; - if let Some(buffer) = self.as_singleton() { - transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); - } else { - while let Some(transaction) = self.history.pop_undo() { - let mut undone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { - undone |= buffer.update(cx, |buffer, cx| { - let undo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_undo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.undo_to_transaction(undo_to, cx) - }); - } - } - - if undone { - transaction_id = Some(transaction.id); - break; - } - } - } - - if let Some(transaction_id) = transaction_id { - cx.emit(Event::TransactionUndone { transaction_id }); - } - - transaction_id - } - - pub fn redo(&mut self, cx: &mut ModelContext) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.redo(cx)); - } - - while let Some(transaction) = self.history.pop_redo() { - let mut redone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { - redone |= buffer.update(cx, |buffer, cx| { - let redo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_redo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.redo_to_transaction(redo_to, cx) - }); - } - } - - if redone { - return Some(transaction.id); - } - } - - None - } - - pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); - } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { - for (buffer_id, transaction_id) in &transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.undo_transaction(*transaction_id, cx) - }); - } - } - } - } - - pub fn stream_excerpts_with_context_lines( - &mut self, - buffer: Model, - ranges: Vec>, - context_line_count: u32, - cx: &mut ModelContext, - ) -> mpsc::Receiver> { - let (buffer_id, buffer_snapshot) = - buffer.update(cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); - - let (mut tx, rx) = mpsc::channel(256); - cx.spawn(move |this, mut cx| async move { - let mut excerpt_ranges = Vec::new(); - let mut range_counts = Vec::new(); - cx.background_executor() - .scoped(|scope| { - scope.spawn(async { - let (ranges, counts) = - build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); - excerpt_ranges = ranges; - range_counts = counts; - }); - }) - .await; - - let mut ranges = ranges.into_iter(); - let mut range_counts = range_counts.into_iter(); - for excerpt_ranges in excerpt_ranges.chunks(100) { - let excerpt_ids = match this.update(&mut cx, |this, cx| { - this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) - }) { - Ok(excerpt_ids) => excerpt_ids, - Err(_) => return, - }; - - for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref()) - { - for range in ranges.by_ref().take(range_count) { - let start = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.start, - }; - let end = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.end, - }; - if tx.send(start..end).await.is_err() { - break; - } - } - } - } - }) - .detach(); - - rx - } - - pub fn push_excerpts( - &mut self, - buffer: Model, - ranges: impl IntoIterator>, - cx: &mut ModelContext, - ) -> Vec - where - O: text::ToOffset, - { - self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) - } - - pub fn push_excerpts_with_context_lines( - &mut self, - buffer: Model, - ranges: Vec>, - context_line_count: u32, - cx: &mut ModelContext, - ) -> Vec> - where - O: text::ToPoint + text::ToOffset, - { - let buffer_id = buffer.read(cx).remote_id(); - let buffer_snapshot = buffer.read(cx).snapshot(); - let (excerpt_ranges, range_counts) = - build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); - - let excerpt_ids = self.push_excerpts(buffer, excerpt_ranges, cx); - - let mut anchor_ranges = Vec::new(); - let mut ranges = ranges.into_iter(); - for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.into_iter()) { - anchor_ranges.extend(ranges.by_ref().take(range_count).map(|range| { - let start = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: buffer_snapshot.anchor_after(range.start), - }; - let end = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: buffer_snapshot.anchor_after(range.end), - }; - start..end - })) - } - anchor_ranges - } - - pub fn insert_excerpts_after( - &mut self, - prev_excerpt_id: ExcerptId, - buffer: Model, - ranges: impl IntoIterator>, - cx: &mut ModelContext, - ) -> Vec - where - O: text::ToOffset, - { - let mut ids = Vec::new(); - let mut next_excerpt_id = self.next_excerpt_id; - self.insert_excerpts_with_ids_after( - prev_excerpt_id, - buffer, - ranges.into_iter().map(|range| { - let id = ExcerptId(post_inc(&mut next_excerpt_id)); - ids.push(id); - (id, range) - }), - cx, - ); - ids - } - - pub fn insert_excerpts_with_ids_after( - &mut self, - prev_excerpt_id: ExcerptId, - buffer: Model, - ranges: impl IntoIterator)>, - cx: &mut ModelContext, - ) where - 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.read(cx).remote_id(); - let buffer_snapshot = buffer.read(cx).snapshot(); - - let mut buffers = self.buffers.borrow_mut(); - let buffer_state = buffers.entry(buffer_id).or_insert_with(|| BufferState { - last_version: buffer_snapshot.version().clone(), - last_parse_count: buffer_snapshot.parse_count(), - last_selections_update_count: buffer_snapshot.selections_update_count(), - last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(), - last_file_update_count: buffer_snapshot.file_update_count(), - last_git_diff_update_count: buffer_snapshot.git_diff_update_count(), - excerpts: Default::default(), - _subscriptions: [ - cx.observe(&buffer, |_, _, cx| cx.notify()), - cx.subscribe(&buffer, Self::on_buffer_event), - ], - buffer: buffer.clone(), - }); - - let mut snapshot = self.snapshot.borrow_mut(); - - let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone(); - let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids); - let mut cursor = snapshot.excerpts.cursor::>(); - let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &()); - prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone(); - - let edit_start = new_excerpts.summary().text.len; - new_excerpts.update_last( - |excerpt| { - excerpt.has_trailing_newline = true; - }, - &(), - ); - - let next_locator = if let Some(excerpt) = cursor.item() { - excerpt.locator.clone() - } else { - Locator::max() - }; - - let mut excerpts = Vec::new(); - while let Some((id, range)) = ranges.next() { - let locator = Locator::between(&prev_locator, &next_locator); - if let Err(ix) = buffer_state.excerpts.binary_search(&locator) { - buffer_state.excerpts.insert(ix, locator.clone()); - } - let range = ExcerptRange { - context: buffer_snapshot.anchor_before(&range.context.start) - ..buffer_snapshot.anchor_after(&range.context.end), - primary: range.primary.map(|primary| { - buffer_snapshot.anchor_before(&primary.start) - ..buffer_snapshot.anchor_after(&primary.end) - }), - }; - if id.0 >= self.next_excerpt_id { - self.next_excerpt_id = id.0 + 1; - } - excerpts.push((id, range.clone())); - let excerpt = Excerpt::new( - id, - locator.clone(), - buffer_id, - buffer_snapshot.clone(), - range, - ranges.peek().is_some() || cursor.item().is_some(), - ); - new_excerpts.push(excerpt, &()); - prev_locator = locator.clone(); - new_excerpt_ids.push(ExcerptIdMapping { id, locator }, &()); - } - - let edit_end = new_excerpts.summary().text.len; - - let suffix = cursor.suffix(&()); - let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.append(suffix, &()); - drop(cursor); - snapshot.excerpts = new_excerpts; - snapshot.excerpt_ids = new_excerpt_ids; - if changed_trailing_excerpt { - snapshot.trailing_excerpt_update_count += 1; - } - - self.subscriptions.publish_mut([Edit { - old: edit_start..edit_start, - new: edit_start..edit_end, - }]); - cx.emit(Event::Edited { - sigleton_buffer_edited: false, - }); - cx.emit(Event::ExcerptsAdded { - buffer, - predecessor: prev_excerpt_id, - excerpts, - }); - cx.notify(); - } - - pub fn clear(&mut self, cx: &mut ModelContext) { - self.sync(cx); - let ids = self.excerpt_ids(); - 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.emit(Event::Edited { - sigleton_buffer_edited: false, - }); - cx.emit(Event::ExcerptsRemoved { ids }); - cx.notify(); - } - - pub fn excerpts_for_buffer( - &self, - buffer: &Model, - cx: &AppContext, - ) -> Vec<(ExcerptId, ExcerptRange)> { - let mut excerpts = Vec::new(); - let snapshot = self.read(cx); - let buffers = self.buffers.borrow(); - let mut cursor = snapshot.excerpts.cursor::>(); - for locator in buffers - .get(&buffer.read(cx).remote_id()) - .map(|state| &state.excerpts) - .into_iter() - .flatten() - { - cursor.seek_forward(&Some(locator), Bias::Left, &()); - if let Some(excerpt) = cursor.item() { - if excerpt.locator == *locator { - excerpts.push((excerpt.id.clone(), excerpt.range.clone())); - } - } - } - - excerpts - } - - pub fn excerpt_ids(&self) -> Vec { - self.snapshot - .borrow() - .excerpts - .iter() - .map(|entry| entry.id) - .collect() - } - - pub fn excerpt_containing( - &self, - position: impl ToOffset, - cx: &AppContext, - ) -> Option<(ExcerptId, Model, Range)> { - let snapshot = self.read(cx); - let position = position.to_offset(&snapshot); - - let mut cursor = snapshot.excerpts.cursor::(); - cursor.seek(&position, Bias::Right, &()); - cursor - .item() - .or_else(|| snapshot.excerpts.last()) - .map(|excerpt| { - ( - excerpt.id.clone(), - self.buffers - .borrow() - .get(&excerpt.buffer_id) - .unwrap() - .buffer - .clone(), - excerpt.range.context.clone(), - ) - }) - } - - // If point is at the end of the buffer, the last excerpt is returned - pub fn point_to_buffer_offset( - &self, - point: T, - cx: &AppContext, - ) -> Option<(Model, usize, ExcerptId)> { - let snapshot = self.read(cx); - let offset = point.to_offset(&snapshot); - let mut cursor = snapshot.excerpts.cursor::(); - cursor.seek(&offset, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - - cursor.item().map(|excerpt| { - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let buffer_point = excerpt_start + offset - *cursor.start(); - let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); - - (buffer, buffer_point, excerpt.id) - }) - } - - pub fn range_to_buffer_ranges( - &self, - range: Range, - cx: &AppContext, - ) -> Vec<(Model, Range, ExcerptId)> { - let snapshot = self.read(cx); - let start = range.start.to_offset(&snapshot); - let end = range.end.to_offset(&snapshot); - - let mut result = Vec::new(); - let mut cursor = snapshot.excerpts.cursor::(); - cursor.seek(&start, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - - while let Some(excerpt) = cursor.item() { - if *cursor.start() > end { - break; - } - - let mut end_before_newline = cursor.end(&()); - if excerpt.has_trailing_newline { - end_before_newline -= 1; - } - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start()); - let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start()); - let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); - result.push((buffer, start..end, excerpt.id)); - cursor.next(&()); - } - - result - } - - pub fn remove_excerpts( - &mut self, - excerpt_ids: impl IntoIterator, - cx: &mut ModelContext, - ) { - self.sync(cx); - let ids = excerpt_ids.into_iter().collect::>(); - if ids.is_empty() { - return; - } - - let mut buffers = self.buffers.borrow_mut(); - let mut snapshot = self.snapshot.borrow_mut(); - let mut new_excerpts = SumTree::new(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); - let mut edits = Vec::new(); - let mut excerpt_ids = ids.iter().copied().peekable(); - - while let Some(excerpt_id) = excerpt_ids.next() { - // Seek to the next excerpt to remove, preserving any preceding excerpts. - let locator = snapshot.excerpt_locator_for_id(excerpt_id); - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); - - if let Some(mut excerpt) = cursor.item() { - if excerpt.id != excerpt_id { - continue; - } - let mut old_start = cursor.start().1; - - // Skip over the removed excerpt. - 'remove_excerpts: loop { - if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) { - buffer_state.excerpts.retain(|l| l != &excerpt.locator); - if buffer_state.excerpts.is_empty() { - buffers.remove(&excerpt.buffer_id); - } - } - cursor.next(&()); - - // Skip over any subsequent excerpts that are also removed. - while let Some(&next_excerpt_id) = excerpt_ids.peek() { - let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id); - if let Some(next_excerpt) = cursor.item() { - if next_excerpt.locator == *next_locator { - excerpt_ids.next(); - excerpt = next_excerpt; - continue 'remove_excerpts; - } - } - break; - } - - break; - } - - // When removing the last excerpt, remove the trailing newline from - // the previous excerpt. - if cursor.item().is_none() && old_start > 0 { - old_start -= 1; - new_excerpts.update_last(|e| e.has_trailing_newline = false, &()); - } - - // Push an edit for the removal of this run of excerpts. - let old_end = cursor.start().1; - let new_start = new_excerpts.summary().text.len; - edits.push(Edit { - old: old_start..old_end, - new: new_start..new_start, - }); - } - } - let suffix = cursor.suffix(&()); - let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.append(suffix, &()); - drop(cursor); - snapshot.excerpts = new_excerpts; - - if changed_trailing_excerpt { - snapshot.trailing_excerpt_update_count += 1; - } - - self.subscriptions.publish_mut(edits); - cx.emit(Event::Edited { - sigleton_buffer_edited: false, - }); - cx.emit(Event::ExcerptsRemoved { ids }); - cx.notify(); - } - - pub fn wait_for_anchors<'a>( - &self, - anchors: impl 'a + Iterator, - cx: &mut ModelContext, - ) -> impl 'static + Future> { - let borrow = self.buffers.borrow(); - let mut error = None; - let mut futures = Vec::new(); - for anchor in anchors { - if let Some(buffer_id) = anchor.buffer_id { - if let Some(buffer) = borrow.get(&buffer_id) { - buffer.buffer.update(cx, |buffer, _| { - futures.push(buffer.wait_for_anchors([anchor.text_anchor])) - }); - } else { - error = Some(anyhow!( - "buffer {buffer_id} is not part of this multi-buffer" - )); - break; - } - } - } - async move { - if let Some(error) = error { - Err(error)?; - } - for future in futures { - future.await?; - } - Ok(()) - } - } - - pub fn text_anchor_for_position( - &self, - position: T, - cx: &AppContext, - ) -> Option<(Model, language::Anchor)> { - let snapshot = self.read(cx); - let anchor = snapshot.anchor_before(position); - let buffer = self - .buffers - .borrow() - .get(&anchor.buffer_id?)? - .buffer - .clone(); - Some((buffer, anchor.text_anchor)) - } - - fn on_buffer_event( - &mut self, - _: Model, - event: &language::Event, - cx: &mut ModelContext, - ) { - cx.emit(match event { - language::Event::Edited => Event::Edited { - sigleton_buffer_edited: true, - }, - language::Event::DirtyChanged => Event::DirtyChanged, - language::Event::Saved => Event::Saved, - language::Event::FileHandleChanged => Event::FileHandleChanged, - language::Event::Reloaded => Event::Reloaded, - language::Event::DiffBaseChanged => Event::DiffBaseChanged, - language::Event::LanguageChanged => Event::LanguageChanged, - language::Event::Reparsed => Event::Reparsed, - language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, - language::Event::Closed => Event::Closed, - - // - language::Event::Operation(_) => return, - }); - } - - pub fn all_buffers(&self) -> HashSet> { - self.buffers - .borrow() - .values() - .map(|state| state.buffer.clone()) - .collect() - } - - pub fn buffer(&self, buffer_id: u64) -> Option> { - self.buffers - .borrow() - .get(&buffer_id) - .map(|state| state.buffer.clone()) - } - - pub fn is_completion_trigger(&self, position: Anchor, text: &str, cx: &AppContext) -> bool { - let mut chars = text.chars(); - let char = if let Some(char) = chars.next() { - char - } else { - return false; - }; - if chars.next().is_some() { - return false; - } - - let snapshot = self.snapshot(cx); - let position = position.to_offset(&snapshot); - let scope = snapshot.language_scope_at(position); - if char_kind(&scope, char) == CharKind::Word { - return true; - } - - let anchor = snapshot.anchor_before(position); - anchor - .buffer_id - .and_then(|buffer_id| { - let buffer = self.buffers.borrow().get(&buffer_id)?.buffer.clone(); - Some( - buffer - .read(cx) - .completion_triggers() - .iter() - .any(|string| string == text), - ) - }) - .unwrap_or(false) - } - - pub fn language_at<'a, T: ToOffset>( - &self, - point: T, - cx: &'a AppContext, - ) -> Option> { - self.point_to_buffer_offset(point, cx) - .and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset)) - } - - pub fn settings_at<'a, T: ToOffset>( - &self, - point: T, - cx: &'a AppContext, - ) -> &'a LanguageSettings { - let mut language = None; - let mut file = None; - if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { - let buffer = buffer.read(cx); - language = buffer.language_at(offset); - file = buffer.file(); - } - language_settings(language.as_ref(), file, cx) - } - - pub fn for_each_buffer(&self, mut f: impl FnMut(&Model)) { - self.buffers - .borrow() - .values() - .for_each(|state| f(&state.buffer)) - } - - pub fn title<'a>(&'a self, cx: &'a AppContext) -> Cow<'a, str> { - if let Some(title) = self.title.as_ref() { - return title.into(); - } - - if let Some(buffer) = self.as_singleton() { - if let Some(file) = buffer.read(cx).file() { - return file.file_name(cx).to_string_lossy(); - } - } - - "untitled".into() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn is_parsing(&self, cx: &AppContext) -> bool { - self.as_singleton().unwrap().read(cx).is_parsing() - } - - fn sync(&self, cx: &AppContext) { - let mut snapshot = self.snapshot.borrow_mut(); - let mut excerpts_to_edit = Vec::new(); - let mut reparsed = false; - let mut diagnostics_updated = false; - let mut git_diff_updated = false; - let mut is_dirty = false; - let mut has_conflict = false; - let mut edited = false; - let mut buffers = self.buffers.borrow_mut(); - for buffer_state in buffers.values_mut() { - let buffer = buffer_state.buffer.read(cx); - let version = buffer.version(); - let parse_count = buffer.parse_count(); - let selections_update_count = buffer.selections_update_count(); - let diagnostics_update_count = buffer.diagnostics_update_count(); - let file_update_count = buffer.file_update_count(); - let git_diff_update_count = buffer.git_diff_update_count(); - - let buffer_edited = version.changed_since(&buffer_state.last_version); - let buffer_reparsed = parse_count > buffer_state.last_parse_count; - let buffer_selections_updated = - selections_update_count > buffer_state.last_selections_update_count; - let buffer_diagnostics_updated = - diagnostics_update_count > buffer_state.last_diagnostics_update_count; - let buffer_file_updated = file_update_count > buffer_state.last_file_update_count; - let buffer_git_diff_updated = - git_diff_update_count > buffer_state.last_git_diff_update_count; - if buffer_edited - || buffer_reparsed - || buffer_selections_updated - || buffer_diagnostics_updated - || buffer_file_updated - || buffer_git_diff_updated - { - buffer_state.last_version = version; - buffer_state.last_parse_count = parse_count; - buffer_state.last_selections_update_count = selections_update_count; - buffer_state.last_diagnostics_update_count = diagnostics_update_count; - buffer_state.last_file_update_count = file_update_count; - buffer_state.last_git_diff_update_count = git_diff_update_count; - excerpts_to_edit.extend( - buffer_state - .excerpts - .iter() - .map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)), - ); - } - - edited |= buffer_edited; - reparsed |= buffer_reparsed; - diagnostics_updated |= buffer_diagnostics_updated; - git_diff_updated |= buffer_git_diff_updated; - is_dirty |= buffer.is_dirty(); - has_conflict |= buffer.has_conflict(); - } - if edited { - snapshot.edit_count += 1; - } - if reparsed { - snapshot.parse_count += 1; - } - if diagnostics_updated { - snapshot.diagnostics_update_count += 1; - } - if git_diff_updated { - snapshot.git_diff_update_count += 1; - } - snapshot.is_dirty = is_dirty; - snapshot.has_conflict = has_conflict; - - excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); - - let mut edits = Vec::new(); - let mut new_excerpts = SumTree::new(); - let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); - - for (locator, buffer, buffer_edited) in excerpts_to_edit { - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); - let old_excerpt = cursor.item().unwrap(); - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - - let mut new_excerpt; - if buffer_edited { - edits.extend( - buffer - .edits_since_in_range::( - old_excerpt.buffer.version(), - old_excerpt.range.context.clone(), - ) - .map(|mut edit| { - let excerpt_old_start = cursor.start().1; - let excerpt_new_start = new_excerpts.summary().text.len; - edit.old.start += excerpt_old_start; - edit.old.end += excerpt_old_start; - edit.new.start += excerpt_new_start; - edit.new.end += excerpt_new_start; - edit - }), - ); - - new_excerpt = Excerpt::new( - old_excerpt.id, - locator.clone(), - buffer_id, - buffer.snapshot(), - old_excerpt.range.clone(), - old_excerpt.has_trailing_newline, - ); - } else { - new_excerpt = old_excerpt.clone(); - new_excerpt.buffer = buffer.snapshot(); - } - - new_excerpts.push(new_excerpt, &()); - cursor.next(&()); - } - new_excerpts.append(cursor.suffix(&()), &()); - - drop(cursor); - snapshot.excerpts = new_excerpts; - - self.subscriptions.publish(edits); - } -} - -#[cfg(any(test, feature = "test-support"))] -impl MultiBuffer { - pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> Model { - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); - cx.new_model(|cx| Self::singleton(buffer, cx)) - } - - pub fn build_multi( - excerpts: [(&str, Vec>); COUNT], - cx: &mut gpui::AppContext, - ) -> Model { - let multi = cx.new_model(|_| Self::new(0)); - for (text, ranges) in excerpts { - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text)); - let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { - context: range, - primary: None, - }); - multi.update(cx, |multi, cx| { - multi.push_excerpts(buffer, excerpt_ranges, cx) - }); - } - - multi - } - - pub fn build_from_buffer(buffer: Model, cx: &mut gpui::AppContext) -> Model { - cx.new_model(|cx| Self::singleton(buffer, cx)) - } - - pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model { - cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - let mutation_count = rng.gen_range(1..=5); - multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); - multibuffer - }) - } - - pub fn randomly_edit( - &mut self, - rng: &mut impl rand::Rng, - edit_count: usize, - cx: &mut ModelContext, - ) { - use util::RandomCharIter; - - let snapshot = self.read(cx); - let mut edits: Vec<(Range, Arc)> = Vec::new(); - let mut last_end = None; - for _ in 0..edit_count { - if last_end.map_or(false, |last_end| last_end >= snapshot.len()) { - break; - } - - let new_start = last_end.map_or(0, |last_end| last_end + 1); - let end = snapshot.clip_offset(rng.gen_range(new_start..=snapshot.len()), Bias::Right); - let start = snapshot.clip_offset(rng.gen_range(new_start..=end), Bias::Right); - last_end = Some(end); - - let mut range = start..end; - if rng.gen_bool(0.2) { - mem::swap(&mut range.start, &mut range.end); - } - - let new_text_len = rng.gen_range(0..10); - let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); - - edits.push((range, new_text.into())); - } - log::info!("mutating multi-buffer with {:?}", edits); - drop(snapshot); - - self.edit(edits, None, cx); - } - - pub fn randomly_edit_excerpts( - &mut self, - rng: &mut impl rand::Rng, - mutation_count: usize, - cx: &mut ModelContext, - ) { - use rand::prelude::*; - use std::env; - use util::RandomCharIter; - - let max_excerpts = env::var("MAX_EXCERPTS") - .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable")) - .unwrap_or(5); - - 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.excerpt_ids(); - if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { - let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { - let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text))); - let buffer = buffers.last().unwrap().read(cx); - log::info!( - "Creating new buffer {} with text: {:?}", - buffer.remote_id(), - buffer.text() - ); - buffers.last().unwrap().clone() - } else { - self.buffers - .borrow() - .values() - .choose(rng) - .unwrap() - .buffer - .clone() - }; - - let buffer = buffer_handle.read(cx); - 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); - ExcerptRange { - context: start_ix..end_ix, - primary: None, - } - }) - .collect::>(); - log::info!( - "Inserting excerpts from buffer {} and ranges {:?}: {:?}", - buffer_handle.read(cx).remote_id(), - ranges.iter().map(|r| &r.context).collect::>(), - ranges - .iter() - .map(|r| &buffer_text[r.context.clone()]) - .collect::>() - ); - - let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); - log::info!("Inserted with ids: {:?}", excerpt_id); - } else { - let remove_count = rng.gen_range(1..=excerpt_ids.len()); - let mut excerpts_to_remove = excerpt_ids - .choose_multiple(rng, remove_count) - .cloned() - .collect::>(); - let snapshot = self.snapshot.borrow(); - excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &*snapshot)); - drop(snapshot); - log::info!("Removing excerpts {:?}", excerpts_to_remove); - self.remove_excerpts(excerpts_to_remove, cx); - } - } - } - - pub fn randomly_mutate( - &mut self, - rng: &mut impl rand::Rng, - mutation_count: usize, - cx: &mut ModelContext, - ) { - use rand::prelude::*; - - if rng.gen_bool(0.7) || self.singleton { - let buffer = self - .buffers - .borrow() - .values() - .choose(rng) - .map(|state| state.buffer.clone()); - - if let Some(buffer) = buffer { - buffer.update(cx, |buffer, cx| { - if rng.gen() { - buffer.randomly_edit(rng, mutation_count, cx); - } else { - buffer.randomly_undo_redo(rng, cx); - } - }); - } else { - self.randomly_edit(rng, mutation_count, cx); - } - } else { - self.randomly_edit_excerpts(rng, mutation_count, cx); - } - - self.check_invariants(cx); - } - - fn check_invariants(&self, cx: &mut ModelContext) { - let snapshot = self.read(cx); - let excerpts = snapshot.excerpts.items(&()); - let excerpt_ids = snapshot.excerpt_ids.items(&()); - - for (ix, excerpt) in excerpts.iter().enumerate() { - if ix == 0 { - if excerpt.locator <= Locator::min() { - panic!("invalid first excerpt locator {:?}", excerpt.locator); - } - } else { - if excerpt.locator <= excerpts[ix - 1].locator { - panic!("excerpts are out-of-order: {:?}", excerpts); - } - } - } - - for (ix, entry) in excerpt_ids.iter().enumerate() { - if ix == 0 { - if entry.id.cmp(&ExcerptId::min(), &*snapshot).is_le() { - panic!("invalid first excerpt id {:?}", entry.id); - } - } else { - if entry.id <= excerpt_ids[ix - 1].id { - panic!("excerpt ids are out-of-order: {:?}", excerpt_ids); - } - } - } - } -} - -impl EventEmitter for MultiBuffer {} - -impl MultiBufferSnapshot { - pub fn text(&self) -> String { - self.chunks(0..self.len(), false) - .map(|chunk| chunk.text) - .collect() - } - - pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { - let mut offset = position.to_offset(self); - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&offset, Bias::Left, &()); - let mut excerpt_chunks = cursor.item().map(|excerpt| { - let end_before_footer = cursor.start() + excerpt.text_summary.len; - let start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let end = start + (cmp::min(offset, end_before_footer) - cursor.start()); - excerpt.buffer.reversed_chunks_in_range(start..end) - }); - iter::from_fn(move || { - if offset == *cursor.start() { - cursor.prev(&()); - let excerpt = cursor.item()?; - excerpt_chunks = Some( - excerpt - .buffer - .reversed_chunks_in_range(excerpt.range.context.clone()), - ); - } - - let excerpt = cursor.item().unwrap(); - if offset == cursor.end(&()) && excerpt.has_trailing_newline { - offset -= 1; - Some("\n") - } else { - let chunk = excerpt_chunks.as_mut().unwrap().next().unwrap(); - offset -= chunk.len(); - Some(chunk) - } - }) - .flat_map(|c| c.chars().rev()) - } - - pub fn chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.text_for_range(offset..self.len()) - .flat_map(|chunk| chunk.chars()) - } - - pub fn text_for_range(&self, range: Range) -> impl Iterator + '_ { - self.chunks(range, false).map(|chunk| chunk.text) - } - - pub fn is_line_blank(&self, row: u32) -> bool { - self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - - pub fn contains_str_at(&self, position: T, needle: &str) -> bool - where - T: ToOffset, - { - let position = position.to_offset(self); - position == self.clip_offset(position, Bias::Left) - && self - .bytes_in_range(position..self.len()) - .flatten() - .copied() - .take(needle.len()) - .eq(needle.bytes()) - } - - pub fn surrounding_word(&self, start: T) -> (Range, Option) { - let mut start = start.to_offset(self); - let mut end = start; - let mut next_chars = self.chars_at(start).peekable(); - let mut prev_chars = self.reversed_chars_at(start).peekable(); - - let scope = self.language_scope_at(start); - let kind = |c| char_kind(&scope, c); - let word_kind = cmp::max( - prev_chars.peek().copied().map(kind), - next_chars.peek().copied().map(kind), - ); - - for ch in prev_chars { - if Some(kind(ch)) == word_kind && ch != '\n' { - start -= ch.len_utf8(); - } else { - break; - } - } - - for ch in next_chars { - if Some(kind(ch)) == word_kind && ch != '\n' { - end += ch.len_utf8(); - } else { - break; - } - } - - (start..end, word_kind) - } - - pub fn as_singleton(&self) -> Option<(&ExcerptId, u64, &BufferSnapshot)> { - if self.singleton { - self.excerpts - .iter() - .next() - .map(|e| (&e.id, e.buffer_id, &e.buffer)) - } else { - None - } - } - - pub fn len(&self) -> usize { - self.excerpts.summary().text.len - } - - pub fn is_empty(&self) -> bool { - self.excerpts.summary().text.len == 0 - } - - pub fn max_buffer_row(&self) -> u32 { - self.excerpts.summary().max_buffer_row - } - - pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_offset(offset, bias); - } - - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&offset, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let buffer_offset = excerpt - .buffer - .clip_offset(excerpt_start + (offset - cursor.start()), bias); - buffer_offset.saturating_sub(excerpt_start) - } else { - 0 - }; - cursor.start() + overshoot - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_point(point, bias); - } - - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&point, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .clip_point(excerpt_start + (point - cursor.start()), bias); - buffer_point.saturating_sub(excerpt_start) - } else { - Point::zero() - }; - *cursor.start() + overshoot - } - - pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_offset_utf16(offset, bias); - } - - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&offset, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); - let buffer_offset = excerpt - .buffer - .clip_offset_utf16(excerpt_start + (offset - cursor.start()), bias); - OffsetUtf16(buffer_offset.0.saturating_sub(excerpt_start.0)) - } else { - OffsetUtf16(0) - }; - *cursor.start() + overshoot - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.clip_point_utf16(point, bias); - } - - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&point.0, Bias::Right, &()); - let overshoot = if let Some(excerpt) = cursor.item() { - let excerpt_start = excerpt - .buffer - .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer)); - let buffer_point = excerpt - .buffer - .clip_point_utf16(Unclipped(excerpt_start + (point.0 - cursor.start())), bias); - buffer_point.saturating_sub(excerpt_start) - } else { - PointUtf16::zero() - }; - *cursor.start() + overshoot - } - - pub fn bytes_in_range(&self, range: Range) -> MultiBufferBytes { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpts = self.excerpts.cursor::(); - excerpts.seek(&range.start, Bias::Right, &()); - - let mut chunk = &[][..]; - let excerpt_bytes = if let Some(excerpt) = excerpts.item() { - let mut excerpt_bytes = excerpt - .bytes_in_range(range.start - excerpts.start()..range.end - excerpts.start()); - chunk = excerpt_bytes.next().unwrap_or(&[][..]); - Some(excerpt_bytes) - } else { - None - }; - MultiBufferBytes { - range, - excerpts, - excerpt_bytes, - chunk, - } - } - - pub fn reversed_bytes_in_range( - &self, - range: Range, - ) -> ReversedMultiBufferBytes { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpts = self.excerpts.cursor::(); - excerpts.seek(&range.end, Bias::Left, &()); - - let mut chunk = &[][..]; - let excerpt_bytes = if let Some(excerpt) = excerpts.item() { - let mut excerpt_bytes = excerpt.reversed_bytes_in_range( - range.start - excerpts.start()..range.end - excerpts.start(), - ); - chunk = excerpt_bytes.next().unwrap_or(&[][..]); - Some(excerpt_bytes) - } else { - None - }; - - ReversedMultiBufferBytes { - range, - excerpts, - excerpt_bytes, - chunk, - } - } - - pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows { - let mut result = MultiBufferRows { - buffer_row_range: 0..0, - excerpts: self.excerpts.cursor(), - }; - result.seek(start_row); - result - } - - pub fn chunks(&self, range: Range, language_aware: bool) -> MultiBufferChunks { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut chunks = MultiBufferChunks { - range: range.clone(), - excerpts: self.excerpts.cursor(), - excerpt_chunks: None, - language_aware, - }; - chunks.seek(range.start); - chunks - } - - pub fn offset_to_point(&self, offset: usize) -> Point { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_to_point(offset); - } - - let mut cursor = self.excerpts.cursor::<(usize, Point)>(); - cursor.seek(&offset, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_point) = cursor.start(); - let overshoot = offset - start_offset; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .offset_to_point(excerpt_start_offset + overshoot); - *start_point + (buffer_point - excerpt_start_point) - } else { - self.excerpts.summary().text.lines - } - } - - pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_to_point_utf16(offset); - } - - let mut cursor = self.excerpts.cursor::<(usize, PointUtf16)>(); - cursor.seek(&offset, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_point) = cursor.start(); - let overshoot = offset - start_offset; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt.range.context.start.to_point_utf16(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .offset_to_point_utf16(excerpt_start_offset + overshoot); - *start_point + (buffer_point - excerpt_start_point) - } else { - self.excerpts.summary().text.lines_utf16() - } - } - - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.point_to_point_utf16(point); - } - - let mut cursor = self.excerpts.cursor::<(Point, PointUtf16)>(); - cursor.seek(&point, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_point) = cursor.start(); - let overshoot = point - start_offset; - let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); - let excerpt_start_point_utf16 = - excerpt.range.context.start.to_point_utf16(&excerpt.buffer); - let buffer_point = excerpt - .buffer - .point_to_point_utf16(excerpt_start_point + overshoot); - *start_point + (buffer_point - excerpt_start_point_utf16) - } else { - self.excerpts.summary().text.lines_utf16() - } - } - - pub fn point_to_offset(&self, point: Point) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.point_to_offset(point); - } - - let mut cursor = self.excerpts.cursor::<(Point, usize)>(); - cursor.seek(&point, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_point, start_offset) = cursor.start(); - let overshoot = point - start_point; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt.range.context.start.to_point(&excerpt.buffer); - let buffer_offset = excerpt - .buffer - .point_to_offset(excerpt_start_point + overshoot); - *start_offset + buffer_offset - excerpt_start_offset - } else { - self.excerpts.summary().text.len - } - } - - pub fn offset_utf16_to_offset(&self, offset_utf16: OffsetUtf16) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_utf16_to_offset(offset_utf16); - } - - let mut cursor = self.excerpts.cursor::<(OffsetUtf16, usize)>(); - cursor.seek(&offset_utf16, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset_utf16, start_offset) = cursor.start(); - let overshoot = offset_utf16 - start_offset_utf16; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_offset_utf16 = - excerpt.buffer.offset_to_offset_utf16(excerpt_start_offset); - let buffer_offset = excerpt - .buffer - .offset_utf16_to_offset(excerpt_start_offset_utf16 + overshoot); - *start_offset + (buffer_offset - excerpt_start_offset) - } else { - self.excerpts.summary().text.len - } - } - - pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.offset_to_offset_utf16(offset); - } - - let mut cursor = self.excerpts.cursor::<(usize, OffsetUtf16)>(); - cursor.seek(&offset, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_offset, start_offset_utf16) = cursor.start(); - let overshoot = offset - start_offset; - let excerpt_start_offset_utf16 = - excerpt.range.context.start.to_offset_utf16(&excerpt.buffer); - let excerpt_start_offset = excerpt - .buffer - .offset_utf16_to_offset(excerpt_start_offset_utf16); - let buffer_offset_utf16 = excerpt - .buffer - .offset_to_offset_utf16(excerpt_start_offset + overshoot); - *start_offset_utf16 + (buffer_offset_utf16 - excerpt_start_offset_utf16) - } else { - self.excerpts.summary().text.len_utf16 - } - } - - pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer.point_utf16_to_offset(point); - } - - let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(); - cursor.seek(&point, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let (start_point, start_offset) = cursor.start(); - let overshoot = point - start_point; - let excerpt_start_offset = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_start_point = excerpt - .buffer - .offset_to_point_utf16(excerpt.range.context.start.to_offset(&excerpt.buffer)); - let buffer_offset = excerpt - .buffer - .point_utf16_to_offset(excerpt_start_point + overshoot); - *start_offset + (buffer_offset - excerpt_start_offset) - } else { - self.excerpts.summary().text.len - } - } - - pub fn point_to_buffer_offset( - &self, - point: T, - ) -> Option<(&BufferSnapshot, usize)> { - let offset = point.to_offset(&self); - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&offset, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - - cursor.item().map(|excerpt| { - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let buffer_point = excerpt_start + offset - *cursor.start(); - (&excerpt.buffer, buffer_point) - }) - } - - pub fn suggested_indents( - &self, - rows: impl IntoIterator, - cx: &AppContext, - ) -> BTreeMap { - let mut result = BTreeMap::new(); - - let mut rows_for_excerpt = Vec::new(); - let mut cursor = self.excerpts.cursor::(); - let mut rows = rows.into_iter().peekable(); - let mut prev_row = u32::MAX; - let mut prev_language_indent_size = IndentSize::default(); - - while let Some(row) = rows.next() { - cursor.seek(&Point::new(row, 0), Bias::Right, &()); - let excerpt = match cursor.item() { - Some(excerpt) => excerpt, - _ => continue, - }; - - // Retrieve the language and indent size once for each disjoint region being indented. - let single_indent_size = if row.saturating_sub(1) == prev_row { - prev_language_indent_size - } else { - excerpt - .buffer - .language_indent_size_at(Point::new(row, 0), cx) - }; - prev_language_indent_size = single_indent_size; - prev_row = row; - - let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row; - let start_multibuffer_row = cursor.start().row; - - rows_for_excerpt.push(row); - while let Some(next_row) = rows.peek().copied() { - if cursor.end(&()).row > next_row { - rows_for_excerpt.push(next_row); - rows.next(); - } else { - break; - } - } - - let buffer_rows = rows_for_excerpt - .drain(..) - .map(|row| start_buffer_row + row - start_multibuffer_row); - let buffer_indents = excerpt - .buffer - .suggested_indents(buffer_rows, single_indent_size); - let multibuffer_indents = buffer_indents - .into_iter() - .map(|(row, indent)| (start_multibuffer_row + row - start_buffer_row, indent)); - result.extend(multibuffer_indents); - } - - result - } - - pub fn indent_size_for_line(&self, row: u32) -> IndentSize { - if let Some((buffer, range)) = self.buffer_line_for_row(row) { - let mut size = buffer.indent_size_for_line(range.start.row); - size.len = size - .len - .min(range.end.column) - .saturating_sub(range.start.column); - size - } else { - IndentSize::spaces(0) - } - } - - pub fn prev_non_blank_row(&self, mut row: u32) -> Option { - while row > 0 { - row -= 1; - if !self.is_line_blank(row) { - return Some(row); - } - } - None - } - - pub fn line_len(&self, row: u32) -> u32 { - if let Some((_, range)) = self.buffer_line_for_row(row) { - range.end.column - range.start.column - } else { - 0 - } - } - - pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range)> { - let mut cursor = self.excerpts.cursor::(); - let point = Point::new(row, 0); - cursor.seek(&point, Bias::Right, &()); - if cursor.item().is_none() && *cursor.start() == point { - cursor.prev(&()); - } - if let Some(excerpt) = cursor.item() { - let overshoot = row - cursor.start().row; - let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer); - let excerpt_end = excerpt.range.context.end.to_point(&excerpt.buffer); - let buffer_row = excerpt_start.row + overshoot; - let line_start = Point::new(buffer_row, 0); - let line_end = Point::new(buffer_row, excerpt.buffer.line_len(buffer_row)); - return Some(( - &excerpt.buffer, - line_start.max(excerpt_start)..line_end.min(excerpt_end), - )); - } - None - } - - pub fn max_point(&self) -> Point { - self.text_summary().lines - } - - pub fn text_summary(&self) -> TextSummary { - self.excerpts.summary().text.clone() - } - - pub fn text_summary_for_range(&self, range: Range) -> D - where - D: TextDimension, - O: ToOffset, - { - let mut summary = D::default(); - let mut range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&range.start, Bias::Right, &()); - if let Some(excerpt) = cursor.item() { - let mut end_before_newline = cursor.end(&()); - if excerpt.has_trailing_newline { - end_before_newline -= 1; - } - - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let start_in_excerpt = excerpt_start + (range.start - cursor.start()); - let end_in_excerpt = - excerpt_start + (cmp::min(end_before_newline, range.end) - cursor.start()); - summary.add_assign( - &excerpt - .buffer - .text_summary_for_range(start_in_excerpt..end_in_excerpt), - ); - - if range.end > end_before_newline { - summary.add_assign(&D::from_text_summary(&TextSummary::from("\n"))); - } - - cursor.next(&()); - } - - if range.end > *cursor.start() { - summary.add_assign(&D::from_text_summary(&cursor.summary::<_, TextSummary>( - &range.end, - Bias::Right, - &(), - ))); - if let Some(excerpt) = cursor.item() { - range.end = cmp::max(*cursor.start(), range.end); - - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let end_in_excerpt = excerpt_start + (range.end - cursor.start()); - summary.add_assign( - &excerpt - .buffer - .text_summary_for_range(excerpt_start..end_in_excerpt), - ); - } - } - - summary - } - - pub fn summary_for_anchor(&self, anchor: &Anchor) -> D - where - D: TextDimension + Ord + Sub, - { - let mut cursor = self.excerpts.cursor::(); - let locator = self.excerpt_locator_for_id(anchor.excerpt_id); - - cursor.seek(locator, Bias::Left, &()); - if cursor.item().is_none() { - cursor.next(&()); - } - - let mut position = D::from_text_summary(&cursor.start().text); - if let Some(excerpt) = cursor.item() { - if excerpt.id == anchor.excerpt_id { - let excerpt_buffer_start = - excerpt.range.context.start.summary::(&excerpt.buffer); - let excerpt_buffer_end = excerpt.range.context.end.summary::(&excerpt.buffer); - let buffer_position = cmp::min( - excerpt_buffer_end, - anchor.text_anchor.summary::(&excerpt.buffer), - ); - if buffer_position > excerpt_buffer_start { - position.add_assign(&(buffer_position - excerpt_buffer_start)); - } - } - } - position - } - - pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec - where - D: TextDimension + Ord + Sub, - I: 'a + IntoIterator, - { - if let Some((_, _, buffer)) = self.as_singleton() { - return buffer - .summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor)) - .collect(); - } - - let mut anchors = anchors.into_iter().peekable(); - let mut cursor = self.excerpts.cursor::(); - let mut summaries = Vec::new(); - while let Some(anchor) = anchors.peek() { - let excerpt_id = anchor.excerpt_id; - let excerpt_anchors = iter::from_fn(|| { - let anchor = anchors.peek()?; - if anchor.excerpt_id == excerpt_id { - Some(&anchors.next().unwrap().text_anchor) - } else { - None - } - }); - - let locator = self.excerpt_locator_for_id(excerpt_id); - cursor.seek_forward(locator, Bias::Left, &()); - if cursor.item().is_none() { - cursor.next(&()); - } - - let position = D::from_text_summary(&cursor.start().text); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - let excerpt_buffer_start = - excerpt.range.context.start.summary::(&excerpt.buffer); - let excerpt_buffer_end = - excerpt.range.context.end.summary::(&excerpt.buffer); - summaries.extend( - excerpt - .buffer - .summaries_for_anchors::(excerpt_anchors) - .map(move |summary| { - let summary = cmp::min(excerpt_buffer_end.clone(), summary); - let mut position = position.clone(); - let excerpt_buffer_start = excerpt_buffer_start.clone(); - if summary > excerpt_buffer_start { - position.add_assign(&(summary - excerpt_buffer_start)); - } - position - }), - ); - continue; - } - } - - summaries.extend(excerpt_anchors.map(|_| position.clone())); - } - - summaries - } - - pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)> - where - I: 'a + IntoIterator, - { - let mut anchors = anchors.into_iter().enumerate().peekable(); - let mut cursor = self.excerpts.cursor::>(); - cursor.next(&()); - - let mut result = Vec::new(); - - while let Some((_, anchor)) = anchors.peek() { - let old_excerpt_id = anchor.excerpt_id; - - // Find the location where this anchor's excerpt should be. - let old_locator = self.excerpt_locator_for_id(old_excerpt_id); - cursor.seek_forward(&Some(old_locator), Bias::Left, &()); - - if cursor.item().is_none() { - cursor.next(&()); - } - - let next_excerpt = cursor.item(); - let prev_excerpt = cursor.prev_item(); - - // Process all of the anchors for this excerpt. - while let Some((_, anchor)) = anchors.peek() { - if anchor.excerpt_id != old_excerpt_id { - break; - } - let (anchor_ix, anchor) = anchors.next().unwrap(); - let mut anchor = *anchor; - - // Leave min and max anchors unchanged if invalid or - // if the old excerpt still exists at this location - let mut kept_position = next_excerpt - .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor)) - || old_excerpt_id == ExcerptId::max() - || old_excerpt_id == ExcerptId::min(); - - // If the old excerpt no longer exists at this location, then attempt to - // find an equivalent position for this anchor in an adjacent excerpt. - if !kept_position { - for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { - if excerpt.contains(&anchor) { - anchor.excerpt_id = excerpt.id.clone(); - kept_position = true; - break; - } - } - } - - // If there's no adjacent excerpt that contains the anchor's position, - // then report that the anchor has lost its position. - if !kept_position { - anchor = if let Some(excerpt) = next_excerpt { - let mut text_anchor = excerpt - .range - .context - .start - .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.context.end, &excerpt.buffer) - .is_gt() - { - text_anchor = excerpt.range.context.end; - } - Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor, - } - } else if let Some(excerpt) = prev_excerpt { - let mut text_anchor = excerpt - .range - .context - .end - .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.context.start, &excerpt.buffer) - .is_lt() - { - text_anchor = excerpt.range.context.start; - } - Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor, - } - } else if anchor.text_anchor.bias == Bias::Left { - Anchor::min() - } else { - Anchor::max() - }; - } - - result.push((anchor_ix, anchor, kept_position)); - } - } - result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self)); - result - } - - pub fn anchor_before(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Left) - } - - pub fn anchor_after(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Right) - } - - pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { - let offset = position.to_offset(self); - if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { - return Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: buffer.anchor_at(offset, bias), - }; - } - - let mut cursor = self.excerpts.cursor::<(usize, Option)>(); - cursor.seek(&offset, Bias::Right, &()); - if cursor.item().is_none() && offset == cursor.start().0 && bias == Bias::Left { - cursor.prev(&()); - } - if let Some(excerpt) = cursor.item() { - let mut overshoot = offset.saturating_sub(cursor.start().0); - if excerpt.has_trailing_newline && offset == cursor.end(&()).0 { - overshoot -= 1; - bias = Bias::Right; - } - - let buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let text_anchor = - excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias)); - Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor, - } - } else if offset == 0 && bias == Bias::Left { - Anchor::min() - } else { - Anchor::max() - } - } - - pub fn anchor_in_excerpt(&self, excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Anchor { - let locator = self.excerpt_locator_for_id(excerpt_id); - let mut cursor = self.excerpts.cursor::>(); - cursor.seek(locator, Bias::Left, &()); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - let text_anchor = excerpt.clip_anchor(text_anchor); - drop(cursor); - return Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id, - text_anchor, - }; - } - } - panic!("excerpt not found"); - } - - pub fn can_resolve(&self, anchor: &Anchor) -> bool { - if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() { - true - } else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) { - excerpt.buffer.can_resolve(&anchor.text_anchor) - } else { - false - } - } - - pub fn excerpts( - &self, - ) -> impl Iterator)> { - self.excerpts - .iter() - .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) - } - - pub fn excerpt_boundaries_in_range( - &self, - range: R, - ) -> impl Iterator + '_ - where - R: RangeBounds, - T: ToOffset, - { - let start_offset; - let start = match range.start_bound() { - Bound::Included(start) => { - start_offset = start.to_offset(self); - Bound::Included(start_offset) - } - Bound::Excluded(start) => { - start_offset = start.to_offset(self); - Bound::Excluded(start_offset) - } - Bound::Unbounded => { - start_offset = 0; - Bound::Unbounded - } - }; - let end = match range.end_bound() { - Bound::Included(end) => Bound::Included(end.to_offset(self)), - Bound::Excluded(end) => Bound::Excluded(end.to_offset(self)), - Bound::Unbounded => Bound::Unbounded, - }; - let bounds = (start, end); - - let mut cursor = self.excerpts.cursor::<(usize, Point)>(); - cursor.seek(&start_offset, Bias::Right, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - if !bounds.contains(&cursor.start().0) { - cursor.next(&()); - } - - let mut prev_buffer_id = cursor.prev_item().map(|excerpt| excerpt.buffer_id); - std::iter::from_fn(move || { - if self.singleton { - None - } else if bounds.contains(&cursor.start().0) { - 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(), - starts_new_buffer, - }; - - prev_buffer_id = Some(excerpt.buffer_id); - cursor.next(&()); - Some(boundary) - } else { - None - } - }) - } - - pub fn edit_count(&self) -> usize { - self.edit_count - } - - pub fn parse_count(&self) -> usize { - self.parse_count - } - - /// Returns the smallest enclosing bracket ranges containing the given range or - /// None if no brackets contain range or the range is not contained in a single - /// excerpt - pub fn innermost_enclosing_bracket_ranges( - &self, - range: Range, - ) -> Option<(Range, Range)> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - - // Get the ranges of the innermost pair of brackets. - let mut result: Option<(Range, Range)> = None; - - let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { - return None; - }; - - for (open, close) in enclosing_bracket_ranges { - let len = close.end - open.start; - - if let Some((existing_open, existing_close)) = &result { - let existing_len = existing_close.end - existing_open.start; - if len > existing_len { - continue; - } - } - - result = Some((open, close)); - } - - result - } - - /// Returns enclosing bracket ranges containing the given range or returns None if the range is - /// not contained in a single excerpt - pub fn enclosing_bracket_ranges<'a, T: ToOffset>( - &'a self, - range: Range, - ) -> Option, Range)> + 'a> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - - self.bracket_ranges(range.clone()).map(|range_pairs| { - range_pairs - .filter(move |(open, close)| open.start <= range.start && close.end >= range.end) - }) - } - - /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is - /// not contained in a single excerpt - pub fn bracket_ranges<'a, T: ToOffset>( - &'a self, - range: Range, - ) -> Option, Range)> + 'a> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let excerpt = self.excerpt_containing(range.clone()); - excerpt.map(|(excerpt, excerpt_offset)| { - let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; - - let start_in_buffer = excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); - let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); - - excerpt - .buffer - .bracket_ranges(start_in_buffer..end_in_buffer) - .filter_map(move |(start_bracket_range, end_bracket_range)| { - if start_bracket_range.start < excerpt_buffer_start - || end_bracket_range.end > excerpt_buffer_end - { - return None; - } - - let mut start_bracket_range = start_bracket_range.clone(); - start_bracket_range.start = - excerpt_offset + (start_bracket_range.start - excerpt_buffer_start); - start_bracket_range.end = - excerpt_offset + (start_bracket_range.end - excerpt_buffer_start); - - let mut end_bracket_range = end_bracket_range.clone(); - end_bracket_range.start = - excerpt_offset + (end_bracket_range.start - excerpt_buffer_start); - end_bracket_range.end = - excerpt_offset + (end_bracket_range.end - excerpt_buffer_start); - Some((start_bracket_range, end_bracket_range)) - }) - }) - } - - pub fn diagnostics_update_count(&self) -> usize { - self.diagnostics_update_count - } - - pub fn git_diff_update_count(&self) -> usize { - self.git_diff_update_count - } - - pub fn trailing_excerpt_update_count(&self) -> usize { - self.trailing_excerpt_update_count - } - - pub fn file_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { - self.point_to_buffer_offset(point) - .and_then(|(buffer, _)| buffer.file()) - } - - pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc> { - self.point_to_buffer_offset(point) - .and_then(|(buffer, offset)| buffer.language_at(offset)) - } - - pub fn settings_at<'a, T: ToOffset>( - &'a self, - point: T, - cx: &'a AppContext, - ) -> &'a LanguageSettings { - let mut language = None; - let mut file = None; - if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { - language = buffer.language_at(offset); - file = buffer.file(); - } - language_settings(language, file, cx) - } - - pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { - self.point_to_buffer_offset(point) - .and_then(|(buffer, offset)| buffer.language_scope_at(offset)) - } - - pub fn language_indent_size_at( - &self, - position: T, - cx: &AppContext, - ) -> Option { - let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?; - Some(buffer_snapshot.language_indent_size_at(offset, cx)) - } - - pub fn is_dirty(&self) -> bool { - self.is_dirty - } - - pub fn has_conflict(&self) -> bool { - self.has_conflict - } - - pub fn diagnostic_group<'a, O>( - &'a self, - group_id: usize, - ) -> impl Iterator> + 'a - where - O: text::FromAnchor + 'a, - { - self.as_singleton() - .into_iter() - .flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id)) - } - - pub fn diagnostics_in_range<'a, T, O>( - &'a self, - range: Range, - reversed: bool, - ) -> impl Iterator> + 'a - where - T: 'a + ToOffset, - O: 'a + text::FromAnchor + Ord, - { - self.as_singleton() - .into_iter() - .flat_map(move |(_, _, buffer)| { - buffer.diagnostics_in_range( - range.start.to_offset(self)..range.end.to_offset(self), - reversed, - ) - }) - } - - pub fn has_git_diffs(&self) -> bool { - for excerpt in self.excerpts.iter() { - if !excerpt.buffer.git_diff.is_empty() { - return true; - } - } - false - } - - pub fn git_diff_hunks_in_range_rev<'a>( - &'a self, - row_range: Range, - ) -> impl 'a + Iterator> { - let mut cursor = self.excerpts.cursor::(); - - cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &()); - if cursor.item().is_none() { - cursor.prev(&()); - } - - std::iter::from_fn(move || { - let excerpt = cursor.item()?; - let multibuffer_start = *cursor.start(); - let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; - if multibuffer_start.row >= row_range.end { - return None; - } - - let mut buffer_start = excerpt.range.context.start; - let mut buffer_end = excerpt.range.context.end; - let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); - let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; - - if row_range.start > multibuffer_start.row { - let buffer_start_point = - excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0); - buffer_start = excerpt.buffer.anchor_before(buffer_start_point); - } - - if row_range.end < multibuffer_end.row { - let buffer_end_point = - excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0); - buffer_end = excerpt.buffer.anchor_before(buffer_end_point); - } - - let buffer_hunks = excerpt - .buffer - .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end) - .filter_map(move |hunk| { - let start = multibuffer_start.row - + hunk - .buffer_range - .start - .saturating_sub(excerpt_start_point.row); - let end = multibuffer_start.row - + hunk - .buffer_range - .end - .min(excerpt_end_point.row + 1) - .saturating_sub(excerpt_start_point.row); - - Some(DiffHunk { - buffer_range: start..end, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }) - }); - - cursor.prev(&()); - - Some(buffer_hunks) - }) - .flatten() - } - - pub fn git_diff_hunks_in_range<'a>( - &'a self, - row_range: Range, - ) -> impl 'a + Iterator> { - let mut cursor = self.excerpts.cursor::(); - - cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &()); - - std::iter::from_fn(move || { - let excerpt = cursor.item()?; - let multibuffer_start = *cursor.start(); - let multibuffer_end = multibuffer_start + excerpt.text_summary.lines; - if multibuffer_start.row >= row_range.end { - return None; - } - - let mut buffer_start = excerpt.range.context.start; - let mut buffer_end = excerpt.range.context.end; - let excerpt_start_point = buffer_start.to_point(&excerpt.buffer); - let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines; - - if row_range.start > multibuffer_start.row { - let buffer_start_point = - excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0); - buffer_start = excerpt.buffer.anchor_before(buffer_start_point); - } - - if row_range.end < multibuffer_end.row { - let buffer_end_point = - excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0); - buffer_end = excerpt.buffer.anchor_before(buffer_end_point); - } - - let buffer_hunks = excerpt - .buffer - .git_diff_hunks_intersecting_range(buffer_start..buffer_end) - .filter_map(move |hunk| { - let start = multibuffer_start.row - + hunk - .buffer_range - .start - .saturating_sub(excerpt_start_point.row); - let end = multibuffer_start.row - + hunk - .buffer_range - .end - .min(excerpt_end_point.row + 1) - .saturating_sub(excerpt_start_point.row); - - Some(DiffHunk { - buffer_range: start..end, - diff_base_byte_range: hunk.diff_base_byte_range.clone(), - }) - }); - - cursor.next(&()); - - Some(buffer_hunks) - }) - .flatten() - } - - pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - - self.excerpt_containing(range.clone()) - .and_then(|(excerpt, excerpt_offset)| { - let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; - - let start_in_buffer = - excerpt_buffer_start + range.start.saturating_sub(excerpt_offset); - let end_in_buffer = excerpt_buffer_start + range.end.saturating_sub(excerpt_offset); - let mut ancestor_buffer_range = excerpt - .buffer - .range_for_syntax_ancestor(start_in_buffer..end_in_buffer)?; - ancestor_buffer_range.start = - cmp::max(ancestor_buffer_range.start, excerpt_buffer_start); - ancestor_buffer_range.end = cmp::min(ancestor_buffer_range.end, excerpt_buffer_end); - - let start = excerpt_offset + (ancestor_buffer_range.start - excerpt_buffer_start); - let end = excerpt_offset + (ancestor_buffer_range.end - excerpt_buffer_start); - Some(start..end) - }) - } - - pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - 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), - text: item.text, - highlight_ranges: item.highlight_ranges, - name_ranges: item.name_ranges, - }) - .collect(), - )) - } - - pub fn symbols_containing( - &self, - offset: T, - theme: Option<&SyntaxTheme>, - ) -> Option<(u64, Vec>)> { - let anchor = self.anchor_before(offset); - let excerpt_id = anchor.excerpt_id; - let excerpt = self.excerpt(excerpt_id)?; - Some(( - excerpt.buffer_id, - excerpt - .buffer - .symbols_containing(anchor.text_anchor, theme) - .into_iter() - .flatten() - .map(|item| OutlineItem { - depth: item.depth, - range: self.anchor_in_excerpt(excerpt_id, item.range.start) - ..self.anchor_in_excerpt(excerpt_id, item.range.end), - text: item.text, - highlight_ranges: item.highlight_ranges, - name_ranges: item.name_ranges, - }) - .collect(), - )) - } - - fn excerpt_locator_for_id<'a>(&'a self, id: ExcerptId) -> &'a Locator { - if id == ExcerptId::min() { - Locator::min_ref() - } else if id == ExcerptId::max() { - Locator::max_ref() - } else { - let mut cursor = self.excerpt_ids.cursor::(); - cursor.seek(&id, Bias::Left, &()); - if let Some(entry) = cursor.item() { - if entry.id == id { - return &entry.locator; - } - } - panic!("invalid excerpt id {:?}", id) - } - } - - pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option { - Some(self.excerpt(excerpt_id)?.buffer_id) - } - - pub fn buffer_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<&BufferSnapshot> { - Some(&self.excerpt(excerpt_id)?.buffer) - } - - fn excerpt<'a>(&'a self, excerpt_id: ExcerptId) -> Option<&'a Excerpt> { - let mut cursor = self.excerpts.cursor::>(); - let locator = self.excerpt_locator_for_id(excerpt_id); - cursor.seek(&Some(locator), Bias::Left, &()); - if let Some(excerpt) = cursor.item() { - if excerpt.id == excerpt_id { - return Some(excerpt); - } - } - None - } - - /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts - fn excerpt_containing<'a, T: ToOffset>( - &'a self, - range: Range, - ) -> Option<(&'a Excerpt, usize)> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - - let mut cursor = self.excerpts.cursor::(); - cursor.seek(&range.start, Bias::Right, &()); - let start_excerpt = cursor.item(); - - if range.start == range.end { - return start_excerpt.map(|excerpt| (excerpt, *cursor.start())); - } - - cursor.seek(&range.end, Bias::Right, &()); - let end_excerpt = cursor.item(); - - start_excerpt - .zip(end_excerpt) - .and_then(|(start_excerpt, end_excerpt)| { - if start_excerpt.id != end_excerpt.id { - return None; - } - - Some((start_excerpt, *cursor.start())) - }) - } - - pub fn remote_selections_in_range<'a>( - &'a self, - range: &'a Range, - ) -> impl 'a + Iterator)> { - let mut cursor = self.excerpts.cursor::(); - let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id); - let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id); - cursor.seek(start_locator, Bias::Left, &()); - cursor - .take_while(move |excerpt| excerpt.locator <= *end_locator) - .flat_map(move |excerpt| { - let mut query_range = excerpt.range.context.start..excerpt.range.context.end; - if excerpt.id == range.start.excerpt_id { - query_range.start = range.start.text_anchor; - } - if excerpt.id == range.end.excerpt_id { - query_range.end = range.end.text_anchor; - } - - excerpt - .buffer - .remote_selections_in_range(query_range) - .flat_map(move |(replica_id, line_mode, cursor_shape, selections)| { - selections.map(move |selection| { - let mut start = Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor: selection.start, - }; - let mut end = Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor: selection.end, - }; - if range.start.cmp(&start, self).is_gt() { - start = range.start.clone(); - } - if range.end.cmp(&end, self).is_lt() { - end = range.end.clone(); - } - - ( - replica_id, - line_mode, - cursor_shape, - Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - }, - ) - }) - }) - }) - } -} - -#[cfg(any(test, feature = "test-support"))] -impl MultiBufferSnapshot { - pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { - let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right); - let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right); - start..end - } -} - -impl History { - fn start_transaction(&mut self, now: Instant) -> Option { - self.transaction_depth += 1; - if self.transaction_depth == 1 { - let id = self.next_transaction_id.tick(); - self.undo_stack.push(Transaction { - id, - buffer_transactions: Default::default(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - Some(id) - } else { - None - } - } - - fn end_transaction( - &mut self, - now: Instant, - buffer_transactions: HashMap, - ) -> bool { - assert_ne!(self.transaction_depth, 0); - self.transaction_depth -= 1; - if self.transaction_depth == 0 { - if buffer_transactions.is_empty() { - self.undo_stack.pop(); - false - } else { - self.redo_stack.clear(); - let transaction = self.undo_stack.last_mut().unwrap(); - transaction.last_edit_at = now; - for (buffer_id, transaction_id) in buffer_transactions { - transaction - .buffer_transactions - .entry(buffer_id) - .or_insert(transaction_id); - } - true - } - } else { - false - } - } - - fn push_transaction<'a, T>( - &mut self, - buffer_transactions: T, - now: Instant, - cx: &mut ModelContext, - ) where - T: IntoIterator, &'a language::Transaction)>, - { - assert_eq!(self.transaction_depth, 0); - let transaction = Transaction { - id: self.next_transaction_id.tick(), - buffer_transactions: buffer_transactions - .into_iter() - .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) - .collect(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }; - if !transaction.buffer_transactions.is_empty() { - self.undo_stack.push(transaction); - self.redo_stack.clear(); - } - } - - fn finalize_last_transaction(&mut self) { - if let Some(transaction) = self.undo_stack.last_mut() { - transaction.suppress_grouping = true; - } - } - - fn forget(&mut self, transaction_id: TransactionId) -> Option { - if let Some(ix) = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.undo_stack.remove(ix)) - } else if let Some(ix) = self - .redo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.redo_stack.remove(ix)) - } else { - None - } - } - - fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { - self.undo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn pop_undo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.undo_stack.pop() { - self.redo_stack.push(transaction); - self.redo_stack.last_mut() - } else { - None - } - } - - fn pop_redo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.redo_stack.pop() { - self.undo_stack.push(transaction); - self.undo_stack.last_mut() - } else { - None - } - } - - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { - let ix = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id)?; - let transaction = self.undo_stack.remove(ix); - self.redo_stack.push(transaction); - self.redo_stack.last() - } - - fn group(&mut self) -> Option { - let mut count = 0; - let mut transactions = self.undo_stack.iter(); - if let Some(mut transaction) = transactions.next_back() { - while let Some(prev_transaction) = transactions.next_back() { - if !prev_transaction.suppress_grouping - && transaction.first_edit_at - prev_transaction.last_edit_at - <= self.group_interval - { - transaction = prev_transaction; - count += 1; - } else { - break; - } - } - } - self.group_trailing(count) - } - - fn group_until(&mut self, transaction_id: TransactionId) { - let mut count = 0; - for transaction in self.undo_stack.iter().rev() { - if transaction.id == transaction_id { - self.group_trailing(count); - break; - } else if transaction.suppress_grouping { - break; - } else { - count += 1; - } - } - } - - fn group_trailing(&mut self, n: usize) -> Option { - let new_len = self.undo_stack.len() - n; - let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); - if let Some(last_transaction) = transactions_to_keep.last_mut() { - if let Some(transaction) = transactions_to_merge.last() { - last_transaction.last_edit_at = transaction.last_edit_at; - } - for to_merge in transactions_to_merge { - for (buffer_id, transaction_id) in &to_merge.buffer_transactions { - last_transaction - .buffer_transactions - .entry(*buffer_id) - .or_insert(*transaction_id); - } - } - } - - self.undo_stack.truncate(new_len); - self.undo_stack.last().map(|t| t.id) - } -} - -impl Excerpt { - fn new( - id: ExcerptId, - locator: Locator, - buffer_id: u64, - buffer: BufferSnapshot, - range: ExcerptRange, - has_trailing_newline: bool, - ) -> Self { - Excerpt { - id, - locator, - max_buffer_row: range.context.end.to_point(&buffer).row, - text_summary: buffer - .text_summary_for_range::(range.context.to_offset(&buffer)), - buffer_id, - buffer, - range, - has_trailing_newline, - } - } - - fn chunks_in_range(&self, range: Range, language_aware: bool) -> ExcerptChunks { - let content_start = self.range.context.start.to_offset(&self.buffer); - let chunks_start = content_start + range.start; - let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); - - let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len - { - 1 - } else { - 0 - }; - - let content_chunks = self.buffer.chunks(chunks_start..chunks_end, language_aware); - - ExcerptChunks { - content_chunks, - footer_height, - } - } - - fn bytes_in_range(&self, range: Range) -> ExcerptBytes { - let content_start = self.range.context.start.to_offset(&self.buffer); - let bytes_start = content_start + range.start; - let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); - let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len - { - 1 - } else { - 0 - }; - let content_bytes = self.buffer.bytes_in_range(bytes_start..bytes_end); - - ExcerptBytes { - content_bytes, - footer_height, - } - } - - fn reversed_bytes_in_range(&self, range: Range) -> ExcerptBytes { - let content_start = self.range.context.start.to_offset(&self.buffer); - let bytes_start = content_start + range.start; - let bytes_end = content_start + cmp::min(range.end, self.text_summary.len); - let footer_height = if self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len - { - 1 - } else { - 0 - }; - let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end); - - ExcerptBytes { - content_bytes, - footer_height, - } - } - - fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { - if text_anchor - .cmp(&self.range.context.start, &self.buffer) - .is_lt() - { - self.range.context.start - } else if text_anchor - .cmp(&self.range.context.end, &self.buffer) - .is_gt() - { - self.range.context.end - } else { - text_anchor - } - } - - fn contains(&self, anchor: &Anchor) -> bool { - Some(self.buffer_id) == anchor.buffer_id - && self - .range - .context - .start - .cmp(&anchor.text_anchor, &self.buffer) - .is_le() - && self - .range - .context - .end - .cmp(&anchor.text_anchor, &self.buffer) - .is_ge() - } -} - -impl ExcerptId { - pub fn min() -> Self { - Self(0) - } - - pub fn max() -> Self { - Self(usize::MAX) - } - - pub fn to_proto(&self) -> u64 { - self.0 as _ - } - - pub fn from_proto(proto: u64) -> Self { - Self(proto as _) - } - - pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { - let a = snapshot.excerpt_locator_for_id(*self); - let b = snapshot.excerpt_locator_for_id(*other); - a.cmp(&b).then_with(|| self.0.cmp(&other.0)) - } -} - -impl Into for ExcerptId { - fn into(self) -> usize { - self.0 - } -} - -impl fmt::Debug for Excerpt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Excerpt") - .field("id", &self.id) - .field("locator", &self.locator) - .field("buffer_id", &self.buffer_id) - .field("range", &self.range) - .field("text_summary", &self.text_summary) - .field("has_trailing_newline", &self.has_trailing_newline) - .finish() - } -} - -impl sum_tree::Item for Excerpt { - type Summary = ExcerptSummary; - - fn summary(&self) -> Self::Summary { - let mut text = self.text_summary.clone(); - if self.has_trailing_newline { - text += TextSummary::from("\n"); - } - ExcerptSummary { - excerpt_id: self.id, - excerpt_locator: self.locator.clone(), - max_buffer_row: self.max_buffer_row, - text, - } - } -} - -impl sum_tree::Item for ExcerptIdMapping { - type Summary = ExcerptId; - - fn summary(&self) -> Self::Summary { - self.id - } -} - -impl sum_tree::KeyedItem for ExcerptIdMapping { - type Key = ExcerptId; - - fn key(&self) -> Self::Key { - self.id - } -} - -impl sum_tree::Summary for ExcerptId { - type Context = (); - - fn add_summary(&mut self, other: &Self, _: &()) { - *self = *other; - } -} - -impl sum_tree::Summary for ExcerptSummary { - type Context = (); - - fn add_summary(&mut self, summary: &Self, _: &()) { - debug_assert!(summary.excerpt_locator > self.excerpt_locator); - self.excerpt_locator = summary.excerpt_locator.clone(); - self.text.add_summary(&summary.text, &()); - self.max_buffer_row = cmp::max(self.max_buffer_row, summary.max_buffer_row); - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for TextSummary { - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += &summary.text; - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for usize { - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.len; - } -} - -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for usize { - fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.text.len) - } -} - -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator { - fn cmp(&self, cursor_location: &Option<&'a Locator>, _: &()) -> cmp::Ordering { - Ord::cmp(&Some(self), cursor_location) - } -} - -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, ExcerptSummary> for Locator { - fn cmp(&self, cursor_location: &ExcerptSummary, _: &()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.excerpt_locator) - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for OffsetUtf16 { - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.len_utf16; - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Point { - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.lines; - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for PointUtf16 { - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self += summary.text.lines_utf16() - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> { - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self = Some(&summary.excerpt_locator); - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option { - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: &()) { - *self = Some(summary.excerpt_id); - } -} - -impl<'a> MultiBufferRows<'a> { - pub fn seek(&mut self, row: u32) { - self.buffer_row_range = 0..0; - - self.excerpts - .seek_forward(&Point::new(row, 0), Bias::Right, &()); - if self.excerpts.item().is_none() { - self.excerpts.prev(&()); - - if self.excerpts.item().is_none() && row == 0 { - self.buffer_row_range = 0..1; - return; - } - } - - if let Some(excerpt) = self.excerpts.item() { - let overshoot = row - self.excerpts.start().row; - let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer).row; - self.buffer_row_range.start = excerpt_start + overshoot; - self.buffer_row_range.end = excerpt_start + excerpt.text_summary.lines.row + 1; - } - } -} - -impl<'a> Iterator for MultiBufferRows<'a> { - type Item = Option; - - fn next(&mut self) -> Option { - loop { - if !self.buffer_row_range.is_empty() { - let row = Some(self.buffer_row_range.start); - self.buffer_row_range.start += 1; - return Some(row); - } - self.excerpts.item()?; - self.excerpts.next(&()); - let excerpt = self.excerpts.item()?; - self.buffer_row_range.start = excerpt.range.context.start.to_point(&excerpt.buffer).row; - self.buffer_row_range.end = - self.buffer_row_range.start + excerpt.text_summary.lines.row + 1; - } - } -} - -impl<'a> MultiBufferChunks<'a> { - pub fn offset(&self) -> usize { - self.range.start - } - - pub fn seek(&mut self, offset: usize) { - self.range.start = offset; - self.excerpts.seek(&offset, Bias::Right, &()); - if let Some(excerpt) = self.excerpts.item() { - self.excerpt_chunks = Some(excerpt.chunks_in_range( - self.range.start - self.excerpts.start()..self.range.end - self.excerpts.start(), - self.language_aware, - )); - } else { - self.excerpt_chunks = None; - } - } -} - -impl<'a> Iterator for MultiBufferChunks<'a> { - type Item = Chunk<'a>; - - fn next(&mut self) -> Option { - if self.range.is_empty() { - None - } else if let Some(chunk) = self.excerpt_chunks.as_mut()?.next() { - self.range.start += chunk.text.len(); - Some(chunk) - } else { - self.excerpts.next(&()); - let excerpt = self.excerpts.item()?; - self.excerpt_chunks = Some(excerpt.chunks_in_range( - 0..self.range.end - self.excerpts.start(), - self.language_aware, - )); - self.next() - } - } -} - -impl<'a> MultiBufferBytes<'a> { - fn consume(&mut self, len: usize) { - self.range.start += len; - self.chunk = &self.chunk[len..]; - - if !self.range.is_empty() && self.chunk.is_empty() { - if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { - self.chunk = chunk; - } else { - self.excerpts.next(&()); - if let Some(excerpt) = self.excerpts.item() { - let mut excerpt_bytes = - excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); - self.chunk = excerpt_bytes.next().unwrap(); - self.excerpt_bytes = Some(excerpt_bytes); - } - } - } - } -} - -impl<'a> Iterator for MultiBufferBytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - let chunk = self.chunk; - if chunk.is_empty() { - None - } else { - self.consume(chunk.len()); - Some(chunk) - } - } -} - -impl<'a> io::Read for MultiBufferBytes<'a> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let len = cmp::min(buf.len(), self.chunk.len()); - buf[..len].copy_from_slice(&self.chunk[..len]); - if len > 0 { - self.consume(len); - } - Ok(len) - } -} - -impl<'a> ReversedMultiBufferBytes<'a> { - fn consume(&mut self, len: usize) { - self.range.end -= len; - self.chunk = &self.chunk[..self.chunk.len() - len]; - - if !self.range.is_empty() && self.chunk.is_empty() { - if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { - self.chunk = chunk; - } else { - self.excerpts.next(&()); - if let Some(excerpt) = self.excerpts.item() { - let mut excerpt_bytes = - excerpt.bytes_in_range(0..self.range.end - self.excerpts.start()); - self.chunk = excerpt_bytes.next().unwrap(); - self.excerpt_bytes = Some(excerpt_bytes); - } - } - } - } -} - -impl<'a> io::Read for ReversedMultiBufferBytes<'a> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let len = cmp::min(buf.len(), self.chunk.len()); - buf[..len].copy_from_slice(&self.chunk[..len]); - buf[..len].reverse(); - if len > 0 { - self.consume(len); - } - Ok(len) - } -} -impl<'a> Iterator for ExcerptBytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - if let Some(chunk) = self.content_bytes.next() { - if !chunk.is_empty() { - return Some(chunk); - } - } - - if self.footer_height > 0 { - let result = &NEWLINES[..self.footer_height]; - self.footer_height = 0; - return Some(result); - } - - None - } -} - -impl<'a> Iterator for ExcerptChunks<'a> { - type Item = Chunk<'a>; - - fn next(&mut self) -> Option { - if let Some(chunk) = self.content_chunks.next() { - return Some(chunk); - } - - if self.footer_height > 0 { - let text = unsafe { str::from_utf8_unchecked(&NEWLINES[..self.footer_height]) }; - self.footer_height = 0; - return Some(Chunk { - text, - ..Default::default() - }); - } - - None - } -} - -impl ToOffset for Point { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { - snapshot.point_to_offset(*self) - } -} - -impl ToOffset for usize { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { - assert!(*self <= snapshot.len(), "offset is out of range"); - *self - } -} - -impl ToOffset for OffsetUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { - snapshot.offset_utf16_to_offset(*self) - } -} - -impl ToOffset for PointUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { - snapshot.point_utf16_to_offset(*self) - } -} - -impl ToOffsetUtf16 for OffsetUtf16 { - fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { - *self - } -} - -impl ToOffsetUtf16 for usize { - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { - snapshot.offset_to_offset_utf16(*self) - } -} - -impl ToPoint for usize { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - snapshot.offset_to_point(*self) - } -} - -impl ToPoint for Point { - fn to_point<'a>(&self, _: &MultiBufferSnapshot) -> Point { - *self - } -} - -impl ToPointUtf16 for usize { - fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { - snapshot.offset_to_point_utf16(*self) - } -} - -impl ToPointUtf16 for Point { - fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { - snapshot.point_to_point_utf16(*self) - } -} - -impl ToPointUtf16 for PointUtf16 { - fn to_point_utf16<'a>(&self, _: &MultiBufferSnapshot) -> PointUtf16 { - *self - } -} - -fn build_excerpt_ranges( - buffer: &BufferSnapshot, - ranges: &[Range], - context_line_count: u32, -) -> (Vec>, Vec) -where - T: text::ToPoint, -{ - let max_point = buffer.max_point(); - let mut range_counts = Vec::new(); - let mut excerpt_ranges = Vec::new(); - let mut range_iter = ranges - .iter() - .map(|range| range.start.to_point(buffer)..range.end.to_point(buffer)) - .peekable(); - while let Some(range) = range_iter.next() { - let excerpt_start = Point::new(range.start.row.saturating_sub(context_line_count), 0); - let mut excerpt_end = Point::new(range.end.row + 1 + context_line_count, 0).min(max_point); - let mut ranges_in_excerpt = 1; - - while let Some(next_range) = range_iter.peek() { - if next_range.start.row <= excerpt_end.row + context_line_count { - excerpt_end = - Point::new(next_range.end.row + 1 + context_line_count, 0).min(max_point); - ranges_in_excerpt += 1; - range_iter.next(); - } else { - break; - } - } - - excerpt_ranges.push(ExcerptRange { - context: excerpt_start..excerpt_end, - primary: Some(range), - }); - range_counts.push(ranges_in_excerpt); - } - - (excerpt_ranges, range_counts) -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::StreamExt; - use gpui::{AppContext, Context, TestAppContext}; - use language::{Buffer, Rope}; - use parking_lot::RwLock; - use rand::prelude::*; - use settings::SettingsStore; - use std::env; - use util::test::sample_text; - - #[gpui::test] - fn test_singleton(cx: &mut AppContext) { - let buffer = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a'))); - let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), buffer.read(cx).text()); - - assert_eq!( - snapshot.buffer_rows(0).collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(snapshot.text(), buffer.read(cx).text()); - assert_eq!( - snapshot.buffer_rows(0).collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - } - - #[gpui::test] - fn test_remote(cx: &mut AppContext) { - let host_buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a")); - let guest_buffer = cx.new_model(|cx| { - let state = host_buffer.read(cx).to_proto(); - let ops = cx - .background_executor() - .block(host_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = Buffer::from_proto(1, state, None).unwrap(); - buffer - .apply_ops( - ops.into_iter() - .map(|op| language::proto::deserialize_operation(op).unwrap()), - cx, - ) - .unwrap(); - buffer - }); - let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "a"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "ab"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "abc"); - } - - #[gpui::test] - fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { - let buffer_1 = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a'))); - let buffer_2 = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'g'))); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - - let events = Arc::new(RwLock::new(Vec::::new())); - multibuffer.update(cx, |_, cx| { - let events = events.clone(); - cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited { .. } = event { - events.write().push(event.clone()) - } - }) - .detach(); - }); - - let subscription = multibuffer.update(cx, |multibuffer, cx| { - let subscription = multibuffer.subscribe(); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: Point::new(1, 2)..Point::new(2, 5), - primary: None, - }], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: 0..0, - new: 0..10 - }] - ); - - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: Point::new(3, 3)..Point::new(4, 4), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: Point::new(3, 1)..Point::new(3, 3), - primary: None, - }], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: 10..10, - new: 10..22 - }] - ); - - subscription - }); - - // Adding excerpts emits an edited event. - assert_eq!( - events.read().as_slice(), - &[ - Event::Edited { - sigleton_buffer_edited: false - }, - Event::Edited { - sigleton_buffer_edited: false - }, - Event::Edited { - sigleton_buffer_edited: false - } - ] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "ccccc\n", // - "ddd\n", // - "eeee\n", // - "jj" // - ) - ); - assert_eq!( - snapshot.buffer_rows(0).collect::>(), - [Some(1), Some(2), Some(3), Some(4), Some(3)] - ); - assert_eq!( - snapshot.buffer_rows(2).collect::>(), - [Some(3), Some(4), Some(3)] - ); - assert_eq!(snapshot.buffer_rows(4).collect::>(), [Some(3)]); - assert_eq!(snapshot.buffer_rows(5).collect::>(), []); - - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot), - &[ - (0, "bbbb\nccccc".to_string(), true), - (2, "ddd\neeee".to_string(), false), - (4, "jj".to_string(), true), - ] - ); - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot), - &[(0, "bbbb\nccccc".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(2, "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(2, "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot), - &[(2, "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot), - &[(4, "jj".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot), - &[] - ); - - buffer_1.update(cx, |buffer, cx| { - let text = "\n"; - buffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), text), - (Point::new(2, 1)..Point::new(2, 3), text), - ], - None, - cx, - ); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee\n", // - "jj" // - ) - ); - - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: 6..8, - new: 6..7 - }] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Left), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Right), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 1), Bias::Right), - Point::new(5, 1) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 2), Bias::Right), - Point::new(5, 2) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 3), Bias::Right), - Point::new(5, 2) - ); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| { - let (buffer_2_excerpt_id, _) = - multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); - multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); - multibuffer.snapshot(cx) - }); - - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee", // - ) - ); - - fn boundaries_in_range( - range: Range, - snapshot: &MultiBufferSnapshot, - ) -> Vec<(u32, String, bool)> { - snapshot - .excerpt_boundaries_in_range(range) - .map(|boundary| { - ( - boundary.row, - boundary - .buffer - .text_for_range(boundary.range.context) - .collect::(), - boundary.starts_new_buffer, - ) - }) - .collect::>() - } - } - - #[gpui::test] - fn test_excerpt_events(cx: &mut AppContext) { - let buffer_1 = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'a'))); - let buffer_2 = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'm'))); - - let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let follower_edit_event_count = Arc::new(RwLock::new(0)); - - follower_multibuffer.update(cx, |_, cx| { - let follower_edit_event_count = follower_edit_event_count.clone(); - cx.subscribe( - &leader_multibuffer, - move |follower, _, event, cx| match event.clone() { - Event::ExcerptsAdded { - buffer, - predecessor, - excerpts, - } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), - Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), - Event::Edited { .. } => { - *follower_edit_event_count.write() += 1; - } - _ => {} - }, - ) - .detach(); - }); - - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts( - buffer_1.clone(), - [ - ExcerptRange { - context: 0..8, - primary: None, - }, - ExcerptRange { - context: 12..16, - primary: None, - }, - ], - cx, - ); - leader.insert_excerpts_after( - leader.excerpt_ids()[0], - buffer_2.clone(), - [ - ExcerptRange { - context: 0..5, - primary: None, - }, - ExcerptRange { - context: 10..15, - primary: None, - }, - ], - cx, - ) - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 2); - - leader_multibuffer.update(cx, |leader, cx| { - let excerpt_ids = leader.excerpt_ids(); - leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Removing an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.remove_excerpts([], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Adding an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts::(buffer_2.clone(), [], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - leader_multibuffer.update(cx, |leader, cx| { - leader.clear(cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 4); - } - - #[gpui::test] - fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { - let buffer = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - vec![ - Point::new(3, 2)..Point::new(4, 2), - Point::new(7, 1)..Point::new(7, 3), - Point::new(15, 0)..Point::new(15, 0), - ], - 2, - cx, - ) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - "bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\nrrr\n" - ); - - assert_eq!( - anchor_ranges - .iter() - .map(|range| range.to_point(&snapshot)) - .collect::>(), - vec![ - Point::new(2, 2)..Point::new(3, 2), - Point::new(6, 1)..Point::new(6, 3), - Point::new(12, 0)..Point::new(12, 0) - ] - ); - } - - #[gpui::test] - async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { - let buffer = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a'))); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { - let snapshot = buffer.read(cx); - let ranges = vec![ - snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)), - snapshot.anchor_before(Point::new(7, 1))..snapshot.anchor_before(Point::new(7, 3)), - snapshot.anchor_before(Point::new(15, 0)) - ..snapshot.anchor_before(Point::new(15, 0)), - ]; - multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx) - }); - - let anchor_ranges = anchor_ranges.collect::>().await; - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - snapshot.text(), - "bbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\n\nnnn\nooo\nppp\nqqq\nrrr\n" - ); - - assert_eq!( - anchor_ranges - .iter() - .map(|range| range.to_point(&snapshot)) - .collect::>(), - vec![ - Point::new(2, 2)..Point::new(3, 2), - Point::new(6, 1)..Point::new(6, 3), - Point::new(12, 0)..Point::new(12, 0) - ] - ); - } - - #[gpui::test] - fn test_empty_multibuffer(cx: &mut AppContext) { - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), ""); - assert_eq!(snapshot.buffer_rows(0).collect::>(), &[Some(0)]); - assert_eq!(snapshot.buffer_rows(1).collect::>(), &[]); - } - - #[gpui::test] - fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { - let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); - let multibuffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "X")], None, cx); - buffer.edit([(5..5, "Y")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd"); - assert_eq!(new_snapshot.text(), "XabcdY"); - - assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); - assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); - assert_eq!(old_snapshot.anchor_before(4).to_offset(&new_snapshot), 5); - assert_eq!(old_snapshot.anchor_after(4).to_offset(&new_snapshot), 6); - } - - #[gpui::test] - fn test_multibuffer_anchors(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); - let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "efghi")); - let multibuffer = cx.new_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..4, - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..5, - primary: None, - }], - cx, - ); - multibuffer - }); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.anchor_before(0).to_offset(&old_snapshot), 0); - assert_eq!(old_snapshot.anchor_after(0).to_offset(&old_snapshot), 0); - assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); - assert_eq!(Anchor::min().to_offset(&old_snapshot), 0); - assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); - assert_eq!(Anchor::max().to_offset(&old_snapshot), 10); - - buffer_1.update(cx, |buffer, cx| { - buffer.edit([(0..0, "W")], None, cx); - buffer.edit([(5..5, "X")], None, cx); - }); - buffer_2.update(cx, |buffer, cx| { - buffer.edit([(0..0, "Y")], None, cx); - buffer.edit([(6..6, "Z")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd\nefghi"); - assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ"); - - assert_eq!(old_snapshot.anchor_before(0).to_offset(&new_snapshot), 0); - assert_eq!(old_snapshot.anchor_after(0).to_offset(&new_snapshot), 1); - assert_eq!(old_snapshot.anchor_before(1).to_offset(&new_snapshot), 2); - assert_eq!(old_snapshot.anchor_after(1).to_offset(&new_snapshot), 2); - assert_eq!(old_snapshot.anchor_before(2).to_offset(&new_snapshot), 3); - assert_eq!(old_snapshot.anchor_after(2).to_offset(&new_snapshot), 3); - assert_eq!(old_snapshot.anchor_before(5).to_offset(&new_snapshot), 7); - assert_eq!(old_snapshot.anchor_after(5).to_offset(&new_snapshot), 8); - assert_eq!(old_snapshot.anchor_before(10).to_offset(&new_snapshot), 13); - assert_eq!(old_snapshot.anchor_after(10).to_offset(&new_snapshot), 14); - } - - #[gpui::test] - fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd")); - let buffer_2 = - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "ABCDEFGHIJKLMNOP")); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - - // Create an insertion id in buffer 1 that doesn't exist in buffer 2. - // Add an excerpt from buffer 1 that spans this new insertion. - buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); - let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..7, - primary: None, - }], - cx, - ) - .pop() - .unwrap() - }); - - let snapshot_1 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_1.text(), "abcd123"); - - // Replace the buffer 1 excerpt with new excerpts from buffer 2. - let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_1], cx); - let mut ids = multibuffer - .push_excerpts( - buffer_2.clone(), - [ - ExcerptRange { - context: 0..4, - primary: None, - }, - ExcerptRange { - context: 6..10, - primary: None, - }, - ExcerptRange { - context: 12..16, - primary: None, - }, - ], - cx, - ) - .into_iter(); - (ids.next().unwrap(), ids.next().unwrap()) - }); - let snapshot_2 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); - - // The old excerpt id doesn't get reused. - assert_ne!(excerpt_id_2, excerpt_id_1); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The current excerpts are from a different buffer, so we don't attempt to - // resolve the old text anchor in the new buffer. - assert_eq!( - snapshot_2.summary_for_anchor::(&snapshot_1.anchor_before(2)), - 0 - ); - assert_eq!( - snapshot_2.summaries_for_anchors::(&[ - snapshot_1.anchor_before(2), - snapshot_1.anchor_after(3) - ]), - vec![0, 0] - ); - - // Refresh anchors from the old snapshot. The return value indicates that both - // anchors lost their original excerpt. - let refresh = - snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]); - assert_eq!( - refresh, - &[ - (0, snapshot_2.anchor_before(0), false), - (1, snapshot_2.anchor_after(0), false), - ] - ); - - // Replace the middle excerpt with a smaller excerpt in buffer 2, - // that intersects the old excerpt. - let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_3], cx); - multibuffer - .insert_excerpts_after( - excerpt_id_2, - buffer_2.clone(), - [ExcerptRange { - context: 5..8, - primary: None, - }], - cx, - ) - .pop() - .unwrap() - }); - - let snapshot_3 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP"); - assert_ne!(excerpt_id_5, excerpt_id_3); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The third anchor can't be resolved, since its excerpt has been removed, - // so it resolves to the same position as its predecessor. - let anchors = [ - snapshot_2.anchor_before(0), - snapshot_2.anchor_after(2), - snapshot_2.anchor_after(6), - snapshot_2.anchor_after(14), - ]; - assert_eq!( - snapshot_3.summaries_for_anchors::(&anchors), - &[0, 2, 9, 13] - ); - - let new_anchors = snapshot_3.refresh_anchors(&anchors); - assert_eq!( - new_anchors.iter().map(|a| (a.0, a.2)).collect::>(), - &[(0, true), (1, true), (2, true), (3, true)] - ); - assert_eq!( - snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.1)), - &[0, 2, 7, 13] - ); - } - - #[gpui::test(iterations = 100)] - fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let mut buffers: Vec> = Vec::new(); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let mut excerpt_ids = Vec::::new(); - let mut expected_excerpts = Vec::<(Model, Range)>::new(); - let mut anchors = Vec::new(); - let mut old_versions = Vec::new(); - - for _ in 0..operations { - match rng.gen_range(0..100) { - 0..=19 if !buffers.is_empty() => { - let buffer = buffers.choose(&mut rng).unwrap(); - buffer.update(cx, |buf, cx| buf.randomly_edit(&mut rng, 5, cx)); - } - 20..=29 if !expected_excerpts.is_empty() => { - let mut ids_to_remove = vec![]; - for _ in 0..rng.gen_range(1..=3) { - if expected_excerpts.is_empty() { - break; - } - - let ix = rng.gen_range(0..expected_excerpts.len()); - ids_to_remove.push(excerpt_ids.remove(ix)); - let (buffer, range) = expected_excerpts.remove(ix); - let buffer = buffer.read(cx); - log::info!( - "Removing excerpt {}: {:?}", - ix, - buffer - .text_for_range(range.to_offset(buffer)) - .collect::(), - ); - } - let snapshot = multibuffer.read(cx).read(cx); - ids_to_remove.sort_unstable_by(|a, b| a.cmp(&b, &snapshot)); - drop(snapshot); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts(ids_to_remove, cx) - }); - } - 30..=39 if !expected_excerpts.is_empty() => { - let multibuffer = multibuffer.read(cx).read(cx); - let offset = - multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left); - let bias = if rng.gen() { Bias::Left } else { Bias::Right }; - log::info!("Creating anchor at {} with bias {:?}", offset, bias); - anchors.push(multibuffer.anchor_at(offset, bias)); - anchors.sort_by(|a, b| a.cmp(b, &multibuffer)); - } - 40..=44 if !anchors.is_empty() => { - let multibuffer = multibuffer.read(cx).read(cx); - let prev_len = anchors.len(); - anchors = multibuffer - .refresh_anchors(&anchors) - .into_iter() - .map(|a| a.1) - .collect(); - - // Ensure the newly-refreshed anchors point to a valid excerpt and don't - // overshoot its boundaries. - assert_eq!(anchors.len(), prev_len); - for anchor in &anchors { - if anchor.excerpt_id == ExcerptId::min() - || anchor.excerpt_id == ExcerptId::max() - { - continue; - } - - let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap(); - assert_eq!(excerpt.id, anchor.excerpt_id); - assert!(excerpt.contains(anchor)); - } - } - _ => { - let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { - let base_text = util::RandomCharIter::new(&mut rng) - .take(10) - .collect::(); - buffers.push( - cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), base_text)), - ); - buffers.last().unwrap() - } else { - buffers.choose(&mut rng).unwrap() - }; - - 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 anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); - let prev_excerpt_ix = rng.gen_range(0..=expected_excerpts.len()); - let prev_excerpt_id = excerpt_ids - .get(prev_excerpt_ix) - .cloned() - .unwrap_or_else(ExcerptId::max); - let excerpt_ix = (prev_excerpt_ix + 1).min(expected_excerpts.len()); - - log::info!( - "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", - excerpt_ix, - expected_excerpts.len(), - buffer_handle.read(cx).remote_id(), - buffer.text(), - start_ix..end_ix, - &buffer.text()[start_ix..end_ix] - ); - - let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .insert_excerpts_after( - prev_excerpt_id, - buffer_handle.clone(), - [ExcerptRange { - context: start_ix..end_ix, - primary: None, - }], - cx, - ) - .pop() - .unwrap() - }); - - excerpt_ids.insert(excerpt_ix, excerpt_id); - expected_excerpts.insert(excerpt_ix, (buffer_handle.clone(), anchor_range)); - } - } - - if rng.gen_bool(0.3) { - multibuffer.update(cx, |multibuffer, cx| { - old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); - }) - } - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let mut excerpt_starts = Vec::new(); - let mut expected_text = String::new(); - let mut expected_buffer_rows = Vec::new(); - for (buffer, range) in &expected_excerpts { - let buffer = buffer.read(cx); - let buffer_range = range.to_offset(buffer); - - excerpt_starts.push(TextSummary::from(expected_text.as_str())); - expected_text.extend(buffer.text_for_range(buffer_range.clone())); - expected_text.push('\n'); - - let buffer_row_range = buffer.offset_to_point(buffer_range.start).row - ..=buffer.offset_to_point(buffer_range.end).row; - for row in buffer_row_range { - expected_buffer_rows.push(Some(row)); - } - } - // Remove final trailing newline. - if !expected_excerpts.is_empty() { - expected_text.pop(); - } - - // Always report one buffer row - if expected_buffer_rows.is_empty() { - expected_buffer_rows.push(Some(0)); - } - - assert_eq!(snapshot.text(), expected_text); - log::info!("MultiBuffer text: {:?}", expected_text); - - assert_eq!( - snapshot.buffer_rows(0).collect::>(), - expected_buffer_rows, - ); - - for _ in 0..5 { - let start_row = rng.gen_range(0..=expected_buffer_rows.len()); - assert_eq!( - snapshot.buffer_rows(start_row as u32).collect::>(), - &expected_buffer_rows[start_row..], - "buffer_rows({})", - start_row - ); - } - - assert_eq!( - snapshot.max_buffer_row(), - expected_buffer_rows.into_iter().flatten().max().unwrap() - ); - - let mut excerpt_starts = excerpt_starts.into_iter(); - for (buffer, range) in &expected_excerpts { - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - let buffer_range = range.to_offset(buffer); - let buffer_start_point = buffer.offset_to_point(buffer_range.start); - let buffer_start_point_utf16 = - buffer.text_summary_for_range::(0..buffer_range.start); - - let excerpt_start = excerpt_starts.next().unwrap(); - let mut offset = excerpt_start.len; - let mut buffer_offset = buffer_range.start; - let mut point = excerpt_start.lines; - let mut buffer_point = buffer_start_point; - let mut point_utf16 = excerpt_start.lines_utf16(); - let mut buffer_point_utf16 = buffer_start_point_utf16; - for ch in buffer - .snapshot() - .chunks(buffer_range.clone(), false) - .flat_map(|c| c.text.chars()) - { - for _ in 0..ch.len_utf8() { - let left_offset = snapshot.clip_offset(offset, Bias::Left); - let right_offset = snapshot.clip_offset(offset, Bias::Right); - let buffer_left_offset = buffer.clip_offset(buffer_offset, Bias::Left); - let buffer_right_offset = buffer.clip_offset(buffer_offset, Bias::Right); - assert_eq!( - left_offset, - excerpt_start.len + (buffer_left_offset - buffer_range.start), - "clip_offset({:?}, Left). buffer: {:?}, buffer offset: {:?}", - offset, - buffer_id, - buffer_offset, - ); - assert_eq!( - right_offset, - excerpt_start.len + (buffer_right_offset - buffer_range.start), - "clip_offset({:?}, Right). buffer: {:?}, buffer offset: {:?}", - offset, - buffer_id, - buffer_offset, - ); - - let left_point = snapshot.clip_point(point, Bias::Left); - let right_point = snapshot.clip_point(point, Bias::Right); - let buffer_left_point = buffer.clip_point(buffer_point, Bias::Left); - let buffer_right_point = buffer.clip_point(buffer_point, Bias::Right); - assert_eq!( - left_point, - excerpt_start.lines + (buffer_left_point - buffer_start_point), - "clip_point({:?}, Left). buffer: {:?}, buffer point: {:?}", - point, - buffer_id, - buffer_point, - ); - assert_eq!( - right_point, - excerpt_start.lines + (buffer_right_point - buffer_start_point), - "clip_point({:?}, Right). buffer: {:?}, buffer point: {:?}", - point, - buffer_id, - buffer_point, - ); - - assert_eq!( - snapshot.point_to_offset(left_point), - left_offset, - "point_to_offset({:?})", - left_point, - ); - assert_eq!( - snapshot.offset_to_point(left_offset), - left_point, - "offset_to_point({:?})", - left_offset, - ); - - offset += 1; - buffer_offset += 1; - if ch == '\n' { - point += Point::new(1, 0); - buffer_point += Point::new(1, 0); - } else { - point += Point::new(0, 1); - buffer_point += Point::new(0, 1); - } - } - - for _ in 0..ch.len_utf16() { - let left_point_utf16 = - snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Left); - let right_point_utf16 = - snapshot.clip_point_utf16(Unclipped(point_utf16), Bias::Right); - let buffer_left_point_utf16 = - buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Left); - let buffer_right_point_utf16 = - buffer.clip_point_utf16(Unclipped(buffer_point_utf16), Bias::Right); - assert_eq!( - left_point_utf16, - excerpt_start.lines_utf16() - + (buffer_left_point_utf16 - buffer_start_point_utf16), - "clip_point_utf16({:?}, Left). buffer: {:?}, buffer point_utf16: {:?}", - point_utf16, - buffer_id, - buffer_point_utf16, - ); - assert_eq!( - right_point_utf16, - excerpt_start.lines_utf16() - + (buffer_right_point_utf16 - buffer_start_point_utf16), - "clip_point_utf16({:?}, Right). buffer: {:?}, buffer point_utf16: {:?}", - point_utf16, - buffer_id, - buffer_point_utf16, - ); - - if ch == '\n' { - point_utf16 += PointUtf16::new(1, 0); - buffer_point_utf16 += PointUtf16::new(1, 0); - } else { - point_utf16 += PointUtf16::new(0, 1); - buffer_point_utf16 += PointUtf16::new(0, 1); - } - } - } - } - - for (row, line) in expected_text.split('\n').enumerate() { - assert_eq!( - snapshot.line_len(row as u32), - line.len() as u32, - "line_len({}).", - row - ); - } - - let text_rope = Rope::from(expected_text.as_str()); - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); - let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); - - let text_for_range = snapshot - .text_for_range(start_ix..end_ix) - .collect::(); - assert_eq!( - text_for_range, - &expected_text[start_ix..end_ix], - "incorrect text for range {:?}", - start_ix..end_ix - ); - - let excerpted_buffer_ranges = multibuffer - .read(cx) - .range_to_buffer_ranges(start_ix..end_ix, cx); - let excerpted_buffers_text = excerpted_buffer_ranges - .iter() - .map(|(buffer, buffer_range, _)| { - buffer - .read(cx) - .text_for_range(buffer_range.clone()) - .collect::() - }) - .collect::>() - .join("\n"); - assert_eq!(excerpted_buffers_text, text_for_range); - if !expected_excerpts.is_empty() { - assert!(!excerpted_buffer_ranges.is_empty()); - } - - let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]); - assert_eq!( - snapshot.text_summary_for_range::(start_ix..end_ix), - expected_summary, - "incorrect summary for range {:?}", - start_ix..end_ix - ); - } - - // Anchor resolution - let summaries = snapshot.summaries_for_anchors::(&anchors); - assert_eq!(anchors.len(), summaries.len()); - for (anchor, resolved_offset) in anchors.iter().zip(summaries) { - assert!(resolved_offset <= snapshot.len()); - assert_eq!( - snapshot.summary_for_anchor::(anchor), - resolved_offset - ); - } - - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right); - assert_eq!( - snapshot.reversed_chars_at(end_ix).collect::(), - expected_text[..end_ix].chars().rev().collect::(), - ); - } - - for _ in 0..10 { - let end_ix = rng.gen_range(0..=text_rope.len()); - let start_ix = rng.gen_range(0..=end_ix); - assert_eq!( - snapshot - .bytes_in_range(start_ix..end_ix) - .flatten() - .copied() - .collect::>(), - expected_text.as_bytes()[start_ix..end_ix].to_vec(), - "bytes_in_range({:?})", - start_ix..end_ix, - ); - } - } - - let snapshot = multibuffer.read(cx).snapshot(cx); - for (old_snapshot, subscription) in old_versions { - let edits = subscription.consume().into_inner(); - - log::info!( - "applying subscription edits to old text: {:?}: {:?}", - old_snapshot.text(), - edits, - ); - - let mut text = old_snapshot.text(); - for edit in edits { - let new_text: String = snapshot.text_for_range(edit.new.clone()).collect(); - text.replace_range(edit.new.start..edit.new.start + edit.old.len(), &new_text); - } - assert_eq!(text.to_string(), snapshot.text()); - } - } - - #[gpui::test] - fn test_history(cx: &mut AppContext) { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - - let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "1234")); - let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "5678")); - let multibuffer = cx.new_model(|_| MultiBuffer::new(0)); - let group_interval = multibuffer.read(cx).history.group_interval; - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange { - context: 0..buffer_1.read(cx).len(), - primary: None, - }], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange { - context: 0..buffer_2.read(cx).len(), - primary: None, - }], - cx, - ); - }); - - let mut now = Instant::now(); - - multibuffer.update(cx, |multibuffer, cx| { - let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); - multibuffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), "A"), - (Point::new(1, 0)..Point::new(1, 0), "A"), - ], - None, - cx, - ); - multibuffer.edit( - [ - (Point::new(0, 1)..Point::new(0, 1), "B"), - (Point::new(1, 1)..Point::new(1, 1), "B"), - ], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - // Edit buffer 1 through the multibuffer - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(2..2, "C")], None, cx); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); - - // Edit buffer 1 independently - buffer_1.update(cx, |buffer_1, cx| { - buffer_1.start_transaction_at(now); - buffer_1.edit([(3..3, "D")], None, cx); - buffer_1.end_transaction_at(now, cx); - - now += 2 * group_interval; - buffer_1.start_transaction_at(now); - buffer_1.edit([(4..4, "E")], None, cx); - buffer_1.end_transaction_at(now, cx); - }); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // An undo in the multibuffer undoes the multibuffer transaction - // and also any individual buffer edits that have occurred since - // that transaction. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // Undo buffer 2 independently. - buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678"); - - // An undo in the multibuffer undoes the components of the - // the last multibuffer transaction that are not already undone. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - - // Redo stack gets cleared after an edit. - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit([(0..0, "X")], None, cx); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - // Transactions can be grouped manually. - multibuffer.redo(cx); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.group_until_transaction(transaction_1, cx); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - }); - } -} diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml index f4e2b849fa..879494d5b4 100644 --- a/crates/outline/Cargo.toml +++ b/crates/outline/Cargo.toml @@ -10,14 +10,16 @@ doctest = false [dependencies] editor = { path = "../editor" } -fuzzy = { path = "../fuzzy" } -gpui = { path = "../gpui" } -language = { path = "../language" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +language = { package = "language2", path = "../language2" } picker = { path = "../picker" } -settings = { path = "../settings" } -text = { path = "../text" } -theme = { path = "../theme" } -workspace = { path = "../workspace" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } +util = { path = "../util" } ordered-float.workspace = true postage.workspace = true diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 18e10678fa..75d1a09357 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,68 +1,109 @@ use editor::{ - combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, - scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor, ToPoint, + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, + DisplayPoint, Editor, EditorMode, ToPoint, }; use fuzzy::StringMatch; use gpui::{ - actions, elements::*, geometry::vector::Vector2F, AppContext, MouseState, Task, ViewContext, - ViewHandle, WindowContext, + actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, + FontStyle, FontWeight, HighlightStyle, ParentElement, Point, Render, Styled, StyledText, Task, + TextStyle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, }; use language::Outline; use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate, PickerEvent}; +use picker::{Picker, PickerDelegate}; +use settings::Settings; use std::{ cmp::{self, Reverse}, sync::Arc, }; -use workspace::Workspace; + +use theme::{color_alpha, ActiveTheme, ThemeSettings}; +use ui::{prelude::*, ListItem, ListItemSpacing}; +use util::ResultExt; +use workspace::ModalView; actions!(outline, [Toggle]); pub fn init(cx: &mut AppContext) { - cx.add_action(toggle); - OutlineView::init(cx); + cx.observe_new_views(OutlineView::register).detach(); } -pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - if let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let outline = editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - .outline(Some(theme::current(cx).editor.syntax.as_ref())); - if let Some(outline) = outline { - workspace.toggle_modal(cx, |_, cx| { - cx.add_view(|cx| { - OutlineView::new(OutlineViewDelegate::new(outline, editor, cx), cx) - .with_max_size(800., 1200.) - }) - }); - } +pub fn toggle(editor: View, _: &Toggle, cx: &mut WindowContext) { + let outline = editor + .read(cx) + .buffer() + .read(cx) + .snapshot(cx) + .outline(Some(&cx.theme().syntax())); + + if let Some((workspace, outline)) = editor.read(cx).workspace().zip(outline) { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx)); + }) } } -type OutlineView = Picker; +pub struct OutlineView { + picker: View>, +} + +impl FocusableView for OutlineView { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter for OutlineView {} +impl ModalView for OutlineView {} + +impl Render for OutlineView { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + v_stack().w(rems(34.)).child(self.picker.clone()) + } +} + +impl OutlineView { + fn register(editor: &mut Editor, cx: &mut ViewContext) { + if editor.mode() == EditorMode::Full { + let handle = cx.view().downgrade(); + editor.register_action(move |action, cx| { + if let Some(editor) = handle.upgrade() { + toggle(editor, action, cx); + } + }); + } + } + + fn new( + outline: Outline, + editor: View, + cx: &mut ViewContext, + ) -> OutlineView { + let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx); + let picker = cx.new_view(|cx| Picker::new(delegate, cx).max_height(vh(0.75, cx))); + OutlineView { picker } + } +} struct OutlineViewDelegate { - active_editor: ViewHandle, + outline_view: WeakView, + active_editor: View, outline: Outline, selected_match_index: usize, - prev_scroll_position: Option, + prev_scroll_position: Option>, matches: Vec, last_query: String, } impl OutlineViewDelegate { fn new( + outline_view: WeakView, outline: Outline, - editor: ViewHandle, + editor: View, cx: &mut ViewContext, ) -> Self { Self { + outline_view, last_query: Default::default(), matches: Default::default(), selected_match_index: 0, @@ -81,11 +122,18 @@ impl OutlineViewDelegate { }) } - fn set_selected_index(&mut self, ix: usize, navigate: bool, cx: &mut ViewContext) { + fn set_selected_index( + &mut self, + ix: usize, + navigate: bool, + cx: &mut ViewContext>, + ) { self.selected_match_index = ix; + if navigate && !self.matches.is_empty() { let selected_match = &self.matches[self.selected_match_index]; let outline_item = &self.outline.items[selected_match.candidate_id]; + self.active_editor.update(cx, |active_editor, cx| { let snapshot = active_editor.snapshot(cx).display_snapshot; let buffer_snapshot = &snapshot.buffer_snapshot; @@ -101,6 +149,8 @@ impl OutlineViewDelegate { } impl PickerDelegate for OutlineViewDelegate { + type ListItem = ListItem; + fn placeholder_text(&self) -> Arc { "Search buffer symbols...".into() } @@ -113,15 +163,15 @@ impl PickerDelegate for OutlineViewDelegate { self.selected_match_index } - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext) { + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { self.set_selected_index(ix, true, cx); } - fn center_selection_after_match_updates(&self) -> bool { - true - } - - fn update_matches(&mut self, query: String, cx: &mut ViewContext) -> Task<()> { + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> Task<()> { let selected_index; if query.is_empty() { self.restore_active_editor(cx); @@ -163,7 +213,10 @@ impl PickerDelegate for OutlineViewDelegate { .map(|(ix, _, _)| ix) .unwrap_or(0); } else { - self.matches = smol::block_on(self.outline.search(&query, cx.background().clone())); + self.matches = smol::block_on( + self.outline + .search(&query, cx.background_executor().clone()), + ); selected_index = self .matches .iter() @@ -177,8 +230,9 @@ impl PickerDelegate for OutlineViewDelegate { Task::ready(()) } - fn confirm(&mut self, _: bool, cx: &mut ViewContext) { + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { self.prev_scroll_position.take(); + self.active_editor.update(cx, |active_editor, cx| { if let Some(rows) = active_editor.highlighted_rows() { let snapshot = active_editor.snapshot(cx).display_snapshot; @@ -187,39 +241,69 @@ impl PickerDelegate for OutlineViewDelegate { s.select_ranges([position..position]) }); active_editor.highlight_rows(None); + active_editor.focus(cx); } }); - cx.emit(PickerEvent::Dismiss); + + self.dismissed(cx); } - fn dismissed(&mut self, cx: &mut ViewContext) { + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.outline_view + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); self.restore_active_editor(cx); } fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &AppContext, - ) -> AnyElement> { - let theme = theme::current(cx); - let style = theme.picker.item.in_state(selected).style_for(mouse_state); - let string_match = &self.matches[ix]; - let outline_item = &self.outline.items[string_match.candidate_id]; + cx: &mut ViewContext>, + ) -> Option { + let settings = ThemeSettings::get_global(cx); - Text::new(outline_item.text.clone(), style.label.text.clone()) - .with_soft_wrap(false) - .with_highlights(combine_syntax_and_fuzzy_match_highlights( - &outline_item.text, - style.label.text.clone().into(), - outline_item.highlight_ranges.iter().cloned(), - &string_match.positions, - )) - .contained() - .with_padding_left(20. * outline_item.depth as f32) - .contained() - .with_style(style.container) - .into_any() + // TODO: We probably shouldn't need to build a whole new text style here + // but I'm not sure how to get the current one and modify it. + // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color. + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features, + font_size: settings.buffer_font_size(cx).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + + let mut highlight_style = HighlightStyle::default(); + highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3)); + + let mat = &self.matches[ix]; + let outline_item = &self.outline.items[mat.candidate_id]; + + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, highlight_style)), + outline_item.highlight_ranges.iter().cloned(), + ); + + let styled_text = + StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights); + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child( + div() + .text_ui() + .pl(rems(outline_item.depth as f32)) + .child(styled_text), + ), + ) } } diff --git a/crates/outline2/Cargo.toml b/crates/outline2/Cargo.toml deleted file mode 100644 index 3075c011c1..0000000000 --- a/crates/outline2/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "outline2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/outline.rs" -doctest = false - -[dependencies] -editor = { path = "../editor" } -fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -gpui = { package = "gpui2", path = "../gpui2" } -ui = { package = "ui2", path = "../ui2" } -language = { package = "language2", path = "../language2" } -picker = { path = "../picker" } -settings = { package = "settings2", path = "../settings2" } -text = { package = "text2", path = "../text2" } -theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2" } -util = { path = "../util" } - -ordered-float.workspace = true -postage.workspace = true -smol.workspace = true - -[dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } diff --git a/crates/outline2/src/outline.rs b/crates/outline2/src/outline.rs deleted file mode 100644 index 75d1a09357..0000000000 --- a/crates/outline2/src/outline.rs +++ /dev/null @@ -1,309 +0,0 @@ -use editor::{ - display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, - DisplayPoint, Editor, EditorMode, ToPoint, -}; -use fuzzy::StringMatch; -use gpui::{ - actions, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, - FontStyle, FontWeight, HighlightStyle, ParentElement, Point, Render, Styled, StyledText, Task, - TextStyle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, -}; -use language::Outline; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use settings::Settings; -use std::{ - cmp::{self, Reverse}, - sync::Arc, -}; - -use theme::{color_alpha, ActiveTheme, ThemeSettings}; -use ui::{prelude::*, ListItem, ListItemSpacing}; -use util::ResultExt; -use workspace::ModalView; - -actions!(outline, [Toggle]); - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(OutlineView::register).detach(); -} - -pub fn toggle(editor: View, _: &Toggle, cx: &mut WindowContext) { - let outline = editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - .outline(Some(&cx.theme().syntax())); - - if let Some((workspace, outline)) = editor.read(cx).workspace().zip(outline) { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx)); - }) - } -} - -pub struct OutlineView { - picker: View>, -} - -impl FocusableView for OutlineView { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for OutlineView {} -impl ModalView for OutlineView {} - -impl Render for OutlineView { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) - } -} - -impl OutlineView { - fn register(editor: &mut Editor, cx: &mut ViewContext) { - if editor.mode() == EditorMode::Full { - let handle = cx.view().downgrade(); - editor.register_action(move |action, cx| { - if let Some(editor) = handle.upgrade() { - toggle(editor, action, cx); - } - }); - } - } - - fn new( - outline: Outline, - editor: View, - cx: &mut ViewContext, - ) -> OutlineView { - let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx); - let picker = cx.new_view(|cx| Picker::new(delegate, cx).max_height(vh(0.75, cx))); - OutlineView { picker } - } -} - -struct OutlineViewDelegate { - outline_view: WeakView, - active_editor: View, - outline: Outline, - selected_match_index: usize, - prev_scroll_position: Option>, - matches: Vec, - last_query: String, -} - -impl OutlineViewDelegate { - fn new( - outline_view: WeakView, - outline: Outline, - editor: View, - cx: &mut ViewContext, - ) -> Self { - Self { - outline_view, - last_query: Default::default(), - matches: Default::default(), - selected_match_index: 0, - prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))), - active_editor: editor, - outline, - } - } - - fn restore_active_editor(&mut self, cx: &mut WindowContext) { - self.active_editor.update(cx, |editor, cx| { - editor.highlight_rows(None); - if let Some(scroll_position) = self.prev_scroll_position { - editor.set_scroll_position(scroll_position, cx); - } - }) - } - - fn set_selected_index( - &mut self, - ix: usize, - navigate: bool, - cx: &mut ViewContext>, - ) { - self.selected_match_index = ix; - - if navigate && !self.matches.is_empty() { - let selected_match = &self.matches[self.selected_match_index]; - let outline_item = &self.outline.items[selected_match.candidate_id]; - - self.active_editor.update(cx, |active_editor, cx| { - let snapshot = active_editor.snapshot(cx).display_snapshot; - let buffer_snapshot = &snapshot.buffer_snapshot; - let start = outline_item.range.start.to_point(buffer_snapshot); - let end = outline_item.range.end.to_point(buffer_snapshot); - let display_rows = start.to_display_point(&snapshot).row() - ..end.to_display_point(&snapshot).row() + 1; - active_editor.highlight_rows(Some(display_rows)); - active_editor.request_autoscroll(Autoscroll::center(), cx); - }); - } - } -} - -impl PickerDelegate for OutlineViewDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self) -> Arc { - "Search buffer symbols...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { - self.set_selected_index(ix, true, cx); - } - - fn update_matches( - &mut self, - query: String, - cx: &mut ViewContext>, - ) -> Task<()> { - let selected_index; - if query.is_empty() { - self.restore_active_editor(cx); - self.matches = self - .outline - .items - .iter() - .enumerate() - .map(|(index, _)| StringMatch { - candidate_id: index, - score: Default::default(), - positions: Default::default(), - string: Default::default(), - }) - .collect(); - - let editor = self.active_editor.read(cx); - let cursor_offset = editor.selections.newest::(cx).head(); - let buffer = editor.buffer().read(cx).snapshot(cx); - selected_index = self - .outline - .items - .iter() - .enumerate() - .map(|(ix, item)| { - let range = item.range.to_offset(&buffer); - let distance_to_closest_endpoint = cmp::min( - (range.start as isize - cursor_offset as isize).abs(), - (range.end as isize - cursor_offset as isize).abs(), - ); - let depth = if range.contains(&cursor_offset) { - Some(item.depth) - } else { - None - }; - (ix, depth, distance_to_closest_endpoint) - }) - .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance))) - .map(|(ix, _, _)| ix) - .unwrap_or(0); - } else { - self.matches = smol::block_on( - self.outline - .search(&query, cx.background_executor().clone()), - ); - selected_index = self - .matches - .iter() - .enumerate() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); - } - self.last_query = query; - self.set_selected_index(selected_index, !self.last_query.is_empty(), cx); - Task::ready(()) - } - - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - self.prev_scroll_position.take(); - - self.active_editor.update(cx, |active_editor, cx| { - if let Some(rows) = active_editor.highlighted_rows() { - let snapshot = active_editor.snapshot(cx).display_snapshot; - let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); - active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { - s.select_ranges([position..position]) - }); - active_editor.highlight_rows(None); - active_editor.focus(cx); - } - }); - - self.dismissed(cx); - } - - fn dismissed(&mut self, cx: &mut ViewContext>) { - self.outline_view - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - self.restore_active_editor(cx); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - cx: &mut ViewContext>, - ) -> Option { - let settings = ThemeSettings::get_global(cx); - - // TODO: We probably shouldn't need to build a whole new text style here - // but I'm not sure how to get the current one and modify it. - // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color. - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features, - font_size: settings.buffer_font_size(cx).into(), - font_weight: FontWeight::NORMAL, - font_style: FontStyle::Normal, - line_height: relative(1.).into(), - background_color: None, - underline: None, - white_space: WhiteSpace::Normal, - }; - - let mut highlight_style = HighlightStyle::default(); - highlight_style.background_color = Some(color_alpha(cx.theme().colors().text_accent, 0.3)); - - let mat = &self.matches[ix]; - let outline_item = &self.outline.items[mat.candidate_id]; - - let highlights = gpui::combine_highlights( - mat.ranges().map(|range| (range, highlight_style)), - outline_item.highlight_ranges.iter().cloned(), - ); - - let styled_text = - StyledText::new(outline_item.text.clone()).with_highlights(&text_style, highlights); - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .selected(selected) - .child( - div() - .text_ui() - .pl(rems(outline_item.depth as f32)) - .child(styled_text), - ), - ) - } -} diff --git a/crates/project2/Cargo.toml b/crates/project2/Cargo.toml index 892ddb91c7..f8f72af5e9 100644 --- a/crates/project2/Cargo.toml +++ b/crates/project2/Cargo.toml @@ -21,7 +21,7 @@ test-support = [ [dependencies] text = { package = "text2", path = "../text2" } -copilot = { package = "copilot2", path = "../copilot2" } +copilot = { path = "../copilot" } client = { package = "client2", path = "../client2" } clock = { path = "../clock" } collections = { path = "../collections" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 294ed5690c..ad5b14de28 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -29,7 +29,7 @@ command_palette = { path = "../command_palette" } # component_test = { path = "../component_test" } client = { package = "client2", path = "../client2" } # clock = { path = "../clock" } -copilot = { package = "copilot2", path = "../copilot2" } +copilot = { path = "../copilot" } copilot_button = { path = "../copilot_button" } diagnostics = { path = "../diagnostics" } db = { package = "db2", path = "../db2" } @@ -51,7 +51,7 @@ language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } notifications = { package = "notifications2", path = "../notifications2" } assistant = { package = "assistant2", path = "../assistant2" } -outline = { package = "outline2", path = "../outline2" } +outline = { path = "../outline" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } project_panel = { path = "../project_panel" }