From 6d8db5f6bb68fe99bf08ae129a7c8e88a8f150ec Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Feb 2022 11:09:27 -0800 Subject: [PATCH 01/15] Convert some project tests to use FakeFs Also, tweak some FakeFs methods to make them slightly more convenient. --- crates/editor/src/editor.rs | 4 +- crates/project/src/fs.rs | 23 +++--- crates/project/src/project.rs | 118 ++++++++++++------------------ crates/project/src/worktree.rs | 2 +- crates/server/src/rpc.rs | 36 +++++---- crates/workspace/src/workspace.rs | 2 +- crates/zed/src/test.rs | 2 +- crates/zed/src/zed.rs | 18 ++--- 8 files changed, 89 insertions(+), 116 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cecf5108bb..037b1fea33 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7760,8 +7760,8 @@ mod tests { " .unindent(); - let fs = Arc::new(FakeFs::new(cx.background().clone())); - fs.insert_file("/file", text).await.unwrap(); + let fs = FakeFs::new(cx.background().clone()); + fs.insert_file("/file", text).await; let project = Project::test(fs, &mut cx); diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 7ba2076fcb..22a1e11ea3 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -7,6 +7,7 @@ use std::{ os::unix::fs::MetadataExt, path::{Path, PathBuf}, pin::Pin, + sync::Arc, time::{Duration, SystemTime}, }; use text::Rope; @@ -268,7 +269,7 @@ pub struct FakeFs { #[cfg(any(test, feature = "test-support"))] impl FakeFs { - pub fn new(executor: std::sync::Arc) -> Self { + pub fn new(executor: std::sync::Arc) -> Arc { let (events_tx, _) = postage::broadcast::channel(2048); let mut entries = std::collections::BTreeMap::new(); entries.insert( @@ -283,20 +284,20 @@ impl FakeFs { content: None, }, ); - Self { + Arc::new(Self { executor, state: futures::lock::Mutex::new(FakeFsState { entries, next_inode: 1, events_tx, }), - } + }) } - pub async fn insert_dir(&self, path: impl AsRef) -> Result<()> { + pub async fn insert_dir(&self, path: impl AsRef) { let mut state = self.state.lock().await; let path = path.as_ref(); - state.validate_path(path)?; + state.validate_path(path).unwrap(); let inode = state.next_inode; state.next_inode += 1; @@ -313,13 +314,12 @@ impl FakeFs { }, ); state.emit_event(&[path]).await; - Ok(()) } - pub async fn insert_file(&self, path: impl AsRef, content: String) -> Result<()> { + pub async fn insert_file(&self, path: impl AsRef, content: String) { let mut state = self.state.lock().await; let path = path.as_ref(); - state.validate_path(path)?; + state.validate_path(path).unwrap(); let inode = state.next_inode; state.next_inode += 1; @@ -336,7 +336,6 @@ impl FakeFs { }, ); state.emit_event(&[path]).await; - Ok(()) } #[must_use] @@ -353,7 +352,7 @@ impl FakeFs { match tree { Object(map) => { - self.insert_dir(path).await.unwrap(); + self.insert_dir(path).await; for (name, contents) in map { let mut path = PathBuf::from(path); path.push(name); @@ -361,10 +360,10 @@ impl FakeFs { } } Null => { - self.insert_dir(&path).await.unwrap(); + self.insert_dir(&path).await; } String(contents) => { - self.insert_file(&path, contents).await.unwrap(); + self.insert_file(&path, contents).await; } _ => { panic!("JSON object must contain only objects, strings, or null"); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 208604bd09..6dc7c2c231 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2980,13 +2980,11 @@ impl From for fs::RemoveOptions { #[cfg(test)] mod tests { use super::{Event, *}; - use client::test::FakeHttpClient; use fs::RealFs; use futures::StreamExt; use gpui::test::subscribe; use language::{ - tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageRegistry, - LanguageServerConfig, Point, + tree_sitter_rust, AnchorRangeExt, Diagnostic, LanguageConfig, LanguageServerConfig, Point, }; use lsp::Url; use serde_json::json; @@ -3066,8 +3064,7 @@ mod tests { .clone() .unwrap(); - let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -3075,30 +3072,26 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); - let dir = temp_tree(json!({ - "a.rs": "fn a() { A }", - "b.rs": "const y: i32 = 1", - })); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "fn a() { A }", + "b.rs": "const y: i32 = 1", + }), + ) + .await; - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - - let project = cx.update(|cx| { - Project::local( - client, - user_store, - Arc::new(languages), - Arc::new(RealFs), - cx, - ) + let project = Project::test(fs, &mut cx); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); }); let (tree, _) = project .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(dir.path(), false, cx) + project.find_or_create_local_worktree("/dir", false, cx) }) .await .unwrap(); @@ -3110,13 +3103,7 @@ mod tests { // Cause worktree to start the fake language server let _buffer = project .update(&mut cx, |project, cx| { - project.open_buffer( - ProjectPath { - worktree_id, - path: Path::new("b.rs").into(), - }, - cx, - ) + project.open_buffer((worktree_id, Path::new("b.rs")), cx) }) .await .unwrap(); @@ -3136,7 +3123,7 @@ mod tests { fake_server .notify::(lsp::PublishDiagnosticsParams { - uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(), + uri: Url::from_file_path("/dir/a.rs").unwrap(), version: None, diagnostics: vec![lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), @@ -3148,10 +3135,7 @@ mod tests { .await; assert_eq!( events.next().await.unwrap(), - Event::DiagnosticsUpdated(ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")) - }) + Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) ); fake_server.end_progress(&progress_token).await; @@ -3226,9 +3210,7 @@ mod tests { #[gpui::test] async fn test_definition(mut cx: gpui::TestAppContext) { let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - - let mut languages = LanguageRegistry::new(); - languages.add(Arc::new(Language::new( + let language = Arc::new(Language::new( LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], @@ -3236,30 +3218,26 @@ mod tests { ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + )); - let dir = temp_tree(json!({ - "a.rs": "const fn a() { A }", - "b.rs": "const y: i32 = crate::a()", - })); - let dir_path = dir.path().to_path_buf(); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.rs": "const fn a() { A }", + "b.rs": "const y: i32 = crate::a()", + }), + ) + .await; - let http_client = FakeHttpClient::with_404_response(); - let client = Client::new(http_client.clone()); - let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project = cx.update(|cx| { - Project::local( - client, - user_store, - Arc::new(languages), - Arc::new(RealFs), - cx, - ) + let project = Project::test(fs, &mut cx); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); }); let (tree, _) = project .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(dir.path().join("b.rs"), false, cx) + project.find_or_create_local_worktree("/dir/b.rs", false, cx) }) .await .unwrap(); @@ -3285,12 +3263,12 @@ mod tests { let params = params.text_document_position_params; assert_eq!( params.text_document.uri.to_file_path().unwrap(), - dir_path.join("b.rs") + Path::new("/dir/b.rs"), ); assert_eq!(params.position, lsp::Position::new(0, 22)); Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path(dir_path.join("a.rs")).unwrap(), + lsp::Url::from_file_path("/dir/a.rs").unwrap(), lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), ))) }); @@ -3311,15 +3289,12 @@ mod tests { .as_local() .unwrap() .abs_path(cx), - dir.path().join("a.rs") + Path::new("/dir/a.rs"), ); assert_eq!(definition.target_range.to_offset(target_buffer), 9..10); assert_eq!( list_worktrees(&project, cx), - [ - (dir.path().join("b.rs"), false), - (dir.path().join("a.rs"), true) - ] + [("/dir/b.rs".as_ref(), false), ("/dir/a.rs".as_ref(), true)] ); drop(definition); @@ -3327,18 +3302,21 @@ mod tests { cx.read(|cx| { assert_eq!( list_worktrees(&project, cx), - [(dir.path().join("b.rs"), false)] + [("/dir/b.rs".as_ref(), false)] ); }); - fn list_worktrees(project: &ModelHandle, cx: &AppContext) -> Vec<(PathBuf, bool)> { + fn list_worktrees<'a>( + project: &'a ModelHandle, + cx: &'a AppContext, + ) -> Vec<(&'a Path, bool)> { project .read(cx) .worktrees(cx) .map(|worktree| { let worktree = worktree.read(cx); ( - worktree.as_local().unwrap().abs_path().to_path_buf(), + worktree.as_local().unwrap().abs_path().as_ref(), worktree.is_weak(), ) }) @@ -3348,7 +3326,7 @@ mod tests { #[gpui::test] async fn test_save_file(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", json!({ @@ -3386,7 +3364,7 @@ mod tests { #[gpui::test] async fn test_save_in_single_file_worktree(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", json!({ @@ -3576,7 +3554,7 @@ mod tests { #[gpui::test] async fn test_buffer_deduping(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/the-dir", json!({ @@ -3865,7 +3843,7 @@ mod tests { #[gpui::test] async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) { - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/the-dir", json!({ diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 074781449d..89b93a0996 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2482,7 +2482,7 @@ mod tests { client, Arc::from(Path::new("/root")), false, - Arc::new(fs), + fs, &mut cx.to_async(), ) .await diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index adb0592df5..93b54cf88c 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1147,7 +1147,7 @@ mod tests { async fn test_share_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { let (window_b, _) = cx_b.add_window(|_| EmptyView); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. @@ -1285,7 +1285,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_unshare_project(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); // Connect to a server as 2 clients. @@ -1386,7 +1386,7 @@ mod tests { mut cx_c: TestAppContext, ) { let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); cx_a.foreground().forbid_parking(); // Connect to a server as 3 clients. @@ -1514,9 +1514,7 @@ mod tests { fs.rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default()) .await .unwrap(); - fs.insert_file(Path::new("/a/file4"), "4".into()) - .await - .unwrap(); + fs.insert_file(Path::new("/a/file4"), "4".into()).await; worktree_a .condition(&cx_a, |tree, _| { @@ -1565,7 +1563,7 @@ mod tests { async fn test_buffer_conflict_after_save(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1653,7 +1651,7 @@ mod tests { async fn test_buffer_reloading(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1738,7 +1736,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1820,7 +1818,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1895,7 +1893,7 @@ mod tests { async fn test_peer_disconnection(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -1969,7 +1967,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -2193,7 +2191,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. let (mut language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -2402,7 +2400,7 @@ mod tests { async fn test_formatting_buffer(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -2504,7 +2502,7 @@ mod tests { async fn test_definition(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", json!({ @@ -2657,7 +2655,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root", json!({ @@ -2766,7 +2764,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); let mut path_openers_b = Vec::new(); cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); @@ -3421,7 +3419,7 @@ mod tests { ) { cx_a.foreground().forbid_parking(); let lang_registry = Arc::new(LanguageRegistry::new()); - let fs = Arc::new(FakeFs::new(cx_a.background())); + let fs = FakeFs::new(cx_a.background()); // Connect to a server as 3 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3605,7 +3603,7 @@ mod tests { None, ))); - let fs = Arc::new(FakeFs::new(cx.background())); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/_collab", json!({ diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ebd7a9e26b..b3e750c261 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -492,7 +492,7 @@ pub struct WorkspaceParams { impl WorkspaceParams { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut MutableAppContext) -> Self { - let fs = Arc::new(project::FakeFs::new(cx.background().clone())); + let fs = project::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::new()); let http_client = client::test::FakeHttpClient::new(|_| async move { Ok(client::http::ServerResponse::new(404)) diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 5bf43b555f..77d5e28d92 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -42,7 +42,7 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)), client, user_store, - fs: Arc::new(FakeFs::new(cx.background().clone())), + fs: FakeFs::new(cx.background().clone()), path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 791c236ec6..4c98e0fa98 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -214,7 +214,7 @@ mod tests { }); let save_task = workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(cx)); - app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + app_state.fs.as_fake().insert_dir("/root").await; cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); save_task.await.unwrap(); editor.read_with(&cx, |editor, cx| { @@ -348,10 +348,10 @@ mod tests { async fn test_open_paths(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); let fs = app_state.fs.as_fake(); - fs.insert_dir("/dir1").await.unwrap(); - fs.insert_dir("/dir2").await.unwrap(); - fs.insert_file("/dir1/a.txt", "".into()).await.unwrap(); - fs.insert_file("/dir2/b.txt", "".into()).await.unwrap(); + fs.insert_dir("/dir1").await; + fs.insert_dir("/dir2").await; + fs.insert_file("/dir1/a.txt", "".into()).await; + fs.insert_file("/dir2/b.txt", "".into()).await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); @@ -456,9 +456,7 @@ mod tests { editor.handle_input(&editor::Input("x".into()), cx) }) }); - fs.insert_file("/root/a.txt", "changed".to_string()) - .await - .unwrap(); + fs.insert_file("/root/a.txt", "changed".to_string()).await; editor .condition(&cx, |editor, cx| editor.has_conflict(cx)) .await; @@ -476,7 +474,7 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); - app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + app_state.fs.as_fake().insert_dir("/root").await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); params @@ -576,7 +574,7 @@ mod tests { #[gpui::test] async fn test_setting_language_when_saving_as_single_file_worktree(mut cx: TestAppContext) { let app_state = cx.update(test_app_state); - app_state.fs.as_fake().insert_dir("/root").await.unwrap(); + app_state.fs.as_fake().insert_dir("/root").await; let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); From 54d76427124d2e79e824ba46ef8a3cb56fac49e4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Feb 2022 12:44:14 -0800 Subject: [PATCH 02/15] Start work on renames --- crates/editor/src/editor.rs | 252 ++++++++++++++----- crates/project/src/lsp_command.rs | 192 +++++++++++++++ crates/project/src/project.rs | 389 +++++++++++++++++++++--------- crates/rpc/proto/zed.proto | 28 +++ crates/rpc/src/proto.rs | 7 + 5 files changed, 683 insertions(+), 185 deletions(-) create mode 100644 crates/project/src/lsp_command.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 037b1fea33..b87e1d697b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -24,8 +24,9 @@ use gpui::{ geometry::vector::{vec2f, Vector2F}, keymap::Binding, platform::CursorStyle, - text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, - MutableAppContext, RenderContext, Task, View, ViewContext, WeakModelHandle, WeakViewHandle, + text_layout, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + WeakModelHandle, WeakViewHandle, }; use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; @@ -40,7 +41,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use postage::watch; -use project::Project; +use project::{Project, ProjectTransaction}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use smol::Timer; @@ -117,6 +118,8 @@ action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); action!(ShowNextDiagnostic); action!(GoToDefinition); +action!(Rename); +action!(ConfirmRename); action!(PageUp); action!(PageDown); action!(Fold); @@ -153,6 +156,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec= range.end - }) - { - return Ok(()); - } + async fn open_project_transaction( + this: ViewHandle, + workspace: ViewHandle, + transaction: ProjectTransaction, + title: String, + mut cx: AsyncAppContext, + ) -> Result<()> { + let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); + + // If the code action's edits are all contained within this editor, then + // avoid opening a new editor to display them. + let mut entries = transaction.0.iter(); + if let Some((buffer, transaction)) = entries.next() { + if entries.next().is_none() { + let excerpt = this.read_with(&cx, |editor, cx| { + editor + .buffer() + .read(cx) + .excerpt_containing(editor.newest_anchor_selection().head(), cx) + }); + if let Some((excerpted_buffer, excerpt_range)) = excerpt { + if excerpted_buffer == *buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + let excerpt_range = excerpt_range.to_offset(&snapshot); + if snapshot + .edited_ranges_for_transaction(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + { + return Ok(()); } } } } + } - let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); - for (buffer, transaction) in &project_transaction.0 { - let snapshot = buffer.read(cx).snapshot(); - ranges_to_highlight.extend( - multibuffer.push_excerpts_with_context_lines( - buffer.clone(), - snapshot - .edited_ranges_for_transaction::(transaction) - .collect(), - 1, - cx, - ), + let mut ranges_to_highlight = Vec::new(); + let excerpt_buffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(replica_id).with_title(title); + for (buffer, transaction) in &transaction.0 { + let snapshot = buffer.read(cx).snapshot(); + ranges_to_highlight.extend( + multibuffer.push_excerpts_with_context_lines( + buffer.clone(), + snapshot + .edited_ranges_for_transaction::(transaction) + .collect(), + 1, + cx, + ), + ); + } + multibuffer.push_transaction(&transaction.0); + multibuffer + }); + + workspace.update(&mut cx, |workspace, cx| { + let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + if let Some(editor) = editor.act_as::(cx) { + editor.update(cx, |editor, cx| { + let settings = (editor.build_settings)(cx); + editor.highlight_ranges::( + ranges_to_highlight, + settings.style.highlighted_line_background, + cx, ); - } - multibuffer.push_transaction(&project_transaction.0); - multibuffer - }); + }); + } + }); - workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let settings = (editor.build_settings)(cx); - editor.highlight_ranges::( - ranges_to_highlight, - settings.style.highlighted_line_background, - cx, - ); - }); - } - }); - - Ok(()) - })) + Ok(()) } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { @@ -4072,6 +4088,105 @@ impl Editor { .detach_and_log_err(cx); } + fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + use language::ToOffset as _; + + let project = self.project.clone()?; + let position = self.newest_anchor_selection().head(); + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position.clone(), cx)?; + let snapshot = buffer.read(cx).snapshot(); + let prepare_rename = project.update(cx, |project, cx| { + project.prepare_rename(buffer.clone(), buffer_position.to_offset(&snapshot), cx) + }); + + Some(cx.spawn(|this, mut cx| async move { + if let Some(range) = prepare_rename.await? { + let buffer_offset_range = range.to_offset(&snapshot); + let buffer_offset = buffer_position.to_offset(&snapshot); + let lookbehind = buffer_offset.saturating_sub(buffer_offset_range.start); + let lookahead = buffer_offset_range.end.saturating_sub(buffer_offset); + + this.update(&mut cx, |this, cx| { + let buffer = this.buffer.read(cx).read(cx); + let offset = position.to_offset(&buffer); + let start = offset - lookbehind; + let end = offset + lookahead; + let highlight_range = buffer.anchor_before(start)..buffer.anchor_after(end); + drop(buffer); + + this.select_ranges([start..end], None, cx); + this.highlight_ranges::(vec![highlight_range], Color::red(), cx); + }); + } + + Ok(()) + })) + } + + fn confirm_rename( + workspace: &mut Workspace, + _: &ConfirmRename, + cx: &mut ViewContext, + ) -> Option>> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + let (buffer, position, new_name) = editor.update(cx, |editor, cx| { + let range = editor.take_rename_range(cx)?; + let multibuffer = editor.buffer.read(cx); + let (buffer, position) = + multibuffer.text_anchor_for_position(range.start.clone(), cx)?; + let snapshot = multibuffer.read(cx); + let new_name = snapshot.text_for_range(range.clone()).collect::(); + Some((buffer, position, new_name)) + })?; + + let rename = workspace.project().clone().update(cx, |project, cx| { + project.perform_rename(buffer, position, new_name.clone(), cx) + }); + + Some(cx.spawn(|workspace, cx| async move { + let project_transaction = rename.await?; + Self::open_project_transaction( + editor, + workspace, + project_transaction, + format!("Rename: {}", new_name), + cx, + ) + .await + })) + } + + fn rename_range(&self) -> Option<&Range> { + self.highlighted_ranges_for_type::() + .and_then(|(_, range)| range.last()) + } + + fn take_rename_range(&mut self, cx: &mut ViewContext) -> Option> { + self.clear_highlighted_ranges::(cx) + .and_then(|(_, mut ranges)| ranges.pop()) + } + + fn invalidate_rename_range( + &mut self, + buffer: &MultiBufferSnapshot, + cx: &mut ViewContext, + ) { + if let Some(range) = &self.rename_range() { + if self.selections.len() == 1 { + let head = self.selections[0].head().to_offset(&buffer); + if range.start.to_offset(&buffer) <= head && range.end.to_offset(&buffer) >= head { + return; + } + } + eprintln!("clearing highlight range"); + self.clear_highlighted_ranges::(cx); + } + } + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); @@ -4484,6 +4599,7 @@ impl Editor { self.select_larger_syntax_node_stack.clear(); self.autoclose_stack.invalidate(&self.selections, &buffer); self.snippet_stack.invalidate(&self.selections, &buffer); + self.invalidate_rename_range(&buffer, cx); let new_cursor_position = self.newest_anchor_selection().head(); @@ -4759,9 +4875,12 @@ impl Editor { cx.notify(); } - pub fn clear_highlighted_ranges(&mut self, cx: &mut ViewContext) { - self.highlighted_ranges.remove(&TypeId::of::()); + pub fn clear_highlighted_ranges( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Color, Vec>)> { cx.notify(); + self.highlighted_ranges.remove(&TypeId::of::()) } #[cfg(feature = "test-support")] @@ -5091,6 +5210,9 @@ impl View for Editor { EditorMode::Full => "full", }; cx.map.insert("mode".into(), mode.into()); + if self.rename_range().is_some() { + cx.set.insert("renaming".into()); + } match self.context_menu.as_ref() { Some(ContextMenu::Completions(_)) => { cx.set.insert("showing_completions".into()); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs new file mode 100644 index 0000000000..9e2f8643b3 --- /dev/null +++ b/crates/project/src/lsp_command.rs @@ -0,0 +1,192 @@ +use crate::{Project, ProjectTransaction}; +use anyhow::{anyhow, Result}; +use client::proto; +use futures::{future::LocalBoxFuture, FutureExt}; +use gpui::{AppContext, AsyncAppContext, ModelHandle}; +use language::{ + proto::deserialize_anchor, range_from_lsp, Anchor, Buffer, PointUtf16, ToLspPosition, +}; +use std::{ops::Range, path::Path}; + +pub(crate) trait LspCommand: 'static { + type Response: 'static + Default + Send; + type LspRequest: 'static + Send + lsp::request::Request; + type ProtoRequest: 'static + Send + proto::RequestMessage; + + fn to_lsp( + &self, + path: &Path, + cx: &AppContext, + ) -> ::Params; + fn to_proto(&self, project_id: u64, cx: &AppContext) -> Self::ProtoRequest; + fn response_from_lsp( + self, + message: ::Result, + project: ModelHandle, + cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>; + fn response_from_proto( + self, + message: ::Response, + project: ModelHandle, + cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>; +} + +pub(crate) struct PrepareRename { + pub buffer: ModelHandle, + pub position: PointUtf16, +} + +#[derive(Debug)] +pub(crate) struct PerformRename { + pub buffer: ModelHandle, + pub position: PointUtf16, + pub new_name: String, +} + +impl LspCommand for PrepareRename { + type Response = Option>; + type LspRequest = lsp::request::PrepareRenameRequest; + type ProtoRequest = proto::PrepareRename; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::TextDocumentPositionParams { + lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + } + } + + fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PrepareRename { + let buffer_id = self.buffer.read(cx).remote_id(); + proto::PrepareRename { + project_id, + buffer_id, + position: None, + } + } + + fn response_from_lsp( + self, + message: Option, + _: ModelHandle, + cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>>> { + async move { + Ok(message.and_then(|result| match result { + lsp::PrepareRenameResponse::Range(range) + | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => { + self.buffer.read_with(&cx, |buffer, _| { + let range = range_from_lsp(range); + Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)) + }) + } + _ => None, + })) + } + .boxed_local() + } + + fn response_from_proto( + self, + message: proto::PrepareRenameResponse, + _: ModelHandle, + _: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result>>> { + async move { + if message.can_rename { + let start = message.start.and_then(deserialize_anchor); + let end = message.end.and_then(deserialize_anchor); + Ok(start.zip(end).map(|(start, end)| start..end)) + } else { + Ok(None) + } + } + .boxed_local() + } +} + +impl LspCommand for PerformRename { + type Response = ProjectTransaction; + type LspRequest = lsp::request::Rename; + type ProtoRequest = proto::PerformRename; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::RenameParams { + lsp::RenameParams { + text_document_position: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + }, + new_name: self.new_name.clone(), + work_done_progress_params: Default::default(), + } + } + + fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PerformRename { + let buffer_id = self.buffer.read(cx).remote_id(); + proto::PerformRename { + project_id, + buffer_id, + position: None, + new_name: self.new_name.clone(), + } + } + + fn response_from_lsp( + self, + message: Option, + project: ModelHandle, + mut cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result> { + async move { + if let Some(edit) = message { + let (language_name, language_server) = + self.buffer.read_with(&cx, |buffer, _| { + let language = buffer + .language() + .ok_or_else(|| anyhow!("buffer's language was removed"))?; + let language_server = buffer + .language_server() + .cloned() + .ok_or_else(|| anyhow!("buffer's language server was removed"))?; + Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) + })?; + Project::deserialize_workspace_edit( + project, + edit, + false, + language_name, + language_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) + } + } + .boxed_local() + } + + fn response_from_proto( + self, + message: proto::PerformRenameResponse, + project: ModelHandle, + mut cx: AsyncAppContext, + ) -> LocalBoxFuture<'static, Result> { + async move { + let message = message + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project + .update(&mut cx, |project, cx| { + project.deserialize_project_transaction(message, false, cx) + }) + .await + } + .boxed_local() + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6dc7c2c231..71ec078f4e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,5 +1,6 @@ pub mod fs; mod ignore; +mod lsp_command; pub mod worktree; use anyhow::{anyhow, Context, Result}; @@ -15,11 +16,12 @@ use gpui::{ use language::{ point_from_lsp, proto::{deserialize_anchor, serialize_anchor}, - range_from_lsp, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel, + range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, LanguageServer}; +use lsp_command::*; use postage::{broadcast, prelude::Stream, sink::Sink, watch}; use smol::block_on; use std::{ @@ -1625,7 +1627,6 @@ impl Project { return Task::ready(Err(anyhow!("buffer does not have a language server"))); }; let range = action.range.to_point_utf16(buffer); - let fs = self.fs.clone(); cx.spawn(|this, mut cx| async move { if let Some(lsp_range) = action @@ -1656,126 +1657,19 @@ impl Project { .lsp_action; } - let mut operations = Vec::new(); if let Some(edit) = action.lsp_action.edit { - if let Some(document_changes) = edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(edits) => operations - .extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)), - lsp::DocumentChanges::Operations(ops) => operations = ops, - } - } else if let Some(changes) = edit.changes { - operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { - text_document: lsp::OptionalVersionedTextDocumentIdentifier { - uri, - version: None, - }, - edits: edits.into_iter().map(lsp::OneOf::Left).collect(), - }) - })); - } + Self::deserialize_workspace_edit( + this, + edit, + push_to_history, + lang_name, + lang_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) } - - let mut project_transaction = ProjectTransaction::default(); - for operation in operations { - match operation { - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - - if let Some(parent_path) = abs_path.parent() { - fs.create_dir(parent_path).await?; - } - if abs_path.ends_with("/") { - fs.create_dir(&abs_path).await?; - } else { - fs.create_file( - &abs_path, - op.options.map(Into::into).unwrap_or_default(), - ) - .await?; - } - } - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { - let source_abs_path = op - .old_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let target_abs_path = op - .new_uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - fs.rename( - &source_abs_path, - &target_abs_path, - op.options.map(Into::into).unwrap_or_default(), - ) - .await?; - } - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|_| anyhow!("can't convert URI to path"))?; - let options = op.options.map(Into::into).unwrap_or_default(); - if abs_path.ends_with("/") { - fs.remove_dir(&abs_path, options).await?; - } else { - fs.remove_file(&abs_path, options).await?; - } - } - lsp::DocumentChangeOperation::Edit(op) => { - let buffer_to_edit = this - .update(&mut cx, |this, cx| { - this.open_local_buffer_from_lsp_path( - op.text_document.uri, - lang_name.clone(), - lang_server.clone(), - cx, - ) - }) - .await?; - - let edits = buffer_to_edit - .update(&mut cx, |buffer, cx| { - let edits = op.edits.into_iter().map(|edit| match edit { - lsp::OneOf::Left(edit) => edit, - lsp::OneOf::Right(edit) => edit.text_edit, - }); - buffer.edits_from_lsp(edits, op.text_document.version, cx) - }) - .await?; - - let transaction = buffer_to_edit.update(&mut cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([range], text, cx); - } - let transaction = if buffer.end_transaction(cx).is_some() { - let transaction = - buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - }; - - transaction - }); - if let Some(transaction) = transaction { - project_transaction.0.insert(buffer_to_edit, transaction); - } - } - } - } - - Ok(project_transaction) }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); @@ -1800,6 +1694,194 @@ impl Project { } } + async fn deserialize_workspace_edit( + this: ModelHandle, + edit: lsp::WorkspaceEdit, + push_to_history: bool, + language_name: String, + language_server: Arc, + cx: &mut AsyncAppContext, + ) -> Result { + let fs = this.read_with(cx, |this, _| this.fs.clone()); + let mut operations = Vec::new(); + if let Some(document_changes) = edit.document_changes { + match document_changes { + lsp::DocumentChanges::Edits(edits) => { + operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)) + } + lsp::DocumentChanges::Operations(ops) => operations = ops, + } + } else if let Some(changes) = edit.changes { + operations.extend(changes.into_iter().map(|(uri, edits)| { + lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { + text_document: lsp::OptionalVersionedTextDocumentIdentifier { + uri, + version: None, + }, + edits: edits.into_iter().map(lsp::OneOf::Left).collect(), + }) + })); + } + + let mut project_transaction = ProjectTransaction::default(); + for operation in operations { + match operation { + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + + if let Some(parent_path) = abs_path.parent() { + fs.create_dir(parent_path).await?; + } + if abs_path.ends_with("/") { + fs.create_dir(&abs_path).await?; + } else { + fs.create_file(&abs_path, op.options.map(Into::into).unwrap_or_default()) + .await?; + } + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { + let source_abs_path = op + .old_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let target_abs_path = op + .new_uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + fs.rename( + &source_abs_path, + &target_abs_path, + op.options.map(Into::into).unwrap_or_default(), + ) + .await?; + } + lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { + let abs_path = op + .uri + .to_file_path() + .map_err(|_| anyhow!("can't convert URI to path"))?; + let options = op.options.map(Into::into).unwrap_or_default(); + if abs_path.ends_with("/") { + fs.remove_dir(&abs_path, options).await?; + } else { + fs.remove_file(&abs_path, options).await?; + } + } + lsp::DocumentChangeOperation::Edit(op) => { + let buffer_to_edit = this + .update(cx, |this, cx| { + this.open_local_buffer_from_lsp_path( + op.text_document.uri, + language_name.clone(), + language_server.clone(), + cx, + ) + }) + .await?; + + let edits = buffer_to_edit + .update(cx, |buffer, cx| { + let edits = op.edits.into_iter().map(|edit| match edit { + lsp::OneOf::Left(edit) => edit, + lsp::OneOf::Right(edit) => edit.text_edit, + }); + buffer.edits_from_lsp(edits, op.text_document.version, cx) + }) + .await?; + + let transaction = buffer_to_edit.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + for (range, text) in edits { + buffer.edit([range], text, cx); + } + let transaction = if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + if !push_to_history { + buffer.forget_transaction(transaction.id); + } + Some(transaction) + } else { + None + }; + + transaction + }); + if let Some(transaction) = transaction { + project_transaction.0.insert(buffer_to_edit, transaction); + } + } + } + } + + Ok(project_transaction) + } + + pub fn prepare_rename( + &self, + buffer: ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>>> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp(buffer.clone(), PrepareRename { buffer, position }, cx) + } + + pub fn perform_rename( + &self, + buffer: ModelHandle, + position: T, + new_name: String, + cx: &mut ModelContext, + ) -> Task> { + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp( + buffer.clone(), + PerformRename { + buffer, + position, + new_name, + }, + cx, + ) + } + + fn request_lsp( + &self, + buffer_handle: ModelHandle, + request: R, + cx: &mut ModelContext, + ) -> Task> + where + ::Result: Send, + { + let buffer = buffer_handle.read(cx); + if self.is_local() { + let file = File::from_dyn(buffer.file()).and_then(File::as_local); + if let Some((file, language_server)) = file.zip(buffer.language_server().cloned()) { + let lsp_params = request.to_lsp(&file.abs_path(cx), cx); + return cx.spawn(|this, cx| async move { + let response = language_server + .request::(lsp_params) + .await + .context("lsp request failed")?; + request.response_from_lsp(response, this, cx).await + }); + } + } else if let Some(project_id) = self.remote_id() { + let rpc = self.client.clone(); + let message = request.to_proto(project_id, cx); + return cx.spawn(|this, cx| async move { + let response = rpc.request(message).await?; + request.response_from_proto(response, this, cx).await + }); + } + Task::ready(Ok(Default::default())) + } + pub fn find_or_create_local_worktree( &self, abs_path: impl AsRef, @@ -4099,4 +4181,71 @@ mod tests { ] ); } + + #[gpui::test] + async fn test_rename(mut cx: gpui::TestAppContext) { + let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + + let project = Project::test(fs.clone(), &mut cx); + project.update(&mut cx, |project, _| { + Arc::get_mut(&mut project.languages).unwrap().add(language); + }); + + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(&cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer((worktree_id, Path::new("one.rs")), cx) + }) + .await + .unwrap(); + + let mut fake_server = fake_servers.next().await.unwrap(); + + let response = project.update(&mut cx, |project, cx| { + project.prepare_rename(buffer.clone(), 7, cx) + }); + fake_server + .handle_request::(|params| { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ))) + }) + .next() + .await + .unwrap(); + let range = response.await.unwrap().unwrap(); + let range = buffer.read_with(&cx, |buffer, _| range.to_offset(buffer)); + assert_eq!(range, 6..9); + } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 9d7baa8992..65622e70d4 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -50,6 +50,10 @@ message Envelope { GetCodeActionsResponse get_code_actions_response = 42; ApplyCodeAction apply_code_action = 43; ApplyCodeActionResponse apply_code_action_response = 44; + PrepareRename prepare_rename = 58; + PrepareRenameResponse prepare_rename_response = 59; + PerformRename perform_rename = 60; + PerformRenameResponse perform_rename_response = 61; GetChannels get_channels = 45; GetChannelsResponse get_channels_response = 46; @@ -274,6 +278,30 @@ message ApplyCodeActionResponse { ProjectTransaction transaction = 1; } +message PrepareRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; +} + +message PrepareRenameResponse { + bool can_rename = 1; + Anchor start = 2; + Anchor end = 3; + repeated VectorClockEntry version = 4; +} + +message PerformRename { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor position = 3; + string new_name = 4; +} + +message PerformRenameResponse { + ProjectTransaction transaction = 2; +} + message CodeAction { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8093f2551f..b5f1d49c06 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -167,6 +167,10 @@ messages!( (LeaveProject, Foreground), (OpenBuffer, Foreground), (OpenBufferResponse, Foreground), + (PerformRename, Background), + (PerformRenameResponse, Background), + (PrepareRename, Background), + (PrepareRenameResponse, Background), (RegisterProjectResponse, Foreground), (Ping, Foreground), (RegisterProject, Foreground), @@ -205,6 +209,8 @@ request_messages!( (JoinProject, JoinProjectResponse), (OpenBuffer, OpenBufferResponse), (Ping, Ack), + (PerformRename, PerformRenameResponse), + (PrepareRename, PrepareRenameResponse), (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (SaveBuffer, BufferSaved), @@ -233,6 +239,7 @@ entity_messages!( JoinProject, LeaveProject, OpenBuffer, + PrepareRename, RemoveProjectCollaborator, SaveBuffer, ShareWorktree, From f9723ae16b9c9aa63d3b37626f9a4141f550f583 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Feb 2022 11:41:47 +0100 Subject: [PATCH 03/15] Undo temporary edits before performing rename or canceling it Also, wire up remote renames. --- crates/editor/src/editor.rs | 76 +++++++++++++++++++-------- crates/editor/src/multi_buffer.rs | 60 +++++++++++++++++++++- crates/language/src/buffer.rs | 6 ++- crates/project/src/lsp_command.rs | 26 +++++++--- crates/project/src/project.rs | 85 +++++++++++++++++++++++++++++++ crates/rpc/src/proto.rs | 1 + crates/server/src/rpc.rs | 30 +++++++++++ crates/text/src/text.rs | 72 ++++++++++++++++---------- crates/util/src/lib.rs | 35 ++++++++++++- 9 files changed, 331 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b1ce9e3454..47238646ea 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -439,6 +439,7 @@ pub struct Editor { next_completion_id: CompletionId, available_code_actions: Option<(ModelHandle, Arc<[CodeAction]>)>, code_actions_task: Option>, + pending_rename: Option, } pub struct EditorSnapshot { @@ -477,6 +478,11 @@ struct SnippetState { active_index: usize, } +struct RenameState { + range: Range, + first_transaction: Option, +} + struct InvalidationStack(Vec); enum ContextMenu { @@ -892,6 +898,7 @@ impl Editor { next_completion_id: 0, available_code_actions: Default::default(), code_actions_task: Default::default(), + pending_rename: Default::default(), }; this.end_selection(cx); this @@ -1913,6 +1920,10 @@ impl Editor { } fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext) { + if self.pending_rename.is_some() { + return; + } + let project = if let Some(project) = self.project.clone() { project } else { @@ -4101,11 +4112,17 @@ impl Editor { let offset = position.to_offset(&buffer); let start = offset - lookbehind; let end = offset + lookahead; - let highlight_range = buffer.anchor_before(start)..buffer.anchor_after(end); + let rename_range = buffer.anchor_before(start)..buffer.anchor_after(end); drop(buffer); + this.buffer + .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); + this.pending_rename = Some(RenameState { + range: rename_range.clone(), + first_transaction: None, + }); this.select_ranges([start..end], None, cx); - this.highlight_ranges::(vec![highlight_range], Color::red(), cx); + this.highlight_ranges::(vec![rename_range], Color::red(), cx); }); } @@ -4121,17 +4138,16 @@ impl Editor { let editor = workspace.active_item(cx)?.act_as::(cx)?; let (buffer, position, new_name) = editor.update(cx, |editor, cx| { - let range = editor.take_rename_range(cx)?; - let multibuffer = editor.buffer.read(cx); - let (buffer, position) = - multibuffer.text_anchor_for_position(range.start.clone(), cx)?; - let snapshot = multibuffer.read(cx); - let new_name = snapshot.text_for_range(range.clone()).collect::(); + let (range, new_name) = editor.take_rename(cx)?; + let (buffer, position) = editor + .buffer + .read(cx) + .text_anchor_for_position(range.start.clone(), cx)?; Some((buffer, position, new_name)) })?; let rename = workspace.project().clone().update(cx, |project, cx| { - project.perform_rename(buffer, position, new_name.clone(), cx) + project.perform_rename(buffer, position, new_name.clone(), true, cx) }); Some(cx.spawn(|workspace, cx| async move { @@ -4147,14 +4163,23 @@ impl Editor { })) } - fn rename_range(&self) -> Option<&Range> { - self.highlighted_ranges_for_type::() - .and_then(|(_, range)| range.last()) - } + fn take_rename(&mut self, cx: &mut ViewContext) -> Option<(Range, String)> { + let rename = self.pending_rename.take()?; + let new_name = self + .buffer + .read(cx) + .read(cx) + .text_for_range(rename.range.clone()) + .collect::(); - fn take_rename_range(&mut self, cx: &mut ViewContext) -> Option> { - self.clear_highlighted_ranges::(cx) - .and_then(|(_, mut ranges)| ranges.pop()) + self.clear_highlighted_ranges::(cx); + if let Some(transaction_id) = rename.first_transaction { + self.buffer.update(cx, |buffer, cx| { + buffer.undo_to_transaction(transaction_id, false, cx) + }); + } + + Some((rename.range, new_name)) } fn invalidate_rename_range( @@ -4162,15 +4187,16 @@ impl Editor { buffer: &MultiBufferSnapshot, cx: &mut ViewContext, ) { - if let Some(range) = &self.rename_range() { + if let Some(rename) = self.pending_rename.as_ref() { if self.selections.len() == 1 { - let head = self.selections[0].head().to_offset(&buffer); - if range.start.to_offset(&buffer) <= head && range.end.to_offset(&buffer) >= head { + let head = self.selections[0].head().to_offset(buffer); + let range = rename.range.to_offset(buffer).to_inclusive(); + if range.contains(&head) { return; } } - eprintln!("clearing highlight range"); - self.clear_highlighted_ranges::(cx); + + self.take_rename(cx); } } @@ -4659,6 +4685,12 @@ impl Editor { .buffer .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) { + if let Some(rename) = self.pending_rename.as_mut() { + if rename.first_transaction.is_none() { + rename.first_transaction = Some(tx_id); + } + } + if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { *end_selections = Some(self.selections.clone()); } else { @@ -5197,7 +5229,7 @@ impl View for Editor { EditorMode::Full => "full", }; cx.map.insert("mode".into(), mode.into()); - if self.rename_range().is_some() { + if self.pending_rename.is_some() { cx.set.insert("renaming".into()); } match self.context_menu.as_ref() { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 8180acacc5..a32e3b2307 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -27,6 +27,7 @@ use text::{ AnchorRangeExt as _, Edit, Point, PointUtf16, TextSummary, }; use theme::SyntaxTheme; +use util::CowMut; const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize]; @@ -58,6 +59,7 @@ pub enum CharKind { Word, } +#[derive(Clone)] struct Transaction { id: TransactionId, buffer_transactions: HashMap, @@ -564,7 +566,7 @@ impl MultiBuffer { if let Some(entry) = buffer.peek_undo_stack() { *buffer_transaction_id = entry.transaction_id(); } - buffer.undo_to_transaction(undo_to, cx) + buffer.undo_to_transaction(undo_to, true, cx) }); } } @@ -577,6 +579,35 @@ impl MultiBuffer { None } + pub fn undo_to_transaction( + &mut self, + transaction_id: TransactionId, + push_redo: bool, + cx: &mut ModelContext, + ) -> bool { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| { + buffer.undo_to_transaction(transaction_id, push_redo, cx) + }); + } + + let mut undone = false; + for transaction in &mut *self.history.remove_from_undo(transaction_id, push_redo) { + 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, true, cx) + }); + } + } + } + undone + } + pub fn redo(&mut self, cx: &mut ModelContext) -> Option { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| buffer.redo(cx)); @@ -591,7 +622,7 @@ impl MultiBuffer { if let Some(entry) = buffer.peek_redo_stack() { *buffer_transaction_id = entry.transaction_id(); } - buffer.redo_to_transaction(redo_to, cx) + buffer.redo_to_transaction(redo_to, true, cx) }); } } @@ -2314,6 +2345,31 @@ impl History { } } + fn remove_from_undo( + &mut self, + transaction_id: TransactionId, + push_redo: bool, + ) -> CowMut<[Transaction]> { + assert_eq!(self.transaction_depth, 0); + + if let Some(entry_ix) = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + let transactions = self.undo_stack.drain(entry_ix..).rev(); + if push_redo { + let redo_stack_start_len = self.redo_stack.len(); + self.redo_stack.extend(transactions); + CowMut::Borrowed(&mut self.redo_stack[redo_stack_start_len..]) + } else { + CowMut::Owned(transactions.collect()) + } + } else { + CowMut::Owned(Default::default()) + } + } + fn pop_redo(&mut self) -> Option<&mut Transaction> { assert_eq!(self.transaction_depth, 0); if let Some(transaction) = self.redo_stack.pop() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b4543b02b0..b8ec2f9884 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1738,12 +1738,13 @@ impl Buffer { pub fn undo_to_transaction( &mut self, transaction_id: TransactionId, + push_redo: bool, cx: &mut ModelContext, ) -> bool { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); - let operations = self.text.undo_to_transaction(transaction_id); + let operations = self.text.undo_to_transaction(transaction_id, push_redo); let undone = !operations.is_empty(); for operation in operations { self.send_operation(Operation::Buffer(operation), cx); @@ -1770,12 +1771,13 @@ impl Buffer { pub fn redo_to_transaction( &mut self, transaction_id: TransactionId, + push_undo: bool, cx: &mut ModelContext, ) -> bool { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); - let operations = self.text.redo_to_transaction(transaction_id); + let operations = self.text.redo_to_transaction(transaction_id, push_undo); let redone = !operations.is_empty(); for operation in operations { self.send_operation(Operation::Buffer(operation), cx); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 9e2f8643b3..0c5c084060 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -43,6 +43,7 @@ pub(crate) struct PerformRename { pub buffer: ModelHandle, pub position: PointUtf16, pub new_name: String, + pub push_to_history: bool, } impl LspCommand for PrepareRename { @@ -60,11 +61,14 @@ impl LspCommand for PrepareRename { } fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PrepareRename { - let buffer_id = self.buffer.read(cx).remote_id(); + let buffer = &self.buffer.read(cx); + let buffer_id = buffer.remote_id(); proto::PrepareRename { project_id, buffer_id, - position: None, + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), } } @@ -93,10 +97,15 @@ impl LspCommand for PrepareRename { self, message: proto::PrepareRenameResponse, _: ModelHandle, - _: AsyncAppContext, + mut cx: AsyncAppContext, ) -> LocalBoxFuture<'static, Result>>> { async move { if message.can_rename { + self.buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(message.version.into()) + }) + .await; let start = message.start.and_then(deserialize_anchor); let end = message.end.and_then(deserialize_anchor); Ok(start.zip(end).map(|(start, end)| start..end)) @@ -127,11 +136,14 @@ impl LspCommand for PerformRename { } fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PerformRename { - let buffer_id = self.buffer.read(cx).remote_id(); + let buffer = &self.buffer.read(cx); + let buffer_id = buffer.remote_id(); proto::PerformRename { project_id, buffer_id, - position: None, + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), new_name: self.new_name.clone(), } } @@ -158,7 +170,7 @@ impl LspCommand for PerformRename { Project::deserialize_workspace_edit( project, edit, - false, + self.push_to_history, language_name, language_server, &mut cx, @@ -183,7 +195,7 @@ impl LspCommand for PerformRename { .ok_or_else(|| anyhow!("missing transaction"))?; project .update(&mut cx, |project, cx| { - project.deserialize_project_transaction(message, false, cx) + project.deserialize_project_transaction(message, self.push_to_history, cx) }) .await } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 71ec078f4e..a8bee2ff43 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -184,6 +184,8 @@ impl Project { client.add_entity_request_handler(Self::handle_get_code_actions); client.add_entity_request_handler(Self::handle_get_completions); client.add_entity_request_handler(Self::handle_get_definition); + client.add_entity_request_handler(Self::handle_prepare_rename); + client.add_entity_request_handler(Self::handle_perform_rename); client.add_entity_request_handler(Self::handle_open_buffer); client.add_entity_request_handler(Self::handle_save_buffer); } @@ -1835,6 +1837,7 @@ impl Project { buffer: ModelHandle, position: T, new_name: String, + push_to_history: bool, cx: &mut ModelContext, ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); @@ -1844,6 +1847,7 @@ impl Project { buffer, position, new_name, + push_to_history, }, cx, ) @@ -2615,6 +2619,87 @@ impl Project { }) } + async fn handle_prepare_rename( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let position = envelope + .payload + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + let (prepare_rename, version) = this.update(&mut cx, |this, cx| { + let buffer_handle = this + .shared_buffers + .get(&sender_id) + .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let buffer = buffer_handle.read(cx); + let version = buffer.version(); + if buffer.can_resolve(&position) { + Ok((this.prepare_rename(buffer_handle, position, cx), version)) + } else { + Err(anyhow!("cannot resolve position")) + } + })?; + + let range = prepare_rename.await?; + Ok(proto::PrepareRenameResponse { + can_rename: range.is_some(), + start: range + .as_ref() + .map(|range| language::proto::serialize_anchor(&range.start)), + end: range + .as_ref() + .map(|range| language::proto::serialize_anchor(&range.end)), + version: (&version).into(), + }) + } + + async fn handle_perform_rename( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let position = envelope + .payload + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + let perform_rename = this.update(&mut cx, |this, cx| { + let buffer_handle = this + .shared_buffers + .get(&sender_id) + .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; + let buffer = buffer_handle.read(cx); + if buffer.can_resolve(&position) { + Ok(this.perform_rename( + buffer_handle, + position, + envelope.payload.new_name, + false, + cx, + )) + } else { + Err(anyhow!("cannot resolve position")) + } + })?; + + let transaction = perform_rename.await?; + let transaction = this.update(&mut cx, |this, cx| { + this.serialize_project_transaction_for_peer(transaction, sender_id, cx) + }); + Ok(proto::PerformRenameResponse { + transaction: Some(transaction), + }) + } + async fn handle_open_buffer( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index b5f1d49c06..fa8b2f692c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -239,6 +239,7 @@ entity_messages!( JoinProject, LeaveProject, OpenBuffer, + PerformRename, PrepareRename, RemoveProjectCollaborator, SaveBuffer, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 93b54cf88c..9c0a1e2ec7 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -91,6 +91,8 @@ impl Server { .add_request_handler(Server::apply_additional_edits_for_completion) .add_request_handler(Server::get_code_actions) .add_request_handler(Server::apply_code_action) + .add_request_handler(Server::prepare_rename) + .add_request_handler(Server::perform_rename) .add_request_handler(Server::get_channels) .add_request_handler(Server::get_users) .add_request_handler(Server::join_channel) @@ -708,6 +710,34 @@ impl Server { .await?) } + async fn prepare_rename( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + let host = self + .state() + .read_project(request.payload.project_id, request.sender_id)? + .host_connection_id; + Ok(self + .peer + .forward_request(request.sender_id, host, request.payload.clone()) + .await?) + } + + async fn perform_rename( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + let host = self + .state() + .read_project(request.payload.project_id, request.sender_id)? + .host_connection_id; + Ok(self + .peer + .forward_request(request.sender_id, host, request.payload.clone()) + .await?) + } + async fn update_buffer( self: Arc, request: TypedEnvelope, diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index da003b5d44..d041518e71 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -285,19 +285,31 @@ impl History { } } - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { + fn remove_from_undo( + &mut self, + transaction_id: TransactionId, + push_redo: bool, + ) -> Vec { assert_eq!(self.transaction_depth, 0); - let redo_stack_start_len = self.redo_stack.len(); + let mut transactions = Vec::new(); if let Some(entry_ix) = self .undo_stack .iter() .rposition(|entry| entry.transaction.id == transaction_id) { - self.redo_stack - .extend(self.undo_stack.drain(entry_ix..).rev()); + transactions.extend( + self.undo_stack[entry_ix..] + .iter() + .rev() + .map(|entry| entry.transaction.clone()), + ); + let transactions = self.undo_stack.drain(entry_ix..).rev(); + if push_redo { + self.redo_stack.extend(transactions); + } } - &self.redo_stack[redo_stack_start_len..] + transactions } fn forget(&mut self, transaction_id: TransactionId) { @@ -327,19 +339,31 @@ impl History { } } - fn remove_from_redo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { + fn remove_from_redo( + &mut self, + transaction_id: TransactionId, + push_undo: bool, + ) -> Vec { assert_eq!(self.transaction_depth, 0); - let undo_stack_start_len = self.undo_stack.len(); + let mut transactions = Vec::new(); if let Some(entry_ix) = self .redo_stack .iter() .rposition(|entry| entry.transaction.id == transaction_id) { - self.undo_stack - .extend(self.redo_stack.drain(entry_ix..).rev()); + transactions.extend( + self.redo_stack[entry_ix..] + .iter() + .rev() + .map(|entry| entry.transaction.clone()), + ); + if push_undo { + self.undo_stack + .extend(self.redo_stack.drain(entry_ix..).rev()); + } } - &self.undo_stack[undo_stack_start_len..] + transactions } } @@ -1215,14 +1239,12 @@ impl Buffer { } } - pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { - let transactions = self - .history - .remove_from_undo(transaction_id) - .iter() - .map(|entry| entry.transaction.clone()) - .collect::>(); - + pub fn undo_to_transaction( + &mut self, + transaction_id: TransactionId, + push_redo: bool, + ) -> Vec { + let transactions = self.history.remove_from_undo(transaction_id, push_redo); transactions .into_iter() .map(|transaction| self.undo_or_redo(transaction).unwrap()) @@ -1244,14 +1266,12 @@ impl Buffer { } } - pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { - let transactions = self - .history - .remove_from_redo(transaction_id) - .iter() - .map(|entry| entry.transaction.clone()) - .collect::>(); - + pub fn redo_to_transaction( + &mut self, + transaction_id: TransactionId, + push_undo: bool, + ) -> Vec { + let transactions = self.history.remove_from_redo(transaction_id, push_undo); transactions .into_iter() .map(|transaction| self.undo_or_redo(transaction).unwrap()) diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 919fecf8f9..26edf7f5a7 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -3,8 +3,9 @@ pub mod test; use futures::Future; use std::{ + borrow::{Borrow, BorrowMut}, cmp::Ordering, - ops::AddAssign, + ops::{AddAssign, Deref, DerefMut}, pin::Pin, task::{Context, Poll}, }; @@ -123,6 +124,38 @@ where } } +pub enum CowMut<'a, T: ?Sized + ToOwned> { + Borrowed(&'a mut T), + Owned(T::Owned), +} + +impl<'a, T> Deref for CowMut<'a, T> +where + T: ?Sized + ToOwned, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + CowMut::Borrowed(value) => value, + CowMut::Owned(value) => value.borrow(), + } + } +} + +impl<'a, T> DerefMut for CowMut<'a, T> +where + T: ?Sized + ToOwned, + T::Owned: BorrowMut, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + CowMut::Borrowed(value) => value, + CowMut::Owned(value) => value.borrow_mut(), + } + } +} + #[cfg(test)] mod tests { use super::*; From a682ebb08d541773f95b75e947108154c46c1418 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Feb 2022 12:18:01 +0100 Subject: [PATCH 04/15] Add test for preparing and performing a rename --- crates/editor/src/editor.rs | 4 +- crates/project/src/lsp_command.rs | 10 +- crates/project/src/project.rs | 75 +++++++++ crates/server/src/rpc.rs | 243 +++++++++++++++++++++++++++++- 4 files changed, 326 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 47238646ea..79f2dac0ec 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4086,7 +4086,7 @@ impl Editor { .detach_and_log_err(cx); } - fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { + pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext) -> Option>> { use language::ToOffset as _; let project = self.project.clone()?; @@ -4130,7 +4130,7 @@ impl Editor { })) } - fn confirm_rename( + pub fn confirm_rename( workspace: &mut Workspace, _: &ConfirmRename, cx: &mut ViewContext, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 0c5c084060..0f2ed5bbce 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -4,7 +4,7 @@ use client::proto; use futures::{future::LocalBoxFuture, FutureExt}; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ - proto::deserialize_anchor, range_from_lsp, Anchor, Buffer, PointUtf16, ToLspPosition, + proto::deserialize_anchor, range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, }; use std::{ops::Range, path::Path}; @@ -84,7 +84,13 @@ impl LspCommand for PrepareRename { | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => { self.buffer.read_with(&cx, |buffer, _| { let range = range_from_lsp(range); - Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)) + if buffer.clip_point_utf16(range.start, Bias::Left) == range.start + && buffer.clip_point_utf16(range.end, Bias::Left) == range.end + { + Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)) + } else { + None + } }) } _ => None, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a8bee2ff43..823ddff7c5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4332,5 +4332,80 @@ mod tests { let range = response.await.unwrap().unwrap(); let range = buffer.read_with(&cx, |buffer, _| range.to_offset(buffer)); assert_eq!(range, 6..9); + + let response = project.update(&mut cx, |project, cx| { + project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) + }); + fake_server + .handle_request::(|params| { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///dir/one.rs" + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 7) + ); + assert_eq!(params.new_name, "THREE"); + Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/dir/one.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ), + "THREE".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/dir/two.rs").unwrap(), + vec![ + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + "THREE".to_string(), + ), + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + "THREE".to_string(), + ), + ], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }) + }) + .next() + .await + .unwrap(); + let mut transaction = response.await.unwrap().0; + assert_eq!(transaction.len(), 2); + assert_eq!( + transaction + .remove_entry(&buffer) + .unwrap() + .0 + .read_with(&cx, |buffer, _| buffer.text()), + "const THREE: usize = 1;" + ); + assert_eq!( + transaction + .into_keys() + .next() + .unwrap() + .read_with(&cx, |buffer, _| buffer.text()), + "const TWO: usize = one::THREE + one::THREE;" + ); } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 9c0a1e2ec7..35330cfc2f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1152,8 +1152,8 @@ mod tests { EstablishConnectionError, UserStore, }, editor::{ - self, ConfirmCodeAction, ConfirmCompletion, Editor, EditorSettings, Input, MultiBuffer, - Redo, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, EditorSettings, + Input, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, }, fs::{FakeFs, Fs as _}, language::{ @@ -3029,6 +3029,218 @@ mod tests { }); } + #[gpui::test(iterations = 10)] + async fn test_collaborating_with_renames(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + let mut lang_registry = Arc::new(LanguageRegistry::new()); + let fs = FakeFs::new(cx_a.background()); + let mut path_openers_b = Vec::new(); + cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); + + // Set up a fake language server. + let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); + Arc::get_mut(&mut lang_registry) + .unwrap() + .add(Arc::new(Language::new( + LanguageConfig { + name: "Rust".to_string(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ))); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;" + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id()); + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let mut params = cx_b.update(WorkspaceParams::test); + params.languages = lang_registry.clone(); + params.client = client_b.client.clone(); + params.user_store = client_b.user_store.clone(); + params.project = project_b; + params.path_openers = path_openers_b.into(); + + let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); + let editor_b = workspace_b + .update(&mut cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "one.rs").into(), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let mut fake_language_server = fake_language_servers.next().await.unwrap(); + + // Move cursor to a location that can be renamed. + let prepare_rename = editor_b.update(&mut cx_b, |editor, cx| { + editor.select_ranges([7..7], None, cx); + editor.rename(&Rename, cx).unwrap() + }); + + fake_language_server + .handle_request::(|params| { + assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); + assert_eq!(params.position, lsp::Position::new(0, 7)); + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ))) + }) + .next() + .await + .unwrap(); + prepare_rename.await.unwrap(); + editor_b.update(&mut cx_b, |editor, cx| { + assert_eq!(editor.selected_ranges(cx), [6..9]); + editor.handle_input(&Input("T".to_string()), cx); + editor.handle_input(&Input("H".to_string()), cx); + editor.handle_input(&Input("R".to_string()), cx); + editor.handle_input(&Input("E".to_string()), cx); + editor.handle_input(&Input("E".to_string()), cx); + }); + + let confirm_rename = workspace_b.update(&mut cx_b, |workspace, cx| { + Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() + }); + fake_language_server + .handle_request::(|params| { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///dir/one.rs" + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(0, 6) + ); + assert_eq!(params.new_name, "THREE"); + Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/dir/one.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 9), + ), + "THREE".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/dir/two.rs").unwrap(), + vec![ + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + "THREE".to_string(), + ), + lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + "THREE".to_string(), + ), + ], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }) + }) + .next() + .await + .unwrap(); + confirm_rename.await.unwrap(); + + let rename_editor = workspace_b.read_with(&cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + rename_editor.update(&mut cx_b, |editor, cx| { + assert_eq!( + editor.text(cx), + "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;" + ); + editor.undo(&Undo, cx); + assert_eq!( + editor.text(cx), + "const TWO: usize = one::ONE + one::ONE;\nconst ONE: usize = 1;" + ); + editor.redo(&Redo, cx); + assert_eq!( + editor.text(cx), + "const TWO: usize = one::THREE + one::THREE;\nconst THREE: usize = 1;" + ); + }); + + // Ensure temporary rename edits cannot be undone/redone. + editor_b.update(&mut cx_b, |editor, cx| { + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.undo(&Undo, cx); + assert_eq!(editor.text(cx), "const ONE: usize = 1;"); + editor.redo(&Redo, cx); + assert_eq!(editor.text(cx), "const THREE: usize = 1;"); + }) + } + #[gpui::test(iterations = 10)] async fn test_basic_chat(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); @@ -3619,6 +3831,13 @@ mod tests { }, )]) }); + + fake_server.handle_request::(|params| { + Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + params.position, + params.position, + ))) + }); }); Arc::get_mut(&mut host_lang_registry) @@ -4251,6 +4470,26 @@ mod tests { save.await; } } + 40..=45 => { + let prepare_rename = project.update(&mut cx, |project, cx| { + log::info!( + "Guest {}: preparing rename for buffer {:?}", + guest_id, + buffer.read(cx).file().unwrap().full_path(cx) + ); + let offset = rng.borrow_mut().gen_range(0..=buffer.read(cx).len()); + project.prepare_rename(buffer, offset, cx) + }); + let prepare_rename = cx.background().spawn(async move { + prepare_rename.await.expect("prepare rename request failed"); + }); + if rng.borrow_mut().gen_bool(0.3) { + log::info!("Guest {}: detaching prepare rename request", guest_id); + prepare_rename.detach(); + } else { + prepare_rename.await; + } + } _ => { buffer.update(&mut cx, |buffer, cx| { log::info!( From 20c1a1e310cadb7b94e86a5923b8657a791a48ea Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Feb 2022 12:20:59 +0100 Subject: [PATCH 05/15] Use theme to highlight in-progress rename --- crates/editor/src/editor.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 79f2dac0ec..20126bb89e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4108,6 +4108,7 @@ impl Editor { let lookahead = buffer_offset_range.end.saturating_sub(buffer_offset); this.update(&mut cx, |this, cx| { + let settings = (this.build_settings)(cx); let buffer = this.buffer.read(cx).read(cx); let offset = position.to_offset(&buffer); let start = offset - lookbehind; @@ -4122,7 +4123,11 @@ impl Editor { first_transaction: None, }); this.select_ranges([start..end], None, cx); - this.highlight_ranges::(vec![rename_range], Color::red(), cx); + this.highlight_ranges::( + vec![rename_range], + settings.style.highlighted_line_background, + cx, + ); }); } From 3a6fb0a8fe065502655188522ee08c6f30211fa3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Feb 2022 12:21:24 +0100 Subject: [PATCH 06/15] Fix warning --- crates/project/src/fs.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 22a1e11ea3..8fbdb3220e 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -7,7 +7,6 @@ use std::{ os::unix::fs::MetadataExt, path::{Path, PathBuf}, pin::Pin, - sync::Arc, time::{Duration, SystemTime}, }; use text::Rope; @@ -269,7 +268,7 @@ pub struct FakeFs { #[cfg(any(test, feature = "test-support"))] impl FakeFs { - pub fn new(executor: std::sync::Arc) -> Arc { + pub fn new(executor: std::sync::Arc) -> std::sync::Arc { let (events_tx, _) = postage::broadcast::channel(2048); let mut entries = std::collections::BTreeMap::new(); entries.insert( @@ -284,7 +283,7 @@ impl FakeFs { content: None, }, ); - Arc::new(Self { + std::sync::Arc::new(Self { executor, state: futures::lock::Mutex::new(FakeFsState { entries, From 62e5947b76fbc319f7dc96efb0daadf4ebe0da64 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Feb 2022 15:24:24 +0100 Subject: [PATCH 07/15] Cancel pending rename when hitting `escape` --- crates/editor/src/editor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 20126bb89e..ae3c10495f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1452,6 +1452,10 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.take_rename(cx).is_some() { + return; + } + if self.hide_context_menu(cx).is_some() { return; } From 284160532897c55d023f405aa1d1f111a7d4d1c5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Feb 2022 10:45:06 -0800 Subject: [PATCH 08/15] Refactor handling of remote renames --- crates/project/src/lsp_command.rs | 206 +++++++++++++++++++++++------- crates/project/src/project.rs | 113 +++++----------- 2 files changed, 193 insertions(+), 126 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 0f2ed5bbce..d9855875ac 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,14 +1,15 @@ use crate::{Project, ProjectTransaction}; use anyhow::{anyhow, Result}; -use client::proto; +use client::{proto, PeerId}; use futures::{future::LocalBoxFuture, FutureExt}; -use gpui::{AppContext, AsyncAppContext, ModelHandle}; +use gpui::{AppContext, AsyncAppContext, ModelContext, ModelHandle}; use language::{ proto::deserialize_anchor, range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, + ToPointUtf16, }; use std::{ops::Range, path::Path}; -pub(crate) trait LspCommand: 'static { +pub(crate) trait LspCommand: 'static + Sized { type Response: 'static + Default + Send; type LspRequest: 'static + Send + lsp::request::Request; type ProtoRequest: 'static + Send + proto::RequestMessage; @@ -18,29 +19,50 @@ pub(crate) trait LspCommand: 'static { path: &Path, cx: &AppContext, ) -> ::Params; - fn to_proto(&self, project_id: u64, cx: &AppContext) -> Self::ProtoRequest; fn response_from_lsp( self, message: ::Result, project: ModelHandle, + buffer: ModelHandle, cx: AsyncAppContext, ) -> LocalBoxFuture<'static, Result>; + + fn to_proto( + &self, + project_id: u64, + buffer: &ModelHandle, + cx: &AppContext, + ) -> Self::ProtoRequest; + fn from_proto( + message: Self::ProtoRequest, + project: &mut Project, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> Result; + fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; + + fn response_to_proto( + response: Self::Response, + project: &mut Project, + peer_id: PeerId, + buffer_version: &clock::Global, + cx: &mut ModelContext, + ) -> ::Response; fn response_from_proto( self, message: ::Response, project: ModelHandle, + buffer: ModelHandle, cx: AsyncAppContext, ) -> LocalBoxFuture<'static, Result>; } pub(crate) struct PrepareRename { - pub buffer: ModelHandle, pub position: PointUtf16, } #[derive(Debug)] pub(crate) struct PerformRename { - pub buffer: ModelHandle, pub position: PointUtf16, pub new_name: String, pub push_to_history: bool, @@ -60,29 +82,18 @@ impl LspCommand for PrepareRename { } } - fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PrepareRename { - let buffer = &self.buffer.read(cx); - let buffer_id = buffer.remote_id(); - proto::PrepareRename { - project_id, - buffer_id, - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - } - } - fn response_from_lsp( self, message: Option, _: ModelHandle, + buffer: ModelHandle, cx: AsyncAppContext, ) -> LocalBoxFuture<'static, Result>>> { async move { Ok(message.and_then(|result| match result { lsp::PrepareRenameResponse::Range(range) - | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => { - self.buffer.read_with(&cx, |buffer, _| { + | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => buffer + .read_with(&cx, |buffer, _| { let range = range_from_lsp(range); if buffer.clip_point_utf16(range.start, Bias::Left) == range.start && buffer.clip_point_utf16(range.end, Bias::Left) == range.end @@ -91,23 +102,80 @@ impl LspCommand for PrepareRename { } else { None } - }) - } + }), _ => None, })) } .boxed_local() } + fn to_proto( + &self, + project_id: u64, + buffer: &ModelHandle, + cx: &AppContext, + ) -> proto::PrepareRename { + proto::PrepareRename { + project_id, + buffer_id: buffer.read(cx).remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.read(cx).anchor_before(self.position), + )), + } + } + + fn buffer_id_from_proto(message: &proto::PrepareRename) -> u64 { + message.buffer_id + } + + fn from_proto( + message: proto::PrepareRename, + _: &mut Project, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> Result { + let buffer = buffer.read(cx); + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + if !buffer.can_resolve(&position) { + Err(anyhow!("cannot resolve position"))?; + } + Ok(Self { + position: position.to_point_utf16(buffer), + }) + } + + fn response_to_proto( + range: Option>, + _: &mut Project, + _: PeerId, + buffer_version: &clock::Global, + _: &mut ModelContext, + ) -> proto::PrepareRenameResponse { + proto::PrepareRenameResponse { + can_rename: range.is_some(), + start: range + .as_ref() + .map(|range| language::proto::serialize_anchor(&range.start)), + end: range + .as_ref() + .map(|range| language::proto::serialize_anchor(&range.end)), + version: buffer_version.into(), + } + } + fn response_from_proto( self, message: proto::PrepareRenameResponse, _: ModelHandle, + buffer: ModelHandle, mut cx: AsyncAppContext, ) -> LocalBoxFuture<'static, Result>>> { async move { if message.can_rename { - self.buffer + buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(message.version.into()) }) @@ -141,38 +209,25 @@ impl LspCommand for PerformRename { } } - fn to_proto(&self, project_id: u64, cx: &AppContext) -> proto::PerformRename { - let buffer = &self.buffer.read(cx); - let buffer_id = buffer.remote_id(); - proto::PerformRename { - project_id, - buffer_id, - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - new_name: self.new_name.clone(), - } - } - fn response_from_lsp( self, message: Option, project: ModelHandle, + buffer: ModelHandle, mut cx: AsyncAppContext, ) -> LocalBoxFuture<'static, Result> { async move { if let Some(edit) = message { - let (language_name, language_server) = - self.buffer.read_with(&cx, |buffer, _| { - let language = buffer - .language() - .ok_or_else(|| anyhow!("buffer's language was removed"))?; - let language_server = buffer - .language_server() - .cloned() - .ok_or_else(|| anyhow!("buffer's language server was removed"))?; - Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) - })?; + let (language_name, language_server) = buffer.read_with(&cx, |buffer, _| { + let language = buffer + .language() + .ok_or_else(|| anyhow!("buffer's language was removed"))?; + let language_server = buffer + .language_server() + .cloned() + .ok_or_else(|| anyhow!("buffer's language server was removed"))?; + Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) + })?; Project::deserialize_workspace_edit( project, edit, @@ -189,10 +244,67 @@ impl LspCommand for PerformRename { .boxed_local() } + fn to_proto( + &self, + project_id: u64, + buffer: &ModelHandle, + cx: &AppContext, + ) -> proto::PerformRename { + let buffer = buffer.read(cx); + let buffer_id = buffer.remote_id(); + proto::PerformRename { + project_id, + buffer_id, + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + new_name: self.new_name.clone(), + } + } + + fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 { + message.buffer_id + } + + fn from_proto( + message: proto::PerformRename, + _: &mut Project, + buffer: &ModelHandle, + cx: &mut ModelContext, + ) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + let buffer = buffer.read(cx); + if !buffer.can_resolve(&position) { + Err(anyhow!("cannot resolve position"))?; + } + Ok(Self { + position: position.to_point_utf16(buffer), + new_name: message.new_name, + push_to_history: false, + }) + } + + fn response_to_proto( + response: ProjectTransaction, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &mut ModelContext, + ) -> proto::PerformRenameResponse { + let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx); + proto::PerformRenameResponse { + transaction: Some(transaction), + } + } + fn response_from_proto( self, message: proto::PerformRenameResponse, project: ModelHandle, + _: ModelHandle, mut cx: AsyncAppContext, ) -> LocalBoxFuture<'static, Result> { async move { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 823ddff7c5..340d146d11 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -184,8 +184,8 @@ impl Project { client.add_entity_request_handler(Self::handle_get_code_actions); client.add_entity_request_handler(Self::handle_get_completions); client.add_entity_request_handler(Self::handle_get_definition); - client.add_entity_request_handler(Self::handle_prepare_rename); - client.add_entity_request_handler(Self::handle_perform_rename); + client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_open_buffer); client.add_entity_request_handler(Self::handle_save_buffer); } @@ -1829,7 +1829,7 @@ impl Project { cx: &mut ModelContext, ) -> Task>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.request_lsp(buffer.clone(), PrepareRename { buffer, position }, cx) + self.request_lsp(buffer, PrepareRename { position }, cx) } pub fn perform_rename( @@ -1842,9 +1842,8 @@ impl Project { ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); self.request_lsp( - buffer.clone(), + buffer, PerformRename { - buffer, position, new_name, push_to_history, @@ -1862,8 +1861,8 @@ impl Project { where ::Result: Send, { - let buffer = buffer_handle.read(cx); if self.is_local() { + let buffer = buffer_handle.read(cx); let file = File::from_dyn(buffer.file()).and_then(File::as_local); if let Some((file, language_server)) = file.zip(buffer.language_server().cloned()) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); @@ -1872,15 +1871,19 @@ impl Project { .request::(lsp_params) .await .context("lsp request failed")?; - request.response_from_lsp(response, this, cx).await + request + .response_from_lsp(response, this, buffer_handle, cx) + .await }); } } else if let Some(project_id) = self.remote_id() { let rpc = self.client.clone(); - let message = request.to_proto(project_id, cx); + let message = request.to_proto(project_id, &buffer_handle, cx); return cx.spawn(|this, cx| async move { let response = rpc.request(message).await?; - request.response_from_proto(response, this, cx).await + request + .response_from_proto(response, this, buffer_handle, cx) + .await }); } Task::ready(Ok(Default::default())) @@ -2619,84 +2622,36 @@ impl Project { }) } - async fn handle_prepare_rename( + async fn handle_lsp_command( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, - ) -> Result { + ) -> Result<::Response> + where + ::Result: Send, + { let sender_id = envelope.original_sender_id()?; - let position = envelope - .payload - .position - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid position"))?; - let (prepare_rename, version) = this.update(&mut cx, |this, cx| { + let (request, buffer_version) = this.update(&mut cx, |this, cx| { + let buffer_id = T::buffer_id_from_proto(&envelope.payload); let buffer_handle = this .shared_buffers .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) - .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; - let buffer = buffer_handle.read(cx); - let version = buffer.version(); - if buffer.can_resolve(&position) { - Ok((this.prepare_rename(buffer_handle, position, cx), version)) - } else { - Err(anyhow!("cannot resolve position")) - } + .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned()) + .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; + let buffer_version = buffer_handle.read(cx).version(); + let request = T::from_proto(envelope.payload, this, &buffer_handle, cx)?; + Ok::<_, anyhow::Error>((this.request_lsp(buffer_handle, request, cx), buffer_version)) })?; - - let range = prepare_rename.await?; - Ok(proto::PrepareRenameResponse { - can_rename: range.is_some(), - start: range - .as_ref() - .map(|range| language::proto::serialize_anchor(&range.start)), - end: range - .as_ref() - .map(|range| language::proto::serialize_anchor(&range.end)), - version: (&version).into(), - }) - } - - async fn handle_perform_rename( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result { - let sender_id = envelope.original_sender_id()?; - let position = envelope - .payload - .position - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid position"))?; - let perform_rename = this.update(&mut cx, |this, cx| { - let buffer_handle = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) - .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; - let buffer = buffer_handle.read(cx); - if buffer.can_resolve(&position) { - Ok(this.perform_rename( - buffer_handle, - position, - envelope.payload.new_name, - false, - cx, - )) - } else { - Err(anyhow!("cannot resolve position")) - } - })?; - - let transaction = perform_rename.await?; - let transaction = this.update(&mut cx, |this, cx| { - this.serialize_project_transaction_for_peer(transaction, sender_id, cx) - }); - Ok(proto::PerformRenameResponse { - transaction: Some(transaction), + let response = request.await?; + this.update(&mut cx, |this, cx| { + Ok(T::response_to_proto( + response, + this, + sender_id, + &buffer_version, + cx, + )) }) } From d70524421051f579a7e7d596a77a1f9889a096bb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Feb 2022 18:12:10 +0100 Subject: [PATCH 09/15] WIP --- crates/client/src/client.rs | 27 +++++++++++++++-------- crates/editor/src/editor.rs | 44 ++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 157ecf3af6..93ab2c6ad6 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -817,19 +817,28 @@ impl Client { self.peer.send(self.connection_id()?, message) } - pub async fn request(&self, request: T) -> Result { + pub fn request( + &self, + request: T, + ) -> impl Future> { + let client_id = self.id; log::debug!( "rpc request start. client_id: {}. name:{}", - self.id, + client_id, T::NAME ); - let response = self.peer.request(self.connection_id()?, request).await; - log::debug!( - "rpc request finish. client_id: {}. name:{}", - self.id, - T::NAME - ); - response + let response = self + .connection_id() + .map(|conn_id| self.peer.request(conn_id, request)); + async move { + let response = response?.await; + log::debug!( + "rpc request finish. client_id: {}. name:{}", + client_id, + T::NAME + ); + response + } } fn respond(&self, receipt: Receipt, response: T::Response) -> Result<()> { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ae3c10495f..465cbac3ca 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4146,21 +4146,49 @@ impl Editor { ) -> Option>> { let editor = workspace.active_item(cx)?.act_as::(cx)?; - let (buffer, position, new_name) = editor.update(cx, |editor, cx| { + let (buffer, range, new_name) = editor.update(cx, |editor, cx| { let (range, new_name) = editor.take_rename(cx)?; - let (buffer, position) = editor - .buffer - .read(cx) - .text_anchor_for_position(range.start.clone(), cx)?; - Some((buffer, position, new_name)) + let buffer = editor.buffer.read(cx); + let (start_buffer, start) = buffer.text_anchor_for_position(range.start.clone(), cx)?; + let (end_buffer, end) = buffer.text_anchor_for_position(range.end.clone(), cx)?; + if start_buffer == end_buffer { + Some((start_buffer, start..end, new_name)) + } else { + None + } })?; let rename = workspace.project().clone().update(cx, |project, cx| { - project.perform_rename(buffer, position, new_name.clone(), true, cx) + project.perform_rename( + buffer.clone(), + range.start.clone(), + new_name.clone(), + true, + cx, + ) }); - Some(cx.spawn(|workspace, cx| async move { + let transaction = buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + buffer.edit([range], &new_name, cx); + if buffer.end_transaction(cx).is_some() { + let transaction = buffer.finalize_last_transaction().unwrap().clone(); + buffer.forget_transaction(transaction.id); + Some(transaction) + } else { + None + } + }); + + Some(cx.spawn(|workspace, mut cx| async move { let project_transaction = rename.await?; + if let Some(transaction) = transaction { + buffer.update(&mut cx, |buffer, cx| { + buffer.push_transaction(transaction, Instant::now()); + buffer.undo(cx); + }); + } Self::open_project_transaction( editor, workspace, From f0a6e8cb9c86caa13e710e7379a1ac5f04a2ee0e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Feb 2022 13:15:22 -0800 Subject: [PATCH 10/15] Use a block decoration for entering rename text Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 138 +++++++++++++++++----------- crates/editor/src/element.rs | 24 +++-- crates/theme/src/theme.rs | 4 + crates/zed/assets/themes/_base.toml | 4 +- crates/zed/assets/themes/black.toml | 2 + crates/zed/assets/themes/dark.toml | 2 + crates/zed/assets/themes/light.toml | 2 + 7 files changed, 113 insertions(+), 63 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 465cbac3ca..cdcd1655e7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -480,7 +480,9 @@ struct SnippetState { struct RenameState { range: Range, - first_transaction: Option, + old_name: String, + editor: ViewHandle, + block_id: BlockId, } struct InvalidationStack(Vec); @@ -3161,6 +3163,26 @@ impl Editor { } pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if let Some((range, column, _, _)) = self.take_rename(cx) { + let snapshot = self.buffer.read(cx).snapshot(cx); + let position = snapshot.clip_point( + range.start.to_point(&snapshot) + Point::new(0, column), + Bias::Left, + ); + self.update_selections( + vec![Selection { + id: self.newest_anchor_selection().id, + start: position, + end: position, + reversed: false, + goal: SelectionGoal::None, + }], + None, + cx, + ); + return; + } + if let Some(context_menu) = self.context_menu.as_mut() { if context_menu.select_prev(cx) { return; @@ -4118,20 +4140,51 @@ impl Editor { let start = offset - lookbehind; let end = offset + lookahead; let rename_range = buffer.anchor_before(start)..buffer.anchor_after(end); + let old_name = buffer.text_for_range(start..end).collect::(); drop(buffer); - this.buffer - .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); - this.pending_rename = Some(RenameState { - range: rename_range.clone(), - first_transaction: None, + let editor = cx.add_view(|cx| { + let mut editor = Editor::single_line(this.build_settings.clone(), cx); + editor + .buffer + .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx)); + editor.select_ranges([0..old_name.len()], None, cx); + editor.highlight_ranges::( + vec![Anchor::min()..Anchor::max()], + settings.style.diff_background_inserted, + cx, + ); + editor }); - this.select_ranges([start..end], None, cx); this.highlight_ranges::( - vec![rename_range], - settings.style.highlighted_line_background, + vec![rename_range.clone()], + settings.style.diff_background_deleted, cx, ); + cx.focus(&editor); + let block_id = this.insert_blocks( + [BlockProperties { + position: rename_range.start.clone(), + height: 1, + render: Arc::new({ + let editor = editor.clone(); + move |cx: &BlockContext| { + ChildView::new(editor.clone()) + .contained() + .with_padding_left(cx.anchor_x) + .boxed() + } + }), + disposition: BlockDisposition::Below, + }], + cx, + )[0]; + this.pending_rename = Some(RenameState { + range: rename_range, + old_name, + editor, + block_id, + }); }); } @@ -4146,13 +4199,13 @@ impl Editor { ) -> Option>> { let editor = workspace.active_item(cx)?.act_as::(cx)?; - let (buffer, range, new_name) = editor.update(cx, |editor, cx| { - let (range, new_name) = editor.take_rename(cx)?; + let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { + let (range, _, old_name, new_name) = editor.take_rename(cx)?; let buffer = editor.buffer.read(cx); let (start_buffer, start) = buffer.text_anchor_for_position(range.start.clone(), cx)?; let (end_buffer, end) = buffer.text_anchor_for_position(range.end.clone(), cx)?; if start_buffer == end_buffer { - Some((start_buffer, start..end, new_name)) + Some((start_buffer, start..end, old_name, new_name)) } else { None } @@ -4168,55 +4221,36 @@ impl Editor { ) }); - let transaction = buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - buffer.edit([range], &new_name, cx); - if buffer.end_transaction(cx).is_some() { - let transaction = buffer.finalize_last_transaction().unwrap().clone(); - buffer.forget_transaction(transaction.id); - Some(transaction) - } else { - None - } - }); - - Some(cx.spawn(|workspace, mut cx| async move { + Some(cx.spawn(|workspace, cx| async move { let project_transaction = rename.await?; - if let Some(transaction) = transaction { - buffer.update(&mut cx, |buffer, cx| { - buffer.push_transaction(transaction, Instant::now()); - buffer.undo(cx); - }); - } Self::open_project_transaction( editor, workspace, project_transaction, - format!("Rename: {}", new_name), + format!("Rename: {} → {}", old_name, new_name), cx, ) .await })) } - fn take_rename(&mut self, cx: &mut ViewContext) -> Option<(Range, String)> { + fn take_rename( + &mut self, + cx: &mut ViewContext, + ) -> Option<(Range, u32, String, String)> { let rename = self.pending_rename.take()?; - let new_name = self - .buffer - .read(cx) - .read(cx) - .text_for_range(rename.range.clone()) - .collect::(); - + let editor = rename.editor.read(cx); + let new_name = editor.text(cx); + let buffer = editor.buffer.read(cx).snapshot(cx); + let rename_position = editor.newest_selection::(&buffer); + self.remove_blocks([rename.block_id].into_iter().collect(), cx); self.clear_highlighted_ranges::(cx); - if let Some(transaction_id) = rename.first_transaction { - self.buffer.update(cx, |buffer, cx| { - buffer.undo_to_transaction(transaction_id, false, cx) - }); - } - - Some((rename.range, new_name)) + Some(( + rename.range, + rename_position.head().column, + rename.old_name, + new_name, + )) } fn invalidate_rename_range( @@ -4722,12 +4756,6 @@ impl Editor { .buffer .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) { - if let Some(rename) = self.pending_rename.as_mut() { - if rename.first_transaction.is_none() { - rename.first_transaction = Some(tx_id); - } - } - if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { *end_selections = Some(self.selections.clone()); } else { @@ -5146,6 +5174,8 @@ impl EditorSettings { gutter_padding_factor: 2., active_line_background: Default::default(), highlighted_line_background: Default::default(), + diff_background_deleted: Default::default(), + diff_background_inserted: Default::default(), line_number: Default::default(), line_number_active: Default::default(), selection: Default::default(), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f72320be42..10a7656f73 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -299,7 +299,7 @@ impl EditorElement { if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() { let mut x = bounds.width() - layout.gutter_padding; let mut y = *row as f32 * layout.line_height - scroll_top; - x += ((layout.gutter_padding + layout.text_offset.x()) - indicator.size().x()) / 2.; + x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; y += (layout.line_height - indicator.size().y()) / 2.; indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); } @@ -321,7 +321,7 @@ impl EditorElement { let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen let max_glyph_width = layout.em_width; let scroll_left = scroll_position.x() * max_glyph_width; - let content_origin = bounds.origin() + layout.text_offset; + let content_origin = bounds.origin() + layout.gutter_margin; cx.scene.push_layer(Some(bounds)); @@ -776,22 +776,24 @@ impl Element for EditorElement { let gutter_padding; let gutter_width; + let gutter_margin; if snapshot.mode == EditorMode::Full { gutter_padding = style.text.em_width(cx.font_cache) * style.gutter_padding_factor; gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; + gutter_margin = -style.text.descent(cx.font_cache); } else { gutter_padding = 0.0; - gutter_width = 0.0 + gutter_width = 0.0; + gutter_margin = 0.0; }; let text_width = size.x() - gutter_width; - let text_offset = vec2f(-style.text.descent(cx.font_cache), 0.); let em_width = style.text.em_width(cx.font_cache); let em_advance = style.text.em_advance(cx.font_cache); let overscroll = vec2f(em_width, 0.); let wrap_width = match self.settings.soft_wrap { SoftWrap::None => None, - SoftWrap::EditorWidth => Some(text_width - text_offset.x() - overscroll.x() - em_width), + SoftWrap::EditorWidth => Some(text_width - gutter_margin - overscroll.x() - em_width), SoftWrap::Column(column) => Some(column as f32 * em_advance), }; let snapshot = self.update_view(cx.app, |view, cx| { @@ -991,7 +993,7 @@ impl Element for EditorElement { gutter_padding, gutter_width, em_width, - gutter_width + text_offset.x(), + gutter_width + gutter_margin, line_height, &style, &line_layouts, @@ -1006,7 +1008,7 @@ impl Element for EditorElement { gutter_size, gutter_padding, text_size, - text_offset, + gutter_margin, snapshot, active_rows, highlighted_rows, @@ -1080,6 +1082,12 @@ impl Element for EditorElement { } } + for (_, block) in &mut layout.blocks { + if block.dispatch_event(event, cx) { + return true; + } + } + match event { Event::LeftMouseDown { position, @@ -1123,6 +1131,7 @@ pub struct LayoutState { scroll_max: Vector2F, gutter_size: Vector2F, gutter_padding: f32, + gutter_margin: f32, text_size: Vector2F, snapshot: EditorSnapshot, active_rows: BTreeMap, @@ -1135,7 +1144,6 @@ pub struct LayoutState { em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, selections: HashMap>>, - text_offset: Vector2F, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index db078cd862..1e63830792 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -278,6 +278,8 @@ pub struct EditorStyle { pub gutter_padding_factor: f32, pub active_line_background: Color, pub highlighted_line_background: Color, + pub diff_background_deleted: Color, + pub diff_background_inserted: Color, pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, @@ -383,6 +385,8 @@ impl InputEditorStyle { gutter_padding_factor: Default::default(), active_line_background: Default::default(), highlighted_line_background: Default::default(), + diff_background_deleted: Default::default(), + diff_background_inserted: Default::default(), line_number: Default::default(), line_number_active: Default::default(), guest_selections: Default::default(), diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 389f457107..5d03b96e74 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -188,7 +188,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -248,6 +248,8 @@ gutter_background = "$surface.1" gutter_padding_factor = 2.5 active_line_background = "$state.active_line" highlighted_line_background = "$state.highlighted_line" +diff_background_deleted = "$state.deleted_line" +diff_background_inserted = "$state.inserted_line" line_number = "$text.2.color" line_number_active = "$text.0.color" selection = "$selection.host" diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml index bc9f6c75d6..8bc60207ae 100644 --- a/crates/zed/assets/themes/black.toml +++ b/crates/zed/assets/themes/black.toml @@ -39,6 +39,8 @@ bad = "#b7372e" [state] active_line = "#161313" highlighted_line = "#faca5033" +deleted_line = "#dd000022" +inserted_line = "#00dd0022" hover = "#00000033" selected = "#00000088" diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml index acfbf083c0..678127e21f 100644 --- a/crates/zed/assets/themes/dark.toml +++ b/crates/zed/assets/themes/dark.toml @@ -39,6 +39,8 @@ bad = "#b7372e" [state] active_line = "#00000022" highlighted_line = "#faca5033" +deleted_line = "#dd000044" +inserted_line = "#00dd0044" hover = "#00000033" selected = "#00000088" diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml index cf8ebe34e6..934dc59a6d 100644 --- a/crates/zed/assets/themes/light.toml +++ b/crates/zed/assets/themes/light.toml @@ -39,6 +39,8 @@ bad = "#b7372e" [state] active_line = "#00000008" highlighted_line = "#faca5033" +deleted_line = "#dd000044" +inserted_line = "#00dd0044" hover = "#0000000D" selected = "#0000001c" From 80bca57bfaa3d1425fff9e28ea84c63cdd464f45 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Feb 2022 15:42:52 -0800 Subject: [PATCH 11/15] Tweak behavior of selections when renaming --- crates/editor/src/editor.rs | 167 +++++++++++++++++----------- crates/server/src/rpc.rs | 19 ++-- crates/zed/assets/themes/black.toml | 6 +- crates/zed/assets/themes/dark.toml | 6 +- crates/zed/assets/themes/light.toml | 6 +- 5 files changed, 124 insertions(+), 80 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cdcd1655e7..a14f37306a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -478,10 +478,10 @@ struct SnippetState { active_index: usize, } -struct RenameState { - range: Range, - old_name: String, - editor: ViewHandle, +pub struct RenameState { + pub range: Range, + pub old_name: String, + pub editor: ViewHandle, block_id: BlockId, } @@ -3163,23 +3163,7 @@ impl Editor { } pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { - if let Some((range, column, _, _)) = self.take_rename(cx) { - let snapshot = self.buffer.read(cx).snapshot(cx); - let position = snapshot.clip_point( - range.start.to_point(&snapshot) + Point::new(0, column), - Bias::Left, - ); - self.update_selections( - vec![Selection { - id: self.newest_anchor_selection().id, - start: position, - end: position, - reversed: false, - goal: SelectionGoal::None, - }], - None, - cx, - ); + if self.take_rename(cx).is_some() { return; } @@ -3227,6 +3211,8 @@ impl Editor { } pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + self.take_rename(cx); + if let Some(context_menu) = self.context_menu.as_mut() { if context_menu.select_next(cx) { return; @@ -4116,39 +4102,57 @@ impl Editor { use language::ToOffset as _; let project = self.project.clone()?; - let position = self.newest_anchor_selection().head(); - let (buffer, buffer_position) = self + let selection = self.newest_anchor_selection().clone(); + let (cursor_buffer, cursor_buffer_position) = self .buffer .read(cx) - .text_anchor_for_position(position.clone(), cx)?; - let snapshot = buffer.read(cx).snapshot(); + .text_anchor_for_position(selection.head(), cx)?; + let (tail_buffer, tail_buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(selection.tail(), cx)?; + if tail_buffer != cursor_buffer { + return None; + } + + let snapshot = cursor_buffer.read(cx).snapshot(); + let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); + let tail_buffer_offset = tail_buffer_position.to_offset(&snapshot); let prepare_rename = project.update(cx, |project, cx| { - project.prepare_rename(buffer.clone(), buffer_position.to_offset(&snapshot), cx) + project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx) }); Some(cx.spawn(|this, mut cx| async move { - if let Some(range) = prepare_rename.await? { - let buffer_offset_range = range.to_offset(&snapshot); - let buffer_offset = buffer_position.to_offset(&snapshot); - let lookbehind = buffer_offset.saturating_sub(buffer_offset_range.start); - let lookahead = buffer_offset_range.end.saturating_sub(buffer_offset); + if let Some(rename_range) = prepare_rename.await? { + let rename_buffer_range = rename_range.to_offset(&snapshot); + let cursor_offset_in_rename_range = + cursor_buffer_offset.saturating_sub(rename_buffer_range.start); + let tail_offset_in_rename_range = + tail_buffer_offset.saturating_sub(rename_buffer_range.start); this.update(&mut cx, |this, cx| { let settings = (this.build_settings)(cx); let buffer = this.buffer.read(cx).read(cx); - let offset = position.to_offset(&buffer); - let start = offset - lookbehind; - let end = offset + lookahead; - let rename_range = buffer.anchor_before(start)..buffer.anchor_after(end); - let old_name = buffer.text_for_range(start..end).collect::(); + let cursor_offset = selection.head().to_offset(&buffer); + let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); + let rename_end = rename_start + rename_buffer_range.len(); + let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); + let old_name = buffer + .text_for_range(rename_start..rename_end) + .collect::(); drop(buffer); - let editor = cx.add_view(|cx| { + // Position the selection in the rename editor so that it matches the current selection. + let rename_editor = cx.add_view(|cx| { let mut editor = Editor::single_line(this.build_settings.clone(), cx); editor .buffer .update(cx, |buffer, cx| buffer.edit([0..0], &old_name, cx)); - editor.select_ranges([0..old_name.len()], None, cx); + editor.select_ranges( + [tail_offset_in_rename_range..cursor_offset_in_rename_range], + None, + cx, + ); editor.highlight_ranges::( vec![Anchor::min()..Anchor::max()], settings.style.diff_background_inserted, @@ -4157,17 +4161,28 @@ impl Editor { editor }); this.highlight_ranges::( - vec![rename_range.clone()], + vec![range.clone()], settings.style.diff_background_deleted, cx, ); - cx.focus(&editor); + this.update_selections( + vec![Selection { + id: selection.id, + start: rename_end, + end: rename_end, + reversed: false, + goal: SelectionGoal::None, + }], + None, + cx, + ); + cx.focus(&rename_editor); let block_id = this.insert_blocks( [BlockProperties { - position: rename_range.start.clone(), + position: range.start.clone(), height: 1, render: Arc::new({ - let editor = editor.clone(); + let editor = rename_editor.clone(); move |cx: &BlockContext| { ChildView::new(editor.clone()) .contained() @@ -4180,9 +4195,9 @@ impl Editor { cx, )[0]; this.pending_rename = Some(RenameState { - range: rename_range, + range, old_name, - editor, + editor: rename_editor, block_id, }); }); @@ -4200,12 +4215,15 @@ impl Editor { let editor = workspace.active_item(cx)?.act_as::(cx)?; let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| { - let (range, _, old_name, new_name) = editor.take_rename(cx)?; + let rename = editor.take_rename(cx)?; let buffer = editor.buffer.read(cx); - let (start_buffer, start) = buffer.text_anchor_for_position(range.start.clone(), cx)?; - let (end_buffer, end) = buffer.text_anchor_for_position(range.end.clone(), cx)?; + let (start_buffer, start) = + buffer.text_anchor_for_position(rename.range.start.clone(), cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(rename.range.end.clone(), cx)?; if start_buffer == end_buffer { - Some((start_buffer, start..end, old_name, new_name)) + let new_name = rename.editor.read(cx).text(cx); + Some((start_buffer, start..end, rename.old_name, new_name)) } else { None } @@ -4234,23 +4252,38 @@ impl Editor { })) } - fn take_rename( - &mut self, - cx: &mut ViewContext, - ) -> Option<(Range, u32, String, String)> { + fn take_rename(&mut self, cx: &mut ViewContext) -> Option { let rename = self.pending_rename.take()?; - let editor = rename.editor.read(cx); - let new_name = editor.text(cx); - let buffer = editor.buffer.read(cx).snapshot(cx); - let rename_position = editor.newest_selection::(&buffer); self.remove_blocks([rename.block_id].into_iter().collect(), cx); self.clear_highlighted_ranges::(cx); - Some(( - rename.range, - rename_position.head().column, - rename.old_name, - new_name, - )) + + let editor = rename.editor.read(cx); + let buffer = editor.buffer.read(cx).snapshot(cx); + let selection = editor.newest_selection::(&buffer); + + // Update the selection to match the position of the selection inside + // the rename editor. + let snapshot = self.buffer.read(cx).snapshot(cx); + let rename_range = rename.range.to_offset(&snapshot); + let start = snapshot + .clip_offset(rename_range.start + selection.start, Bias::Left) + .min(rename_range.end); + let end = snapshot + .clip_offset(rename_range.start + selection.end, Bias::Left) + .min(rename_range.end); + self.update_selections( + vec![Selection { + id: self.newest_anchor_selection().id, + start, + end, + reversed: selection.reversed, + goal: SelectionGoal::None, + }], + None, + cx, + ); + + Some(rename) } fn invalidate_rename_range( @@ -4266,11 +4299,17 @@ impl Editor { return; } } - - self.take_rename(cx); + let rename = self.pending_rename.take().unwrap(); + self.remove_blocks([rename.block_id].into_iter().collect(), cx); + self.clear_highlighted_ranges::(cx); } } + #[cfg(any(test, feature = "test-support"))] + pub fn pending_rename(&self) -> Option<&RenameState> { + self.pending_rename.as_ref() + } + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { let buffer = self.buffer.read(cx).snapshot(cx); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 35330cfc2f..c6a0ef2be6 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1153,7 +1153,7 @@ mod tests { }, editor::{ self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, EditorSettings, - Input, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, + Input, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }, fs::{FakeFs, Fs as _}, language::{ @@ -3140,12 +3140,17 @@ mod tests { .unwrap(); prepare_rename.await.unwrap(); editor_b.update(&mut cx_b, |editor, cx| { - assert_eq!(editor.selected_ranges(cx), [6..9]); - editor.handle_input(&Input("T".to_string()), cx); - editor.handle_input(&Input("H".to_string()), cx); - editor.handle_input(&Input("R".to_string()), cx); - editor.handle_input(&Input("E".to_string()), cx); - editor.handle_input(&Input("E".to_string()), cx); + let rename = editor.pending_rename().unwrap(); + let buffer = editor.buffer().read(cx).snapshot(cx); + assert_eq!( + rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer), + 6..9 + ); + rename.editor.update(cx, |rename_editor, cx| { + rename_editor.buffer().update(cx, |rename_buffer, cx| { + rename_buffer.edit([0..3], "THREE", cx); + }); + }); }); let confirm_rename = workspace_b.update(&mut cx_b, |workspace, cx| { diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml index 8bc60207ae..769076645f 100644 --- a/crates/zed/assets/themes/black.toml +++ b/crates/zed/assets/themes/black.toml @@ -19,7 +19,7 @@ extends = "_base" 0 = "#00000052" [selection] -host = { selection = "#3B57BC33", cursor = "$text.0.color" } +host = { selection = "#3B57BC55", cursor = "$text.0.color" } guests = [ { selection = "#FDF35133", cursor = "#FDF351" }, { selection = "#4EACAD33", cursor = "#4EACAD" }, @@ -39,8 +39,8 @@ bad = "#b7372e" [state] active_line = "#161313" highlighted_line = "#faca5033" -deleted_line = "#dd000022" -inserted_line = "#00dd0022" +deleted_line = "#dd000036" +inserted_line = "#00dd0036" hover = "#00000033" selected = "#00000088" diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml index 678127e21f..ed6deed040 100644 --- a/crates/zed/assets/themes/dark.toml +++ b/crates/zed/assets/themes/dark.toml @@ -19,7 +19,7 @@ extends = "_base" 0 = "#00000052" [selection] -host = { selection = "#3B57BC33", cursor = "$text.0.color" } +host = { selection = "#3B57BC55", cursor = "$text.0.color" } guests = [ { selection = "#FDF35133", cursor = "#FDF351" }, { selection = "#4EACAD33", cursor = "#4EACAD" }, @@ -39,8 +39,8 @@ bad = "#b7372e" [state] active_line = "#00000022" highlighted_line = "#faca5033" -deleted_line = "#dd000044" -inserted_line = "#00dd0044" +deleted_line = "#dd000036" +inserted_line = "#00dd0036" hover = "#00000033" selected = "#00000088" diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml index 934dc59a6d..f51b3f4656 100644 --- a/crates/zed/assets/themes/light.toml +++ b/crates/zed/assets/themes/light.toml @@ -19,7 +19,7 @@ extends = "_base" 0 = "#0000000D" [selection] -host = { selection = "#3B57BC33", cursor = "$text.0.color" } +host = { selection = "#3B57BC55", cursor = "$text.0.color" } guests = [ { selection = "#D0453B33", cursor = "#D0453B" }, { selection = "#3B874B33", cursor = "#3B874B" }, @@ -39,8 +39,8 @@ bad = "#b7372e" [state] active_line = "#00000008" highlighted_line = "#faca5033" -deleted_line = "#dd000044" -inserted_line = "#00dd0044" +deleted_line = "#dd000036" +inserted_line = "#00dd0036" hover = "#0000000D" selected = "#0000001c" From de87fa58f6fae876a340bbd65b2023e2669d9646 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Feb 2022 16:07:30 -0800 Subject: [PATCH 12/15] Use async_trait for LspCommand --- crates/project/src/lsp_command.rs | 152 ++++++++++++++---------------- 1 file changed, 71 insertions(+), 81 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d9855875ac..b6ca563efe 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,7 +1,7 @@ use crate::{Project, ProjectTransaction}; use anyhow::{anyhow, Result}; +use async_trait::async_trait; use client::{proto, PeerId}; -use futures::{future::LocalBoxFuture, FutureExt}; use gpui::{AppContext, AsyncAppContext, ModelContext, ModelHandle}; use language::{ proto::deserialize_anchor, range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, @@ -9,6 +9,7 @@ use language::{ }; use std::{ops::Range, path::Path}; +#[async_trait(?Send)] pub(crate) trait LspCommand: 'static + Sized { type Response: 'static + Default + Send; type LspRequest: 'static + Send + lsp::request::Request; @@ -19,13 +20,13 @@ pub(crate) trait LspCommand: 'static + Sized { path: &Path, cx: &AppContext, ) -> ::Params; - fn response_from_lsp( + async fn response_from_lsp( self, message: ::Result, project: ModelHandle, buffer: ModelHandle, cx: AsyncAppContext, - ) -> LocalBoxFuture<'static, Result>; + ) -> Result; fn to_proto( &self, @@ -48,26 +49,26 @@ pub(crate) trait LspCommand: 'static + Sized { buffer_version: &clock::Global, cx: &mut ModelContext, ) -> ::Response; - fn response_from_proto( + async fn response_from_proto( self, message: ::Response, project: ModelHandle, buffer: ModelHandle, cx: AsyncAppContext, - ) -> LocalBoxFuture<'static, Result>; + ) -> Result; } pub(crate) struct PrepareRename { pub position: PointUtf16, } -#[derive(Debug)] pub(crate) struct PerformRename { pub position: PointUtf16, pub new_name: String, pub push_to_history: bool, } +#[async_trait(?Send)] impl LspCommand for PrepareRename { type Response = Option>; type LspRequest = lsp::request::PrepareRenameRequest; @@ -82,31 +83,28 @@ impl LspCommand for PrepareRename { } } - fn response_from_lsp( + async fn response_from_lsp( self, message: Option, _: ModelHandle, buffer: ModelHandle, cx: AsyncAppContext, - ) -> LocalBoxFuture<'static, Result>>> { - async move { - Ok(message.and_then(|result| match result { + ) -> Result>> { + buffer.read_with(&cx, |buffer, _| { + if let Some( lsp::PrepareRenameResponse::Range(range) - | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. } => buffer - .read_with(&cx, |buffer, _| { - let range = range_from_lsp(range); - if buffer.clip_point_utf16(range.start, Bias::Left) == range.start - && buffer.clip_point_utf16(range.end, Bias::Left) == range.end - { - Some(buffer.anchor_after(range.start)..buffer.anchor_before(range.end)) - } else { - None - } - }), - _ => None, - })) - } - .boxed_local() + | lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }, + ) = message + { + let Range { start, end } = range_from_lsp(range); + if buffer.clip_point_utf16(start, Bias::Left) == start + && buffer.clip_point_utf16(end, Bias::Left) == end + { + return Ok(Some(buffer.anchor_after(start)..buffer.anchor_before(end))); + } + } + Ok(None) + }) } fn to_proto( @@ -166,31 +164,29 @@ impl LspCommand for PrepareRename { } } - fn response_from_proto( + async fn response_from_proto( self, message: proto::PrepareRenameResponse, _: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> LocalBoxFuture<'static, Result>>> { - async move { - if message.can_rename { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(message.version.into()) - }) - .await; - let start = message.start.and_then(deserialize_anchor); - let end = message.end.and_then(deserialize_anchor); - Ok(start.zip(end).map(|(start, end)| start..end)) - } else { - Ok(None) - } + ) -> Result>> { + if message.can_rename { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(message.version.into()) + }) + .await; + let start = message.start.and_then(deserialize_anchor); + let end = message.end.and_then(deserialize_anchor); + Ok(start.zip(end).map(|(start, end)| start..end)) + } else { + Ok(None) } - .boxed_local() } } +#[async_trait(?Send)] impl LspCommand for PerformRename { type Response = ProjectTransaction; type LspRequest = lsp::request::Rename; @@ -209,39 +205,36 @@ impl LspCommand for PerformRename { } } - fn response_from_lsp( + async fn response_from_lsp( self, message: Option, project: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> LocalBoxFuture<'static, Result> { - async move { - if let Some(edit) = message { - let (language_name, language_server) = buffer.read_with(&cx, |buffer, _| { - let language = buffer - .language() - .ok_or_else(|| anyhow!("buffer's language was removed"))?; - let language_server = buffer - .language_server() - .cloned() - .ok_or_else(|| anyhow!("buffer's language server was removed"))?; - Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) - })?; - Project::deserialize_workspace_edit( - project, - edit, - self.push_to_history, - language_name, - language_server, - &mut cx, - ) - .await - } else { - Ok(ProjectTransaction::default()) - } + ) -> Result { + if let Some(edit) = message { + let (language_name, language_server) = buffer.read_with(&cx, |buffer, _| { + let language = buffer + .language() + .ok_or_else(|| anyhow!("buffer's language was removed"))?; + let language_server = buffer + .language_server() + .cloned() + .ok_or_else(|| anyhow!("buffer's language server was removed"))?; + Ok::<_, anyhow::Error>((language.name().to_string(), language_server)) + })?; + Project::deserialize_workspace_edit( + project, + edit, + self.push_to_history, + language_name, + language_server, + &mut cx, + ) + .await + } else { + Ok(ProjectTransaction::default()) } - .boxed_local() } fn to_proto( @@ -300,23 +293,20 @@ impl LspCommand for PerformRename { } } - fn response_from_proto( + async fn response_from_proto( self, message: proto::PerformRenameResponse, project: ModelHandle, _: ModelHandle, mut cx: AsyncAppContext, - ) -> LocalBoxFuture<'static, Result> { - async move { - let message = message - .transaction - .ok_or_else(|| anyhow!("missing transaction"))?; - project - .update(&mut cx, |project, cx| { - project.deserialize_project_transaction(message, self.push_to_history, cx) - }) - .await - } - .boxed_local() + ) -> Result { + let message = message + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + project + .update(&mut cx, |project, cx| { + project.deserialize_project_transaction(message, self.push_to_history, cx) + }) + .await } } From aece541255fafe54c4c1331f80429bf0173be12c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Feb 2022 16:45:52 -0800 Subject: [PATCH 13/15] Handle GetDefinition via LspCommand trait --- crates/project/src/lsp_command.rs | 249 +++++++++++++++++++++++------- crates/project/src/project.rs | 192 ++--------------------- 2 files changed, 204 insertions(+), 237 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index b6ca563efe..432963a57f 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,11 +1,12 @@ -use crate::{Project, ProjectTransaction}; +use crate::{Definition, Project, ProjectTransaction}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use client::{proto, PeerId}; -use gpui::{AppContext, AsyncAppContext, ModelContext, ModelHandle}; +use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ - proto::deserialize_anchor, range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, - ToPointUtf16, + point_from_lsp, + proto::{deserialize_anchor, serialize_anchor}, + range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16, }; use std::{ops::Range, path::Path}; @@ -28,26 +29,18 @@ pub(crate) trait LspCommand: 'static + Sized { cx: AsyncAppContext, ) -> Result; - fn to_proto( - &self, - project_id: u64, - buffer: &ModelHandle, - cx: &AppContext, - ) -> Self::ProtoRequest; + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest; fn from_proto( message: Self::ProtoRequest, project: &mut Project, - buffer: &ModelHandle, - cx: &mut ModelContext, + buffer: &Buffer, ) -> Result; - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; - fn response_to_proto( response: Self::Response, project: &mut Project, peer_id: PeerId, buffer_version: &clock::Global, - cx: &mut ModelContext, + cx: &AppContext, ) -> ::Response; async fn response_from_proto( self, @@ -56,6 +49,7 @@ pub(crate) trait LspCommand: 'static + Sized { buffer: ModelHandle, cx: AsyncAppContext, ) -> Result; + fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; } pub(crate) struct PrepareRename { @@ -68,6 +62,10 @@ pub(crate) struct PerformRename { pub push_to_history: bool, } +pub(crate) struct GetDefinition { + pub position: PointUtf16, +} + #[async_trait(?Send)] impl LspCommand for PrepareRename { type Response = Option>; @@ -107,32 +105,17 @@ impl LspCommand for PrepareRename { }) } - fn to_proto( - &self, - project_id: u64, - buffer: &ModelHandle, - cx: &AppContext, - ) -> proto::PrepareRename { + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename { proto::PrepareRename { project_id, - buffer_id: buffer.read(cx).remote_id(), + buffer_id: buffer.remote_id(), position: Some(language::proto::serialize_anchor( - &buffer.read(cx).anchor_before(self.position), + &buffer.anchor_before(self.position), )), } } - fn buffer_id_from_proto(message: &proto::PrepareRename) -> u64 { - message.buffer_id - } - - fn from_proto( - message: proto::PrepareRename, - _: &mut Project, - buffer: &ModelHandle, - cx: &mut ModelContext, - ) -> Result { - let buffer = buffer.read(cx); + fn from_proto(message: proto::PrepareRename, _: &mut Project, buffer: &Buffer) -> Result { let position = message .position .and_then(deserialize_anchor) @@ -150,7 +133,7 @@ impl LspCommand for PrepareRename { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - _: &mut ModelContext, + _: &AppContext, ) -> proto::PrepareRenameResponse { proto::PrepareRenameResponse { can_rename: range.is_some(), @@ -184,6 +167,10 @@ impl LspCommand for PrepareRename { Ok(None) } } + + fn buffer_id_from_proto(message: &proto::PrepareRename) -> u64 { + message.buffer_id + } } #[async_trait(?Send)] @@ -237,17 +224,10 @@ impl LspCommand for PerformRename { } } - fn to_proto( - &self, - project_id: u64, - buffer: &ModelHandle, - cx: &AppContext, - ) -> proto::PerformRename { - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename { proto::PerformRename { project_id, - buffer_id, + buffer_id: buffer.remote_id(), position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), @@ -255,21 +235,11 @@ impl LspCommand for PerformRename { } } - fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 { - message.buffer_id - } - - fn from_proto( - message: proto::PerformRename, - _: &mut Project, - buffer: &ModelHandle, - cx: &mut ModelContext, - ) -> Result { + fn from_proto(message: proto::PerformRename, _: &mut Project, buffer: &Buffer) -> Result { let position = message .position .and_then(deserialize_anchor) .ok_or_else(|| anyhow!("invalid position"))?; - let buffer = buffer.read(cx); if !buffer.can_resolve(&position) { Err(anyhow!("cannot resolve position"))?; } @@ -285,7 +255,7 @@ impl LspCommand for PerformRename { project: &mut Project, peer_id: PeerId, _: &clock::Global, - cx: &mut ModelContext, + cx: &AppContext, ) -> proto::PerformRenameResponse { let transaction = project.serialize_project_transaction_for_peer(response, peer_id, cx); proto::PerformRenameResponse { @@ -309,4 +279,171 @@ impl LspCommand for PerformRename { }) .await } + + fn buffer_id_from_proto(message: &proto::PerformRename) -> u64 { + message.buffer_id + } +} + +#[async_trait(?Send)] +impl LspCommand for GetDefinition { + type Response = Vec; + type LspRequest = lsp::request::GotoDefinition; + type ProtoRequest = proto::GetDefinition; + + fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::GotoDefinitionParams { + lsp::GotoDefinitionParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + position: self.position.to_lsp_position(), + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option, + project: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut definitions = Vec::new(); + let (language, language_server) = buffer + .read_with(&cx, |buffer, _| { + buffer + .language() + .cloned() + .zip(buffer.language_server().cloned()) + }) + .ok_or_else(|| anyhow!("buffer no longer has language server"))?; + + if let Some(message) = message { + let mut unresolved_locations = Vec::new(); + match message { + lsp::GotoDefinitionResponse::Scalar(loc) => { + unresolved_locations.push((loc.uri, loc.range)); + } + lsp::GotoDefinitionResponse::Array(locs) => { + unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range))); + } + lsp::GotoDefinitionResponse::Link(links) => { + unresolved_locations.extend( + links + .into_iter() + .map(|l| (l.target_uri, l.target_selection_range)), + ); + } + } + + for (target_uri, target_range) in unresolved_locations { + let target_buffer_handle = project + .update(&mut cx, |this, cx| { + this.open_local_buffer_from_lsp_path( + target_uri, + language.name().to_string(), + language_server.clone(), + cx, + ) + }) + .await?; + + cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer + .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); + let target_end = target_buffer + .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); + definitions.push(Definition { + target_buffer: target_buffer_handle, + target_range: target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end), + }); + }); + } + } + + Ok(definitions) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { + proto::GetDefinition { + project_id, + buffer_id: buffer.remote_id(), + position: Some(language::proto::serialize_anchor( + &buffer.anchor_before(self.position), + )), + } + } + + fn from_proto(message: proto::GetDefinition, _: &mut Project, buffer: &Buffer) -> Result { + let position = message + .position + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("invalid position"))?; + if !buffer.can_resolve(&position) { + Err(anyhow!("cannot resolve position"))?; + } + Ok(Self { + position: position.to_point_utf16(buffer), + }) + } + + fn response_to_proto( + response: Vec, + project: &mut Project, + peer_id: PeerId, + _: &clock::Global, + cx: &AppContext, + ) -> proto::GetDefinitionResponse { + let definitions = response + .into_iter() + .map(|definition| { + let buffer = + project.serialize_buffer_for_peer(&definition.target_buffer, peer_id, cx); + proto::Definition { + target_start: Some(serialize_anchor(&definition.target_range.start)), + target_end: Some(serialize_anchor(&definition.target_range.end)), + buffer: Some(buffer), + } + }) + .collect(); + proto::GetDefinitionResponse { definitions } + } + + async fn response_from_proto( + self, + message: proto::GetDefinitionResponse, + project: ModelHandle, + _: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + let mut definitions = Vec::new(); + for definition in message.definitions { + let buffer = definition.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + let target_buffer = project + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let target_start = definition + .target_start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let target_end = definition + .target_end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + definitions.push(Definition { + target_buffer, + target_range: target_start..target_end, + }) + } + Ok(definitions) + } + + fn buffer_id_from_proto(message: &proto::GetDefinition) -> u64 { + message.buffer_id + } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 340d146d11..9a231b707b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -14,8 +14,6 @@ use gpui::{ UpgradeModelHandle, WeakModelHandle, }; use language::{ - point_from_lsp, - proto::{deserialize_anchor, serialize_anchor}, range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, Completion, CompletionLabel, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, @@ -183,9 +181,9 @@ impl Project { client.add_entity_request_handler(Self::handle_format_buffers); client.add_entity_request_handler(Self::handle_get_code_actions); client.add_entity_request_handler(Self::handle_get_completions); - client.add_entity_request_handler(Self::handle_get_definition); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_open_buffer); client.add_entity_request_handler(Self::handle_save_buffer); } @@ -1175,137 +1173,12 @@ impl Project { pub fn definition( &self, - source_buffer_handle: &ModelHandle, + buffer: &ModelHandle, position: T, cx: &mut ModelContext, ) -> Task>> { - let source_buffer_handle = source_buffer_handle.clone(); - let source_buffer = source_buffer_handle.read(cx); - let worktree; - let buffer_abs_path; - if let Some(file) = File::from_dyn(source_buffer.file()) { - worktree = file.worktree.clone(); - buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); - } else { - return Task::ready(Ok(Default::default())); - }; - - let position = position.to_point_utf16(source_buffer); - - if worktree.read(cx).as_local().is_some() { - let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_name; - let lang_server; - if let Some(lang) = source_buffer.language() { - lang_name = lang.name().to_string(); - if let Some(server) = self - .language_servers - .get(&(worktree.read(cx).id(), lang_name.clone())) - { - lang_server = server.clone(); - } else { - return Task::ready(Ok(Default::default())); - }; - } else { - return Task::ready(Ok(Default::default())); - } - - cx.spawn(|this, mut cx| async move { - let response = lang_server - .request::(lsp::GotoDefinitionParams { - text_document_position_params: lsp::TextDocumentPositionParams { - text_document: lsp::TextDocumentIdentifier::new( - lsp::Url::from_file_path(&buffer_abs_path).unwrap(), - ), - position: lsp::Position::new(position.row, position.column), - }, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - .await?; - - let mut definitions = Vec::new(); - if let Some(response) = response { - let mut unresolved_locations = Vec::new(); - match response { - lsp::GotoDefinitionResponse::Scalar(loc) => { - unresolved_locations.push((loc.uri, loc.range)); - } - lsp::GotoDefinitionResponse::Array(locs) => { - unresolved_locations.extend(locs.into_iter().map(|l| (l.uri, l.range))); - } - lsp::GotoDefinitionResponse::Link(links) => { - unresolved_locations.extend( - links - .into_iter() - .map(|l| (l.target_uri, l.target_selection_range)), - ); - } - } - - for (target_uri, target_range) in unresolved_locations { - let target_buffer_handle = this - .update(&mut cx, |this, cx| { - this.open_local_buffer_from_lsp_path( - target_uri, - lang_name.clone(), - lang_server.clone(), - cx, - ) - }) - .await?; - - cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - definitions.push(Definition { - target_buffer: target_buffer_handle, - target_range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }); - }); - } - } - - Ok(definitions) - }) - } else if let Some(project_id) = self.remote_id() { - let client = self.client.clone(); - let request = proto::GetDefinition { - project_id, - buffer_id: source_buffer.remote_id(), - position: Some(serialize_anchor(&source_buffer.anchor_before(position))), - }; - cx.spawn(|this, mut cx| async move { - let response = client.request(request).await?; - let mut definitions = Vec::new(); - for definition in response.definitions { - let buffer = definition.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - let target_buffer = this - .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) - .await?; - let target_start = definition - .target_start - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target start"))?; - let target_end = definition - .target_end - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("missing target end"))?; - definitions.push(Definition { - target_buffer, - target_range: target_start..target_end, - }) - } - - Ok(definitions) - }) - } else { - Task::ready(Ok(Default::default())) - } + let position = position.to_point_utf16(buffer.read(cx)); + self.request_lsp(buffer.clone(), GetDefinition { position }, cx) } pub fn completions( @@ -1861,8 +1734,8 @@ impl Project { where ::Result: Send, { + let buffer = buffer_handle.read(cx); if self.is_local() { - let buffer = buffer_handle.read(cx); let file = File::from_dyn(buffer.file()).and_then(File::as_local); if let Some((file, language_server)) = file.zip(buffer.language_server().cloned()) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); @@ -1878,7 +1751,7 @@ impl Project { } } else if let Some(project_id) = self.remote_id() { let rpc = self.client.clone(); - let message = request.to_proto(project_id, &buffer_handle, cx); + let message = request.to_proto(project_id, buffer); return cx.spawn(|this, cx| async move { let response = rpc.request(message).await?; request @@ -2578,50 +2451,6 @@ impl Project { }) } - async fn handle_get_definition( - this: ModelHandle, - envelope: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result { - let sender_id = envelope.original_sender_id()?; - let position = envelope - .payload - .position - .and_then(deserialize_anchor) - .ok_or_else(|| anyhow!("invalid position"))?; - let definitions = this.update(&mut cx, |this, cx| { - let source_buffer = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) - .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; - if source_buffer.read(cx).can_resolve(&position) { - Ok(this.definition(&source_buffer, position, cx)) - } else { - Err(anyhow!("cannot resolve position")) - } - })?; - - let definitions = definitions.await?; - - this.update(&mut cx, |this, cx| { - let mut response = proto::GetDefinitionResponse { - definitions: Default::default(), - }; - for definition in definitions { - let buffer = - this.serialize_buffer_for_peer(&definition.target_buffer, sender_id, cx); - response.definitions.push(proto::Definition { - target_start: Some(serialize_anchor(&definition.target_range.start)), - target_end: Some(serialize_anchor(&definition.target_range.end)), - buffer: Some(buffer), - }); - } - Ok(response) - }) - } - async fn handle_lsp_command( this: ModelHandle, envelope: TypedEnvelope, @@ -2639,8 +2468,9 @@ impl Project { .get(&sender_id) .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; - let buffer_version = buffer_handle.read(cx).version(); - let request = T::from_proto(envelope.payload, this, &buffer_handle, cx)?; + let buffer = buffer_handle.read(cx); + let buffer_version = buffer.version(); + let request = T::from_proto(envelope.payload, this, buffer)?; Ok::<_, anyhow::Error>((this.request_lsp(buffer_handle, request, cx), buffer_version)) })?; let response = request.await?; From b573a39cbc4375b33afb2e2d88155069e3fc030f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Feb 2022 17:00:57 -0800 Subject: [PATCH 14/15] Ensure we never leak block decorations when renaming --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a14f37306a..ebe24da4e2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4131,6 +4131,7 @@ impl Editor { tail_buffer_offset.saturating_sub(rename_buffer_range.start); this.update(&mut cx, |this, cx| { + this.take_rename(cx); let settings = (this.build_settings)(cx); let buffer = this.buffer.read(cx).read(cx); let cursor_offset = selection.head().to_offset(&buffer); From cf7cc83f8548288dc8ad3d97ed59e673cec57ede Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Feb 2022 10:52:21 +0100 Subject: [PATCH 15/15] Simplify `undo_to_transaction` and `redo_to_transaction` We don't need to mutate the history anymore now that we render pending renames with a block decoration. --- crates/editor/src/multi_buffer.rs | 59 +------------------------- crates/language/src/buffer.rs | 6 +-- crates/text/src/text.rs | 70 +++++++++++-------------------- crates/util/src/lib.rs | 35 +--------------- 4 files changed, 29 insertions(+), 141 deletions(-) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a32e3b2307..4fc4488af9 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -27,7 +27,6 @@ use text::{ AnchorRangeExt as _, Edit, Point, PointUtf16, TextSummary, }; use theme::SyntaxTheme; -use util::CowMut; const NEWLINES: &'static [u8] = &[b'\n'; u8::MAX as usize]; @@ -566,7 +565,7 @@ impl MultiBuffer { if let Some(entry) = buffer.peek_undo_stack() { *buffer_transaction_id = entry.transaction_id(); } - buffer.undo_to_transaction(undo_to, true, cx) + buffer.undo_to_transaction(undo_to, cx) }); } } @@ -579,35 +578,6 @@ impl MultiBuffer { None } - pub fn undo_to_transaction( - &mut self, - transaction_id: TransactionId, - push_redo: bool, - cx: &mut ModelContext, - ) -> bool { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| { - buffer.undo_to_transaction(transaction_id, push_redo, cx) - }); - } - - let mut undone = false; - for transaction in &mut *self.history.remove_from_undo(transaction_id, push_redo) { - 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, true, cx) - }); - } - } - } - undone - } - pub fn redo(&mut self, cx: &mut ModelContext) -> Option { if let Some(buffer) = self.as_singleton() { return buffer.update(cx, |buffer, cx| buffer.redo(cx)); @@ -622,7 +592,7 @@ impl MultiBuffer { if let Some(entry) = buffer.peek_redo_stack() { *buffer_transaction_id = entry.transaction_id(); } - buffer.redo_to_transaction(redo_to, true, cx) + buffer.redo_to_transaction(redo_to, cx) }); } } @@ -2345,31 +2315,6 @@ impl History { } } - fn remove_from_undo( - &mut self, - transaction_id: TransactionId, - push_redo: bool, - ) -> CowMut<[Transaction]> { - assert_eq!(self.transaction_depth, 0); - - if let Some(entry_ix) = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - let transactions = self.undo_stack.drain(entry_ix..).rev(); - if push_redo { - let redo_stack_start_len = self.redo_stack.len(); - self.redo_stack.extend(transactions); - CowMut::Borrowed(&mut self.redo_stack[redo_stack_start_len..]) - } else { - CowMut::Owned(transactions.collect()) - } - } else { - CowMut::Owned(Default::default()) - } - } - fn pop_redo(&mut self) -> Option<&mut Transaction> { assert_eq!(self.transaction_depth, 0); if let Some(transaction) = self.redo_stack.pop() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index b8ec2f9884..b4543b02b0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1738,13 +1738,12 @@ impl Buffer { pub fn undo_to_transaction( &mut self, transaction_id: TransactionId, - push_redo: bool, cx: &mut ModelContext, ) -> bool { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); - let operations = self.text.undo_to_transaction(transaction_id, push_redo); + let operations = self.text.undo_to_transaction(transaction_id); let undone = !operations.is_empty(); for operation in operations { self.send_operation(Operation::Buffer(operation), cx); @@ -1771,13 +1770,12 @@ impl Buffer { pub fn redo_to_transaction( &mut self, transaction_id: TransactionId, - push_undo: bool, cx: &mut ModelContext, ) -> bool { let was_dirty = self.is_dirty(); let old_version = self.version.clone(); - let operations = self.text.redo_to_transaction(transaction_id, push_undo); + let operations = self.text.redo_to_transaction(transaction_id); let redone = !operations.is_empty(); for operation in operations { self.send_operation(Operation::Buffer(operation), cx); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index d041518e71..d92a36dd43 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -285,31 +285,19 @@ impl History { } } - fn remove_from_undo( - &mut self, - transaction_id: TransactionId, - push_redo: bool, - ) -> Vec { + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { assert_eq!(self.transaction_depth, 0); - let mut transactions = Vec::new(); + let redo_stack_start_len = self.redo_stack.len(); if let Some(entry_ix) = self .undo_stack .iter() .rposition(|entry| entry.transaction.id == transaction_id) { - transactions.extend( - self.undo_stack[entry_ix..] - .iter() - .rev() - .map(|entry| entry.transaction.clone()), - ); - let transactions = self.undo_stack.drain(entry_ix..).rev(); - if push_redo { - self.redo_stack.extend(transactions); - } + self.redo_stack + .extend(self.undo_stack.drain(entry_ix..).rev()); } - transactions + &self.redo_stack[redo_stack_start_len..] } fn forget(&mut self, transaction_id: TransactionId) { @@ -339,31 +327,19 @@ impl History { } } - fn remove_from_redo( - &mut self, - transaction_id: TransactionId, - push_undo: bool, - ) -> Vec { + fn remove_from_redo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { assert_eq!(self.transaction_depth, 0); - let mut transactions = Vec::new(); + let undo_stack_start_len = self.undo_stack.len(); if let Some(entry_ix) = self .redo_stack .iter() .rposition(|entry| entry.transaction.id == transaction_id) { - transactions.extend( - self.redo_stack[entry_ix..] - .iter() - .rev() - .map(|entry| entry.transaction.clone()), - ); - if push_undo { - self.undo_stack - .extend(self.redo_stack.drain(entry_ix..).rev()); - } + self.undo_stack + .extend(self.redo_stack.drain(entry_ix..).rev()); } - transactions + &self.undo_stack[undo_stack_start_len..] } } @@ -1239,12 +1215,13 @@ impl Buffer { } } - pub fn undo_to_transaction( - &mut self, - transaction_id: TransactionId, - push_redo: bool, - ) -> Vec { - let transactions = self.history.remove_from_undo(transaction_id, push_redo); + pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { + let transactions = self + .history + .remove_from_undo(transaction_id) + .iter() + .map(|entry| entry.transaction.clone()) + .collect::>(); transactions .into_iter() .map(|transaction| self.undo_or_redo(transaction).unwrap()) @@ -1266,12 +1243,13 @@ impl Buffer { } } - pub fn redo_to_transaction( - &mut self, - transaction_id: TransactionId, - push_undo: bool, - ) -> Vec { - let transactions = self.history.remove_from_redo(transaction_id, push_undo); + pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { + let transactions = self + .history + .remove_from_redo(transaction_id) + .iter() + .map(|entry| entry.transaction.clone()) + .collect::>(); transactions .into_iter() .map(|transaction| self.undo_or_redo(transaction).unwrap()) diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 26edf7f5a7..919fecf8f9 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -3,9 +3,8 @@ pub mod test; use futures::Future; use std::{ - borrow::{Borrow, BorrowMut}, cmp::Ordering, - ops::{AddAssign, Deref, DerefMut}, + ops::AddAssign, pin::Pin, task::{Context, Poll}, }; @@ -124,38 +123,6 @@ where } } -pub enum CowMut<'a, T: ?Sized + ToOwned> { - Borrowed(&'a mut T), - Owned(T::Owned), -} - -impl<'a, T> Deref for CowMut<'a, T> -where - T: ?Sized + ToOwned, -{ - type Target = T; - - fn deref(&self) -> &Self::Target { - match self { - CowMut::Borrowed(value) => value, - CowMut::Owned(value) => value.borrow(), - } - } -} - -impl<'a, T> DerefMut for CowMut<'a, T> -where - T: ?Sized + ToOwned, - T::Owned: BorrowMut, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - match self { - CowMut::Borrowed(value) => value, - CowMut::Owned(value) => value.borrow_mut(), - } - } -} - #[cfg(test)] mod tests { use super::*;