Detailed changes
@@ -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",
@@ -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",
@@ -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" }
@@ -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]
@@ -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"] }
@@ -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<dyn NodeRuntime>,
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::<Suggest>(),
+ TypeId::of::<NextSuggestion>(),
+ TypeId::of::<PreviousSuggestion>(),
+ TypeId::of::<Reinstall>(),
+ ];
+ let copilot_auth_action_types = [TypeId::of::<SignOut>()];
+ let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
let status = handle.read(cx).status();
- cx.update_default_global::<collections::CommandPaletteFilter, _, _>(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);
+ let filter = cx.default_global::<collections::CommandPaletteFilter>();
+
+ 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_namespaces.insert(COPILOT_NAMESPACE);
- filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
+ }
+ _ => {
+ 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<LanguageServer>,
sign_in_status: SignInStatus,
- registered_buffers: HashMap<usize, RegisteredBuffer>,
+ registered_buffers: HashMap<EntityId, RegisteredBuffer>,
}
#[derive(Clone, Debug)]
@@ -180,7 +203,7 @@ struct RegisteredBuffer {
impl RegisteredBuffer {
fn report_changes(
&mut self,
- buffer: &ModelHandle<Buffer>,
+ buffer: &Model<Buffer>,
cx: &mut ModelContext<Copilot>,
) -> 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::notification::DidChangeTextDocument>(
- 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::notification::DidChangeTextDocument>(
+ 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<dyn HttpClient>,
node_runtime: Arc<dyn NodeRuntime>,
server: CopilotServer,
- buffers: HashSet<WeakModelHandle<Buffer>>,
+ buffers: HashSet<WeakModel<Buffer>>,
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<Pin<Box<dyn 'static + Future<Output = ()>>>> {
- 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<Event> for Copilot {}
impl Copilot {
- pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
- if cx.has_global::<ModelHandle<Self>>() {
- Some(cx.global::<ModelHandle<Self>>().clone())
+ pub fn global(cx: &AppContext) -> Option<Model<Self>> {
+ if cx.has_global::<Model<Self>>() {
+ Some(cx.global::<Model<Self>>().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::<SettingsStore, _>(move |this, cx| this.enable_or_disable_copilot(cx))
+ cx.observe_global::<SettingsStore>(move |this, cx| this.enable_or_disable_copilot(cx))
.detach();
this
}
- fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
+ fn shutdown_language_server(
+ &mut self,
+ _cx: &mut ModelContext<Self>,
+ ) -> impl Future<Output = ()> {
+ 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<Self>) {
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<Self>, lsp::FakeLanguageServer) {
+ pub fn fake(cx: &mut gpui::TestAppContext) -> (Model<Self>, 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<dyn HttpClient>,
node_runtime: Arc<dyn NodeRuntime>,
- this: ModelHandle<Self>,
+ this: WeakModel<Self>,
mut cx: AsyncAppContext,
) -> impl Future<Output = ()> {
async move {
@@ -448,6 +474,7 @@ impl Copilot {
}
}
})
+ .ok();
}
}
@@ -489,7 +516,7 @@ impl Copilot {
cx.notify();
}
}
- });
+ })?;
let response = lsp
.request::<request::SignInConfirm>(
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::SignOut>(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<LanguageServer>)> {
@@ -581,7 +608,7 @@ impl Copilot {
}
}
- pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
+ pub fn register_buffer(&mut self, buffer: &Model<Buffer>, cx: &mut ModelContext<Self>) {
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::notification::DidOpenTextDocument>(
- 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::notification::DidOpenTextDocument>(
+ 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>,
+ buffer: Model<Buffer>,
event: &language::Event,
cx: &mut ModelContext<Self>,
) -> 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<Buffer>) {
+ fn unregister_buffer(&mut self, buffer: &WeakModel<Buffer>) {
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::<lsp::notification::DidCloseTextDocument>(
@@ -711,7 +741,7 @@ impl Copilot {
pub fn completions<T>(
&mut self,
- buffer: &ModelHandle<Buffer>,
+ buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>>
@@ -723,7 +753,7 @@ impl Copilot {
pub fn completions_cycling<T>(
&mut self,
- buffer: &ModelHandle<Buffer>,
+ buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>>
@@ -748,7 +778,7 @@ impl Copilot {
.request::<request::NotifyAccepted>(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<R, T>(
&mut self,
- buffer: &ModelHandle<Buffer>,
+ buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>>
@@ -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::<R>(request::GetCompletionsParams {
@@ -869,7 +902,7 @@ impl Copilot {
lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>,
) {
- 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::<Vec<_>>() {
- 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::<Vec<_>>() {
+ for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
self.unregister_buffer(&buffer);
}
}
request::SignInStatus::NotSignedIn => {
server.sign_in_status = SignInStatus::SignedOut;
- for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
+ for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
self.unregister_buffer(&buffer);
}
}
@@ -911,11 +944,11 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
}
}
-fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
+fn uri_for_buffer(buffer: &Model<Buffer>, 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<dyn HttpClient>) -> anyhow::Result<PathBuf> {
#[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<Deterministic>, 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::<lsp::notification::DidOpenTextDocument>()
@@ -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::<lsp::notification::DidOpenTextDocument>()
@@ -1114,6 +1150,7 @@ mod tests {
.update(cx, |copilot, cx| copilot.sign_in(cx))
.await
.unwrap();
+
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.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!(
@@ -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<CopilotCodeVerification> {
- 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<Self>,
- ) -> impl Element<Self> {
+ ) -> 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::<Self, _>(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<Self>,
- ) -> AnyElement<Self> {
- 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),
- )
- .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::<ConnectButton, _, _, _>(
- if connect_clicked {
- "Waiting for connection..."
- } else {
- "Connect to GitHub"
- },
- style.auth.content_width,
- &style.auth.cta_button,
- cx,
- {
- let verification_uri = data.verification_uri.clone();
- move |_, verification, cx| {
- cx.platform().open_url(&verification_uri);
- verification.connect_clicked = true;
- }
- },
+ ) -> 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",
))
- .align_children_center()
- .into_any()
- }
-
- fn render_enabled_modal(
- style: &theme::Copilot,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- 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(),
+ .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(
- 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),
+ .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;
+ })
+ }),
)
- .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
- "Done",
- style.auth.content_width,
- &style.auth.cta_button,
- cx,
- |_, _, cx| cx.remove_window(),
+ }
+ 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_unauthorized_modal(
- style: &theme::Copilot,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- 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),
- )
- .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),
+ 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),
)
- .with_child(theme::ui::cta_button::<Self, _, _, _>(
- "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<Self>) {
- cx.notify()
- }
-
- fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
- cx.notify()
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- enum ConnectModal {}
-
- let style = theme::current(cx).clone();
-
- modal::<ConnectModal, _, _, _, _>(
- "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<Self>) -> 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)
}
}
@@ -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"] }
@@ -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<dyn HttpClient>,
- node_runtime: Arc<dyn NodeRuntime>,
- 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::<Suggest>(),
- TypeId::of::<NextSuggestion>(),
- TypeId::of::<PreviousSuggestion>(),
- TypeId::of::<Reinstall>(),
- ];
- let copilot_auth_action_types = [TypeId::of::<SignOut>()];
- let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
- let status = handle.read(cx).status();
- let filter = cx.default_global::<collections::CommandPaletteFilter>();
-
- 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<Task<()>> },
- Error(Arc<str>),
- 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<LanguageServer>,
- sign_in_status: SignInStatus,
- registered_buffers: HashMap<EntityId, RegisteredBuffer>,
-}
-
-#[derive(Clone, Debug)]
-enum SignInStatus {
- Authorized,
- Unauthorized,
- SigningIn {
- prompt: Option<request::PromptUserDeviceFlow>,
- task: Shared<Task<Result<(), Arc<anyhow::Error>>>>,
- },
- SignedOut,
-}
-
-#[derive(Debug, Clone)]
-pub enum Status {
- Starting {
- task: Shared<Task<()>>,
- },
- Error(Arc<str>),
- Disabled,
- SignedOut,
- SigningIn {
- prompt: Option<request::PromptUserDeviceFlow>,
- },
- 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<Option<()>>,
-}
-
-impl RegisteredBuffer {
- fn report_changes(
- &mut self,
- buffer: &Model<Buffer>,
- cx: &mut ModelContext<Copilot>,
- ) -> 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::<Vec<_>>()
- }
- })
- .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::notification::DidChangeTextDocument>(
- 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<Anchor>,
- pub text: String,
-}
-
-pub struct Copilot {
- http: Arc<dyn HttpClient>,
- node_runtime: Arc<dyn NodeRuntime>,
- server: CopilotServer,
- buffers: HashSet<WeakModel<Buffer>>,
- server_id: LanguageServerId,
- _subscription: gpui::Subscription,
-}
-
-pub enum Event {
- CopilotLanguageServerStarted,
-}
-
-impl EventEmitter<Event> for Copilot {}
-
-impl Copilot {
- pub fn global(cx: &AppContext) -> Option<Model<Self>> {
- if cx.has_global::<Model<Self>>() {
- Some(cx.global::<Model<Self>>().clone())
- } else {
- None
- }
- }
-
- fn start(
- new_server_id: LanguageServerId,
- http: Arc<dyn HttpClient>,
- node_runtime: Arc<dyn NodeRuntime>,
- cx: &mut ModelContext<Self>,
- ) -> 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::<SettingsStore>(move |this, cx| this.enable_or_disable_copilot(cx))
- .detach();
- this
- }
-
- fn shutdown_language_server(
- &mut self,
- _cx: &mut ModelContext<Self>,
- ) -> impl Future<Output = ()> {
- 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<Self>) {
- 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<Self>, 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<dyn HttpClient>,
- node_runtime: Arc<dyn NodeRuntime>,
- this: WeakModel<Self>,
- mut cx: AsyncAppContext,
- ) -> impl Future<Output = ()> {
- 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<OsString> = 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::<StatusNotification, _>(
- |_, _| { /* Silence the notification */ },
- )
- .detach();
-
- let server = server.initialize(Default::default()).await?;
-
- let status = server
- .request::<request::CheckStatus>(request::CheckStatusParams {
- local_checks_only: false,
- })
- .await?;
-
- server
- .request::<request::SetEditorInfo>(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<Self>) -> Task<Result<()>> {
- 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::SignInInitiate>(
- 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::SignInConfirm>(
- 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<Self>) -> Task<Result<()>> {
- 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::SignOut>(request::SignOutParams {})
- .await?;
- anyhow::Ok(())
- })
- } else {
- Task::ready(Err(anyhow!("copilot hasn't started yet")))
- }
- }
-
- pub fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> 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<LanguageServer>)> {
- if let CopilotServer::Running(server) = &self.server {
- Some((&server.name, &server.lsp))
- } else {
- None
- }
- }
-
- pub fn register_buffer(&mut self, buffer: &Model<Buffer>, cx: &mut ModelContext<Self>) {
- 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::notification::DidOpenTextDocument>(
- 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<Buffer>,
- event: &language::Event,
- cx: &mut ModelContext<Self>,
- ) -> 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::notification::DidSaveTextDocument>(
- 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::notification::DidCloseTextDocument>(
- lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(old_uri),
- },
- )?;
- server
- .lsp
- .notify::<lsp::notification::DidOpenTextDocument>(
- 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<Buffer>) {
- if let Ok(server) = self.server.as_running() {
- if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) {
- server
- .lsp
- .notify::<lsp::notification::DidCloseTextDocument>(
- lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
- },
- )
- .log_err();
- }
- }
- }
-
- pub fn completions<T>(
- &mut self,
- buffer: &Model<Buffer>,
- position: T,
- cx: &mut ModelContext<Self>,
- ) -> Task<Result<Vec<Completion>>>
- where
- T: ToPointUtf16,
- {
- self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
- }
-
- pub fn completions_cycling<T>(
- &mut self,
- buffer: &Model<Buffer>,
- position: T,
- cx: &mut ModelContext<Self>,
- ) -> Task<Result<Vec<Completion>>>
- where
- T: ToPointUtf16,
- {
- self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
- }
-
- pub fn accept_completion(
- &mut self,
- completion: &Completion,
- cx: &mut ModelContext<Self>,
- ) -> Task<Result<()>> {
- let server = match self.server.as_authenticated() {
- Ok(server) => server,
- Err(error) => return Task::ready(Err(error)),
- };
- let request =
- server
- .lsp
- .request::<request::NotifyAccepted>(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<Self>,
- ) -> Task<Result<()>> {
- let server = match self.server.as_authenticated() {
- Ok(server) => server,
- Err(error) => return Task::ready(Err(error)),
- };
- let request =
- server
- .lsp
- .request::<request::NotifyRejected>(request::NotifyRejectedParams {
- uuids: completions
- .iter()
- .map(|completion| completion.uuid.clone())
- .collect(),
- });
- cx.background_executor().spawn(async move {
- request.await?;
- Ok(())
- })
- }
-
- fn request_completions<R, T>(
- &mut self,
- buffer: &Model<Buffer>,
- position: T,
- cx: &mut ModelContext<Self>,
- ) -> Task<Result<Vec<Completion>>>
- 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::<R>(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>,
- ) {
- 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::<Vec<_>>() {
- 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::<Vec<_>>() {
- self.unregister_buffer(&buffer);
- }
- }
- request::SignInStatus::NotSignedIn => {
- server.sign_in_status = SignInStatus::SignedOut;
- for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
- self.unregister_buffer(&buffer);
- }
- }
- }
-
- cx.notify();
- }
- }
-}
-
-fn id_for_language(language: Option<&Arc<Language>>) -> 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<Buffer>, 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<dyn HttpClient>) -> anyhow::Result<PathBuf> {
- 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<dyn HttpClient>) -> anyhow::Result<PathBuf> {
- 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::<lsp::notification::DidOpenTextDocument>()
- .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::<lsp::notification::DidOpenTextDocument>()
- .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::<lsp::notification::DidChangeTextDocument>()
- .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::<lsp::notification::DidCloseTextDocument>()
- .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::<lsp::notification::DidOpenTextDocument>()
- .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::<request::SignOut, _, _>(|_, _| async {
- Ok(request::SignOutResult {})
- });
- copilot
- .update(cx, |copilot, cx| copilot.sign_out(cx))
- .await
- .unwrap();
- assert_eq!(
- lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
- .await,
- lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
- }
- );
- assert_eq!(
- lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
- .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::<request::SignInInitiate, _, _>(|_, _| 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::<lsp::notification::DidOpenTextDocument>()
- .await,
- lsp::DidOpenTextDocumentParams {
- text_document: lsp::TextDocumentItem::new(
- buffer_1_uri.clone(),
- "plaintext".into(),
- 0,
- "Hello world".into()
- ),
- }
- );
- assert_eq!(
- lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
- .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::<lsp::notification::DidCloseTextDocument>()
- .await,
- lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
- }
- );
- }
-
- struct File {
- abs_path: PathBuf,
- path: Arc<Path>,
- }
-
- 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<Path> {
- &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<Result<String>> {
- unimplemented!()
- }
-
- fn buffer_reloaded(
- &self,
- _: u64,
- _: &clock::Global,
- _: language::RopeFingerprint,
- _: language::LineEnding,
- _: std::time::SystemTime,
- _: &mut AppContext,
- ) {
- unimplemented!()
- }
- }
-}
@@ -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<Completion>,
-}
-
-#[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<String>,
-}
-
-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<String>,
-}
-
-impl lsp::request::Request for NotifyRejected {
- type Params = NotifyRejectedParams;
- type Result = String;
- const METHOD: &'static str = "notifyRejected";
-}
@@ -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<WindowHandle<CopilotCodeVerification>> = 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<CopilotCodeVerification> {
- 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>) {
- self.status = status;
- cx.notify();
- }
-
- fn render_device_code(
- data: &PromptUserDeviceFlow,
- cx: &mut ViewContext<Self>,
- ) -> 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<Self>,
- ) -> 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<Self>) -> 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)
- }
-}
@@ -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"}
@@ -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
@@ -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::<f32>::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::<f32>::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::<f32>::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::<f32>::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::<f32>::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::<f32>::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::<Vec<_>>(),
- snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
- );
- assert_set_eq!(
- cloned_editor
- .update(cx, |editor, cx| editor.selections.ranges::<Point>(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<NavigationEntry> {
- 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::<f32>::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::<f32>::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::<f32>::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::<f32>::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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<Point>(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::<crate::EditorEvent>(&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::<crate::EditorEvent>(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#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .unindent(),
- );
-
- // Precondition: different languages are active at different locations.
- cx.update_editor(|editor, cx| {
- let snapshot = editor.snapshot(cx);
- let cursors = editor.selections.ranges::<usize>(cx);
- let languages = cursors
- .iter()
- .map(|c| snapshot.language_at(c.start).unwrap().name())
- .collect::<Vec<_>>();
- 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#"
- <body><aˇ>
- <script>
- var x = 1;<aˇ
- </script>
- </body><aˇ>
- "#
- .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#"
- <body><a b={c(ˇ)}>
- <script>
- var x = 1;<a b={c(ˇ)}
- </script>
- </body><a b={c(ˇ)}>
- "#
- .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#"
- <body><a b={c()d}ˇ>
- <script>
- var x = 1;<a b={c()d}ˇ
- </script>
- </body><a b={c()d}ˇ>
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| {
- editor.handle_input(">", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><a b={c()d}>ˇ
- <script>
- var x = 1;<a b={c()d}>ˇ
- </script>
- </body><a b={c()d}>ˇ
- "#
- .unindent(),
- );
-
- // Reset
- cx.set_state(
- &r#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .unindent(),
- );
-
- cx.update_editor(|editor, cx| {
- editor.handle_input("<", cx);
- });
- cx.assert_editor_state(
- &r#"
- <body><ˇ>
- <script>
- var x = 1;<ˇ
- </script>
- </body><ˇ>
- "#
- .unindent(),
- );
-
- // When backspacing, the closing angle brackets are removed.
- cx.update_editor(|editor, cx| {
- editor.backspace(&Backspace, cx);
- });
- cx.assert_editor_state(
- &r#"
- <body>ˇ
- <script>
- var x = 1;ˇ
- </script>
- </body>ˇ
- "#
- .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#"
- <body>/*ˇ
- <script>
- var x = 1;/*ˇ */
- </script>
- </body>/*ˇ
- "#
- .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::<crate::EditorEvent>(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::<crate::EditorEvent>(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::<Point>(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::<Point>(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::<Point>(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<Editor>, 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::<usize>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::RangeFormatting, _, _>(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::<lsp::request::RangeFormatting, _, _>(
- 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::<lsp::request::RangeFormatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::request::Formatting, _, _>(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::<lsp::notification::DidChangeTextDocument, _>({
- 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::<lsp::request::Formatting, _, _>({
- 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 <s|>
- 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 <si|>
- 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::<SettingsStore, _>(|settings, cx| {
- settings.update_user_settings::<EditorSettings>(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.<clo|>", 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(), " -->".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#"
- <p>A</p>ˇ
- <p>B</p>ˇ
- <p>C</p>ˇ
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <!-- <p>A</p>ˇ -->
- <!-- <p>B</p>ˇ -->
- <!-- <p>C</p>ˇ -->
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <p>A</p>ˇ
- <p>B</p>ˇ
- <p>C</p>ˇ
- "#
- .unindent(),
- );
-
- // Toggle comments for mixture of empty and non-empty selections, where
- // multiple selections occupy a given line.
- cx.set_state(
- &r#"
- <p>A«</p>
- <p>ˇ»B</p>ˇ
- <p>C«</p>
- <p>ˇ»D</p>ˇ
- "#
- .unindent(),
- );
-
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <!-- <p>A«</p>
- <p>ˇ»B</p>ˇ -->
- <!-- <p>C«</p>
- <p>ˇ»D</p>ˇ -->
- "#
- .unindent(),
- );
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <p>A«</p>
- <p>ˇ»B</p>ˇ
- <p>C«</p>
- <p>ˇ»D</p>ˇ
- "#
- .unindent(),
- );
-
- // Toggle comments when different languages are active for different
- // selections.
- cx.set_state(
- &r#"
- ˇ<script>
- ˇvar x = new Y();
- ˇ</script>
- "#
- .unindent(),
- );
- cx.executor().run_until_parked();
- cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
- cx.assert_editor_state(
- &r#"
- <!-- ˇ<script> -->
- // ˇvar x = new Y();
- <!-- ˇ</script> -->
- "#
- .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::<crate::EditorEvent>(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<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
-
- editor.highlight_background::<Type1>(
- 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::<Type2>(
- 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::<copilot::request::GetCompletions, _, _>(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::<Editor>()
- .unwrap();
-
- fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|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<str> = "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::<lsp::request::Shutdown, _, _>(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::<lsp::request::Completion, _, _>(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::<lsp::request::ResolveCompletionItem, _, _>(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::<lsp::request::Completion, _, _>(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#"<p class="bgˇ" />"#);
-
- // 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::<Vec<_>>(),
- &["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::<Vec<_>>(),
- &["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#"<p class="yelˇ" />"#);
- 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::<Vec<_>>(),
- &["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<DisplayPoint> {
- 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<Editor>) {
- 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<Output = ()> {
- 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::<lsp::request::Completion, _, _>(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<Vec<(&'static str, &'static str)>>,
-) -> impl Future<Output = ()> {
- 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::<Vec<_>>()
- });
-
- let mut request =
- cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(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<copilot::request::Completion>,
- completions_cycling: Vec<copilot::request::Completion>,
-) {
- lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
- let completions = completions.clone();
- async move {
- Ok(copilot::request::GetCompletionsResult {
- completions: completions.clone(),
- })
- }
- });
- lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(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::<AllLanguageSettings>(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::<ProjectSettings>(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);
-}
@@ -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
@@ -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>,
+ buffer: Model<Buffer>,
predecessor: ExcerptId,
excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
},
@@ -119,7 +122,7 @@ pub trait ToPointUtf16: 'static + fmt::Debug {
}
struct BufferState {
- buffer: ModelHandle<Buffer>,
+ buffer: Model<Buffer>,
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<Buffer>, cx: &mut ModelContext<Self>) -> Self {
+ pub fn singleton(buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> 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<ModelHandle<Buffer>> {
+ pub fn as_singleton(&self) -> Option<Model<Buffer>> {
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<Self>)
where
- T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
+ T: IntoIterator<Item = (&'a Model<Buffer>, &'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>,
+ buffer: Model<Buffer>,
ranges: Vec<Range<text::Anchor>>,
context_line_count: u32,
cx: &mut ModelContext<Self>,
) -> mpsc::Receiver<Range<Anchor>> {
- 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<O>(
&mut self,
- buffer: ModelHandle<Buffer>,
+ buffer: Model<Buffer>,
ranges: impl IntoIterator<Item = ExcerptRange<O>>,
cx: &mut ModelContext<Self>,
) -> Vec<ExcerptId>
@@ -932,7 +938,7 @@ impl MultiBuffer {
pub fn push_excerpts_with_context_lines<O>(
&mut self,
- buffer: ModelHandle<Buffer>,
+ buffer: Model<Buffer>,
ranges: Vec<Range<O>>,
context_line_count: u32,
cx: &mut ModelContext<Self>,
@@ -970,7 +976,7 @@ impl MultiBuffer {
pub fn insert_excerpts_after<O>(
&mut self,
prev_excerpt_id: ExcerptId,
- buffer: ModelHandle<Buffer>,
+ buffer: Model<Buffer>,
ranges: impl IntoIterator<Item = ExcerptRange<O>>,
cx: &mut ModelContext<Self>,
) -> Vec<ExcerptId>
@@ -995,7 +1001,7 @@ impl MultiBuffer {
pub fn insert_excerpts_with_ids_after<O>(
&mut self,
prev_excerpt_id: ExcerptId,
- buffer: ModelHandle<Buffer>,
+ buffer: Model<Buffer>,
ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
cx: &mut ModelContext<Self>,
) where
@@ -1132,7 +1138,7 @@ impl MultiBuffer {
pub fn excerpts_for_buffer(
&self,
- buffer: &ModelHandle<Buffer>,
+ buffer: &Model<Buffer>,
cx: &AppContext,
) -> Vec<(ExcerptId, ExcerptRange<text::Anchor>)> {
let mut excerpts = Vec::new();
@@ -1169,7 +1175,7 @@ impl MultiBuffer {
&self,
position: impl ToOffset,
cx: &AppContext,
- ) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
+ ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
let snapshot = self.read(cx);
let position = position.to_offset(&snapshot);
@@ -1197,7 +1203,7 @@ impl MultiBuffer {
&self,
point: T,
cx: &AppContext,
- ) -> Option<(ModelHandle<Buffer>, usize, ExcerptId)> {
+ ) -> Option<(Model<Buffer>, usize, ExcerptId)> {
let snapshot = self.read(cx);
let offset = point.to_offset(&snapshot);
let mut cursor = snapshot.excerpts.cursor::<usize>();
@@ -1219,7 +1225,7 @@ impl MultiBuffer {
&self,
range: Range<T>,
cx: &AppContext,
- ) -> Vec<(ModelHandle<Buffer>, Range<usize>, ExcerptId)> {
+ ) -> Vec<(Model<Buffer>, Range<usize>, 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<Buffer>, language::Anchor)> {
+ ) -> Option<(Model<Buffer>, 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<Buffer>,
+ _: Model<Buffer>,
event: &language::Event,
cx: &mut ModelContext<Self>,
) {
@@ -1414,7 +1420,7 @@ impl MultiBuffer {
});
}
- pub fn all_buffers(&self) -> HashSet<ModelHandle<Buffer>> {
+ pub fn all_buffers(&self) -> HashSet<Model<Buffer>> {
self.buffers
.borrow()
.values()
@@ -1422,7 +1428,7 @@ impl MultiBuffer {
.collect()
}
- pub fn buffer(&self, buffer_id: u64) -> Option<ModelHandle<Buffer>> {
+ pub fn buffer(&self, buffer_id: u64) -> Option<Model<Buffer>> {
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<Buffer>)) {
+ pub fn for_each_buffer(&self, mut f: impl FnMut(&Model<Buffer>)) {
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<Self> {
- 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<Self> {
+ 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<const COUNT: usize>(
excerpts: [(&str, Vec<Range<Point>>); COUNT],
cx: &mut gpui::AppContext,
- ) -> ModelHandle<Self> {
- let multi = cx.add_model(|_| Self::new(0));
+ ) -> Model<Self> {
+ 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<Buffer>,
- cx: &mut gpui::AppContext,
- ) -> ModelHandle<Self> {
- cx.add_model(|cx| Self::singleton(buffer, cx))
+ pub fn build_from_buffer(buffer: Model<Buffer>, cx: &mut gpui::AppContext) -> Model<Self> {
+ cx.new_model(|cx| Self::singleton(buffer, cx))
}
- pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> ModelHandle<Self> {
- cx.add_model(|cx| {
+ pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model<Self> {
+ 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::<String>();
- 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<Event> for MultiBuffer {}
impl MultiBufferSnapshot {
pub fn text(&self) -> String {
@@ -3405,7 +3406,7 @@ impl History {
now: Instant,
cx: &mut ModelContext<MultiBuffer>,
) where
- T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
+ T: IntoIterator<Item = (&'a Model<Buffer>, &'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::<Event>::new()));
+ let events = Arc::new(RwLock::new(Vec::<Event>::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::<Vec<_>>().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<ModelHandle<Buffer>> = Vec::new();
- let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+ let mut buffers: Vec<Model<Buffer>> = Vec::new();
+ let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
let mut excerpt_ids = Vec::<ExcerptId>::new();
- let mut expected_excerpts = Vec::<(ModelHandle<Buffer>, Range<text::Anchor>)>::new();
+ let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::new();
let mut anchors = Vec::new();
let mut old_versions = Vec::new();
@@ -4918,7 +4921,7 @@ mod tests {
.take(10)
.collect::<String>();
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(
@@ -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
@@ -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<u64>,
- 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<D>(&self, snapshot: &MultiBufferSnapshot) -> D
- where
- D: TextDimension + Ord + Sub<D, Output = D>,
- {
- 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<Anchor>, buffer: &MultiBufferSnapshot) -> Ordering;
- fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize>;
- fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point>;
-}
-
-impl AnchorRangeExt for Range<Anchor> {
- fn cmp(&self, other: &Range<Anchor>, 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<usize> {
- self.start.to_offset(content)..self.end.to_offset(content)
- }
-
- fn to_point(&self, content: &MultiBufferSnapshot) -> Range<Point> {
- self.start.to_point(content)..self.end.to_point(content)
- }
-}
@@ -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<MultiBufferSnapshot>,
- buffers: RefCell<HashMap<u64, BufferState>>,
- next_excerpt_id: usize,
- subscriptions: Topic,
- singleton: bool,
- replica_id: ReplicaId,
- history: History,
- title: Option<String>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Event {
- ExcerptsAdded {
- buffer: Model<Buffer>,
- predecessor: ExcerptId,
- excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
- },
- ExcerptsRemoved {
- ids: Vec<ExcerptId>,
- },
- ExcerptsEdited {
- ids: Vec<ExcerptId>,
- },
- 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<Transaction>,
- redo_stack: Vec<Transaction>,
- transaction_depth: usize,
- group_interval: Duration,
-}
-
-#[derive(Clone)]
-struct Transaction {
- id: TransactionId,
- buffer_transactions: HashMap<u64, text::TransactionId>,
- 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<Buffer>,
- 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<Locator>,
- _subscriptions: [gpui::Subscription; 2],
-}
-
-#[derive(Clone, Default)]
-pub struct MultiBufferSnapshot {
- singleton: bool,
- excerpts: SumTree<Excerpt>,
- excerpt_ids: SumTree<ExcerptIdMapping>,
- 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<text::Anchor>,
- pub starts_new_buffer: bool,
-}
-
-#[derive(Clone)]
-struct Excerpt {
- id: ExcerptId,
- locator: Locator,
- buffer_id: u64,
- buffer: BufferSnapshot,
- range: ExcerptRange<text::Anchor>,
- 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<T> {
- pub context: Range<T>,
- pub primary: Option<Range<T>>,
-}
-
-#[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<u32>,
- excerpts: Cursor<'a, Excerpt, Point>,
-}
-
-pub struct MultiBufferChunks<'a> {
- range: Range<usize>,
- excerpts: Cursor<'a, Excerpt, usize>,
- excerpt_chunks: Option<ExcerptChunks<'a>>,
- language_aware: bool,
-}
-
-pub struct MultiBufferBytes<'a> {
- range: Range<usize>,
- excerpts: Cursor<'a, Excerpt, usize>,
- excerpt_bytes: Option<ExcerptBytes<'a>>,
- chunk: &'a [u8],
-}
-
-pub struct ReversedMultiBufferBytes<'a> {
- range: Range<usize>,
- excerpts: Cursor<'a, Excerpt, usize>,
- excerpt_bytes: Option<ExcerptBytes<'a>>,
- 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>) -> 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<Buffer>, cx: &mut ModelContext<Self>) -> 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<MultiBufferSnapshot> {
- self.sync(cx);
- self.snapshot.borrow()
- }
-
- pub fn as_singleton(&self) -> Option<Model<Buffer>> {
- 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<T: ToOffset>(
- &self,
- offset: T,
- theme: Option<&SyntaxTheme>,
- cx: &AppContext,
- ) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
- self.read(cx).symbols_containing(offset, theme)
- }
-
- pub fn edit<I, S, T>(
- &mut self,
- edits: I,
- mut autoindent_mode: Option<AutoindentMode>,
- cx: &mut ModelContext<Self>,
- ) where
- I: IntoIterator<Item = (Range<S>, T)>,
- S: ToOffset,
- T: Into<Arc<str>>,
- {
- 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<usize>,
- new_text: Arc<str>,
- is_insertion: bool,
- original_indent_column: u32,
- }
- let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
- let mut edited_excerpt_ids = Vec::new();
- let mut cursor = snapshot.excerpts.cursor::<usize>();
- for (ix, (range, new_text)) in edits.enumerate() {
- let new_text: Arc<str> = 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<u64, Vec<BufferEdit>>,
- autoindent_mode: Option<AutoindentMode>,
- edited_excerpt_ids: Vec<ExcerptId>,
- cx: &mut ModelContext<MultiBuffer>,
- ) {
- 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<str> = "".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<Self>) -> Option<TransactionId> {
- self.start_transaction_at(Instant::now(), cx)
- }
-
- pub fn start_transaction_at(
- &mut self,
- now: Instant,
- cx: &mut ModelContext<Self>,
- ) -> Option<TransactionId> {
- 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<Self>) -> Option<TransactionId> {
- self.end_transaction_at(Instant::now(), cx)
- }
-
- pub fn end_transaction_at(
- &mut self,
- now: Instant,
- cx: &mut ModelContext<Self>,
- ) -> Option<TransactionId> {
- 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<Self>,
- ) {
- 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>) {
- 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<Self>)
- where
- T: IntoIterator<Item = (&'a Model<Buffer>, &'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<Self>,
- ) {
- 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<Anchor>],
- line_mode: bool,
- cursor_shape: CursorShape,
- cx: &mut ModelContext<Self>,
- ) {
- let mut selections_by_buffer: HashMap<u64, Vec<Selection<text::Anchor>>> =
- Default::default();
- let snapshot = self.read(cx);
- let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
- 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<Self>) {
- 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<Self>) -> Option<TransactionId> {
- 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<Self>) -> Option<TransactionId> {
- 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<Self>) {
- 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<Buffer>,
- ranges: Vec<Range<text::Anchor>>,
- context_line_count: u32,
- cx: &mut ModelContext<Self>,
- ) -> mpsc::Receiver<Range<Anchor>> {
- 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<O>(
- &mut self,
- buffer: Model<Buffer>,
- ranges: impl IntoIterator<Item = ExcerptRange<O>>,
- cx: &mut ModelContext<Self>,
- ) -> Vec<ExcerptId>
- where
- O: text::ToOffset,
- {
- self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx)
- }
-
- pub fn push_excerpts_with_context_lines<O>(
- &mut self,
- buffer: Model<Buffer>,
- ranges: Vec<Range<O>>,
- context_line_count: u32,
- cx: &mut ModelContext<Self>,
- ) -> Vec<Range<Anchor>>
- 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<O>(
- &mut self,
- prev_excerpt_id: ExcerptId,
- buffer: Model<Buffer>,
- ranges: impl IntoIterator<Item = ExcerptRange<O>>,
- cx: &mut ModelContext<Self>,
- ) -> Vec<ExcerptId>
- 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<O>(
- &mut self,
- prev_excerpt_id: ExcerptId,
- buffer: Model<Buffer>,
- ranges: impl IntoIterator<Item = (ExcerptId, ExcerptRange<O>)>,
- cx: &mut ModelContext<Self>,
- ) 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::<Option<&Locator>>();
- 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>) {
- 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<Buffer>,
- cx: &AppContext,
- ) -> Vec<(ExcerptId, ExcerptRange<text::Anchor>)> {
- let mut excerpts = Vec::new();
- let snapshot = self.read(cx);
- let buffers = self.buffers.borrow();
- let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
- 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<ExcerptId> {
- self.snapshot
- .borrow()
- .excerpts
- .iter()
- .map(|entry| entry.id)
- .collect()
- }
-
- pub fn excerpt_containing(
- &self,
- position: impl ToOffset,
- cx: &AppContext,
- ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
- let snapshot = self.read(cx);
- let position = position.to_offset(&snapshot);
-
- let mut cursor = snapshot.excerpts.cursor::<usize>();
- 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<T: ToOffset>(
- &self,
- point: T,
- cx: &AppContext,
- ) -> Option<(Model<Buffer>, usize, ExcerptId)> {
- let snapshot = self.read(cx);
- let offset = point.to_offset(&snapshot);
- let mut cursor = snapshot.excerpts.cursor::<usize>();
- 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<T: ToOffset>(
- &self,
- range: Range<T>,
- cx: &AppContext,
- ) -> Vec<(Model<Buffer>, Range<usize>, 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::<usize>();
- 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<Item = ExcerptId>,
- cx: &mut ModelContext<Self>,
- ) {
- self.sync(cx);
- let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
- 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<Item = Anchor>,
- cx: &mut ModelContext<Self>,
- ) -> impl 'static + Future<Output = Result<()>> {
- 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<T: ToOffset>(
- &self,
- position: T,
- cx: &AppContext,
- ) -> Option<(Model<Buffer>, 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<Buffer>,
- event: &language::Event,
- cx: &mut ModelContext<Self>,
- ) {
- 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<Model<Buffer>> {
- self.buffers
- .borrow()
- .values()
- .map(|state| state.buffer.clone())
- .collect()
- }
-
- pub fn buffer(&self, buffer_id: u64) -> Option<Model<Buffer>> {
- 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<Arc<Language>> {
- 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<Buffer>)) {
- 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::<usize>(
- 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<Self> {
- 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<const COUNT: usize>(
- excerpts: [(&str, Vec<Range<Point>>); COUNT],
- cx: &mut gpui::AppContext,
- ) -> Model<Self> {
- 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<Buffer>, cx: &mut gpui::AppContext) -> Model<Self> {
- cx.new_model(|cx| Self::singleton(buffer, cx))
- }
-
- pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model<Self> {
- 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<Self>,
- ) {
- use util::RandomCharIter;
-
- let snapshot = self.read(cx);
- let mut edits: Vec<(Range<usize>, Arc<str>)> = 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<Self>,
- ) {
- 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::<String>();
- 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::<Vec<_>>();
- log::info!(
- "Inserting excerpts from buffer {} and ranges {:?}: {:?}",
- buffer_handle.read(cx).remote_id(),
- ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
- ranges
- .iter()
- .map(|r| &buffer_text[r.context.clone()])
- .collect::<Vec<_>>()
- );
-
- 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::<Vec<_>>();
- 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<Self>,
- ) {
- 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<Self>) {
- 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<Event> 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<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + '_ {
- let mut offset = position.to_offset(self);
- let mut cursor = self.excerpts.cursor::<usize>();
- 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<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + '_ {
- let offset = position.to_offset(self);
- self.text_for_range(offset..self.len())
- .flat_map(|chunk| chunk.chars())
- }
-
- pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> impl Iterator<Item = &str> + '_ {
- 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<T>(&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<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
- 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::<usize>();
- 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::<Point>();
- 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::<OffsetUtf16>();
- 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<PointUtf16>, bias: Bias) -> PointUtf16 {
- if let Some((_, _, buffer)) = self.as_singleton() {
- return buffer.clip_point_utf16(point, bias);
- }
-
- let mut cursor = self.excerpts.cursor::<PointUtf16>();
- 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<T: ToOffset>(&self, range: Range<T>) -> MultiBufferBytes {
- let range = range.start.to_offset(self)..range.end.to_offset(self);
- let mut excerpts = self.excerpts.cursor::<usize>();
- 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<T: ToOffset>(
- &self,
- range: Range<T>,
- ) -> ReversedMultiBufferBytes {
- let range = range.start.to_offset(self)..range.end.to_offset(self);
- let mut excerpts = self.excerpts.cursor::<usize>();
- 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<T: ToOffset>(&self, range: Range<T>, 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<T: ToOffset>(
- &self,
- point: T,
- ) -> Option<(&BufferSnapshot, usize)> {
- let offset = point.to_offset(&self);
- let mut cursor = self.excerpts.cursor::<usize>();
- 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<Item = u32>,
- cx: &AppContext,
- ) -> BTreeMap<u32, IndentSize> {
- let mut result = BTreeMap::new();
-
- let mut rows_for_excerpt = Vec::new();
- let mut cursor = self.excerpts.cursor::<Point>();
- 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<u32> {
- 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<Point>)> {
- let mut cursor = self.excerpts.cursor::<Point>();
- 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<D, O>(&self, range: Range<O>) -> 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::<usize>();
- 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<D>(&self, anchor: &Anchor) -> D
- where
- D: TextDimension + Ord + Sub<D, Output = D>,
- {
- let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
- 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::<D>(&excerpt.buffer);
- let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer);
- let buffer_position = cmp::min(
- excerpt_buffer_end,
- anchor.text_anchor.summary::<D>(&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<D>
- where
- D: TextDimension + Ord + Sub<D, Output = D>,
- I: 'a + IntoIterator<Item = &'a Anchor>,
- {
- 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::<ExcerptSummary>();
- 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::<D>(&excerpt.buffer);
- let excerpt_buffer_end =
- excerpt.range.context.end.summary::<D>(&excerpt.buffer);
- summaries.extend(
- excerpt
- .buffer
- .summaries_for_anchors::<D, _>(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<Item = &'a Anchor>,
- {
- let mut anchors = anchors.into_iter().enumerate().peekable();
- let mut cursor = self.excerpts.cursor::<Option<&Locator>>();
- 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<T: ToOffset>(&self, position: T) -> Anchor {
- self.anchor_at(position, Bias::Left)
- }
-
- pub fn anchor_after<T: ToOffset>(&self, position: T) -> Anchor {
- self.anchor_at(position, Bias::Right)
- }
-
- pub fn anchor_at<T: ToOffset>(&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<ExcerptId>)>();
- 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::<Option<&Locator>>();
- 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<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<text::Anchor>)> {
- self.excerpts
- .iter()
- .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone()))
- }
-
- pub fn excerpt_boundaries_in_range<R, T>(
- &self,
- range: R,
- ) -> impl Iterator<Item = ExcerptBoundary> + '_
- where
- R: RangeBounds<T>,
- 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<T: ToOffset>(
- &self,
- range: Range<T>,
- ) -> Option<(Range<usize>, Range<usize>)> {
- 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<usize>, Range<usize>)> = 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<T>,
- ) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + '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<T>,
- ) -> Option<impl Iterator<Item = (Range<usize>, Range<usize>)> + '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<dyn File>> {
- 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<Language>> {
- 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<LanguageScope> {
- self.point_to_buffer_offset(point)
- .and_then(|(buffer, offset)| buffer.language_scope_at(offset))
- }
-
- pub fn language_indent_size_at<T: ToOffset>(
- &self,
- position: T,
- cx: &AppContext,
- ) -> Option<IndentSize> {
- 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<Item = DiagnosticEntry<O>> + '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<T>,
- reversed: bool,
- ) -> impl Iterator<Item = DiagnosticEntry<O>> + '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<u32>,
- ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
- let mut cursor = self.excerpts.cursor::<Point>();
-
- 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<u32>,
- ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
- let mut cursor = self.excerpts.cursor::<Point>();
-
- 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<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
- 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<Outline<Anchor>> {
- 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<T: ToOffset>(
- &self,
- offset: T,
- theme: Option<&SyntaxTheme>,
- ) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
- 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::<ExcerptId>();
- 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<u64> {
- 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::<Option<&Locator>>();
- 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<T>,
- ) -> Option<(&'a Excerpt, usize)> {
- let range = range.start.to_offset(self)..range.end.to_offset(self);
-
- let mut cursor = self.excerpts.cursor::<usize>();
- 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<Anchor>,
- ) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
- let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
- 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<usize> {
- 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<TransactionId> {
- 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<u64, TransactionId>,
- ) -> 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<MultiBuffer>,
- ) where
- T: IntoIterator<Item = (&'a Model<Buffer>, &'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<Transaction> {
- 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<TransactionId> {
- 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<TransactionId> {
- 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<text::Anchor>,
- 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::<TextSummary, _>(range.context.to_offset(&buffer)),
- buffer_id,
- buffer,
- range,
- has_trailing_newline,
- }
- }
-
- fn chunks_in_range(&self, range: Range<usize>, 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<usize>) -> 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<usize>) -> 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<usize> 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<ExcerptId> {
- 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<u32>;
-
- fn next(&mut self) -> Option<Self::Item> {
- 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<Self::Item> {
- 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<Self::Item> {
- 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<usize> {
- 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<usize> {
- 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<Self::Item> {
- 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<Self::Item> {
- 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<T>(
- buffer: &BufferSnapshot,
- ranges: &[Range<T>],
- context_line_count: u32,
-) -> (Vec<ExcerptRange<Point>>, Vec<usize>)
-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::<Vec<_>>(),
- (0..buffer.read(cx).row_count())
- .map(Some)
- .collect::<Vec<_>>()
- );
-
- 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::<Vec<_>>(),
- (0..buffer.read(cx).row_count())
- .map(Some)
- .collect::<Vec<_>>()
- );
- }
-
- #[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::<Event>::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::<Vec<_>>(),
- [Some(1), Some(2), Some(3), Some(4), Some(3)]
- );
- assert_eq!(
- snapshot.buffer_rows(2).collect::<Vec<_>>(),
- [Some(3), Some(4), Some(3)]
- );
- assert_eq!(snapshot.buffer_rows(4).collect::<Vec<_>>(), [Some(3)]);
- assert_eq!(snapshot.buffer_rows(5).collect::<Vec<_>>(), []);
-
- 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<Point>,
- 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::<String>(),
- boundary.starts_new_buffer,
- )
- })
- .collect::<Vec<_>>()
- }
- }
-
- #[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::<usize>(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<_>>(),
- 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::<Vec<_>>().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<_>>(),
- 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::<Vec<_>>(), &[Some(0)]);
- assert_eq!(snapshot.buffer_rows(1).collect::<Vec<_>>(), &[]);
- }
-
- #[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::<usize>(&snapshot_1.anchor_before(2)),
- 0
- );
- assert_eq!(
- snapshot_2.summaries_for_anchors::<usize, _>(&[
- 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::<usize, _>(&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::<Vec<_>>(),
- &[(0, true), (1, true), (2, true), (3, true)]
- );
- assert_eq!(
- snapshot_3.summaries_for_anchors::<usize, _>(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<Model<Buffer>> = Vec::new();
- let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
- let mut excerpt_ids = Vec::<ExcerptId>::new();
- let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::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::<String>(),
- );
- }
- 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::<String>();
- 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::<Vec<_>>(),
- 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::<Vec<_>>(),
- &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::<PointUtf16, _>(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::<String>();
- 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::<String>()
- })
- .collect::<Vec<_>>()
- .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::<TextSummary, _>(start_ix..end_ix),
- expected_summary,
- "incorrect summary for range {:?}",
- start_ix..end_ix
- );
- }
-
- // Anchor resolution
- let summaries = snapshot.summaries_for_anchors::<usize, _>(&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::<usize>(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::<String>(),
- expected_text[..end_ix].chars().rev().collect::<String>(),
- );
- }
-
- 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::<Vec<_>>(),
- 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");
- });
- }
-}
@@ -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
@@ -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<Workspace>) {
- if let Some(editor) = workspace
- .active_item(cx)
- .and_then(|item| item.downcast::<Editor>())
- {
- 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<Editor>, _: &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<Picker<OutlineViewDelegate>>,
+}
+
+impl FocusableView for OutlineView {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for OutlineView {}
+impl ModalView for OutlineView {}
+
+impl Render for OutlineView {
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+ v_stack().w(rems(34.)).child(self.picker.clone())
+ }
+}
+
+impl OutlineView {
+ fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+ 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);
+ }
});
}
}
-}
-type OutlineView = Picker<OutlineViewDelegate>;
+ fn new(
+ outline: Outline<Anchor>,
+ editor: View<Editor>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Editor>,
+ outline_view: WeakView<OutlineView>,
+ active_editor: View<Editor>,
outline: Outline<Anchor>,
selected_match_index: usize,
- prev_scroll_position: Option<Vector2F>,
+ prev_scroll_position: Option<Point<f32>>,
matches: Vec<StringMatch>,
last_query: String,
}
impl OutlineViewDelegate {
fn new(
+ outline_view: WeakView<OutlineView>,
outline: Outline<Anchor>,
- editor: ViewHandle<Editor>,
+ editor: View<Editor>,
cx: &mut ViewContext<OutlineView>,
) -> 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<OutlineView>) {
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ navigate: bool,
+ cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+ ) {
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<str> {
"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<OutlineView>) {
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
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<OutlineView>) -> Task<()> {
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+ ) -> 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<OutlineView>) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
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<OutlineView>) {
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+ 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<Picker<Self>> {
- 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];
-
- 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()
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ 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),
+ ),
+ )
}
}
@@ -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"] }
@@ -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<Editor>, _: &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<Picker<OutlineViewDelegate>>,
-}
-
-impl FocusableView for OutlineView {
- fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.picker.focus_handle(cx)
- }
-}
-
-impl EventEmitter<DismissEvent> for OutlineView {}
-impl ModalView for OutlineView {}
-
-impl Render for OutlineView {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
- v_stack().w(rems(34.)).child(self.picker.clone())
- }
-}
-
-impl OutlineView {
- fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
- 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<Anchor>,
- editor: View<Editor>,
- cx: &mut ViewContext<Self>,
- ) -> 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<OutlineView>,
- active_editor: View<Editor>,
- outline: Outline<Anchor>,
- selected_match_index: usize,
- prev_scroll_position: Option<Point<f32>>,
- matches: Vec<StringMatch>,
- last_query: String,
-}
-
-impl OutlineViewDelegate {
- fn new(
- outline_view: WeakView<OutlineView>,
- outline: Outline<Anchor>,
- editor: View<Editor>,
- cx: &mut ViewContext<OutlineView>,
- ) -> 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<Picker<OutlineViewDelegate>>,
- ) {
- 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<str> {
- "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<Picker<OutlineViewDelegate>>) {
- self.set_selected_index(ix, true, cx);
- }
-
- fn update_matches(
- &mut self,
- query: String,
- cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
- ) -> 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::<usize>(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<Picker<OutlineViewDelegate>>) {
- 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<Picker<OutlineViewDelegate>>) {
- 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<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- 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),
- ),
- )
- }
-}
@@ -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" }
@@ -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" }