Skip to content

Commit 90ddd58

Browse files
benbrandtbennetboMrSubidubi
authored
agent: Move file_read_times logic to ActionLog instead of Thread (zed-industries#50688)
Since the read times always correspond to an action log call anyway, we can let the action log track this internally, and we don't have to provide a reference to the Thread in as many tools. Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de> Co-authored-by: MrSubidubi <dev@bahn.sh>
1 parent de10776 commit 90ddd58

10 files changed

Lines changed: 349 additions & 336 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/action_log/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ buffer_diff.workspace = true
2020
log.workspace = true
2121
clock.workspace = true
2222
collections.workspace = true
23+
fs.workspace = true
2324
futures.workspace = true
2425
gpui.workspace = true
2526
language.workspace = true

crates/action_log/src/action_log.rs

Lines changed: 280 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
use anyhow::{Context as _, Result};
22
use buffer_diff::BufferDiff;
33
use clock;
4-
use collections::BTreeMap;
4+
use collections::{BTreeMap, HashMap};
5+
use fs::MTime;
56
use futures::{FutureExt, StreamExt, channel::mpsc};
67
use gpui::{
78
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
89
};
910
use language::{Anchor, Buffer, BufferEvent, Point, ToOffset, ToPoint};
1011
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
11-
use std::{cmp, ops::Range, sync::Arc};
12+
use std::{
13+
cmp,
14+
ops::Range,
15+
path::{Path, PathBuf},
16+
sync::Arc,
17+
};
1218
use text::{Edit, Patch, Rope};
1319
use util::{RangeExt, ResultExt as _};
1420

@@ -54,6 +60,8 @@ pub struct ActionLog {
5460
linked_action_log: Option<Entity<ActionLog>>,
5561
/// Stores undo information for the most recent reject operation
5662
last_reject_undo: Option<LastRejectUndo>,
63+
/// Tracks the last time files were read by the agent, to detect external modifications
64+
file_read_times: HashMap<PathBuf, MTime>,
5765
}
5866

5967
impl ActionLog {
@@ -64,6 +72,7 @@ impl ActionLog {
6472
project,
6573
linked_action_log: None,
6674
last_reject_undo: None,
75+
file_read_times: HashMap::default(),
6776
}
6877
}
6978

@@ -76,6 +85,32 @@ impl ActionLog {
7685
&self.project
7786
}
7887

88+
pub fn file_read_time(&self, path: &Path) -> Option<MTime> {
89+
self.file_read_times.get(path).copied()
90+
}
91+
92+
fn update_file_read_time(&mut self, buffer: &Entity<Buffer>, cx: &App) {
93+
let buffer = buffer.read(cx);
94+
if let Some(file) = buffer.file() {
95+
if let Some(local_file) = file.as_local() {
96+
if let Some(mtime) = file.disk_state().mtime() {
97+
let abs_path = local_file.abs_path(cx);
98+
self.file_read_times.insert(abs_path, mtime);
99+
}
100+
}
101+
}
102+
}
103+
104+
fn remove_file_read_time(&mut self, buffer: &Entity<Buffer>, cx: &App) {
105+
let buffer = buffer.read(cx);
106+
if let Some(file) = buffer.file() {
107+
if let Some(local_file) = file.as_local() {
108+
let abs_path = local_file.abs_path(cx);
109+
self.file_read_times.remove(&abs_path);
110+
}
111+
}
112+
}
113+
79114
fn track_buffer_internal(
80115
&mut self,
81116
buffer: Entity<Buffer>,
@@ -506,24 +541,69 @@ impl ActionLog {
506541

507542
/// Track a buffer as read by agent, so we can notify the model about user edits.
508543
pub fn buffer_read(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
509-
if let Some(linked_action_log) = &mut self.linked_action_log {
510-
linked_action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
544+
self.buffer_read_impl(buffer, true, cx);
545+
}
546+
547+
fn buffer_read_impl(
548+
&mut self,
549+
buffer: Entity<Buffer>,
550+
record_file_read_time: bool,
551+
cx: &mut Context<Self>,
552+
) {
553+
if let Some(linked_action_log) = &self.linked_action_log {
554+
// We don't want to share read times since the other agent hasn't read it necessarily
555+
linked_action_log.update(cx, |log, cx| {
556+
log.buffer_read_impl(buffer.clone(), false, cx);
557+
});
558+
}
559+
if record_file_read_time {
560+
self.update_file_read_time(&buffer, cx);
511561
}
512562
self.track_buffer_internal(buffer, false, cx);
513563
}
514564

515565
/// Mark a buffer as created by agent, so we can refresh it in the context
516566
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
517-
if let Some(linked_action_log) = &mut self.linked_action_log {
518-
linked_action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
567+
self.buffer_created_impl(buffer, true, cx);
568+
}
569+
570+
fn buffer_created_impl(
571+
&mut self,
572+
buffer: Entity<Buffer>,
573+
record_file_read_time: bool,
574+
cx: &mut Context<Self>,
575+
) {
576+
if let Some(linked_action_log) = &self.linked_action_log {
577+
// We don't want to share read times since the other agent hasn't read it necessarily
578+
linked_action_log.update(cx, |log, cx| {
579+
log.buffer_created_impl(buffer.clone(), false, cx);
580+
});
581+
}
582+
if record_file_read_time {
583+
self.update_file_read_time(&buffer, cx);
519584
}
520585
self.track_buffer_internal(buffer, true, cx);
521586
}
522587

523588
/// Mark a buffer as edited by agent, so we can refresh it in the context
524589
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
525-
if let Some(linked_action_log) = &mut self.linked_action_log {
526-
linked_action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
590+
self.buffer_edited_impl(buffer, true, cx);
591+
}
592+
593+
fn buffer_edited_impl(
594+
&mut self,
595+
buffer: Entity<Buffer>,
596+
record_file_read_time: bool,
597+
cx: &mut Context<Self>,
598+
) {
599+
if let Some(linked_action_log) = &self.linked_action_log {
600+
// We don't want to share read times since the other agent hasn't read it necessarily
601+
linked_action_log.update(cx, |log, cx| {
602+
log.buffer_edited_impl(buffer.clone(), false, cx);
603+
});
604+
}
605+
if record_file_read_time {
606+
self.update_file_read_time(&buffer, cx);
527607
}
528608
let new_version = buffer.read(cx).version();
529609
let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
@@ -536,6 +616,8 @@ impl ActionLog {
536616
}
537617

538618
pub fn will_delete_buffer(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
619+
// Ok to propagate file read time removal to linked action log
620+
self.remove_file_read_time(&buffer, cx);
539621
let has_linked_action_log = self.linked_action_log.is_some();
540622
let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
541623
match tracked_buffer.status {
@@ -2976,6 +3058,196 @@ mod tests {
29763058
);
29773059
}
29783060

3061+
#[gpui::test]
3062+
async fn test_file_read_time_recorded_on_buffer_read(cx: &mut TestAppContext) {
3063+
init_test(cx);
3064+
3065+
let fs = FakeFs::new(cx.executor());
3066+
fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3067+
.await;
3068+
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3069+
let action_log = cx.new(|_| ActionLog::new(project.clone()));
3070+
3071+
let file_path = project
3072+
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3073+
.unwrap();
3074+
let buffer = project
3075+
.update(cx, |project, cx| project.open_buffer(file_path, cx))
3076+
.await
3077+
.unwrap();
3078+
3079+
let abs_path = PathBuf::from(path!("/dir/file"));
3080+
assert!(
3081+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3082+
"file_read_time should be None before buffer_read"
3083+
);
3084+
3085+
cx.update(|cx| {
3086+
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3087+
});
3088+
3089+
assert!(
3090+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3091+
"file_read_time should be recorded after buffer_read"
3092+
);
3093+
}
3094+
3095+
#[gpui::test]
3096+
async fn test_file_read_time_recorded_on_buffer_edited(cx: &mut TestAppContext) {
3097+
init_test(cx);
3098+
3099+
let fs = FakeFs::new(cx.executor());
3100+
fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3101+
.await;
3102+
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3103+
let action_log = cx.new(|_| ActionLog::new(project.clone()));
3104+
3105+
let file_path = project
3106+
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3107+
.unwrap();
3108+
let buffer = project
3109+
.update(cx, |project, cx| project.open_buffer(file_path, cx))
3110+
.await
3111+
.unwrap();
3112+
3113+
let abs_path = PathBuf::from(path!("/dir/file"));
3114+
assert!(
3115+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3116+
"file_read_time should be None before buffer_edited"
3117+
);
3118+
3119+
cx.update(|cx| {
3120+
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3121+
});
3122+
3123+
assert!(
3124+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3125+
"file_read_time should be recorded after buffer_edited"
3126+
);
3127+
}
3128+
3129+
#[gpui::test]
3130+
async fn test_file_read_time_recorded_on_buffer_created(cx: &mut TestAppContext) {
3131+
init_test(cx);
3132+
3133+
let fs = FakeFs::new(cx.executor());
3134+
fs.insert_tree(path!("/dir"), json!({"file": "existing content"}))
3135+
.await;
3136+
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3137+
let action_log = cx.new(|_| ActionLog::new(project.clone()));
3138+
3139+
let file_path = project
3140+
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3141+
.unwrap();
3142+
let buffer = project
3143+
.update(cx, |project, cx| project.open_buffer(file_path, cx))
3144+
.await
3145+
.unwrap();
3146+
3147+
let abs_path = PathBuf::from(path!("/dir/file"));
3148+
assert!(
3149+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3150+
"file_read_time should be None before buffer_created"
3151+
);
3152+
3153+
cx.update(|cx| {
3154+
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
3155+
});
3156+
3157+
assert!(
3158+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3159+
"file_read_time should be recorded after buffer_created"
3160+
);
3161+
}
3162+
3163+
#[gpui::test]
3164+
async fn test_file_read_time_removed_on_delete(cx: &mut TestAppContext) {
3165+
init_test(cx);
3166+
3167+
let fs = FakeFs::new(cx.executor());
3168+
fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3169+
.await;
3170+
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3171+
let action_log = cx.new(|_| ActionLog::new(project.clone()));
3172+
3173+
let file_path = project
3174+
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3175+
.unwrap();
3176+
let buffer = project
3177+
.update(cx, |project, cx| project.open_buffer(file_path, cx))
3178+
.await
3179+
.unwrap();
3180+
3181+
let abs_path = PathBuf::from(path!("/dir/file"));
3182+
3183+
cx.update(|cx| {
3184+
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3185+
});
3186+
assert!(
3187+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3188+
"file_read_time should exist after buffer_read"
3189+
);
3190+
3191+
cx.update(|cx| {
3192+
action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx));
3193+
});
3194+
assert!(
3195+
action_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3196+
"file_read_time should be removed after will_delete_buffer"
3197+
);
3198+
}
3199+
3200+
#[gpui::test]
3201+
async fn test_file_read_time_not_forwarded_to_linked_action_log(cx: &mut TestAppContext) {
3202+
init_test(cx);
3203+
3204+
let fs = FakeFs::new(cx.executor());
3205+
fs.insert_tree(path!("/dir"), json!({"file": "hello world"}))
3206+
.await;
3207+
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3208+
let parent_log = cx.new(|_| ActionLog::new(project.clone()));
3209+
let child_log =
3210+
cx.new(|_| ActionLog::new(project.clone()).with_linked_action_log(parent_log.clone()));
3211+
3212+
let file_path = project
3213+
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
3214+
.unwrap();
3215+
let buffer = project
3216+
.update(cx, |project, cx| project.open_buffer(file_path, cx))
3217+
.await
3218+
.unwrap();
3219+
3220+
let abs_path = PathBuf::from(path!("/dir/file"));
3221+
3222+
cx.update(|cx| {
3223+
child_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
3224+
});
3225+
assert!(
3226+
child_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_some()),
3227+
"child should record file_read_time on buffer_read"
3228+
);
3229+
assert!(
3230+
parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3231+
"parent should NOT get file_read_time from child's buffer_read"
3232+
);
3233+
3234+
cx.update(|cx| {
3235+
child_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
3236+
});
3237+
assert!(
3238+
parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3239+
"parent should NOT get file_read_time from child's buffer_edited"
3240+
);
3241+
3242+
cx.update(|cx| {
3243+
child_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
3244+
});
3245+
assert!(
3246+
parent_log.read_with(cx, |log, _| log.file_read_time(&abs_path).is_none()),
3247+
"parent should NOT get file_read_time from child's buffer_created"
3248+
);
3249+
}
3250+
29793251
#[derive(Debug, PartialEq)]
29803252
struct HunkStatus {
29813253
range: Range<Point>,

crates/agent/src/tests/edit_file_thread_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ async fn test_edit_file_tool_in_thread_context(cx: &mut TestAppContext) {
5050
// Add just the tools we need for this test
5151
let language_registry = project.read(cx).languages().clone();
5252
thread.add_tool(crate::ReadFileTool::new(
53-
cx.weak_entity(),
5453
project.clone(),
5554
thread.action_log().clone(),
55+
true,
5656
));
5757
thread.add_tool(crate::EditFileTool::new(
5858
project.clone(),

0 commit comments

Comments
 (0)