| @@ -9,10 +9,15 @@ use collections::HashSet; · 8 unmodified lines |
| 9 | pub use connection::*; | 9 | pub use connection::*; |
| 10 | pub use diff::*; | 10 | pub use diff::*; |
| 11 | use futures::{FutureExt, channel::oneshot, future::BoxFuture}; | 11 | use futures::{FutureExt, channel::oneshot, future::BoxFuture}; |
| 12 | use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; | 12 | use gpui::{ |
| | 13 | AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, |
| | 14 | WeakEntity, |
| | 15 | }; |
| 13 | use itertools::Itertools; | 16 | use itertools::Itertools; |
| 14 | use language::language_settings::FormatOnSave; | 17 | use language::language_settings::FormatOnSave; |
| 15 | use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; | 18 | use language::{ |
| | 19 | Anchor, Buffer, BufferEvent, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff, |
| | 20 | }; |
| 16 | use markdown::Markdown; | 21 | use markdown::Markdown; |
| 17 | pub use mention::*; | 22 | pub use mention::*; |
| 18 | use project::lsp_store::{FormatTrigger, LspFormatTarget}; | 23 | use 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)] | 1021 | struct InferredEditCandidateReady { |
| 1017 | struct 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)] | | |
| 1025 | enum InferredEditCandidateState { | 1032 | enum 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 | |
| 1035 | impl InferredEditCandidateState { | 1037 | impl 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) = this | 2287 | 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 = candidate | 2323 | 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 = this | 2345 | 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); |