11use anyhow:: { Context as _, Result } ;
22use buffer_diff:: BufferDiff ;
33use clock;
4- use collections:: BTreeMap ;
4+ use collections:: { BTreeMap , HashMap } ;
5+ use fs:: MTime ;
56use futures:: { FutureExt , StreamExt , channel:: mpsc} ;
67use gpui:: {
78 App , AppContext , AsyncApp , Context , Entity , SharedString , Subscription , Task , WeakEntity ,
89} ;
910use language:: { Anchor , Buffer , BufferEvent , Point , ToOffset , ToPoint } ;
1011use 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+ } ;
1218use text:: { Edit , Patch , Rope } ;
1319use 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
5967impl 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 > ,
0 commit comments