@@ -297,8 +297,16 @@ fn op_log(#[string] level: Option<String>, #[serde] msg: serde_json::Value) {
297297 }
298298}
299299
300+ use std:: time:: { Duration , Instant } ;
301+
302+ #[ derive( Debug ) ]
303+ struct PendingTimeout {
304+ id : String ,
305+ expires_at : Instant ,
306+ }
307+
300308lazy_static:: lazy_static! {
301- static ref TIMEOUTS : Mutex <HashMap < String , tokio :: task :: JoinHandle < ( ) >>> = Mutex :: new( HashMap :: new( ) ) ;
309+ static ref PENDING_TIMEOUTS : Mutex <Vec < PendingTimeout >> = Mutex :: new( Vec :: new( ) ) ;
302310 static ref INTERVALS : Mutex <HashMap <String , IntervalHandle >> = Mutex :: new( HashMap :: new( ) ) ;
303311 static ref INTERVAL_CALLBACKS : Mutex <HashMap <String , String >> = Mutex :: new( HashMap :: new( ) ) ;
304312}
@@ -308,13 +316,34 @@ struct IntervalHandle {
308316 cancel_sender : Option < tokio:: sync:: oneshot:: Sender < ( ) > > ,
309317}
310318
319+ pub fn poll_timer_callbacks ( ) -> Vec < PluginRequest > {
320+ let mut requests = Vec :: new ( ) ;
321+ let now = Instant :: now ( ) ;
322+
323+ let mut timeouts = PENDING_TIMEOUTS . lock ( ) . unwrap ( ) ;
324+ let mut i = 0 ;
325+ while i < timeouts. len ( ) {
326+ if timeouts[ i] . expires_at <= now {
327+ let timeout = timeouts. remove ( i) ;
328+ log ! ( "[TIMER] Timer {} expired, dispatching callback" , timeout. id) ;
329+ requests. push ( PluginRequest :: TimeoutCallback {
330+ timer_id : timeout. id ,
331+ } ) ;
332+ } else {
333+ i += 1 ;
334+ }
335+ }
336+
337+ requests
338+ }
339+
311340#[ op2]
312341#[ string]
313342fn op_set_timeout ( delay : f64 ) -> Result < String , AnyError > {
314343 // Limit the number of concurrent timers per plugin runtime
315344 const MAX_TIMERS : usize = 1000 ;
316345
317- let timeouts = TIMEOUTS . lock ( ) . unwrap ( ) ;
346+ let mut timeouts = PENDING_TIMEOUTS . lock ( ) . unwrap ( ) ;
318347 if timeouts. len ( ) >= MAX_TIMERS {
319348 return Err ( anyhow:: anyhow!(
320349 "Too many timers, maximum {} allowed" ,
@@ -323,30 +352,27 @@ fn op_set_timeout(delay: f64) -> Result<String, AnyError> {
323352 }
324353
325354 let id = Uuid :: new_v4 ( ) . to_string ( ) ;
326- let id_clone = id. clone ( ) ;
327- let handle = tokio:: spawn ( async move {
328- tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( delay as u64 ) ) . await ;
355+ let expires_at = Instant :: now ( ) + Duration :: from_millis ( delay as u64 ) ;
329356
330- // Send callback request to the editor
331- ACTION_DISPATCHER . send_request ( PluginRequest :: TimeoutCallback {
332- timer_id : id_clone. clone ( ) ,
333- } ) ;
357+ log ! (
358+ "[TIMER] Creating timeout {} with delay {}ms, expires at {:?}" ,
359+ id,
360+ delay,
361+ expires_at
362+ ) ;
334363
335- // Clean up the handle from the map after completion
336- TIMEOUTS . lock ( ) . unwrap ( ) . remove ( & id_clone) ;
364+ timeouts. push ( PendingTimeout {
365+ id : id. clone ( ) ,
366+ expires_at,
337367 } ) ;
338368
339- // Store the handle
340- TIMEOUTS . lock ( ) . unwrap ( ) . insert ( id. clone ( ) , handle) ;
341-
342369 Ok ( id)
343370}
344371
345372#[ op2( fast) ]
346373fn op_clear_timeout ( #[ string] id : String ) -> Result < ( ) , AnyError > {
347- if let Some ( handle) = TIMEOUTS . lock ( ) . unwrap ( ) . remove ( & id) {
348- handle. abort ( ) ;
349- }
374+ let mut timeouts = PENDING_TIMEOUTS . lock ( ) . unwrap ( ) ;
375+ timeouts. retain ( |t| t. id != id) ;
350376 Ok ( ( ) )
351377}
352378
@@ -357,7 +383,7 @@ async fn op_set_interval(delay: f64, #[string] callback_id: String) -> Result<St
357383 const MAX_TIMERS : usize = 1000 ;
358384
359385 // Check combined limit of timeouts and intervals
360- let timeout_count = TIMEOUTS . lock ( ) . unwrap ( ) . len ( ) ;
386+ let timeout_count = PENDING_TIMEOUTS . lock ( ) . unwrap ( ) . len ( ) ;
361387 let interval_count = INTERVALS . lock ( ) . unwrap ( ) . len ( ) ;
362388 if timeout_count + interval_count >= MAX_TIMERS {
363389 return Err ( anyhow:: anyhow!(
@@ -485,21 +511,6 @@ fn op_set_cursor_position(x: u32, y: u32) -> Result<(), AnyError> {
485511 Ok ( ( ) )
486512}
487513
488- #[ op2( fast) ]
489- fn op_get_cursor_display_column ( ) -> Result < ( ) , AnyError > {
490- ACTION_DISPATCHER . send_request ( PluginRequest :: GetCursorDisplayColumn ) ;
491- Ok ( ( ) )
492- }
493-
494- #[ op2( fast) ]
495- fn op_set_cursor_display_column ( column : u32 , y : u32 ) -> Result < ( ) , AnyError > {
496- ACTION_DISPATCHER . send_request ( PluginRequest :: SetCursorDisplayColumn {
497- column : column as usize ,
498- y : y as usize ,
499- } ) ;
500- Ok ( ( ) )
501- }
502-
503514#[ op2]
504515fn op_get_buffer_text ( start_line : Option < u32 > , end_line : Option < u32 > ) -> Result < ( ) , AnyError > {
505516 ACTION_DISPATCHER . send_request ( PluginRequest :: GetBufferText {
@@ -515,30 +526,6 @@ fn op_get_config(#[string] key: Option<String>) -> Result<(), AnyError> {
515526 Ok ( ( ) )
516527}
517528
518- #[ op2( fast) ]
519- fn op_get_text_display_width ( #[ string] text : String ) -> Result < ( ) , AnyError > {
520- ACTION_DISPATCHER . send_request ( PluginRequest :: GetTextDisplayWidth { text } ) ;
521- Ok ( ( ) )
522- }
523-
524- #[ op2( fast) ]
525- fn op_char_index_to_display_column ( x : u32 , y : u32 ) -> Result < ( ) , AnyError > {
526- ACTION_DISPATCHER . send_request ( PluginRequest :: CharIndexToDisplayColumn {
527- x : x as usize ,
528- y : y as usize ,
529- } ) ;
530- Ok ( ( ) )
531- }
532-
533- #[ op2( fast) ]
534- fn op_display_column_to_char_index ( column : u32 , y : u32 ) -> Result < ( ) , AnyError > {
535- ACTION_DISPATCHER . send_request ( PluginRequest :: DisplayColumnToCharIndex {
536- column : column as usize ,
537- y : y as usize ,
538- } ) ;
539- Ok ( ( ) )
540- }
541-
542529#[ op2]
543530fn op_create_overlay (
544531 #[ string] id : String ,
@@ -645,13 +632,8 @@ extension!(
645632 op_buffer_replace,
646633 op_get_cursor_position,
647634 op_set_cursor_position,
648- op_get_cursor_display_column,
649- op_set_cursor_display_column,
650635 op_get_buffer_text,
651636 op_get_config,
652- op_get_text_display_width,
653- op_char_index_to_display_column,
654- op_display_column_to_char_index,
655637 op_create_overlay,
656638 op_update_overlay,
657639 op_remove_overlay,
@@ -697,16 +679,39 @@ mod tests {
697679 }
698680
699681 #[ tokio:: test]
700- #[ ignore = "Timer implementation requires editor event loop to process callbacks" ]
701682 async fn test_runtime_timer ( ) {
702- // This test is disabled because the timer implementation requires
703- // the full editor event loop to process timer callbacks.
704- // In the test environment, the ACTION_DISPATCHER sends PluginRequest::TimeoutCallback
705- // but there's no editor to receive and process these requests.
706-
707- // TODO: Implement a test-specific timer mechanism that doesn't rely on
708- // the editor event loop, or create an integration test that runs with
709- // a full editor instance.
683+ let mut runtime = Runtime :: new ( ) ;
684+ runtime
685+ . add_module (
686+ r#"
687+ globalThis.timerFired = false;
688+
689+ globalThis.setTimeout(() => {
690+ globalThis.timerFired = true;
691+ console.log("Timer fired!");
692+ }, 10).then(timerId => {
693+ console.log("Timer scheduled with ID:", timerId);
694+ });
695+
696+ // Check that timer hasn't fired immediately
697+ console.log("Timer fired immediately?", globalThis.timerFired);
698+ "# ,
699+ )
700+ . await
701+ . unwrap ( ) ;
702+
703+ // Wait for timer to fire
704+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( 50 ) ) . await ;
705+
706+ // Check that the timer callback was executed
707+ runtime
708+ . run (
709+ r#"
710+ console.log("Timer fired after delay?", globalThis.timerFired);
711+ "# ,
712+ )
713+ . await
714+ . unwrap ( ) ;
710715 }
711716
712717 #[ tokio:: test]
0 commit comments