From b8c853a63d7a391475a1787532249576c0894f7f Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 28 May 2026 09:27:55 -0400 Subject: [PATCH] ep: Fix agent edits triggering edit predictions due to diagnostic refresh (#57832) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A or Added/Fixed/Improved ... --------- Co-authored-by: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> --- crates/acp_thread/src/acp_thread.rs | 6 +- crates/agent/src/tools/edit_session.rs | 4 +- crates/agent_ui/src/buffer_codegen.rs | 6 +- crates/edit_prediction/src/edit_prediction.rs | 30 +++++- .../src/edit_prediction_tests.rs | 101 +++++++++++++++++- crates/editor/src/editor.rs | 4 +- crates/language/src/buffer.rs | 54 +++++++--- crates/language/src/buffer_tests.rs | 24 +++-- crates/multi_buffer/src/multi_buffer.rs | 28 ++--- crates/multi_buffer/src/multi_buffer_tests.rs | 6 +- crates/multi_buffer/src/path_key.rs | 6 +- crates/multi_buffer/src/transaction.rs | 31 +++++- crates/project/src/project.rs | 14 +-- .../tests/integration/project_tests.rs | 24 +++-- 14 files changed, 274 insertions(+), 64 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 3270bac05f4..e27f09da557 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -16,7 +16,9 @@ use gpui::{ }; use itertools::Itertools; use language::language_settings::FormatOnSave; -use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; +use language::{ + Anchor, Buffer, BufferEditSource, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff, +}; use markdown::{Markdown, MarkdownOptions}; pub use mention::*; use project::lsp_store::{FormatTrigger, LspFormatTarget}; @@ -2912,7 +2914,9 @@ impl AcpThread { }); let format_on_save = buffer.update(cx, |buffer, cx| { + buffer.start_transaction(); buffer.edit(edits, None, cx); + buffer.end_transaction_with_source(BufferEditSource::Agent, cx); let settings = language::language_settings::LanguageSettings::for_buffer(buffer, cx); diff --git a/crates/agent/src/tools/edit_session.rs b/crates/agent/src/tools/edit_session.rs index ed5112e908e..fcf3a98f678 100644 --- a/crates/agent/src/tools/edit_session.rs +++ b/crates/agent/src/tools/edit_session.rs @@ -12,7 +12,7 @@ use collections::HashSet; use futures::{FutureExt, channel::oneshot}; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use language::language_settings::{self, FormatOnSave}; -use language::{Buffer, BufferEvent, LanguageRegistry}; +use language::{Buffer, BufferEditSource, BufferEvent, LanguageRegistry}; use language_model::LanguageModelToolResultContent; use project::lsp_store::{FormatTrigger, LspFormatTarget}; use project::{AgentLocation, Project, ProjectPath}; @@ -975,7 +975,9 @@ fn agent_edit_buffer( { cx.update(|cx| { buffer.update(cx, |buffer, cx| { + buffer.start_transaction(); buffer.edit(edits, None, cx); + buffer.end_transaction_with_source(BufferEditSource::Agent, cx); }); action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); }); diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 9848a78bc2d..dd43d1fbd17 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -12,7 +12,9 @@ use futures::{ stream::BoxStream, }; use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task}; -use language::{Buffer, IndentKind, LanguageName, Point, TransactionId, line_diff}; +use language::{ + Buffer, BufferEditSource, IndentKind, LanguageName, Point, TransactionId, line_diff, +}; use language_model::{ CompletionIntent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, @@ -978,7 +980,7 @@ impl CodegenAlternative { buffer.finalize_last_transaction(cx); buffer.start_transaction(cx); buffer.edit(edits, None, cx); - buffer.end_transaction(cx) + buffer.end_transaction_with_source(BufferEditSource::Agent, cx) }); if let Some(transaction) = transaction { diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index b5449ff5390..f310e9865e3 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -38,9 +38,9 @@ use gpui::{ }; use heapless::Vec as ArrayVec; use language::{ - Anchor, Buffer, BufferSnapshot, EditPredictionPromptFormat, EditPredictionsMode, EditPreview, - File, OffsetRangeExt, Point, TextBufferSnapshot, ToOffset, ToPoint, - language_settings::all_language_settings, + Anchor, Buffer, BufferEditSource, BufferSnapshot, EditPredictionPromptFormat, + EditPredictionsMode, EditPreview, File, OffsetRangeExt, Point, TextBufferSnapshot, ToOffset, + ToPoint, language_settings::all_language_settings, }; use project::{DisableAiSettings, Project, ProjectPath, WorktreeId}; use release_channel::AppVersion; @@ -324,6 +324,7 @@ struct ProjectState { recent_paths: VecDeque, registered_buffers: HashMap, current_prediction: Option, + last_edit_source: Option, next_pending_prediction_id: usize, pending_predictions: ArrayVec, debug_tx: Option>, @@ -1212,6 +1213,7 @@ impl EditPredictionStore { debug_tx: None, registered_buffers: HashMap::default(), current_prediction: None, + last_edit_source: None, cancelled_predictions: HashSet::default(), pending_predictions: ArrayVec::new(), next_pending_prediction_id: 0, @@ -1315,6 +1317,9 @@ impl EditPredictionStore { } // TODO [zeta2] init with recent paths match event { + project::Event::BufferEdited { source } => { + self.get_or_init_project(&project, cx).last_edit_source = Some(*source); + } project::Event::ActiveEntryChanged(Some(active_entry_id)) => { let Some(project_state) = self.projects.get_mut(&project.entity_id()) else { return; @@ -1332,6 +1337,15 @@ impl EditPredictionStore { } } project::Event::DiagnosticsUpdated { .. } => { + if self + .projects + .get(&project.entity_id()) + .and_then(|project_state| project_state.last_edit_source) + == Some(BufferEditSource::Agent) + { + return; + } + if cx.has_flag::() { self.refresh_prediction_from_diagnostics( project, @@ -1391,11 +1405,17 @@ impl EditPredictionStore { cx.subscribe(buffer, { let project = project.downgrade(); move |this, buffer, event, cx| { - if let language::BufferEvent::Edited { is_local } = event + if let language::BufferEvent::Edited { source } = event && let Some(project) = project.upgrade() { + let project_state = this.get_or_init_project(&project, cx); + project_state.last_edit_source = Some(*source); this.report_changes_for_buffer( - &buffer, &project, false, *is_local, cx, + &buffer, + &project, + false, + source.is_local(), + cx, ); } } diff --git a/crates/edit_prediction/src/edit_prediction_tests.rs b/crates/edit_prediction/src/edit_prediction_tests.rs index 5eb4662af51..fb348616ba6 100644 --- a/crates/edit_prediction/src/edit_prediction_tests.rs +++ b/crates/edit_prediction/src/edit_prediction_tests.rs @@ -26,8 +26,8 @@ use gpui::{ }; use indoc::indoc; use language::{ - Anchor, Buffer, Capability, CursorShape, Diagnostic, DiagnosticEntry, DiagnosticSet, - DiagnosticSeverity, Operation, Point, Selection, SelectionGoal, + Anchor, Buffer, BufferEditSource, Capability, CursorShape, Diagnostic, DiagnosticEntry, + DiagnosticSet, DiagnosticSeverity, Operation, Point, Selection, SelectionGoal, }; use lsp::LanguageServerId; @@ -352,6 +352,70 @@ async fn test_diagnostics_refresh_suppressed_while_following(cx: &mut TestAppCon }); } +#[gpui::test] +async fn test_diagnostics_refresh_suppressed_after_agent_edit(cx: &mut TestAppContext) { + let (ep_store, mut requests) = init_test_with_fake_client(cx); + + cx.update(|cx| { + cx.update_flags( + false, + vec![EditPredictionJumpsFeatureFlag::NAME.to_string()], + ); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/root", + json!({ + "1.txt": "Hello!\nHow\nBye\n", + "2.txt": "Hola!\nComo\nAdios\n" + }), + ) + .await; + let project = Project::test(fs, vec![path!("/root").as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + let path = project.find_project_path(path!("root/1.txt"), cx).unwrap(); + project.set_active_path(Some(path.clone()), cx); + project.open_buffer(path, cx) + }) + .await + .unwrap(); + + ep_store.update(cx, |ep_store, cx| { + ep_store.register_project(&project, cx); + ep_store.register_buffer(&buffer, &project, cx); + }); + + buffer.update(cx, |buffer, cx| { + buffer.start_transaction(); + buffer.edit([(Point::new(1, 3)..Point::new(1, 3), "!")], None, cx); + buffer.end_transaction_with_source(BufferEditSource::Agent, cx); + }); + cx.run_until_parked(); + + update_test_diagnostics(&project, path!("/root/2.txt"), "Sentence is incomplete", cx); + cx.run_until_parked(); + assert_no_predict_request_ready(&mut requests.predict); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(Point::new(1, 4)..Point::new(1, 4), "?")], None, cx); + }); + cx.run_until_parked(); + + update_test_diagnostics( + &project, + path!("/root/2.txt"), + "Sentence is still incomplete", + cx, + ); + + let (_request, respond_tx) = requests.predict.next().await.unwrap(); + respond_tx.send(empty_response()).unwrap(); + cx.run_until_parked(); +} + #[gpui::test] async fn test_simple_request(cx: &mut TestAppContext) { let (ep_store, mut requests) = init_test_with_fake_client(cx); @@ -2498,6 +2562,39 @@ fn assert_no_predict_request_ready( } } +fn update_test_diagnostics( + project: &Entity, + path: &str, + message: &str, + cx: &mut TestAppContext, +) { + let diagnostic = lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 1), lsp::Position::new(1, 5)), + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: message.to_string(), + ..Default::default() + }; + + project.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Uri::from_file_path(path).unwrap(), + diagnostics: vec![diagnostic], + version: None, + }, + None, + language::DiagnosticSourceKind::Pushed, + &[], + cx, + ) + .unwrap(); + }); + }); +} + struct RequestChannels { predict: mpsc::UnboundedReceiver<( PredictEditsV3Request, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4df67f8fdee..0ced11ed2e6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9231,7 +9231,7 @@ impl Editor { match event { multi_buffer::Event::Edited { edited_buffer, - is_local, + source, } => { self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; @@ -9242,7 +9242,7 @@ impl Editor { self.refresh_matching_bracket_highlights(&snapshot, cx); self.refresh_outline_symbols_at_cursor(cx); self.refresh_sticky_headers(&snapshot, cx); - if *is_local && self.has_active_edit_prediction() { + if source.is_local() && self.has_active_edit_prediction() { self.update_visible_edit_prediction(window, cx); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ec3b4327e1b..310788bc6b1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -297,6 +297,19 @@ pub enum Operation { }, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BufferEditSource { + User, + Agent, + Remote, +} + +impl BufferEditSource { + pub fn is_local(self) -> bool { + !matches!(self, Self::Remote) + } +} + /// An event that occurs in a buffer. #[derive(Clone, Debug, PartialEq)] pub enum BufferEvent { @@ -307,7 +320,7 @@ pub enum BufferEvent { is_local: bool, }, /// The buffer was edited. - Edited { is_local: bool }, + Edited { source: BufferEditSource }, /// The buffer's `dirty` bit changed. DirtyChanged, /// The buffer was saved. @@ -2433,6 +2446,14 @@ impl Buffer { self.end_transaction_at(Instant::now(), cx) } + pub fn end_transaction_with_source( + &mut self, + source: BufferEditSource, + cx: &mut Context, + ) -> Option { + self.end_transaction_at_internal(Instant::now(), source, cx) + } + /// Terminates the current transaction, providing the current time. Subsequent transactions /// that occur within a short period of time will be grouped together. This /// is controlled by the buffer's undo grouping duration. @@ -2440,6 +2461,15 @@ impl Buffer { &mut self, now: Instant, cx: &mut Context, + ) -> Option { + self.end_transaction_at_internal(now, BufferEditSource::User, cx) + } + + fn end_transaction_at_internal( + &mut self, + now: Instant, + source: BufferEditSource, + cx: &mut Context, ) -> Option { assert!(self.transaction_depth > 0); self.transaction_depth -= 1; @@ -2449,7 +2479,7 @@ impl Buffer { false }; if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) { - self.did_edit(&start_version, was_dirty, true, cx); + self.did_edit(&start_version, was_dirty, source, cx); Some(transaction_id) } else { None @@ -2844,7 +2874,7 @@ impl Buffer { &mut self, old_version: &clock::Global, was_dirty: bool, - is_local: bool, + source: BufferEditSource, cx: &mut Context, ) { self.was_changed(); @@ -2854,7 +2884,7 @@ impl Buffer { } self.reparse(cx, true); - cx.emit(BufferEvent::Edited { is_local }); + cx.emit(BufferEvent::Edited { source }); let is_dirty = self.is_dirty(); if was_dirty != is_dirty { cx.emit(BufferEvent::DirtyChanged); @@ -2976,7 +3006,7 @@ impl Buffer { self.text.apply_ops(buffer_ops); self.deferred_ops.insert(deferred_ops); self.flush_deferred_ops(cx); - self.did_edit(&old_version, was_dirty, false, cx); + self.did_edit(&old_version, was_dirty, BufferEditSource::Remote, cx); // Notify independently of whether the buffer was edited as the operations could include a // selection update. cx.notify(); @@ -3131,7 +3161,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.undo() { self.send_operation(Operation::Buffer(operation), true, cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx); self.restore_encoding_for_transaction(transaction_id, was_dirty); Some(transaction_id) } else { @@ -3149,7 +3179,7 @@ impl Buffer { let old_version = self.version.clone(); if let Some(operation) = self.text.undo_transaction(transaction_id) { self.send_operation(Operation::Buffer(operation), true, cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx); true } else { false @@ -3171,7 +3201,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), true, cx); } if undone { - self.did_edit(&old_version, was_dirty, true, cx) + self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx) } undone } @@ -3181,7 +3211,7 @@ impl Buffer { let operation = self.text.undo_operations(counts); let old_version = self.version.clone(); self.send_operation(Operation::Buffer(operation), true, cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx); } /// Manually redoes a specific transaction in the buffer's redo history. @@ -3191,7 +3221,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.redo() { self.send_operation(Operation::Buffer(operation), true, cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx); self.restore_encoding_for_transaction(transaction_id, was_dirty); Some(transaction_id) } else { @@ -3232,7 +3262,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), true, cx); } if redone { - self.did_edit(&old_version, was_dirty, true, cx) + self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx) } redone } @@ -3342,7 +3372,7 @@ impl Buffer { if !ops.is_empty() { for op in ops { self.send_operation(Operation::Buffer(op), true, cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, BufferEditSource::User, cx); } } } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 493003c5b17..b46b3611a5d 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -460,16 +460,24 @@ fn test_edit_events(cx: &mut gpui::App) { assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ - BufferEvent::Edited { is_local: true }, + BufferEvent::Edited { + source: BufferEditSource::User + }, BufferEvent::DirtyChanged, - BufferEvent::Edited { is_local: true }, - BufferEvent::Edited { is_local: true }, + BufferEvent::Edited { + source: BufferEditSource::User + }, + BufferEvent::Edited { + source: BufferEditSource::User + }, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![ - BufferEvent::Edited { is_local: false }, + BufferEvent::Edited { + source: BufferEditSource::Remote + }, BufferEvent::DirtyChanged ] ); @@ -487,14 +495,18 @@ fn test_edit_events(cx: &mut gpui::App) { assert_eq!( mem::take(&mut *buffer_1_events.lock()), vec![ - BufferEvent::Edited { is_local: true }, + BufferEvent::Edited { + source: BufferEditSource::User + }, BufferEvent::DirtyChanged, ] ); assert_eq!( mem::take(&mut *buffer_2_events.lock()), vec![ - BufferEvent::Edited { is_local: false }, + BufferEvent::Edited { + source: BufferEditSource::Remote + }, BufferEvent::DirtyChanged ] ); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 4b1231af45a..809f23bc394 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -20,11 +20,11 @@ use futures_lite::future::yield_now; use gpui::{App, Context, Entity, EventEmitter}; use itertools::Itertools; use language::{ - AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier, - CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, File, IndentGuideSettings, - IndentSize, Language, LanguageAwareStyling, LanguageScope, OffsetRangeExt, OffsetUtf16, - Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, - ToPoint as _, TransactionId, TreeSitterOptions, Unclipped, + AutoindentMode, Buffer, BufferChunks, BufferEditSource, BufferRow, BufferSnapshot, Capability, + CharClassifier, CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, File, + IndentGuideSettings, IndentSize, Language, LanguageAwareStyling, LanguageScope, OffsetRangeExt, + OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, + ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, Unclipped, language_settings::{AllLanguageSettings, LanguageSettings}, }; @@ -110,7 +110,7 @@ pub enum Event { DiffHunksToggled, Edited { edited_buffer: Option>, - is_local: bool, + source: BufferEditSource, }, TransactionUndone { transaction_id: TransactionId, @@ -1828,7 +1828,7 @@ impl MultiBuffer { } cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); cx.emit(Event::BuffersRemoved { removed_buffer_ids }); cx.notify(); @@ -1952,9 +1952,9 @@ impl MultiBuffer { use language::BufferEvent; let buffer_id = buffer.read(cx).remote_id(); cx.emit(match event { - &BufferEvent::Edited { is_local } => Event::Edited { + &BufferEvent::Edited { source } => Event::Edited { edited_buffer: Some(buffer), - is_local, + source, }, BufferEvent::DirtyChanged => Event::DirtyChanged, BufferEvent::Saved => Event::Saved, @@ -2044,7 +2044,7 @@ impl MultiBuffer { } cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); } @@ -2090,7 +2090,7 @@ impl MultiBuffer { } cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); } @@ -2313,7 +2313,7 @@ impl MultiBuffer { cx.emit(Event::DiffHunksToggled); cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); } @@ -2449,7 +2449,7 @@ impl MultiBuffer { cx.emit(Event::DiffHunksToggled); cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); } @@ -3102,7 +3102,7 @@ impl MultiBuffer { cx.emit(Event::DiffHunksToggled); cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); } } diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index b90b3425616..3e71deb8f85 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -192,15 +192,15 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut App) { &[ Event::Edited { edited_buffer: None, - is_local: true, + source: language::BufferEditSource::User, }, Event::Edited { edited_buffer: None, - is_local: true, + source: language::BufferEditSource::User, }, Event::Edited { edited_buffer: None, - is_local: true, + source: language::BufferEditSource::User, } ] ); diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs index a2fd1ae2646..8545827ef52 100644 --- a/crates/multi_buffer/src/path_key.rs +++ b/crates/multi_buffer/src/path_key.rs @@ -2,7 +2,7 @@ use std::{ops::Range, rc::Rc, sync::Arc}; use gpui::{App, AppContext, Context, Entity}; use itertools::Itertools; -use language::{Buffer, BufferSnapshot}; +use language::{Buffer, BufferEditSource, BufferSnapshot}; use rope::Point; use sum_tree::{Dimensions, SumTree}; use text::{Bias, BufferId, Edit, OffsetRangeExt, Patch}; @@ -603,7 +603,7 @@ impl MultiBuffer { cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); cx.emit(Event::BufferRangesUpdated { buffer, @@ -687,7 +687,7 @@ impl MultiBuffer { cx.emit(Event::Edited { edited_buffer: None, - is_local: true, + source: BufferEditSource::User, }); cx.notify(); } diff --git a/crates/multi_buffer/src/transaction.rs b/crates/multi_buffer/src/transaction.rs index 8161df2b7e4..47ab39f9363 100644 --- a/crates/multi_buffer/src/transaction.rs +++ b/crates/multi_buffer/src/transaction.rs @@ -1,5 +1,5 @@ use gpui::{App, Context, Entity}; -use language::{self, Buffer, TransactionId}; +use language::{self, Buffer, BufferEditSource, TransactionId}; use std::{ collections::HashMap, ops::Range, @@ -288,6 +288,35 @@ impl MultiBuffer { self.end_transaction_at(Instant::now(), cx) } + pub fn end_transaction_with_source( + &mut self, + source: BufferEditSource, + cx: &mut Context, + ) -> Option { + let now = Instant::now(); + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| { + buffer.end_transaction_with_source(source, cx) + }); + } + + let mut buffer_transactions = HashMap::default(); + for BufferState { buffer, .. } in self.buffers.values() { + if let Some(transaction_id) = buffer.update(cx, |buffer, cx| { + buffer.end_transaction_with_source(source, cx) + }) { + buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); + } + } + + if self.history.end_transaction(now, buffer_transactions) { + let transaction_id = self.history.group().unwrap(); + Some(transaction_id) + } else { + None + } + } + pub fn end_transaction_at( &mut self, now: Instant, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 12726349a8b..8544e0b833d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -89,9 +89,9 @@ use gpui::{ Task, TaskExt, WeakEntity, Window, }; use language::{ - Buffer, BufferEvent, Capability, CodeLabel, CursorShape, DiskState, Language, LanguageName, - LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainMetadata, - ToolchainScope, Transaction, Unclipped, language_settings::InlayHintKind, + Buffer, BufferEditSource, BufferEvent, Capability, CodeLabel, CursorShape, DiskState, Language, + LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, + ToolchainMetadata, ToolchainScope, Transaction, Unclipped, language_settings::InlayHintKind, proto::split_operations, }; use lsp::{ @@ -410,7 +410,9 @@ pub enum Event { EntryRenamed(ProjectTransaction, ProjectPath, PathBuf), WorkspaceEditApplied(ProjectTransaction), AgentLocationChanged, - BufferEdited, + BufferEdited { + source: BufferEditSource, + }, } pub struct AgentLocationChanged; @@ -3810,8 +3812,8 @@ impl Project { self.request_buffer_diff_recalculation(&buffer, cx); } - if matches!(event, BufferEvent::Edited { .. }) { - cx.emit(Event::BufferEdited); + if let BufferEvent::Edited { source } = event { + cx.emit(Event::BufferEdited { source: *source }); } let buffer_id = buffer.read(cx).remote_id(); diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index e95e3b7e6c5..97f6b4b437b 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -6073,7 +6073,9 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!( *events.lock(), &[ - language::BufferEvent::Edited { is_local: true }, + language::BufferEvent::Edited { + source: language::BufferEditSource::User + }, language::BufferEvent::DirtyChanged ] ); @@ -6102,9 +6104,13 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!( *events.lock(), &[ - language::BufferEvent::Edited { is_local: true }, + language::BufferEvent::Edited { + source: language::BufferEditSource::User + }, language::BufferEvent::DirtyChanged, - language::BufferEvent::Edited { is_local: true }, + language::BufferEvent::Edited { + source: language::BufferEditSource::User + }, ], ); events.lock().clear(); @@ -6119,7 +6125,9 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!( *events.lock(), &[ - language::BufferEvent::Edited { is_local: true }, + language::BufferEvent::Edited { + source: language::BufferEditSource::User + }, language::BufferEvent::DirtyChanged ] ); @@ -6159,7 +6167,9 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!( mem::take(&mut *events.lock()), &[ - language::BufferEvent::Edited { is_local: true }, + language::BufferEvent::Edited { + source: language::BufferEditSource::User + }, language::BufferEvent::DirtyChanged ] ); @@ -6174,7 +6184,9 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { assert_eq!( *events.lock(), &[ - language::BufferEvent::Edited { is_local: true }, + language::BufferEvent::Edited { + source: language::BufferEditSource::User + }, language::BufferEvent::DirtyChanged ] );