History

refactor

Ben Brandt committed 7 months ago

Commit 0761df21 parent 03f91712 files · +1115 −209Browse files

Unified · Split

2 files
  1. crates/acp_thread/src/acp_thread.rs +768 −203
  2. crates/action_log/src/action_log.rs +347 −6
crates/acp_thread/src/acp_thread.rs+768−203
@@ -9,10 +9,15 @@ use collections::HashSet; · 8 unmodified lines
9pub use connection::*;9pub use connection::*;
10pub use diff::*;10pub use diff::*;
11use futures::{FutureExt, channel::oneshot, future::BoxFuture};11use futures::{FutureExt, channel::oneshot, future::BoxFuture};
12use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};12use gpui::{
13 AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
14 WeakEntity,
15};
13use itertools::Itertools;16use itertools::Itertools;
14use language::language_settings::FormatOnSave;17use language::language_settings::FormatOnSave;
15use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff};18use language::{
19 Anchor, Buffer, BufferEvent, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff,
20};
16use markdown::Markdown;21use markdown::Markdown;
17pub use mention::*;22pub use mention::*;
18use project::lsp_store::{FormatTrigger, LspFormatTarget};23use project::lsp_store::{FormatTrigger, LspFormatTarget};
@@ -1013,29 +1018,27 @@ struct RunningTurn { · 994 unmodified lines
1013 send_task: Task<()>,1018 send_task: Task<()>,
1014}1019}
1015 1020
1016#[derive(Clone)]1021struct InferredEditCandidateReady {
1017struct InferredEditCandidate {1022 nonce: u64,
1018 buffer: Entity<Buffer>,1023 buffer: Entity<Buffer>,
1019 baseline_snapshot: text::BufferSnapshot,1024 baseline_snapshot: text::BufferSnapshot,
1020 existed_on_disk: bool,1025 existed_on_disk: bool,
1021 was_dirty: bool,1026 was_dirty: bool,
1027 observed_external_file_change: bool,
1028 has_inferred_edit: bool,
1029 _subscription: Subscription,
1022}1030}
1023 1031
1024#[derive(Clone)]
1025enum InferredEditCandidateState {1032enum InferredEditCandidateState {
1026 Pending {1033 Pending { nonce: u64 },
1027 nonce: u64,1034 Ready(InferredEditCandidateReady),
1028 },
1029 Ready {
1030 nonce: u64,
1031 candidate: InferredEditCandidate,
1032 },
1033}1035}
1034 1036
1035impl InferredEditCandidateState {1037impl InferredEditCandidateState {
1036 fn nonce(&self) -> u64 {1038 fn nonce(&self) -> u64 {
1037 match self {1039 match self {
1038 Self::Pending { nonce } | Self::Ready { nonce, .. } => *nonce,1040 Self::Pending { nonce } => *nonce,
1041 Self::Ready(candidate) => candidate.nonce,
1039 }1042 }
1040 }1043 }
1041}1044}
@@ -1874,26 +1877,46 @@ impl AcpThread { · 832 unmodified lines
1874 InferredEditCandidateState::Pending { nonce },1877 InferredEditCandidateState::Pending { nonce },
1875 );1878 );
1876 1879
1877 let project = self.project.clone();1880 let open_buffer = self.project.update(cx, |project, cx| {
1878 let tool_call_id = tool_call_id.clone();1881 let project_path = project.project_path_for_absolute_path(&abs_path, cx)?;
1879 cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {1882 if let Some(buffer) = project.get_open_buffer(&project_path, cx) {
1880 let open_buffer = project.update(cx, |project, cx| {1883 let (baseline_snapshot, existed_on_disk, was_dirty) =
1881 let project_path = project.project_path_for_absolute_path(&abs_path, cx)?;1884 Self::inferred_edit_candidate_baseline(&buffer, cx);
1882 Some(project.open_buffer(project_path, cx))1885 return Some((
1883 });1886 Some((buffer, baseline_snapshot, existed_on_disk, was_dirty)),
1887 None,
1888 ));
1889 }
1884 1890
1885 let Some(open_buffer) = open_buffer else {1891 Some((None, Some(project.open_buffer(project_path, cx))))
1886 this.update(cx, |this, _| {1892 });
1887 this.remove_inferred_edit_candidate_if_matching(
1888 &tool_call_id,
1889 &abs_path,
1890 nonce,
1891 );
1892 })
1893 .ok();
1894 return Ok(());
1895 };
1896 1893
1894 let Some((ready_candidate, open_buffer)) = open_buffer else {
1895 self.remove_inferred_edit_candidate_if_matching(&tool_call_id, &abs_path, nonce);
1896 continue;
1897 };
1898
1899 if let Some((buffer, baseline_snapshot, existed_on_disk, was_dirty)) = ready_candidate {
1900 self.set_inferred_edit_candidate_ready(
1901 tool_call_id.clone(),
1902 abs_path,
1903 nonce,
1904 buffer,
1905 baseline_snapshot,
1906 existed_on_disk,
1907 was_dirty,
1908 cx,
1909 );
1910 continue;
1911 }
1912
1913 let Some(open_buffer) = open_buffer else {
1914 self.remove_inferred_edit_candidate_if_matching(&tool_call_id, &abs_path, nonce);
1915 continue;
1916 };
1917
1918 let tool_call_id = tool_call_id.clone();
1919 cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
1897 let buffer = match open_buffer.await {1920 let buffer = match open_buffer.await {
1898 Ok(buffer) => buffer,1921 Ok(buffer) => buffer,
1899 Err(_) => {1922 Err(_) => {
@@ -1910,34 +1933,19 @@ impl AcpThread { · 10 unmodified lines
1910 };1933 };
1911 1934
1912 let (baseline_snapshot, existed_on_disk, was_dirty) =1935 let (baseline_snapshot, existed_on_disk, was_dirty) =
1913 buffer.read_with(cx, |buffer, _| {1936 Self::inferred_edit_candidate_baseline(&buffer, cx);
1914 (
1915 buffer.text_snapshot(),
1916 buffer.file().is_some_and(|file| file.disk_state().exists()),
1917 buffer.is_dirty(),
1918 )
1919 });
1920 1937
1921 this.update(cx, |this, _| {1938 this.update(cx, |this, cx| {
1922 let Some(candidates) = this.inferred_edit_candidates.get_mut(&tool_call_id)1939 this.set_inferred_edit_candidate_ready(
1923 else {1940 tool_call_id.clone(),
1924 return;1941 abs_path.clone(),
1925 };
1926 let Some(candidate_state) = candidates.get_mut(&abs_path) else {
1927 return;
1928 };
1929 if candidate_state.nonce() != nonce {
1930 return;
1931 }
1932 *candidate_state = InferredEditCandidateState::Ready {
1933 nonce,1942 nonce,
1934 candidate: InferredEditCandidate {1943 buffer,
1935 buffer,1944 baseline_snapshot,
1936 baseline_snapshot,1945 existed_on_disk,
1937 existed_on_disk,1946 was_dirty,
1938 was_dirty,1947 cx,
1939 },1948 );
1940 };
1941 })1949 })
1942 .ok();1950 .ok();
1943 1951
@@ -1947,6 +1955,308 @@ impl AcpThread { · 3 unmodified lines
1947 }1955 }
1948 }1956 }
1949 1957
1958 fn inferred_edit_candidate_baseline(
1959 buffer: &Entity<Buffer>,
1960 cx: &mut impl AppContext,
1961 ) -> (text::BufferSnapshot, bool, bool) {
1962 buffer.read_with(cx, |buffer, _| {
1963 (
1964 buffer.text_snapshot(),
1965 buffer.file().is_some_and(|file| file.disk_state().exists()),
1966 buffer.is_dirty(),
1967 )
1968 })
1969 }
1970
1971 fn update_ready_inferred_edit_candidate_if_matching(
1972 &mut self,
1973 tool_call_id: &acp::ToolCallId,
1974 abs_path: &PathBuf,
1975 nonce: u64,
1976 update: impl FnOnce(&mut InferredEditCandidateReady),
1977 ) -> bool {
1978 let Some(candidates) = self.inferred_edit_candidates.get_mut(tool_call_id) else {
1979 return false;
1980 };
1981 let Some(InferredEditCandidateState::Ready(candidate)) = candidates.get_mut(abs_path)
1982 else {
1983 return false;
1984 };
1985 if candidate.nonce != nonce {
1986 return false;
1987 }
1988
1989 update(candidate);
1990 true
1991 }
1992
1993 fn set_inferred_edit_candidate_ready(
1994 &mut self,
1995 tool_call_id: acp::ToolCallId,
1996 abs_path: PathBuf,
1997 nonce: u64,
1998 buffer: Entity<Buffer>,
1999 baseline_snapshot: text::BufferSnapshot,
2000 existed_on_disk: bool,
2001 was_dirty: bool,
2002 cx: &mut Context<Self>,
2003 ) {
2004 let subscription = cx.subscribe(&buffer, {
2005 let tool_call_id = tool_call_id.clone();
2006 let abs_path = abs_path.clone();
2007 move |this, buffer, event: &BufferEvent, cx| {
2008 this.handle_inferred_edit_candidate_buffer_event(
2009 tool_call_id.clone(),
2010 abs_path.clone(),
2011 nonce,
2012 buffer,
2013 event,
2014 cx,
2015 );
2016 }
2017 });
2018
2019 let Some(candidates) = self.inferred_edit_candidates.get_mut(&tool_call_id) else {
2020 return;
2021 };
2022 let Some(candidate_state) = candidates.get_mut(&abs_path) else {
2023 return;
2024 };
2025 if candidate_state.nonce() != nonce {
2026 return;
2027 }
2028
2029 *candidate_state = InferredEditCandidateState::Ready(InferredEditCandidateReady {
2030 nonce,
2031 buffer,
2032 baseline_snapshot,
2033 existed_on_disk,
2034 was_dirty,
2035 observed_external_file_change: false,
2036 has_inferred_edit: false,
2037 _subscription: subscription,
2038 });
2039 }
2040
2041 fn can_infer_initial_external_edit_for_candidate(
2042 &self,
2043 buffer: &Entity<Buffer>,
2044 was_dirty: bool,
2045 cx: &App,
2046 ) -> bool {
2047 if was_dirty {
2048 return false;
2049 }
2050
2051 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) {
2052 return false;
2053 }
2054
2055 !self.action_log.read_with(cx, |action_log, cx| {
2056 action_log.has_changed_buffer(buffer, cx)
2057 })
2058 }
2059
2060 fn inferred_edit_candidate_has_meaningful_change(
2061 existed_on_disk: bool,
2062 baseline_snapshot: &text::BufferSnapshot,
2063 buffer: &Entity<Buffer>,
2064 cx: &App,
2065 ) -> bool {
2066 let (current_snapshot, current_exists) = buffer.read_with(cx, |buffer, _| {
2067 (
2068 buffer.text_snapshot(),
2069 buffer.file().is_some_and(|file| file.disk_state().exists()),
2070 )
2071 });
2072
2073 if !existed_on_disk {
2074 current_exists || current_snapshot.text() != baseline_snapshot.text()
2075 } else {
2076 current_snapshot.text() != baseline_snapshot.text()
2077 }
2078 }
2079
2080 fn infer_inferred_edit_candidate_from_baseline(
2081 &mut self,
2082 buffer: Entity<Buffer>,
2083 baseline_snapshot: text::BufferSnapshot,
2084 existed_on_disk: bool,
2085 cx: &mut Context<Self>,
2086 ) {
2087 if existed_on_disk {
2088 self.action_log.update(cx, |action_log, cx| {
2089 action_log.infer_buffer_edited_from_snapshot(buffer.clone(), baseline_snapshot, cx);
2090 });
2091 } else {
2092 self.action_log.update(cx, |action_log, cx| {
2093 action_log.infer_buffer_created(buffer.clone(), baseline_snapshot, cx);
2094 });
2095 }
2096 }
2097
2098 fn handle_inferred_edit_candidate_buffer_event(
2099 &mut self,
2100 tool_call_id: acp::ToolCallId,
2101 abs_path: PathBuf,
2102 nonce: u64,
2103 buffer: Entity<Buffer>,
2104 event: &BufferEvent,
2105 cx: &mut Context<Self>,
2106 ) {
2107 let Some(InferredEditCandidateState::Ready(candidate)) = self
2108 .inferred_edit_candidates
2109 .get(&tool_call_id)
2110 .and_then(|candidates| candidates.get(&abs_path))
2111 else {
2112 return;
2113 };
2114 if candidate.nonce != nonce {
2115 return;
2116 }
2117
2118 let baseline_snapshot = candidate.baseline_snapshot.clone();
2119 let existed_on_disk = candidate.existed_on_disk;
2120 let was_dirty = candidate.was_dirty;
2121 let observed_external_file_change = candidate.observed_external_file_change;
2122 let has_inferred_edit = candidate.has_inferred_edit;
2123
2124 enum Action {
2125 None,
2126 Remove,
2127 SetObservedExternalFileChange,
2128 ClearObservedExternalFileChange,
2129 ClearObservedAndTrackEdited,
2130 ClearObservedAndInferCreatedOrEdited,
2131 RemoveAndWillDeleteBuffer,
2132 RemoveAndInferDeleted,
2133 }
2134
2135 let action = match event {
2136 BufferEvent::Edited { is_local: true }
2137 if !has_inferred_edit && !observed_external_file_change =>
2138 {
2139 Action::Remove
2140 }
2141 BufferEvent::Saved if !has_inferred_edit => Action::Remove,
2142 BufferEvent::FileHandleChanged => {
2143 let is_deleted = buffer.read_with(cx, |buffer, _| {
2144 buffer
2145 .file()
2146 .is_some_and(|file| file.disk_state().is_deleted())
2147 });
2148
2149 if is_deleted {
2150 if has_inferred_edit {
2151 Action::RemoveAndWillDeleteBuffer
2152 } else if self
2153 .can_infer_initial_external_edit_for_candidate(&buffer, was_dirty, cx)
2154 {
2155 Action::RemoveAndInferDeleted
2156 } else {
2157 Action::Remove
2158 }
2159 } else {
2160 Action::SetObservedExternalFileChange
2161 }
2162 }
2163 BufferEvent::Reloaded if observed_external_file_change => {
2164 if has_inferred_edit {
2165 Action::ClearObservedAndTrackEdited
2166 } else if self.can_infer_initial_external_edit_for_candidate(&buffer, was_dirty, cx)
2167 && Self::inferred_edit_candidate_has_meaningful_change(
2168 existed_on_disk,
2169 &baseline_snapshot,
2170 &buffer,
2171 cx,
2172 )
2173 {
2174 Action::ClearObservedAndInferCreatedOrEdited
2175 } else {
2176 Action::ClearObservedExternalFileChange
2177 }
2178 }
2179 _ => Action::None,
2180 };
2181
2182 match action {
2183 Action::None => {}
2184 Action::Remove => {
2185 self.remove_inferred_edit_candidate_if_matching(&tool_call_id, &abs_path, nonce);
2186 }
2187 Action::SetObservedExternalFileChange => {
2188 self.update_ready_inferred_edit_candidate_if_matching(
2189 &tool_call_id,
2190 &abs_path,
2191 nonce,
2192 |candidate| {
2193 candidate.observed_external_file_change = true;
2194 },
2195 );
2196 }
2197 Action::ClearObservedExternalFileChange => {
2198 self.update_ready_inferred_edit_candidate_if_matching(
2199 &tool_call_id,
2200 &abs_path,
2201 nonce,
2202 |candidate| {
2203 candidate.observed_external_file_change = false;
2204 },
2205 );
2206 }
2207 Action::ClearObservedAndTrackEdited => {
2208 let updated = self.update_ready_inferred_edit_candidate_if_matching(
2209 &tool_call_id,
2210 &abs_path,
2211 nonce,
2212 |candidate| {
2213 candidate.observed_external_file_change = false;
2214 },
2215 );
2216 if updated {
2217 self.action_log.update(cx, |action_log, cx| {
2218 action_log.buffer_edited(buffer.clone(), cx);
2219 });
2220 }
2221 }
2222 Action::ClearObservedAndInferCreatedOrEdited => {
2223 let updated = self.update_ready_inferred_edit_candidate_if_matching(
2224 &tool_call_id,
2225 &abs_path,
2226 nonce,
2227 |candidate| {
2228 candidate.observed_external_file_change = false;
2229 candidate.has_inferred_edit = true;
2230 },
2231 );
2232 if updated {
2233 self.infer_inferred_edit_candidate_from_baseline(
2234 buffer,
2235 baseline_snapshot,
2236 existed_on_disk,
2237 cx,
2238 );
2239 }
2240 }
2241 Action::RemoveAndWillDeleteBuffer => {
2242 self.remove_inferred_edit_candidate_if_matching(&tool_call_id, &abs_path, nonce);
2243 self.action_log.update(cx, |action_log, cx| {
2244 action_log.will_delete_buffer(buffer.clone(), cx);
2245 });
2246 }
2247 Action::RemoveAndInferDeleted => {
2248 self.remove_inferred_edit_candidate_if_matching(&tool_call_id, &abs_path, nonce);
2249 self.action_log.update(cx, |action_log, cx| {
2250 action_log.infer_buffer_deleted_from_snapshot(
2251 buffer.clone(),
2252 baseline_snapshot,
2253 cx,
2254 );
2255 });
2256 }
2257 }
2258 }
2259
1950 fn finalize_inferred_edit_tool_call(2260 fn finalize_inferred_edit_tool_call(
1951 &mut self,2261 &mut self,
1952 tool_call_id: acp::ToolCallId,2262 tool_call_id: acp::ToolCallId,
@@ -1969,187 +2279,71 @@ impl AcpThread { · 16 unmodified lines
1969 }2279 }
1970 2280
1971 let project = self.project.clone();2281 let project = self.project.clone();
1972 let action_log = self.action_log.clone();
1973 cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {2282 cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
1974 const MAX_ATTEMPTS: usize = 3;2283 const MAX_ATTEMPTS: usize = 3;
1975 const ATTEMPT_DELAY: Duration = Duration::from_millis(50);2284 const ATTEMPT_DELAY: Duration = Duration::from_millis(50);
1976 2285
1977 for attempt in 0..MAX_ATTEMPTS {2286 for attempt in 0..MAX_ATTEMPTS {
1978 let (ready_candidates, has_pending) = this2287 let (buffers_to_reload, has_pending, has_observed_external_file_change) = this
1979 .read_with(cx, |this, _| {2288 .read_with(cx, |this, _| {
1980 let Some(candidates) = this.inferred_edit_candidates.get(&tool_call_id)2289 let Some(candidates) = this.inferred_edit_candidates.get(&tool_call_id)
1981 else {2290 else {
1982 return (Vec::new(), false);2291 return (Vec::new(), false, false);
1983 };2292 };
1984 2293
1985 let mut ready_candidates = Vec::new();2294 let mut buffers_to_reload = HashSet::default();
1986 let mut has_pending = false;2295 let mut has_pending = false;
1987 for (abs_path, candidate_state) in candidates {2296 let mut has_observed_external_file_change = false;
2297
2298 for candidate_state in candidates.values() {
1988 match candidate_state {2299 match candidate_state {
1989 InferredEditCandidateState::Pending { .. } => has_pending = true,2300 InferredEditCandidateState::Pending { .. } => has_pending = true,
1990 InferredEditCandidateState::Ready { nonce, candidate } => {2301 InferredEditCandidateState::Ready(candidate) => {
1991 ready_candidates.push((2302 if candidate.observed_external_file_change {
1992 abs_path.clone(),2303 has_observed_external_file_change = true;
1993 *nonce,2304 buffers_to_reload.insert(candidate.buffer.clone());
1994 candidate.clone(),2305 }
1995 ));
1996 }2306 }
1997 }2307 }
1998 }2308 }
1999 2309
2000 (ready_candidates, has_pending)2310 (
2311 buffers_to_reload.into_iter().collect::<Vec<_>>(),
2312 has_pending,
2313 has_observed_external_file_change,
2314 )
2001 })2315 })
2002 .unwrap_or((Vec::new(), false));2316 .unwrap_or((Vec::new(), false, false));
2003 2317
2004 if ready_candidates.is_empty() && !has_pending {2318 if buffers_to_reload.is_empty() && !has_pending {
2005 break;2319 break;
2006 }2320 }
2007 2321
2008 for (_, _, candidate) in &ready_candidates {2322 for buffer in buffers_to_reload {
2009 let should_reload = candidate2323 let should_reload = buffer.read_with(cx, |buffer, _| !buffer.is_dirty());
2010 .buffer
2011 .read_with(cx, |buffer, _| !buffer.is_dirty());
2012 if !should_reload {2324 if !should_reload {
2013 continue;2325 continue;
2014 }2326 }
2015 2327
2016 let reload = project.update(cx, |project, cx| {2328 let reload = project.update(cx, |project, cx| {
2017 let mut buffers = HashSet::default();2329 let mut buffers = HashSet::default();
2018 buffers.insert(candidate.buffer.clone());2330 buffers.insert(buffer.clone());
2019 project.reload_buffers(buffers, false, cx)2331 project.reload_buffers(buffers, false, cx)
2020 });2332 });
2021 reload.await.log_err();2333 reload.await.log_err();
2022 }2334 }
2023 2335
2024 if !has_pending || attempt + 1 == MAX_ATTEMPTS {2336 if (!has_pending && !has_observed_external_file_change)
2337 || attempt + 1 == MAX_ATTEMPTS
2338 {
2025 break;2339 break;
2026 }2340 }
2027 2341
2028 cx.background_executor().timer(ATTEMPT_DELAY).await;2342 cx.background_executor().timer(ATTEMPT_DELAY).await;
2029 }2343 }
2030 2344
2031 let ready_candidates = this2345 this.update(cx, |this, _| {
2032 .read_with(cx, |this, _| {2346 this.clear_inferred_edit_candidates_for_tool_calls([tool_call_id.clone()]);
2033 this.inferred_edit_candidates
2034 .get(&tool_call_id)
2035 .into_iter()
2036 .flat_map(|candidates| candidates.iter())
2037 .filter_map(|(abs_path, candidate_state)| match candidate_state {
2038 InferredEditCandidateState::Pending { .. } => None,
2039 InferredEditCandidateState::Ready { nonce, candidate } => {
2040 Some((abs_path.clone(), *nonce, candidate.clone()))
2041 }
2042 })
2043 .collect::<Vec<_>>()
2044 })
2045 .unwrap_or_default();
2046
2047 let mut processed_candidates = Vec::new();
2048 for (abs_path, nonce, candidate) in ready_candidates {
2049 let still_current = this
2050 .read_with(cx, |this, _| {
2051 this.inferred_edit_candidates
2052 .get(&tool_call_id)
2053 .and_then(|candidates| candidates.get(&abs_path))
2054 .is_some_and(|candidate_state| candidate_state.nonce() == nonce)
2055 })
2056 .unwrap_or(false);
2057 if !still_current {
2058 continue;
2059 }
2060
2061 if candidate.was_dirty {
2062 processed_candidates.push((abs_path, nonce));
2063 continue;
2064 }
2065
2066 let already_changed = action_log.read_with(cx, |action_log, cx| {
2067 action_log.has_changed_buffer(&candidate.buffer, cx)
2068 });
2069 if already_changed {
2070 processed_candidates.push((abs_path, nonce));
2071 continue;
2072 }
2073
2074 let (current_snapshot, current_exists, current_dirty) =
2075 candidate.buffer.read_with(cx, |buffer, _| {
2076 (
2077 buffer.text_snapshot(),
2078 buffer.file().is_some_and(|file| file.disk_state().exists()),
2079 buffer.is_dirty(),
2080 )
2081 });
2082
2083 if current_dirty {
2084 processed_candidates.push((abs_path, nonce));
2085 continue;
2086 }
2087
2088 let buffer_changed = current_snapshot.text() != candidate.baseline_snapshot.text();
2089
2090 if !candidate.existed_on_disk {
2091 if current_exists || buffer_changed {
2092 action_log.update(cx, |action_log, cx| {
2093 action_log.infer_buffer_created(
2094 candidate.buffer.clone(),
2095 candidate.baseline_snapshot.clone(),
2096 cx,
2097 );
2098 });
2099 }
2100 processed_candidates.push((abs_path, nonce));
2101 } else if !current_exists {
2102 action_log.update(cx, |action_log, cx| {
2103 action_log.infer_buffer_deleted_from_snapshot(
2104 candidate.buffer.clone(),
2105 candidate.baseline_snapshot.clone(),
2106 cx,
2107 );
2108 });
2109 processed_candidates.push((abs_path, nonce));
2110 } else if buffer_changed {
2111 action_log.update(cx, |action_log, cx| {
2112 action_log.infer_buffer_edited_from_snapshot(
2113 candidate.buffer.clone(),
2114 candidate.baseline_snapshot.clone(),
2115 cx,
2116 );
2117 });
2118 processed_candidates.push((abs_path, nonce));
2119 } else {
2120 processed_candidates.push((abs_path, nonce));
2121 }
2122 }
2123
2124 this.update(cx, |this, cx| {
2125 for (abs_path, nonce) in processed_candidates {
2126 this.remove_inferred_edit_candidate_if_matching(
2127 &tool_call_id,
2128 &abs_path,
2129 nonce,
2130 );
2131 }
2132
2133 this.finalizing_inferred_edit_tool_calls
2134 .remove(&tool_call_id);
2135
2136 let should_retry = this
2137 .inferred_edit_candidates
2138 .get(&tool_call_id)
2139 .is_some_and(|candidates| !candidates.is_empty());
2140
2141 if should_retry {
2142 let tool_call_id = tool_call_id.clone();
2143 cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
2144 cx.background_executor().timer(ATTEMPT_DELAY).await;
2145 this.update(cx, |this, cx| {
2146 this.finalize_inferred_edit_tool_call(tool_call_id, cx);
2147 })
2148 .ok();
2149 Ok(())
2150 })
2151 .detach_and_log_err(cx);
2152 }
2153 })2347 })
2154 .ok();2348 .ok();
2155 2349
@@ -2695,7 +2889,10 @@ impl AcpThread { · 539 unmodified lines
2695 2889
2696 let canceled = matches!(r.stop_reason, acp::StopReason::Cancelled);2890 let canceled = matches!(r.stop_reason, acp::StopReason::Cancelled);
2697 if canceled {2891 if canceled {
2698 this.mark_pending_tools_as_canceled();2892 let canceled_tool_call_ids = this.mark_pending_tools_as_canceled();
2893 this.clear_inferred_edit_candidates_for_tool_calls(
2894 canceled_tool_call_ids,
2895 );
2699 this.finalize_all_inferred_edit_tool_calls(cx);2896 this.finalize_all_inferred_edit_tool_calls(cx);
2700 }2897 }
2701 2898
@@ -2775,14 +2972,16 @@ impl AcpThread { · 73 unmodified lines
2775 self.connection.cancel(&self.session_id, cx);2972 self.connection.cancel(&self.session_id, cx);
2776 2973
2777 Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);2974 Self::flush_streaming_text(&mut self.streaming_text_buffer, cx);
2778 self.mark_pending_tools_as_canceled();2975 let canceled_tool_call_ids = self.mark_pending_tools_as_canceled();
2976 self.clear_inferred_edit_candidates_for_tool_calls(canceled_tool_call_ids);
2779 self.finalize_all_inferred_edit_tool_calls(cx);2977 self.finalize_all_inferred_edit_tool_calls(cx);
2780 2978
2781 // Wait for the send task to complete
2782 cx.background_spawn(turn.send_task)2979 cx.background_spawn(turn.send_task)
2783 }2980 }
2784 2981
2785 fn mark_pending_tools_as_canceled(&mut self) {2982 fn mark_pending_tools_as_canceled(&mut self) -> Vec<acp::ToolCallId> {
2983 let mut canceled_tool_call_ids = Vec::new();
2984
2786 for entry in self.entries.iter_mut() {2985 for entry in self.entries.iter_mut() {
2787 if let AgentThreadEntry::ToolCall(call) = entry {2986 if let AgentThreadEntry::ToolCall(call) = entry {
2788 let cancel = matches!(2987 let cancel = matches!(
@@ -2794,9 +2993,12 @@ impl AcpThread { · 5 unmodified lines
2794 2993
2795 if cancel {2994 if cancel {
2796 call.status = ToolCallStatus::Canceled;2995 call.status = ToolCallStatus::Canceled;
2996 canceled_tool_call_ids.push(call.id.clone());
2797 }2997 }
2798 }2998 }
2799 }2999 }
3000
3001 canceled_tool_call_ids
2800 }3002 }
2801 3003
2802 /// Restores the git working tree to the state at the given checkpoint (if one exists)3004 /// Restores the git working tree to the state at the given checkpoint (if one exists)
@@ -4494,6 +4696,369 @@ mod tests { · 1691 unmodified lines
4494 assert!(fs.read_file_sync(path!("/test/new.txt")).is_err());4696 assert!(fs.read_file_sync(path!("/test/new.txt")).is_err());
4495 }4697 }
4496 4698
4699 fn start_external_edit_tool_call(
4700 thread: &Entity<AcpThread>,
4701 tool_call_id: &acp::ToolCallId,
4702 locations: Vec<acp::ToolCallLocation>,
4703 cx: &mut TestAppContext,
4704 ) {
4705 let tool_call_id = tool_call_id.clone();
4706 thread
4707 .update(cx, move |thread, cx| {
4708 thread.handle_session_update(
4709 acp::SessionUpdate::ToolCall(
4710 acp::ToolCall::new(tool_call_id, "Label")
4711 .kind(acp::ToolKind::Edit)
4712 .status(acp::ToolCallStatus::InProgress)
4713 .locations(locations),
4714 ),
4715 cx,
4716 )
4717 })
4718 .unwrap();
4719 }
4720
4721 fn update_external_edit_tool_call_locations(
4722 thread: &Entity<AcpThread>,
4723 tool_call_id: &acp::ToolCallId,
4724 locations: Vec<acp::ToolCallLocation>,
4725 cx: &mut TestAppContext,
4726 ) {
4727 let tool_call_id = tool_call_id.clone();
4728 thread
4729 .update(cx, move |thread, cx| {
4730 thread.handle_session_update(
4731 acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
4732 tool_call_id,
4733 acp::ToolCallUpdateFields::new().locations(locations),
4734 )),
4735 cx,
4736 )
4737 })
4738 .unwrap();
4739 }
4740
4741 fn complete_external_edit_tool_call(
4742 thread: &Entity<AcpThread>,
4743 tool_call_id: &acp::ToolCallId,
4744 cx: &mut TestAppContext,
4745 ) {
4746 let tool_call_id = tool_call_id.clone();
4747 thread
4748 .update(cx, move |thread, cx| {
4749 thread.handle_session_update(
4750 acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
4751 tool_call_id,
4752 acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
4753 )),
4754 cx,
4755 )
4756 })
4757 .unwrap();
4758 }
4759
4760 fn changed_buffer_count(thread: &Entity<AcpThread>, cx: &TestAppContext) -> usize {
4761 let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
4762 action_log.read_with(cx, |action_log, cx| action_log.changed_buffers(cx).len())
4763 }
4764
4765 fn inferred_edit_candidate_count(thread: &Entity<AcpThread>, cx: &TestAppContext) -> usize {
4766 thread.read_with(cx, |thread, _| {
4767 thread
4768 .inferred_edit_candidates
4769 .values()
4770 .map(|candidates| candidates.len())
4771 .sum()
4772 })
4773 }
4774
4775 fn inferred_edit_candidate_is_ready(
4776 thread: &Entity<AcpThread>,
4777 tool_call_id: &acp::ToolCallId,
4778 abs_path: &PathBuf,
4779 cx: &TestAppContext,
4780 ) -> bool {
4781 thread.read_with(cx, |thread, _| {
4782 thread
4783 .inferred_edit_candidates
4784 .get(tool_call_id)
4785 .and_then(|candidates| candidates.get(abs_path))
4786 .is_some_and(|candidate_state| {
4787 matches!(candidate_state, InferredEditCandidateState::Ready(_))
4788 })
4789 })
4790 }
4791
4792 async fn open_test_buffer(
4793 project: &Entity<Project>,
4794 abs_path: &Path,
4795 cx: &mut TestAppContext,
4796 ) -> Entity<Buffer> {
4797 project
4798 .update(cx, |project, cx| {
4799 let project_path = project
4800 .project_path_for_absolute_path(abs_path, cx)
4801 .unwrap();
4802 project.open_buffer(project_path, cx)
4803 })
4804 .await
4805 .unwrap()
4806 }
4807
4808 #[gpui::test]
4809 async fn test_cancel_clears_inferred_candidate_state_for_external_edit_calls_with_locations(
4810 cx: &mut TestAppContext,
4811 ) {
4812 init_test(cx);
4813 let fs = FakeFs::new(cx.background_executor.clone());
4814 fs.insert_tree(path!("/test"), json!({"file.txt": "one\ntwo\n"}))
4815 .await;
4816 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
4817 let connection = Rc::new(FakeAgentConnection::new());
4818
4819 let thread = cx
4820 .update(|cx| {
4821 connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
4822 })
4823 .await
4824 .unwrap();
4825
4826 let tool_call_id = acp::ToolCallId::new("test");
4827 start_external_edit_tool_call(
4828 &thread,
4829 &tool_call_id,
4830 vec![acp::ToolCallLocation::new(path!("/test/file.txt"))],
4831 cx,
4832 );
4833 cx.run_until_parked();
4834
4835 assert_eq!(inferred_edit_candidate_count(&thread, cx), 1);
4836
4837 let cancel = thread.update(cx, |thread, cx| {
4838 thread.running_turn = Some(RunningTurn {
4839 id: 1,
4840 send_task: Task::ready(()),
4841 });
4842 thread.cancel(cx)
4843 });
4844 cancel.await;
4845 cx.run_until_parked();
4846
4847 assert_eq!(inferred_edit_candidate_count(&thread, cx), 0);
4848 thread.read_with(cx, |thread, _| {
4849 let (_, tool_call) = thread.tool_call(&tool_call_id).unwrap();
4850 assert!(matches!(tool_call.status, ToolCallStatus::Canceled));
4851 });
4852 }
4853
4854 #[gpui::test]
4855 async fn test_completed_update_after_cancel_does_not_reuse_stale_inferred_candidate_baseline(
4856 cx: &mut TestAppContext,
4857 ) {
4858 init_test(cx);
4859 let fs = FakeFs::new(cx.background_executor.clone());
4860 fs.insert_tree(path!("/test"), json!({"file.txt": "one\ntwo\n"}))
4861 .await;
4862 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
4863 let connection = Rc::new(FakeAgentConnection::new());
4864
4865 let thread = cx
4866 .update(|cx| {
4867 connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
4868 })
4869 .await
4870 .unwrap();
4871
4872 let tool_call_id = acp::ToolCallId::new("test");
4873 start_external_edit_tool_call(
4874 &thread,
4875 &tool_call_id,
4876 vec![acp::ToolCallLocation::new(path!("/test/file.txt"))],
4877 cx,
4878 );
4879 cx.run_until_parked();
4880
4881 let cancel = thread.update(cx, |thread, cx| {
4882 thread.running_turn = Some(RunningTurn {
4883 id: 1,
4884 send_task: Task::ready(()),
4885 });
4886 thread.cancel(cx)
4887 });
4888 cancel.await;
4889 cx.run_until_parked();
4890
4891 assert_eq!(inferred_edit_candidate_count(&thread, cx), 0);
4892
4893 fs.save(
4894 path!("/test/file.txt").as_ref(),
4895 &"one\ntwo\nthree\n".into(),
4896 Default::default(),
4897 )
4898 .await
4899 .unwrap();
4900 cx.run_until_parked();
4901
4902 complete_external_edit_tool_call(&thread, &tool_call_id, cx);
4903 cx.executor().advance_clock(Duration::from_millis(200));
4904 cx.run_until_parked();
4905
4906 assert_eq!(changed_buffer_count(&thread, cx), 0);
4907 assert_eq!(inferred_edit_candidate_count(&thread, cx), 0);
4908 }
4909
4910 #[gpui::test]
4911 async fn test_user_edit_and_save_during_external_tool_call_does_not_infer_edit(
4912 cx: &mut TestAppContext,
4913 ) {
4914 init_test(cx);
4915 let fs = FakeFs::new(cx.background_executor.clone());
4916 fs.insert_tree(path!("/test"), json!({"file.txt": "one\ntwo\n"}))
4917 .await;
4918 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
4919 let connection = Rc::new(FakeAgentConnection::new());
4920
4921 let thread = cx
4922 .update(|cx| {
4923 connection.new_session(
4924 project.clone(),
4925 PathList::new(&[Path::new(path!("/test"))]),
4926 cx,
4927 )
4928 })
4929 .await
4930 .unwrap();
4931
4932 let tool_call_id = acp::ToolCallId::new("test");
4933 start_external_edit_tool_call(
4934 &thread,
4935 &tool_call_id,
4936 vec![acp::ToolCallLocation::new(path!("/test/file.txt"))],
4937 cx,
4938 );
4939 cx.run_until_parked();
4940
4941 let buffer = open_test_buffer(&project, Path::new(path!("/test/file.txt")), cx).await;
4942 buffer.update(cx, |buffer, cx| {
4943 buffer.edit([(0..0, "zero\n")], None, cx);
4944 });
4945
4946 project
4947 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
4948 .await
4949 .unwrap();
4950 cx.run_until_parked();
4951
4952 complete_external_edit_tool_call(&thread, &tool_call_id, cx);
4953 cx.executor().advance_clock(Duration::from_millis(200));
4954 cx.run_until_parked();
4955
4956 assert_eq!(changed_buffer_count(&thread, cx), 0);
4957 assert_eq!(inferred_edit_candidate_count(&thread, cx), 0);
4958 }
4959
4960 #[gpui::test]
4961 async fn test_already_open_buffer_captures_inferred_edit_baseline_synchronously(
4962 cx: &mut TestAppContext,
4963 ) {
4964 init_test(cx);
4965 let fs = FakeFs::new(cx.background_executor.clone());
4966 fs.insert_tree(path!("/test"), json!({"file.txt": "one\ntwo\n"}))
4967 .await;
4968 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
4969 let connection = Rc::new(FakeAgentConnection::new());
4970
4971 let thread = cx
4972 .update(|cx| {
4973 connection.new_session(
4974 project.clone(),
4975 PathList::new(&[Path::new(path!("/test"))]),
4976 cx,
4977 )
4978 })
4979 .await
4980 .unwrap();
4981
4982 let abs_path = PathBuf::from(path!("/test/file.txt"));
4983 let _buffer = open_test_buffer(&project, Path::new(path!("/test/file.txt")), cx).await;
4984
4985 let tool_call_id = acp::ToolCallId::new("test");
4986 start_external_edit_tool_call(&thread, &tool_call_id, Vec::new(), cx);
4987 update_external_edit_tool_call_locations(
4988 &thread,
4989 &tool_call_id,
4990 vec![acp::ToolCallLocation::new(abs_path.clone())],
4991 cx,
4992 );
4993
4994 assert!(inferred_edit_candidate_is_ready(
4995 &thread,
4996 &tool_call_id,
4997 &abs_path,
4998 cx
4999 ));
5000
5001 fs.save(
5002 path!("/test/file.txt").as_ref(),
5003 &"one\ntwo\nthree\n".into(),
5004 Default::default(),
5005 )
5006 .await
5007 .unwrap();
5008 cx.run_until_parked();
5009
5010 complete_external_edit_tool_call(&thread, &tool_call_id, cx);
5011 cx.executor().advance_clock(Duration::from_millis(200));
5012 cx.run_until_parked();
5013
5014 assert_eq!(changed_buffer_count(&thread, cx), 1);
5015 }
5016
5017 #[gpui::test]
5018 async fn test_location_registration_after_external_write_does_not_infer_without_prior_baseline(
5019 cx: &mut TestAppContext,
5020 ) {
5021 init_test(cx);
5022 let fs = FakeFs::new(cx.background_executor.clone());
5023 fs.insert_tree(path!("/test"), json!({"file.txt": "one\ntwo\n"}))
5024 .await;
5025 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
5026 let connection = Rc::new(FakeAgentConnection::new());
5027
5028 let thread = cx
5029 .update(|cx| {
5030 connection.new_session(project, PathList::new(&[Path::new(path!("/test"))]), cx)
5031 })
5032 .await
5033 .unwrap();
5034
5035 let tool_call_id = acp::ToolCallId::new("test");
5036 start_external_edit_tool_call(&thread, &tool_call_id, Vec::new(), cx);
5037
5038 fs.save(
5039 path!("/test/file.txt").as_ref(),
5040 &"one\ntwo\nthree\n".into(),
5041 Default::default(),
5042 )
5043 .await
5044 .unwrap();
5045 cx.run_until_parked();
5046
5047 update_external_edit_tool_call_locations(
5048 &thread,
5049 &tool_call_id,
5050 vec![acp::ToolCallLocation::new(path!("/test/file.txt"))],
5051 cx,
5052 );
5053 cx.run_until_parked();
5054
5055 complete_external_edit_tool_call(&thread, &tool_call_id, cx);
5056 cx.executor().advance_clock(Duration::from_millis(200));
5057 cx.run_until_parked();
5058
5059 assert_eq!(changed_buffer_count(&thread, cx), 0);
5060 }
5061
4497 #[gpui::test(iterations = 10)]5062 #[gpui::test(iterations = 10)]
4498 async fn test_checkpoints(cx: &mut TestAppContext) {5063 async fn test_checkpoints(cx: &mut TestAppContext) {
4499 init_test(cx);5064 init_test(cx);
crates/action_log/src/action_log.rs+347−6
@@ -649,17 +649,34 @@ impl ActionLog { · 648 unmodified lines
649 buffer: Entity<Buffer>,649 buffer: Entity<Buffer>,
650 baseline_snapshot: text::BufferSnapshot,650 baseline_snapshot: text::BufferSnapshot,
651 cx: &mut Context<Self>,651 cx: &mut Context<Self>,
652 ) {
653 self.infer_buffer_created_impl(buffer, baseline_snapshot, true, cx);
654 }
655
656 fn infer_buffer_created_impl(
657 &mut self,
658 buffer: Entity<Buffer>,
659 baseline_snapshot: text::BufferSnapshot,
660 record_file_read_time: bool,
661 cx: &mut Context<Self>,
652 ) {662 ) {
653 if let Some(linked_action_log) = &self.linked_action_log {663 if let Some(linked_action_log) = &self.linked_action_log {
654 let linked_baseline_snapshot = baseline_snapshot.clone();664 let linked_baseline_snapshot = baseline_snapshot.clone();
655 if !linked_action_log.read(cx).has_changed_buffer(&buffer, cx) {665 if !linked_action_log.read(cx).has_changed_buffer(&buffer, cx) {
656 linked_action_log.update(cx, |log, cx| {666 linked_action_log.update(cx, |log, cx| {
657 log.infer_buffer_created(buffer.clone(), linked_baseline_snapshot, cx);667 log.infer_buffer_created_impl(
668 buffer.clone(),
669 linked_baseline_snapshot,
670 false,
671 cx,
672 );
658 });673 });
659 }674 }
660 }675 }
661 676
662 self.update_file_read_time(&buffer, cx);677 if record_file_read_time {
678 self.update_file_read_time(&buffer, cx);
679 }
663 self.prime_tracked_buffer_from_snapshot(680 self.prime_tracked_buffer_from_snapshot(
664 buffer.clone(),681 buffer.clone(),
665 baseline_snapshot,682 baseline_snapshot,
@@ -679,21 +696,34 @@ impl ActionLog { · 13 unmodified lines
679 buffer: Entity<Buffer>,696 buffer: Entity<Buffer>,
680 baseline_snapshot: text::BufferSnapshot,697 baseline_snapshot: text::BufferSnapshot,
681 cx: &mut Context<Self>,698 cx: &mut Context<Self>,
699 ) {
700 self.infer_buffer_edited_from_snapshot_impl(buffer, baseline_snapshot, true, cx);
701 }
702
703 fn infer_buffer_edited_from_snapshot_impl(
704 &mut self,
705 buffer: Entity<Buffer>,
706 baseline_snapshot: text::BufferSnapshot,
707 record_file_read_time: bool,
708 cx: &mut Context<Self>,
682 ) {709 ) {
683 if let Some(linked_action_log) = &self.linked_action_log {710 if let Some(linked_action_log) = &self.linked_action_log {
684 let linked_baseline_snapshot = baseline_snapshot.clone();711 let linked_baseline_snapshot = baseline_snapshot.clone();
685 if !linked_action_log.read(cx).has_changed_buffer(&buffer, cx) {712 if !linked_action_log.read(cx).has_changed_buffer(&buffer, cx) {
686 linked_action_log.update(cx, |log, cx| {713 linked_action_log.update(cx, |log, cx| {
687 log.infer_buffer_edited_from_snapshot(714 log.infer_buffer_edited_from_snapshot_impl(
688 buffer.clone(),715 buffer.clone(),
689 linked_baseline_snapshot,716 linked_baseline_snapshot,
717 false,
690 cx,718 cx,
691 );719 );
692 });720 });
693 }721 }
694 }722 }
695 723
696 self.update_file_read_time(&buffer, cx);724 if record_file_read_time {
725 self.update_file_read_time(&buffer, cx);
726 }
697 self.prime_tracked_buffer_from_snapshot(727 self.prime_tracked_buffer_from_snapshot(
698 buffer.clone(),728 buffer.clone(),
699 baseline_snapshot,729 baseline_snapshot,
@@ -711,21 +741,34 @@ impl ActionLog { · 11 unmodified lines
711 buffer: Entity<Buffer>,741 buffer: Entity<Buffer>,
712 baseline_snapshot: text::BufferSnapshot,742 baseline_snapshot: text::BufferSnapshot,
713 cx: &mut Context<Self>,743 cx: &mut Context<Self>,
744 ) {
745 self.infer_buffer_deleted_from_snapshot_impl(buffer, baseline_snapshot, true, cx);
746 }
747
748 fn infer_buffer_deleted_from_snapshot_impl(
749 &mut self,
750 buffer: Entity<Buffer>,
751 baseline_snapshot: text::BufferSnapshot,
752 record_file_read_time: bool,
753 cx: &mut Context<Self>,
714 ) {754 ) {
715 if let Some(linked_action_log) = &self.linked_action_log {755 if let Some(linked_action_log) = &self.linked_action_log {
716 let linked_baseline_snapshot = baseline_snapshot.clone();756 let linked_baseline_snapshot = baseline_snapshot.clone();
717 if !linked_action_log.read(cx).has_changed_buffer(&buffer, cx) {757 if !linked_action_log.read(cx).has_changed_buffer(&buffer, cx) {
718 linked_action_log.update(cx, |log, cx| {758 linked_action_log.update(cx, |log, cx| {
719 log.infer_buffer_deleted_from_snapshot(759 log.infer_buffer_deleted_from_snapshot_impl(
720 buffer.clone(),760 buffer.clone(),
721 linked_baseline_snapshot,761 linked_baseline_snapshot,
762 false,
722 cx,763 cx,
723 );764 );
724 });765 });
725 }766 }
726 }767 }
727 768
728 self.remove_file_read_time(&buffer, cx);769 if record_file_read_time {
770 self.remove_file_read_time(&buffer, cx);
771 }
729 let has_linked_action_log = self.linked_action_log.is_some();772 let has_linked_action_log = self.linked_action_log.is_some();
730 self.prime_tracked_buffer_from_snapshot(773 self.prime_tracked_buffer_from_snapshot(
731 buffer.clone(),774 buffer.clone(),
@@ -3433,6 +3476,304 @@ mod tests { · 2701 unmodified lines
3433 );3476 );
3434 }3477 }
3435 3478
3479 #[gpui::test]
3480 async fn test_file_read_time_not_forwarded_to_linked_action_log_for_inferred_edits(
3481 cx: &mut TestAppContext,
3482 ) {
3483 init_test(cx);
3484
3485 let fs = FakeFs::new(cx.executor());
3486 fs.insert_tree(
3487 path!("/dir"),
3488 json!({
3489 "edit": "hello world\n",
3490 "delete": "goodbye world\n",
3491 }),
3492 )
3493 .await;
3494 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3495 let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3496 let child_log =
3497 cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3498
3499 let edit_file_path = project
3500 .read_with(cx, |project, cx| project.find_project_path("dir/edit", cx))
3501 .unwrap();
3502 let edit_buffer = project
3503 .update(cx, |project, cx| project.open_buffer(edit_file_path, cx))
3504 .await
3505 .unwrap();
3506 let edit_abs_path = PathBuf::from(path!("/dir/edit"));
3507 let edit_baseline_snapshot = edit_buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3508
3509 edit_buffer.update(cx, |buffer, cx| buffer.set_text("hello world!\n", cx));
3510 project
3511 .update(cx, |project, cx| {
3512 project.save_buffer(edit_buffer.clone(), cx)
3513 })
3514 .await
3515 .unwrap();
3516
3517 cx.update(|cx| {
3518 child_log.update(cx, |log, cx| {
3519 log.infer_buffer_edited_from_snapshot(
3520 edit_buffer.clone(),
3521 edit_baseline_snapshot.clone(),
3522 cx,
3523 );
3524 });
3525 });
3526
3527 assert!(
3528 child_log.read_with(cx, |log, _| log.file_read_time(&edit_abs_path).is_some()),
3529 "child should record file_read_time on inferred edit"
3530 );
3531 assert!(
3532 parent_log.read_with(cx, |log, _| log.file_read_time(&edit_abs_path).is_none()),
3533 "parent should NOT get file_read_time from child's inferred edit"
3534 );
3535
3536 let create_file_path = project
3537 .read_with(cx, |project, cx| {
3538 project.find_project_path("dir/new_file", cx)
3539 })
3540 .unwrap();
3541 let create_buffer = project
3542 .update(cx, |project, cx| project.open_buffer(create_file_path, cx))
3543 .await
3544 .unwrap();
3545 let create_abs_path = PathBuf::from(path!("/dir/new_file"));
3546 let create_baseline_snapshot =
3547 create_buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3548
3549 create_buffer.update(cx, |buffer, cx| buffer.set_text("new file\n", cx));
3550 project
3551 .update(cx, |project, cx| {
3552 project.save_buffer(create_buffer.clone(), cx)
3553 })
3554 .await
3555 .unwrap();
3556
3557 cx.update(|cx| {
3558 child_log.update(cx, |log, cx| {
3559 log.infer_buffer_created(
3560 create_buffer.clone(),
3561 create_baseline_snapshot.clone(),
3562 cx,
3563 );
3564 });
3565 });
3566
3567 assert!(
3568 child_log.read_with(cx, |log, _| log.file_read_time(&create_abs_path).is_some()),
3569 "child should record file_read_time on inferred create"
3570 );
3571 assert!(
3572 parent_log.read_with(cx, |log, _| log.file_read_time(&create_abs_path).is_none()),
3573 "parent should NOT get file_read_time from child's inferred create"
3574 );
3575
3576 let delete_file_path = project
3577 .read_with(cx, |project, cx| {
3578 project.find_project_path("dir/delete", cx)
3579 })
3580 .unwrap();
3581 let delete_buffer = project
3582 .update(cx, |project, cx| project.open_buffer(delete_file_path, cx))
3583 .await
3584 .unwrap();
3585 let delete_abs_path = PathBuf::from(path!("/dir/delete"));
3586 let delete_baseline_snapshot =
3587 delete_buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3588
3589 cx.update(|cx| {
3590 parent_log.update(cx, |log, cx| log.buffer_read(delete_buffer.clone(), cx));
3591 child_log.update(cx, |log, cx| log.buffer_read(delete_buffer.clone(), cx));
3592 });
3593
3594 assert!(
3595 parent_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_some()),
3596 "parent should record its own file_read_time before inferred delete"
3597 );
3598 assert!(
3599 child_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_some()),
3600 "child should record its own file_read_time before inferred delete"
3601 );
3602
3603 fs.remove_file(path!("/dir/delete").as_ref(), RemoveOptions::default())
3604 .await
3605 .unwrap();
3606 cx.run_until_parked();
3607
3608 cx.update(|cx| {
3609 child_log.update(cx, |log, cx| {
3610 log.infer_buffer_deleted_from_snapshot(
3611 delete_buffer.clone(),
3612 delete_baseline_snapshot.clone(),
3613 cx,
3614 );
3615 });
3616 });
3617
3618 assert!(
3619 child_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_none()),
3620 "child should remove file_read_time on inferred delete"
3621 );
3622 assert!(
3623 parent_log.read_with(cx, |log, _| log.file_read_time(&delete_abs_path).is_some()),
3624 "parent should keep its own file_read_time on linked inferred delete"
3625 );
3626 }
3627
3628 #[gpui::test]
3629 async fn test_linked_action_log_infer_buffer_edited_from_snapshot(cx: &mut TestAppContext) {
3630 init_test(cx);
3631
3632 let fs = FakeFs::new(cx.executor());
3633 fs.insert_tree(path!("/dir"), json!({"file": "one\ntwo\n"}))
3634 .await;
3635 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3636 let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3637 let child_log =
3638 cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3639
3640 let file_path = project
3641 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3642 .unwrap();
3643 let buffer = project
3644 .update(cx, |project, cx| project.open_buffer(file_path, cx))
3645 .await
3646 .unwrap();
3647
3648 let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3649
3650 buffer.update(cx, |buffer, cx| buffer.set_text("one\ntwo\nthree\n", cx));
3651 project
3652 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3653 .await
3654 .unwrap();
3655
3656 cx.update(|cx| {
3657 child_log.update(cx, |log, cx| {
3658 log.infer_buffer_edited_from_snapshot(
3659 buffer.clone(),
3660 baseline_snapshot.clone(),
3661 cx,
3662 );
3663 });
3664 });
3665 cx.run_until_parked();
3666
3667 let child_hunks = unreviewed_hunks(&child_log, cx);
3668 assert!(
3669 !child_hunks.is_empty(),
3670 "child should track the inferred edit"
3671 );
3672 assert_eq!(
3673 unreviewed_hunks(&parent_log, cx),
3674 child_hunks,
3675 "parent should also track the inferred edit via linked log forwarding"
3676 );
3677 }
3678
3679 #[gpui::test]
3680 async fn test_linked_action_log_infer_buffer_created(cx: &mut TestAppContext) {
3681 init_test(cx);
3682
3683 let fs = FakeFs::new(cx.executor());
3684 fs.insert_tree(path!("/dir"), json!({})).await;
3685 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3686 let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3687 let child_log =
3688 cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3689
3690 let file_path = project
3691 .read_with(cx, |project, cx| {
3692 project.find_project_path("dir/new_file", cx)
3693 })
3694 .unwrap();
3695 let buffer = project
3696 .update(cx, |project, cx| project.open_buffer(file_path, cx))
3697 .await
3698 .unwrap();
3699
3700 let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3701
3702 buffer.update(cx, |buffer, cx| buffer.set_text("hello\n", cx));
3703 project
3704 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3705 .await
3706 .unwrap();
3707
3708 cx.update(|cx| {
3709 child_log.update(cx, |log, cx| {
3710 log.infer_buffer_created(buffer.clone(), baseline_snapshot.clone(), cx);
3711 });
3712 });
3713 cx.run_until_parked();
3714
3715 let child_hunks = unreviewed_hunks(&child_log, cx);
3716 assert!(
3717 !child_hunks.is_empty(),
3718 "child should track the inferred creation"
3719 );
3720 assert_eq!(
3721 unreviewed_hunks(&parent_log, cx),
3722 child_hunks,
3723 "parent should also track the inferred creation via linked log forwarding"
3724 );
3725 }
3726
3727 #[gpui::test]
3728 async fn test_linked_action_log_infer_buffer_deleted_from_snapshot(cx: &mut TestAppContext) {
3729 init_test(cx);
3730
3731 let fs = FakeFs::new(cx.executor());
3732 fs.insert_tree(path!("/dir"), json!({"file": "hello\n"}))
3733 .await;
3734 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3735 let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3736 let child_log =
3737 cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3738
3739 let file_path = project
3740 .read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3741 .unwrap();
3742 let buffer = project
3743 .update(cx, |project, cx| project.open_buffer(file_path, cx))
3744 .await
3745 .unwrap();
3746
3747 let baseline_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
3748
3749 fs.remove_file(path!("/dir/file").as_ref(), RemoveOptions::default())
3750 .await
3751 .unwrap();
3752 cx.run_until_parked();
3753
3754 cx.update(|cx| {
3755 child_log.update(cx, |log, cx| {
3756 log.infer_buffer_deleted_from_snapshot(
3757 buffer.clone(),
3758 baseline_snapshot.clone(),
3759 cx,
3760 );
3761 });
3762 });
3763 cx.run_until_parked();
3764
3765 let child_hunks = unreviewed_hunks(&child_log, cx);
3766 assert!(
3767 !child_hunks.is_empty(),
3768 "child should track the inferred deletion"
3769 );
3770 assert_eq!(
3771 unreviewed_hunks(&parent_log, cx),
3772 child_hunks,
3773 "parent should also track the inferred deletion via linked log forwarding"
3774 );
3775 }
3776
3436 #[gpui::test]3777 #[gpui::test]
3437 async fn test_infer_buffer_edited_from_snapshot(cx: &mut TestAppContext) {3778 async fn test_infer_buffer_edited_from_snapshot(cx: &mut TestAppContext) {
3438 init_test(cx);3779 init_test(cx);