1mod action_events;
2mod active_focus;
3mod async_dispatch;
4mod async_messages;
5mod bookmark_actions;
6mod bookmarks;
7mod buffer_close;
8mod buffer_config_resolve;
9mod buffer_groups;
10mod buffer_management;
11mod calibration_actions;
12pub mod calibration_wizard;
13mod click_geometry;
14mod click_handlers;
15mod clipboard;
16mod composite_buffer_actions;
17mod dabbrev_actions;
18mod diagnostic_jumps;
19mod editor_accessors;
20mod editor_init;
21mod event_apply;
22pub mod event_debug;
23mod event_debug_actions;
24mod file_explorer;
25pub mod file_open;
26mod file_open_input;
27mod file_open_orchestrators;
28mod file_open_queue;
29mod file_operations;
30mod help;
31mod help_actions;
32mod hover;
33mod input;
34mod input_dispatch;
35mod input_helpers;
36pub mod keybinding_editor;
37mod keybinding_editor_actions;
38mod lifecycle;
39mod line_scan;
40mod lsp_actions;
41mod lsp_event_notify;
42mod lsp_requests;
43mod lsp_status;
44mod macro_actions;
45mod macros;
46mod menu_actions;
47mod menu_context;
48mod mouse_input;
49mod navigation;
50mod on_save_actions;
51mod path_utils;
52mod plugin_commands;
53mod plugin_dispatch;
54mod popup_actions;
55mod popup_dialogs;
56mod popup_overlay_actions;
57mod prompt_actions;
58mod prompt_lifecycle;
59mod recovery_actions;
60mod regex_replace;
61mod render;
62mod scan_orchestrators;
63mod scroll_sync;
64mod scrollbar_input;
65mod scrollbar_math;
66mod search_ops;
67mod search_scan;
68mod settings_actions;
69mod settings_prompts;
70mod shell_command;
71mod smart_home;
72mod split_actions;
73mod stdin_stream;
74mod tab_drag;
75mod terminal;
76mod terminal_input;
77mod terminal_mouse;
78mod text_ops;
79mod theme_inspect;
80mod toggle_actions;
81pub mod types;
82mod undo_actions;
83mod view_actions;
84mod virtual_buffers;
85pub mod warning_domains;
86pub mod workspace;
87
88use anyhow::Result as AnyhowResult;
89use rust_i18n::t;
90
91pub fn editor_tick(
96 editor: &mut Editor,
97 mut clear_terminal: impl FnMut() -> AnyhowResult<()>,
98) -> AnyhowResult<bool> {
99 let mut needs_render = false;
100
101 let async_messages = {
102 let _s = tracing::info_span!("process_async_messages").entered();
103 editor.process_async_messages()
104 };
105 if async_messages {
106 needs_render = true;
107 }
108 let pending_file_opens = {
109 let _s = tracing::info_span!("process_pending_file_opens").entered();
110 editor.process_pending_file_opens()
111 };
112 if pending_file_opens {
113 needs_render = true;
114 }
115 if editor.process_line_scan() {
116 needs_render = true;
117 }
118 let search_scan = {
119 let _s = tracing::info_span!("process_search_scan").entered();
120 editor.process_search_scan()
121 };
122 if search_scan {
123 needs_render = true;
124 }
125 let search_overlay_refresh = {
126 let _s = tracing::info_span!("check_search_overlay_refresh").entered();
127 editor.check_search_overlay_refresh()
128 };
129 if search_overlay_refresh {
130 needs_render = true;
131 }
132 if editor.check_mouse_hover_timer() {
133 needs_render = true;
134 }
135 if editor.check_semantic_highlight_timer() {
136 needs_render = true;
137 }
138 if editor.check_completion_trigger_timer() {
139 needs_render = true;
140 }
141 editor.check_diagnostic_pull_timer();
142 if editor.check_warning_log() {
143 needs_render = true;
144 }
145 if editor.poll_stdin_streaming() {
146 needs_render = true;
147 }
148
149 if let Err(e) = editor.auto_recovery_save_dirty_buffers() {
150 tracing::debug!("Auto-recovery-save error: {}", e);
151 }
152 if let Err(e) = editor.auto_save_persistent_buffers() {
153 tracing::debug!("Auto-save (disk) error: {}", e);
154 }
155
156 if editor.take_full_redraw_request() {
157 clear_terminal()?;
158 needs_render = true;
159 }
160
161 Ok(needs_render)
162}
163
164pub(crate) use path_utils::normalize_path;
165
166use self::types::{
167 CachedLayout, FileExplorerContextMenu, InteractiveReplaceState, LspMessageEntry,
168 LspProgressInfo, MouseState, SearchState, TabContextMenu, DEFAULT_BACKGROUND_FILE,
169};
170use crate::config::Config;
171use crate::config_io::DirectoryContext;
172use crate::input::buffer_mode::ModeRegistry;
173use crate::input::command_registry::CommandRegistry;
174use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
175use crate::input::position_history::PositionHistory;
176use crate::input::quick_open::{
177 BufferProvider, CommandProvider, FileProvider, GotoLineProvider, QuickOpenRegistry,
178};
179use crate::model::cursor::Cursors;
180use crate::model::event::{Event, EventLog, LeafId, SplitDirection};
181use crate::model::filesystem::FileSystem;
182use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
183use crate::services::fs::FsManager;
184use crate::services::lsp::manager::LspManager;
185use crate::services::plugins::PluginManager;
186use crate::services::recovery::{RecoveryConfig, RecoveryService};
187use crate::services::time_source::{RealTimeSource, SharedTimeSource};
188use crate::state::EditorState;
189use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
190use crate::view::file_tree::{FileTree, FileTreeView};
191use crate::view::prompt::{Prompt, PromptType};
192use crate::view::scroll_sync::ScrollSyncManager;
193use crate::view::split::{SplitManager, SplitViewState};
194use crate::view::ui::{
195 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
196};
197use crossterm::event::{KeyCode, KeyModifiers};
198use ratatui::{
199 layout::{Constraint, Direction, Layout},
200 Frame,
201};
202use std::collections::{HashMap, HashSet};
203use std::ops::Range;
204use std::path::{Path, PathBuf};
205use std::sync::{Arc, RwLock};
206use std::time::Instant;
207
208pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
210pub use self::warning_domains::{
211 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
212 WarningDomainRegistry, WarningLevel, WarningPopupContent,
213};
214pub use crate::model::event::BufferId;
215
216fn lsp_uri_to_host_path(
225 uri: &crate::app::types::LspUri,
226 translation: Option<&crate::services::authority::PathTranslation>,
227) -> Result<PathBuf, String> {
228 uri.to_host_path(translation)
229 .ok_or_else(|| "URI is not a file path".to_string())
230}
231
232#[derive(Clone, Debug)]
234pub struct PendingGrammar {
235 pub language: String,
237 pub grammar_path: String,
239 pub extensions: Vec<String>,
241}
242
243#[derive(Clone, Debug)]
245struct SemanticTokenRangeRequest {
246 buffer_id: BufferId,
247 version: u64,
248 range: Range<usize>,
249 start_line: usize,
250 end_line: usize,
251}
252
253#[derive(Clone, Copy, Debug)]
254enum SemanticTokensFullRequestKind {
255 Full,
256 FullDelta,
257}
258
259#[derive(Clone, Debug)]
260struct SemanticTokenFullRequest {
261 buffer_id: BufferId,
262 version: u64,
263 kind: SemanticTokensFullRequestKind,
264}
265
266#[derive(Clone, Debug)]
267struct FoldingRangeRequest {
268 buffer_id: BufferId,
269 version: u64,
270}
271
272#[derive(Clone, Debug)]
273struct InlayHintsRequest {
274 buffer_id: BufferId,
275 version: u64,
276}
277
278#[derive(Debug, Clone)]
284pub struct DabbrevCycleState {
285 pub original_prefix: String,
287 pub word_start: usize,
289 pub candidates: Vec<String>,
291 pub index: usize,
293}
294
295#[derive(Debug, Clone)]
310pub(crate) struct GotoLinePreviewSnapshot {
311 pub buffer_id: BufferId,
312 pub split_id: LeafId,
313 pub cursor_id: crate::model::event::CursorId,
314 pub position: usize,
315 pub anchor: Option<usize>,
316 pub sticky_column: usize,
317 pub viewport_top_byte: usize,
318 pub viewport_top_view_line_offset: usize,
319 pub viewport_left_column: usize,
320 pub last_jump_position: usize,
321}
322
323pub struct Editor {
325 buffers: HashMap<BufferId, EditorState>,
327
328 event_logs: HashMap<BufferId, EventLog>,
333
334 next_buffer_id: usize,
336
337 config: Arc<Config>,
357
358 config_snapshot_anchor: Arc<Config>,
360
361 config_cached_json: Arc<serde_json::Value>,
364
365 user_config_raw: Arc<serde_json::Value>,
367
368 dir_context: DirectoryContext,
370
371 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
373
374 pending_grammars: Vec<PendingGrammar>,
376
377 grammar_reload_pending: bool,
381
382 grammar_build_in_progress: bool,
385
386 needs_full_grammar_build: bool,
390
391 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
393
394 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
398
399 theme: crate::view::theme::Theme,
401
402 theme_registry: Arc<crate::view::theme::ThemeRegistry>,
405
406 expanded_menus_cache: crate::view::ui::ExpandedMenusCache,
409
410 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
412
413 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
415
416 ansi_background_path: Option<PathBuf>,
418
419 background_fade: f32,
421
422 keybindings: Arc<RwLock<KeybindingResolver>>,
424
425 clipboard: crate::services::clipboard::Clipboard,
427
428 should_quit: bool,
430
431 should_detach: bool,
433
434 session_mode: bool,
436
437 software_cursor_only: bool,
439
440 session_name: Option<String>,
442
443 pending_escape_sequences: Vec<u8>,
446
447 restart_with_dir: Option<PathBuf>,
450
451 status_message: Option<String>,
453
454 plugin_status_message: Option<String>,
456
457 last_window_title: Option<String>,
461
462 plugin_errors: Vec<String>,
465
466 prompt: Option<Prompt>,
468
469 terminal_width: u16,
471 terminal_height: u16,
472
473 lsp: Option<LspManager>,
475
476 buffer_metadata: HashMap<BufferId, BufferMetadata>,
478
479 mode_registry: ModeRegistry,
481
482 tokio_runtime: Option<tokio::runtime::Runtime>,
484
485 async_bridge: Option<AsyncBridge>,
487
488 split_manager: SplitManager,
490
491 split_view_states: HashMap<LeafId, SplitViewState>,
495
496 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
500
501 scroll_sync_manager: ScrollSyncManager,
504
505 file_explorer: Option<FileTreeView>,
507
508 preview: Option<(LeafId, BufferId)>,
522
523 suppress_position_history_once: bool,
528
529 fs_manager: Arc<FsManager>,
531
532 authority: crate::services::authority::Authority,
542
543 pending_authority: Option<crate::services::authority::Authority>,
549
550 pub remote_indicator_override: Option<crate::view::ui::status_bar::RemoteIndicatorOverride>,
556
557 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
562
563 file_explorer_visible: bool,
565
566 file_explorer_sync_in_progress: bool,
569
570 file_explorer_width: crate::config::ExplorerWidth,
574
575 file_explorer_side: crate::config::FileExplorerSide,
577
578 pending_file_explorer_show_hidden: Option<bool>,
580
581 pending_file_explorer_show_gitignored: Option<bool>,
583
584 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
586
587 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
589
590 pub(crate) file_explorer_clipboard: Option<crate::app::file_explorer::FileExplorerClipboard>,
592
593 menu_bar_visible: bool,
595
596 menu_bar_auto_shown: bool,
599
600 tab_bar_visible: bool,
602
603 status_bar_visible: bool,
605
606 prompt_line_visible: bool,
608
609 mouse_enabled: bool,
611
612 same_buffer_scroll_sync: bool,
614
615 mouse_cursor_position: Option<(u16, u16)>,
619
620 gpm_active: bool,
622
623 key_context: KeyContext,
625
626 menu_state: crate::view::ui::MenuState,
628
629 menus: crate::config::MenuConfig,
631
632 working_dir: PathBuf,
634
635 pub position_history: PositionHistory,
637
638 in_navigation: bool,
640
641 next_lsp_request_id: u64,
643
644 pending_completion_requests: HashSet<u64>,
646
647 completion_items: Option<Vec<lsp_types::CompletionItem>>,
650
651 scheduled_completion_trigger: Option<Instant>,
654
655 completion_service: crate::services::completion::CompletionService,
658
659 dabbrev_state: Option<DabbrevCycleState>,
663
664 pending_goto_definition_request: Option<u64>,
666
667 pending_references_request: Option<u64>,
669
670 pending_references_symbol: String,
672
673 pending_signature_help_request: Option<u64>,
675
676 pending_code_actions_requests: HashSet<u64>,
678
679 pending_code_actions_server_names: HashMap<u64, String>,
681
682 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
686
687 pending_inlay_hints_requests: HashMap<u64, InlayHintsRequest>,
698
699 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
701
702 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
704
705 folding_ranges_debounce: HashMap<BufferId, Instant>,
707
708 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
710
711 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
713
714 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
716
717 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
719
720 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
722
723 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
725
726 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
728
729 hover: hover::HoverState,
732
733 search_state: Option<SearchState>,
735
736 search_namespace: crate::view::overlay::OverlayNamespace,
738
739 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
741
742 pending_search_range: Option<Range<usize>>,
744
745 interactive_replace_state: Option<InteractiveReplaceState>,
747
748 mouse_state: MouseState,
750
751 tab_context_menu: Option<TabContextMenu>,
753
754 file_explorer_context_menu: Option<FileExplorerContextMenu>,
756
757 theme_info_popup: Option<types::ThemeInfoPopup>,
759
760 pub(crate) cached_layout: CachedLayout,
762
763 command_registry: Arc<RwLock<CommandRegistry>>,
765
766 quick_open_registry: QuickOpenRegistry,
768
769 plugin_manager: PluginManager,
771
772 plugin_dev_workspaces:
776 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
777
778 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
782
783 panel_ids: HashMap<String, BufferId>,
786
787 pub(crate) live_grep_last_state: Option<crate::services::live_grep_state::LiveGrepLastState>,
794
795 pub(crate) overlay_preview_state: Option<crate::app::types::OverlayPreviewState>,
804
805 buffer_groups: HashMap<types::BufferGroupId, types::BufferGroup>,
807 buffer_to_group: HashMap<BufferId, types::BufferGroupId>,
809 next_buffer_group_id: usize,
811
812 pub(crate) grouped_subtrees:
820 HashMap<crate::model::event::LeafId, crate::view::split::SplitNode>,
821
822 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
825
826 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
832
833 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
836
837 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
841
842 pending_next_key_callbacks: std::collections::VecDeque<fresh_core::api::JsCallbackId>,
847
848 key_capture_active: bool,
855
856 pending_key_capture_buffer: std::collections::VecDeque<fresh_core::api::KeyEventPayload>,
861
862 goto_line_preview: Option<GotoLinePreviewSnapshot>,
867
868 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
870
871 lsp_server_statuses:
873 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
874
875 lsp_window_messages: Vec<LspMessageEntry>,
877
878 lsp_log_messages: Vec<LspMessageEntry>,
880
881 diagnostic_result_ids: HashMap<String, String>,
884
885 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
888
889 scheduled_inlay_hints_request: Option<(BufferId, Instant)>,
892
893 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
896
897 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
899
900 stored_diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
905
906 stored_folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
909
910 event_broadcaster: crate::model::control_event::EventBroadcaster,
912
913 bookmarks: bookmarks::BookmarkState,
915
916 search_case_sensitive: bool,
918 search_whole_word: bool,
919 search_use_regex: bool,
920 search_confirm_each: bool,
922
923 macros: macros::MacroState,
926
927 #[cfg(feature = "plugins")]
929 pending_plugin_actions: Vec<(
930 String,
931 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
932 )>,
933
934 #[cfg(feature = "plugins")]
936 plugin_render_requested: bool,
937
938 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
941
942 user_dismissed_lsp_languages: std::collections::HashSet<String>,
956
957 pending_close_buffer: Option<BufferId>,
960
961 pending_quit_unnamed_save: Vec<BufferId>,
968
969 auto_revert_enabled: bool,
971
972 last_auto_revert_poll: std::time::Instant,
974
975 last_file_tree_poll: std::time::Instant,
977
978 git_index_resolved: bool,
980
981 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
984
985 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
988
989 #[allow(clippy::type_complexity)]
993 pending_file_poll_rx:
994 Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
995
996 #[allow(clippy::type_complexity)]
999 pending_dir_poll_rx: Option<
1000 std::sync::mpsc::Receiver<(
1001 Vec<(
1002 crate::view::file_tree::NodeId,
1003 PathBuf,
1004 Option<std::time::SystemTime>,
1005 )>,
1006 Option<(PathBuf, std::time::SystemTime)>,
1007 )>,
1008 >,
1009
1010 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
1013
1014 file_open_state: Option<file_open::FileOpenState>,
1016
1017 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
1019
1020 recovery_service: RecoveryService,
1022
1023 full_redraw_requested: bool,
1025
1026 suspend_requested: bool,
1029
1030 time_source: SharedTimeSource,
1032
1033 last_auto_recovery_save: std::time::Instant,
1035
1036 last_persistent_auto_save: std::time::Instant,
1038
1039 active_custom_contexts: HashSet<String>,
1042
1043 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
1046
1047 editor_mode: Option<String>,
1050
1051 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
1053
1054 status_log_path: Option<PathBuf>,
1056
1057 warning_domains: WarningDomainRegistry,
1060
1061 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
1063
1064 terminal_manager: crate::services::terminal::TerminalManager,
1066
1067 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
1069
1070 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
1072
1073 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
1075
1076 ephemeral_terminals: std::collections::HashSet<crate::services::terminal::TerminalId>,
1082
1083 terminal_mode: bool,
1085
1086 keyboard_capture: bool,
1090
1091 terminal_mode_resume: std::collections::HashSet<BufferId>,
1095
1096 previous_click_time: Option<std::time::Instant>,
1098
1099 previous_click_position: Option<(u16, u16)>,
1102
1103 click_count: u8,
1105
1106 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
1108
1109 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
1111
1112 pub(crate) event_debug: Option<event_debug::EventDebug>,
1114
1115 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
1117
1118 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
1120
1121 color_capability: crate::view::color_support::ColorCapability,
1123
1124 review_hunks: Vec<fresh_core::api::ReviewHunk>,
1126
1127 pub(crate) global_popups: crate::view::popup::PopupManager,
1135
1136 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
1139
1140 composite_view_states:
1143 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
1144
1145 pending_file_opens: Vec<PendingFileOpen>,
1149
1150 pending_hot_exit_recovery: bool,
1152
1153 wait_tracking: HashMap<BufferId, (u64, bool)>,
1155 completed_waits: Vec<u64>,
1157
1158 stdin_stream: stdin_stream::StdinStream,
1160
1161 line_scan: line_scan::LineScan,
1163
1164 search_scan: search_scan::SearchScan,
1166
1167 search_overlay_top_byte: Option<usize>,
1170
1171 pub animations: crate::view::animation::AnimationRunner,
1175
1176 pub(crate) previous_cursor_screen_pos: Option<((u16, u16), LeafId)>,
1184 pub(crate) cursor_jump_animation: Option<crate::view::animation::AnimationId>,
1187
1188 pub(crate) pending_vb_animations: Vec<(u64, BufferId, fresh_core::api::PluginAnimationKind)>,
1194}
1195
1196#[derive(Debug, Clone)]
1198pub struct PendingFileOpen {
1199 pub path: PathBuf,
1201 pub line: Option<usize>,
1203 pub column: Option<usize>,
1205 pub end_line: Option<usize>,
1207 pub end_column: Option<usize>,
1209 pub message: Option<String>,
1211 pub wait_id: Option<u64>,
1213}
1214
1215impl Editor {
1216 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1218 let trimmed = input.trim();
1219
1220 if trimmed.is_empty() {
1221 self.ansi_background = None;
1222 self.ansi_background_path = None;
1223 self.set_status_message(t!("status.background_cleared").to_string());
1224 return Ok(());
1225 }
1226
1227 let input_path = Path::new(trimmed);
1228 let resolved = if input_path.is_absolute() {
1229 input_path.to_path_buf()
1230 } else {
1231 self.working_dir.join(input_path)
1232 };
1233
1234 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1235
1236 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1237
1238 self.ansi_background = Some(parsed);
1239 self.ansi_background_path = Some(canonical.clone());
1240 self.set_status_message(
1241 t!(
1242 "view.background_set",
1243 path = canonical.display().to_string()
1244 )
1245 .to_string(),
1246 );
1247
1248 Ok(())
1249 }
1250
1251 fn effective_tabs_width(&self) -> u16 {
1256 if self.file_explorer_visible && self.file_explorer.is_some() {
1257 let explorer = self.file_explorer_width.to_cols(self.terminal_width);
1258 self.terminal_width.saturating_sub(explorer)
1259 } else {
1260 self.terminal_width
1261 }
1262 }
1263
1264 #[doc(hidden)]
1268 pub fn buffer_count_for_tests(&self) -> usize {
1269 self.buffers.len()
1270 }
1271
1272 #[doc(hidden)]
1276 pub fn all_buffer_ids_for_tests(&self) -> Vec<BufferId> {
1277 let mut ids: Vec<BufferId> = self.buffers.keys().copied().collect();
1278 ids.sort_by_key(|id| id.0);
1279 ids
1280 }
1281
1282 pub fn active_state(&self) -> &EditorState {
1284 self.buffers.get(&self.active_buffer()).unwrap()
1285 }
1286
1287 pub fn active_state_mut(&mut self) -> &mut EditorState {
1289 self.buffers.get_mut(&self.active_buffer()).unwrap()
1290 }
1291
1292 pub fn active_cursors(&self) -> &Cursors {
1296 let split_id = self.effective_active_split();
1297 &self.split_view_states.get(&split_id).unwrap().cursors
1298 }
1299
1300 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1302 let split_id = self.effective_active_split();
1303 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
1304 }
1305
1306 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1308 self.completion_items = Some(items);
1309 }
1310
1311 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1313 let active_split = self.split_manager.active_split();
1314 &self.split_view_states.get(&active_split).unwrap().viewport
1315 }
1316
1317 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1319 let active_split = self.split_manager.active_split();
1320 &mut self
1321 .split_view_states
1322 .get_mut(&active_split)
1323 .unwrap()
1324 .viewport
1325 }
1326
1327 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1329 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1331 return composite.name.clone();
1332 }
1333
1334 self.buffer_metadata
1335 .get(&buffer_id)
1336 .map(|m| m.display_name.clone())
1337 .or_else(|| {
1338 self.buffers.get(&buffer_id).and_then(|state| {
1339 state
1340 .buffer
1341 .file_path()
1342 .and_then(|p| p.file_name())
1343 .and_then(|n| n.to_str())
1344 .map(|s| s.to_string())
1345 })
1346 })
1347 .unwrap_or_else(|| "[No Name]".to_string())
1348 }
1349
1350 pub fn active_event_log(&self) -> &EventLog {
1360 self.event_logs.get(&self.active_buffer()).unwrap()
1361 }
1362
1363 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1365 self.event_logs.get_mut(&self.active_buffer()).unwrap()
1366 }
1367
1368 pub(super) fn update_modified_from_event_log(&mut self) {
1372 let is_at_saved = self
1373 .event_logs
1374 .get(&self.active_buffer())
1375 .map(|log| log.is_at_saved_position())
1376 .unwrap_or(false);
1377
1378 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1379 state.buffer.set_modified(!is_at_saved);
1380 }
1381 }
1382}
1383
1384fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1393 use crossterm::event::{KeyCode, KeyModifiers};
1394
1395 let mut modifiers = KeyModifiers::NONE;
1396 let mut remaining = key_str;
1397
1398 loop {
1400 if remaining.starts_with("C-") {
1401 modifiers |= KeyModifiers::CONTROL;
1402 remaining = &remaining[2..];
1403 } else if remaining.starts_with("M-") {
1404 modifiers |= KeyModifiers::ALT;
1405 remaining = &remaining[2..];
1406 } else if remaining.starts_with("S-") {
1407 modifiers |= KeyModifiers::SHIFT;
1408 remaining = &remaining[2..];
1409 } else {
1410 break;
1411 }
1412 }
1413
1414 let upper = remaining.to_uppercase();
1417 let code = match upper.as_str() {
1418 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1419 "TAB" => KeyCode::Tab,
1420 "BACKTAB" => KeyCode::BackTab,
1421 "ESC" | "ESCAPE" => KeyCode::Esc,
1422 "SPC" | "SPACE" => KeyCode::Char(' '),
1423 "DEL" | "DELETE" => KeyCode::Delete,
1424 "BS" | "BACKSPACE" => KeyCode::Backspace,
1425 "UP" => KeyCode::Up,
1426 "DOWN" => KeyCode::Down,
1427 "LEFT" => KeyCode::Left,
1428 "RIGHT" => KeyCode::Right,
1429 "HOME" => KeyCode::Home,
1430 "END" => KeyCode::End,
1431 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1432 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1433 s if s.starts_with('F') && s.len() > 1 => {
1434 if let Ok(n) = s[1..].parse::<u8>() {
1436 KeyCode::F(n)
1437 } else {
1438 return None;
1439 }
1440 }
1441 _ if remaining.len() == 1 => {
1442 let c = remaining.chars().next()?;
1445 if c.is_ascii_uppercase() {
1446 modifiers |= KeyModifiers::SHIFT;
1447 }
1448 KeyCode::Char(c.to_ascii_lowercase())
1449 }
1450 _ => return None,
1451 };
1452
1453 Some((code, modifiers))
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458 use super::*;
1459 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1460 use tempfile::TempDir;
1461
1462 fn test_dir_context() -> (DirectoryContext, TempDir) {
1464 let temp_dir = TempDir::new().unwrap();
1465 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1466 (dir_context, temp_dir)
1467 }
1468
1469 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1471 Arc::new(crate::model::filesystem::StdFileSystem)
1472 }
1473
1474 #[test]
1475 fn test_editor_new() {
1476 let config = Config::default();
1477 let (dir_context, _temp) = test_dir_context();
1478 let editor = Editor::new(
1479 config,
1480 80,
1481 24,
1482 dir_context,
1483 crate::view::color_support::ColorCapability::TrueColor,
1484 test_filesystem(),
1485 )
1486 .unwrap();
1487
1488 assert_eq!(editor.buffers.len(), 1);
1489 assert!(!editor.should_quit());
1490 }
1491
1492 #[test]
1493 fn test_new_buffer() {
1494 let config = Config::default();
1495 let (dir_context, _temp) = test_dir_context();
1496 let mut editor = Editor::new(
1497 config,
1498 80,
1499 24,
1500 dir_context,
1501 crate::view::color_support::ColorCapability::TrueColor,
1502 test_filesystem(),
1503 )
1504 .unwrap();
1505
1506 let id = editor.new_buffer();
1507 assert_eq!(editor.buffers.len(), 2);
1508 assert_eq!(editor.active_buffer(), id);
1509 }
1510
1511 #[test]
1512 #[ignore]
1513 fn test_clipboard() {
1514 let config = Config::default();
1515 let (dir_context, _temp) = test_dir_context();
1516 let mut editor = Editor::new(
1517 config,
1518 80,
1519 24,
1520 dir_context,
1521 crate::view::color_support::ColorCapability::TrueColor,
1522 test_filesystem(),
1523 )
1524 .unwrap();
1525
1526 editor.clipboard.set_internal("test".to_string());
1528
1529 editor.paste();
1531
1532 let content = editor.active_state().buffer.to_string().unwrap();
1533 assert_eq!(content, "test");
1534 }
1535
1536 #[test]
1537 fn test_action_to_events_insert_char() {
1538 let config = Config::default();
1539 let (dir_context, _temp) = test_dir_context();
1540 let mut editor = Editor::new(
1541 config,
1542 80,
1543 24,
1544 dir_context,
1545 crate::view::color_support::ColorCapability::TrueColor,
1546 test_filesystem(),
1547 )
1548 .unwrap();
1549
1550 let events = editor.action_to_events(Action::InsertChar('a'));
1551 assert!(events.is_some());
1552
1553 let events = events.unwrap();
1554 assert_eq!(events.len(), 1);
1555
1556 match &events[0] {
1557 Event::Insert { position, text, .. } => {
1558 assert_eq!(*position, 0);
1559 assert_eq!(text, "a");
1560 }
1561 _ => panic!("Expected Insert event"),
1562 }
1563 }
1564
1565 #[test]
1566 fn test_action_to_events_move_right() {
1567 let config = Config::default();
1568 let (dir_context, _temp) = test_dir_context();
1569 let mut editor = Editor::new(
1570 config,
1571 80,
1572 24,
1573 dir_context,
1574 crate::view::color_support::ColorCapability::TrueColor,
1575 test_filesystem(),
1576 )
1577 .unwrap();
1578
1579 let cursor_id = editor.active_cursors().primary_id();
1581 editor.apply_event_to_active_buffer(&Event::Insert {
1582 position: 0,
1583 text: "hello".to_string(),
1584 cursor_id,
1585 });
1586
1587 let events = editor.action_to_events(Action::MoveRight);
1588 assert!(events.is_some());
1589
1590 let events = events.unwrap();
1591 assert_eq!(events.len(), 1);
1592
1593 match &events[0] {
1594 Event::MoveCursor {
1595 new_position,
1596 new_anchor,
1597 ..
1598 } => {
1599 assert_eq!(*new_position, 5);
1601 assert_eq!(*new_anchor, None); }
1603 _ => panic!("Expected MoveCursor event"),
1604 }
1605 }
1606
1607 #[test]
1608 fn test_action_to_events_move_up_down() {
1609 let config = Config::default();
1610 let (dir_context, _temp) = test_dir_context();
1611 let mut editor = Editor::new(
1612 config,
1613 80,
1614 24,
1615 dir_context,
1616 crate::view::color_support::ColorCapability::TrueColor,
1617 test_filesystem(),
1618 )
1619 .unwrap();
1620
1621 let cursor_id = editor.active_cursors().primary_id();
1623 editor.apply_event_to_active_buffer(&Event::Insert {
1624 position: 0,
1625 text: "line1\nline2\nline3".to_string(),
1626 cursor_id,
1627 });
1628
1629 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1631 cursor_id,
1632 old_position: 0, new_position: 6,
1634 old_anchor: None, new_anchor: None,
1636 old_sticky_column: 0,
1637 new_sticky_column: 0,
1638 });
1639
1640 let events = editor.action_to_events(Action::MoveUp);
1642 assert!(events.is_some());
1643 let events = events.unwrap();
1644 assert_eq!(events.len(), 1);
1645
1646 match &events[0] {
1647 Event::MoveCursor { new_position, .. } => {
1648 assert_eq!(*new_position, 0); }
1650 _ => panic!("Expected MoveCursor event"),
1651 }
1652 }
1653
1654 #[test]
1655 fn test_action_to_events_insert_newline() {
1656 let config = Config::default();
1657 let (dir_context, _temp) = test_dir_context();
1658 let mut editor = Editor::new(
1659 config,
1660 80,
1661 24,
1662 dir_context,
1663 crate::view::color_support::ColorCapability::TrueColor,
1664 test_filesystem(),
1665 )
1666 .unwrap();
1667
1668 let events = editor.action_to_events(Action::InsertNewline);
1669 assert!(events.is_some());
1670
1671 let events = events.unwrap();
1672 assert_eq!(events.len(), 1);
1673
1674 match &events[0] {
1675 Event::Insert { text, .. } => {
1676 assert_eq!(text, "\n");
1677 }
1678 _ => panic!("Expected Insert event"),
1679 }
1680 }
1681
1682 #[test]
1683 fn test_action_to_events_unimplemented() {
1684 let config = Config::default();
1685 let (dir_context, _temp) = test_dir_context();
1686 let mut editor = Editor::new(
1687 config,
1688 80,
1689 24,
1690 dir_context,
1691 crate::view::color_support::ColorCapability::TrueColor,
1692 test_filesystem(),
1693 )
1694 .unwrap();
1695
1696 assert!(editor.action_to_events(Action::Save).is_none());
1698 assert!(editor.action_to_events(Action::Quit).is_none());
1699 assert!(editor.action_to_events(Action::Undo).is_none());
1700 }
1701
1702 #[test]
1703 fn test_action_to_events_delete_backward() {
1704 let config = Config::default();
1705 let (dir_context, _temp) = test_dir_context();
1706 let mut editor = Editor::new(
1707 config,
1708 80,
1709 24,
1710 dir_context,
1711 crate::view::color_support::ColorCapability::TrueColor,
1712 test_filesystem(),
1713 )
1714 .unwrap();
1715
1716 let cursor_id = editor.active_cursors().primary_id();
1718 editor.apply_event_to_active_buffer(&Event::Insert {
1719 position: 0,
1720 text: "hello".to_string(),
1721 cursor_id,
1722 });
1723
1724 let events = editor.action_to_events(Action::DeleteBackward);
1725 assert!(events.is_some());
1726
1727 let events = events.unwrap();
1728 assert_eq!(events.len(), 1);
1729
1730 match &events[0] {
1731 Event::Delete {
1732 range,
1733 deleted_text,
1734 ..
1735 } => {
1736 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1738 }
1739 _ => panic!("Expected Delete event"),
1740 }
1741 }
1742
1743 #[test]
1744 fn test_action_to_events_delete_forward() {
1745 let config = Config::default();
1746 let (dir_context, _temp) = test_dir_context();
1747 let mut editor = Editor::new(
1748 config,
1749 80,
1750 24,
1751 dir_context,
1752 crate::view::color_support::ColorCapability::TrueColor,
1753 test_filesystem(),
1754 )
1755 .unwrap();
1756
1757 let cursor_id = editor.active_cursors().primary_id();
1759 editor.apply_event_to_active_buffer(&Event::Insert {
1760 position: 0,
1761 text: "hello".to_string(),
1762 cursor_id,
1763 });
1764
1765 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1767 cursor_id,
1768 old_position: 0, new_position: 0,
1770 old_anchor: None, new_anchor: None,
1772 old_sticky_column: 0,
1773 new_sticky_column: 0,
1774 });
1775
1776 let events = editor.action_to_events(Action::DeleteForward);
1777 assert!(events.is_some());
1778
1779 let events = events.unwrap();
1780 assert_eq!(events.len(), 1);
1781
1782 match &events[0] {
1783 Event::Delete {
1784 range,
1785 deleted_text,
1786 ..
1787 } => {
1788 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1790 }
1791 _ => panic!("Expected Delete event"),
1792 }
1793 }
1794
1795 #[test]
1796 fn test_action_to_events_select_right() {
1797 let config = Config::default();
1798 let (dir_context, _temp) = test_dir_context();
1799 let mut editor = Editor::new(
1800 config,
1801 80,
1802 24,
1803 dir_context,
1804 crate::view::color_support::ColorCapability::TrueColor,
1805 test_filesystem(),
1806 )
1807 .unwrap();
1808
1809 let cursor_id = editor.active_cursors().primary_id();
1811 editor.apply_event_to_active_buffer(&Event::Insert {
1812 position: 0,
1813 text: "hello".to_string(),
1814 cursor_id,
1815 });
1816
1817 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1819 cursor_id,
1820 old_position: 0, new_position: 0,
1822 old_anchor: None, new_anchor: None,
1824 old_sticky_column: 0,
1825 new_sticky_column: 0,
1826 });
1827
1828 let events = editor.action_to_events(Action::SelectRight);
1829 assert!(events.is_some());
1830
1831 let events = events.unwrap();
1832 assert_eq!(events.len(), 1);
1833
1834 match &events[0] {
1835 Event::MoveCursor {
1836 new_position,
1837 new_anchor,
1838 ..
1839 } => {
1840 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1843 _ => panic!("Expected MoveCursor event"),
1844 }
1845 }
1846
1847 #[test]
1848 fn test_action_to_events_select_all() {
1849 let config = Config::default();
1850 let (dir_context, _temp) = test_dir_context();
1851 let mut editor = Editor::new(
1852 config,
1853 80,
1854 24,
1855 dir_context,
1856 crate::view::color_support::ColorCapability::TrueColor,
1857 test_filesystem(),
1858 )
1859 .unwrap();
1860
1861 let cursor_id = editor.active_cursors().primary_id();
1863 editor.apply_event_to_active_buffer(&Event::Insert {
1864 position: 0,
1865 text: "hello world".to_string(),
1866 cursor_id,
1867 });
1868
1869 let events = editor.action_to_events(Action::SelectAll);
1870 assert!(events.is_some());
1871
1872 let events = events.unwrap();
1873 assert_eq!(events.len(), 1);
1874
1875 match &events[0] {
1876 Event::MoveCursor {
1877 new_position,
1878 new_anchor,
1879 ..
1880 } => {
1881 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1884 _ => panic!("Expected MoveCursor event"),
1885 }
1886 }
1887
1888 #[test]
1889 fn test_action_to_events_document_nav() {
1890 let config = Config::default();
1891 let (dir_context, _temp) = test_dir_context();
1892 let mut editor = Editor::new(
1893 config,
1894 80,
1895 24,
1896 dir_context,
1897 crate::view::color_support::ColorCapability::TrueColor,
1898 test_filesystem(),
1899 )
1900 .unwrap();
1901
1902 let cursor_id = editor.active_cursors().primary_id();
1904 editor.apply_event_to_active_buffer(&Event::Insert {
1905 position: 0,
1906 text: "line1\nline2\nline3".to_string(),
1907 cursor_id,
1908 });
1909
1910 let events = editor.action_to_events(Action::MoveDocumentStart);
1912 assert!(events.is_some());
1913 let events = events.unwrap();
1914 match &events[0] {
1915 Event::MoveCursor { new_position, .. } => {
1916 assert_eq!(*new_position, 0);
1917 }
1918 _ => panic!("Expected MoveCursor event"),
1919 }
1920
1921 let events = editor.action_to_events(Action::MoveDocumentEnd);
1923 assert!(events.is_some());
1924 let events = events.unwrap();
1925 match &events[0] {
1926 Event::MoveCursor { new_position, .. } => {
1927 assert_eq!(*new_position, 17); }
1929 _ => panic!("Expected MoveCursor event"),
1930 }
1931 }
1932
1933 #[test]
1934 fn test_action_to_events_remove_secondary_cursors() {
1935 use crate::model::event::CursorId;
1936
1937 let config = Config::default();
1938 let (dir_context, _temp) = test_dir_context();
1939 let mut editor = Editor::new(
1940 config,
1941 80,
1942 24,
1943 dir_context,
1944 crate::view::color_support::ColorCapability::TrueColor,
1945 test_filesystem(),
1946 )
1947 .unwrap();
1948
1949 let cursor_id = editor.active_cursors().primary_id();
1951 editor.apply_event_to_active_buffer(&Event::Insert {
1952 position: 0,
1953 text: "hello world test".to_string(),
1954 cursor_id,
1955 });
1956
1957 editor.apply_event_to_active_buffer(&Event::AddCursor {
1959 cursor_id: CursorId(1),
1960 position: 5,
1961 anchor: None,
1962 });
1963 editor.apply_event_to_active_buffer(&Event::AddCursor {
1964 cursor_id: CursorId(2),
1965 position: 10,
1966 anchor: None,
1967 });
1968
1969 assert_eq!(editor.active_cursors().count(), 3);
1970
1971 let first_id = editor
1973 .active_cursors()
1974 .iter()
1975 .map(|(id, _)| id)
1976 .min_by_key(|id| id.0)
1977 .expect("Should have at least one cursor");
1978
1979 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
1981 assert!(events.is_some());
1982
1983 let events = events.unwrap();
1984 let remove_cursor_events: Vec<_> = events
1987 .iter()
1988 .filter_map(|e| match e {
1989 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1990 _ => None,
1991 })
1992 .collect();
1993
1994 assert_eq!(remove_cursor_events.len(), 2);
1996
1997 for cursor_id in &remove_cursor_events {
1998 assert_ne!(*cursor_id, first_id);
2000 }
2001 }
2002
2003 #[test]
2004 fn test_action_to_events_scroll() {
2005 let config = Config::default();
2006 let (dir_context, _temp) = test_dir_context();
2007 let mut editor = Editor::new(
2008 config,
2009 80,
2010 24,
2011 dir_context,
2012 crate::view::color_support::ColorCapability::TrueColor,
2013 test_filesystem(),
2014 )
2015 .unwrap();
2016
2017 let events = editor.action_to_events(Action::ScrollUp);
2019 assert!(events.is_some());
2020 let events = events.unwrap();
2021 assert_eq!(events.len(), 1);
2022 match &events[0] {
2023 Event::Scroll { line_offset } => {
2024 assert_eq!(*line_offset, -1);
2025 }
2026 _ => panic!("Expected Scroll event"),
2027 }
2028
2029 let events = editor.action_to_events(Action::ScrollDown);
2031 assert!(events.is_some());
2032 let events = events.unwrap();
2033 assert_eq!(events.len(), 1);
2034 match &events[0] {
2035 Event::Scroll { line_offset } => {
2036 assert_eq!(*line_offset, 1);
2037 }
2038 _ => panic!("Expected Scroll event"),
2039 }
2040 }
2041
2042 #[test]
2043 fn test_action_to_events_none() {
2044 let config = Config::default();
2045 let (dir_context, _temp) = test_dir_context();
2046 let mut editor = Editor::new(
2047 config,
2048 80,
2049 24,
2050 dir_context,
2051 crate::view::color_support::ColorCapability::TrueColor,
2052 test_filesystem(),
2053 )
2054 .unwrap();
2055
2056 let events = editor.action_to_events(Action::None);
2058 assert!(events.is_none());
2059 }
2060
2061 #[test]
2062 fn test_lsp_incremental_insert_generates_correct_range() {
2063 use crate::model::buffer::Buffer;
2066
2067 let buffer = Buffer::from_str_test("hello\nworld");
2068
2069 let position = 0;
2072 let (line, character) = buffer.position_to_lsp_position(position);
2073
2074 assert_eq!(line, 0, "Insertion at start should be line 0");
2075 assert_eq!(character, 0, "Insertion at start should be char 0");
2076
2077 let lsp_pos = Position::new(line as u32, character as u32);
2079 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2080
2081 assert_eq!(lsp_range.start.line, 0);
2082 assert_eq!(lsp_range.start.character, 0);
2083 assert_eq!(lsp_range.end.line, 0);
2084 assert_eq!(lsp_range.end.character, 0);
2085 assert_eq!(
2086 lsp_range.start, lsp_range.end,
2087 "Insert should have zero-width range"
2088 );
2089
2090 let position = 3;
2092 let (line, character) = buffer.position_to_lsp_position(position);
2093
2094 assert_eq!(line, 0);
2095 assert_eq!(character, 3);
2096
2097 let position = 6;
2099 let (line, character) = buffer.position_to_lsp_position(position);
2100
2101 assert_eq!(line, 1, "Position after newline should be line 1");
2102 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2103 }
2104
2105 #[test]
2106 fn test_lsp_incremental_delete_generates_correct_range() {
2107 use crate::model::buffer::Buffer;
2110
2111 let buffer = Buffer::from_str_test("hello\nworld");
2112
2113 let range_start = 1;
2115 let range_end = 5;
2116
2117 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2118 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2119
2120 assert_eq!(start_line, 0);
2121 assert_eq!(start_char, 1);
2122 assert_eq!(end_line, 0);
2123 assert_eq!(end_char, 5);
2124
2125 let lsp_range = LspRange::new(
2126 Position::new(start_line as u32, start_char as u32),
2127 Position::new(end_line as u32, end_char as u32),
2128 );
2129
2130 assert_eq!(lsp_range.start.line, 0);
2131 assert_eq!(lsp_range.start.character, 1);
2132 assert_eq!(lsp_range.end.line, 0);
2133 assert_eq!(lsp_range.end.character, 5);
2134 assert_ne!(
2135 lsp_range.start, lsp_range.end,
2136 "Delete should have non-zero range"
2137 );
2138
2139 let range_start = 4;
2141 let range_end = 8;
2142
2143 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2144 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2145
2146 assert_eq!(start_line, 0, "Delete start on line 0");
2147 assert_eq!(start_char, 4, "Delete start at char 4");
2148 assert_eq!(end_line, 1, "Delete end on line 1");
2149 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2150 }
2151
2152 #[test]
2153 fn test_lsp_incremental_utf16_encoding() {
2154 use crate::model::buffer::Buffer;
2157
2158 let buffer = Buffer::from_str_test("😀hello");
2160
2161 let (line, character) = buffer.position_to_lsp_position(4);
2163
2164 assert_eq!(line, 0);
2165 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2166
2167 let (line, character) = buffer.position_to_lsp_position(9);
2169
2170 assert_eq!(line, 0);
2171 assert_eq!(
2172 character, 7,
2173 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2174 );
2175
2176 let buffer = Buffer::from_str_test("café");
2178
2179 let (line, character) = buffer.position_to_lsp_position(3);
2181
2182 assert_eq!(line, 0);
2183 assert_eq!(character, 3);
2184
2185 let (line, character) = buffer.position_to_lsp_position(5);
2187
2188 assert_eq!(line, 0);
2189 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2190 }
2191
2192 #[test]
2193 fn test_lsp_content_change_event_structure() {
2194 let insert_change = TextDocumentContentChangeEvent {
2198 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2199 range_length: None,
2200 text: "NEW".to_string(),
2201 };
2202
2203 assert!(insert_change.range.is_some());
2204 assert_eq!(insert_change.text, "NEW");
2205 let range = insert_change.range.unwrap();
2206 assert_eq!(
2207 range.start, range.end,
2208 "Insert should have zero-width range"
2209 );
2210
2211 let delete_change = TextDocumentContentChangeEvent {
2213 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2214 range_length: None,
2215 text: String::new(),
2216 };
2217
2218 assert!(delete_change.range.is_some());
2219 assert_eq!(delete_change.text, "");
2220 let range = delete_change.range.unwrap();
2221 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2222 assert_eq!(range.start.line, 0);
2223 assert_eq!(range.start.character, 2);
2224 assert_eq!(range.end.line, 0);
2225 assert_eq!(range.end.character, 7);
2226 }
2227
2228 #[test]
2229 fn test_goto_matching_bracket_forward() {
2230 let config = Config::default();
2231 let (dir_context, _temp) = test_dir_context();
2232 let mut editor = Editor::new(
2233 config,
2234 80,
2235 24,
2236 dir_context,
2237 crate::view::color_support::ColorCapability::TrueColor,
2238 test_filesystem(),
2239 )
2240 .unwrap();
2241
2242 let cursor_id = editor.active_cursors().primary_id();
2244 editor.apply_event_to_active_buffer(&Event::Insert {
2245 position: 0,
2246 text: "fn main() { let x = (1 + 2); }".to_string(),
2247 cursor_id,
2248 });
2249
2250 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2252 cursor_id,
2253 old_position: 31,
2254 new_position: 10,
2255 old_anchor: None,
2256 new_anchor: None,
2257 old_sticky_column: 0,
2258 new_sticky_column: 0,
2259 });
2260
2261 assert_eq!(editor.active_cursors().primary().position, 10);
2262
2263 editor.goto_matching_bracket();
2265
2266 assert_eq!(editor.active_cursors().primary().position, 29);
2271 }
2272
2273 #[test]
2274 fn test_goto_matching_bracket_backward() {
2275 let config = Config::default();
2276 let (dir_context, _temp) = test_dir_context();
2277 let mut editor = Editor::new(
2278 config,
2279 80,
2280 24,
2281 dir_context,
2282 crate::view::color_support::ColorCapability::TrueColor,
2283 test_filesystem(),
2284 )
2285 .unwrap();
2286
2287 let cursor_id = editor.active_cursors().primary_id();
2289 editor.apply_event_to_active_buffer(&Event::Insert {
2290 position: 0,
2291 text: "fn main() { let x = (1 + 2); }".to_string(),
2292 cursor_id,
2293 });
2294
2295 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2297 cursor_id,
2298 old_position: 31,
2299 new_position: 26,
2300 old_anchor: None,
2301 new_anchor: None,
2302 old_sticky_column: 0,
2303 new_sticky_column: 0,
2304 });
2305
2306 editor.goto_matching_bracket();
2308
2309 assert_eq!(editor.active_cursors().primary().position, 20);
2311 }
2312
2313 #[test]
2314 fn test_goto_matching_bracket_nested() {
2315 let config = Config::default();
2316 let (dir_context, _temp) = test_dir_context();
2317 let mut editor = Editor::new(
2318 config,
2319 80,
2320 24,
2321 dir_context,
2322 crate::view::color_support::ColorCapability::TrueColor,
2323 test_filesystem(),
2324 )
2325 .unwrap();
2326
2327 let cursor_id = editor.active_cursors().primary_id();
2329 editor.apply_event_to_active_buffer(&Event::Insert {
2330 position: 0,
2331 text: "{a{b{c}d}e}".to_string(),
2332 cursor_id,
2333 });
2334
2335 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2337 cursor_id,
2338 old_position: 11,
2339 new_position: 0,
2340 old_anchor: None,
2341 new_anchor: None,
2342 old_sticky_column: 0,
2343 new_sticky_column: 0,
2344 });
2345
2346 editor.goto_matching_bracket();
2348
2349 assert_eq!(editor.active_cursors().primary().position, 10);
2351 }
2352
2353 #[test]
2354 fn test_search_case_sensitive() {
2355 let config = Config::default();
2356 let (dir_context, _temp) = test_dir_context();
2357 let mut editor = Editor::new(
2358 config,
2359 80,
2360 24,
2361 dir_context,
2362 crate::view::color_support::ColorCapability::TrueColor,
2363 test_filesystem(),
2364 )
2365 .unwrap();
2366
2367 let cursor_id = editor.active_cursors().primary_id();
2369 editor.apply_event_to_active_buffer(&Event::Insert {
2370 position: 0,
2371 text: "Hello hello HELLO".to_string(),
2372 cursor_id,
2373 });
2374
2375 editor.search_case_sensitive = false;
2377 editor.perform_search("hello");
2378
2379 let search_state = editor.search_state.as_ref().unwrap();
2380 assert_eq!(
2381 search_state.matches.len(),
2382 3,
2383 "Should find all 3 matches case-insensitively"
2384 );
2385
2386 editor.search_case_sensitive = true;
2388 editor.perform_search("hello");
2389
2390 let search_state = editor.search_state.as_ref().unwrap();
2391 assert_eq!(
2392 search_state.matches.len(),
2393 1,
2394 "Should find only 1 exact match"
2395 );
2396 assert_eq!(
2397 search_state.matches[0], 6,
2398 "Should find 'hello' at position 6"
2399 );
2400 }
2401
2402 #[test]
2403 fn test_search_whole_word() {
2404 let config = Config::default();
2405 let (dir_context, _temp) = test_dir_context();
2406 let mut editor = Editor::new(
2407 config,
2408 80,
2409 24,
2410 dir_context,
2411 crate::view::color_support::ColorCapability::TrueColor,
2412 test_filesystem(),
2413 )
2414 .unwrap();
2415
2416 let cursor_id = editor.active_cursors().primary_id();
2418 editor.apply_event_to_active_buffer(&Event::Insert {
2419 position: 0,
2420 text: "test testing tested attest test".to_string(),
2421 cursor_id,
2422 });
2423
2424 editor.search_whole_word = false;
2426 editor.search_case_sensitive = true;
2427 editor.perform_search("test");
2428
2429 let search_state = editor.search_state.as_ref().unwrap();
2430 assert_eq!(
2431 search_state.matches.len(),
2432 5,
2433 "Should find 'test' in all occurrences"
2434 );
2435
2436 editor.search_whole_word = true;
2438 editor.perform_search("test");
2439
2440 let search_state = editor.search_state.as_ref().unwrap();
2441 assert_eq!(
2442 search_state.matches.len(),
2443 2,
2444 "Should find only whole word 'test'"
2445 );
2446 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2447 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2448 }
2449
2450 #[test]
2451 fn test_search_scan_completes_when_capped() {
2452 let config = Config::default();
2458 let (dir_context, _temp) = test_dir_context();
2459 let mut editor = Editor::new(
2460 config,
2461 80,
2462 24,
2463 dir_context,
2464 crate::view::color_support::ColorCapability::TrueColor,
2465 test_filesystem(),
2466 )
2467 .unwrap();
2468
2469 let buffer_id = editor.active_buffer();
2472 let regex = regex::bytes::Regex::new("test").unwrap();
2473 let fake_chunks = vec![
2474 crate::model::buffer::LineScanChunk {
2475 leaf_index: 0,
2476 byte_len: 100,
2477 already_known: true,
2478 },
2479 crate::model::buffer::LineScanChunk {
2480 leaf_index: 1,
2481 byte_len: 100,
2482 already_known: true,
2483 },
2484 ];
2485
2486 let chunked = crate::model::buffer::ChunkedSearchState {
2487 chunks: fake_chunks,
2488 next_chunk: 1, next_doc_offset: 100,
2490 total_bytes: 200,
2491 scanned_bytes: 100,
2492 regex,
2493 matches: vec![
2494 crate::model::buffer::SearchMatch {
2495 byte_offset: 10,
2496 length: 4,
2497 line: 1,
2498 column: 11,
2499 context: String::new(),
2500 },
2501 crate::model::buffer::SearchMatch {
2502 byte_offset: 50,
2503 length: 4,
2504 line: 1,
2505 column: 51,
2506 context: String::new(),
2507 },
2508 ],
2509 overlap_tail: Vec::new(),
2510 overlap_doc_offset: 0,
2511 max_matches: 10_000,
2512 capped: true, query_len: 4,
2514 running_line: 1,
2515 };
2516
2517 editor.search_scan.start(
2518 buffer_id,
2519 Vec::new(),
2520 chunked,
2521 "test".to_string(),
2522 None,
2523 false,
2524 false,
2525 false,
2526 );
2527
2528 let result = editor.process_search_scan();
2530 assert!(
2531 result,
2532 "process_search_scan should return true (needs render)"
2533 );
2534
2535 assert_eq!(
2537 editor.search_scan.buffer_id(),
2538 None,
2539 "search_scan should be drained after capped scan completes"
2540 );
2541
2542 let search_state = editor
2544 .search_state
2545 .as_ref()
2546 .expect("search_state should be set after scan finishes");
2547 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2548 assert_eq!(search_state.query, "test");
2549 assert!(
2550 search_state.capped,
2551 "search_state should be marked as capped"
2552 );
2553 }
2554
2555 #[test]
2556 fn test_bookmarks() {
2557 let config = Config::default();
2558 let (dir_context, _temp) = test_dir_context();
2559 let mut editor = Editor::new(
2560 config,
2561 80,
2562 24,
2563 dir_context,
2564 crate::view::color_support::ColorCapability::TrueColor,
2565 test_filesystem(),
2566 )
2567 .unwrap();
2568
2569 let cursor_id = editor.active_cursors().primary_id();
2571 editor.apply_event_to_active_buffer(&Event::Insert {
2572 position: 0,
2573 text: "Line 1\nLine 2\nLine 3".to_string(),
2574 cursor_id,
2575 });
2576
2577 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2579 cursor_id,
2580 old_position: 21,
2581 new_position: 7,
2582 old_anchor: None,
2583 new_anchor: None,
2584 old_sticky_column: 0,
2585 new_sticky_column: 0,
2586 });
2587
2588 editor.set_bookmark('1');
2590 assert_eq!(editor.bookmarks.get('1').map(|b| b.position), Some(7));
2591
2592 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2594 cursor_id,
2595 old_position: 7,
2596 new_position: 14,
2597 old_anchor: None,
2598 new_anchor: None,
2599 old_sticky_column: 0,
2600 new_sticky_column: 0,
2601 });
2602
2603 editor.jump_to_bookmark('1');
2605 assert_eq!(editor.active_cursors().primary().position, 7);
2606
2607 editor.clear_bookmark('1');
2609 assert_eq!(editor.bookmarks.get('1'), None);
2610 }
2611
2612 #[test]
2613 fn test_action_enum_new_variants() {
2614 use serde_json::json;
2616
2617 let args = HashMap::new();
2618 assert_eq!(
2619 Action::from_str("smart_home", &args),
2620 Some(Action::SmartHome)
2621 );
2622 assert_eq!(
2623 Action::from_str("dedent_selection", &args),
2624 Some(Action::DedentSelection)
2625 );
2626 assert_eq!(
2627 Action::from_str("toggle_comment", &args),
2628 Some(Action::ToggleComment)
2629 );
2630 assert_eq!(
2631 Action::from_str("goto_matching_bracket", &args),
2632 Some(Action::GoToMatchingBracket)
2633 );
2634 assert_eq!(
2635 Action::from_str("list_bookmarks", &args),
2636 Some(Action::ListBookmarks)
2637 );
2638 assert_eq!(
2639 Action::from_str("toggle_search_case_sensitive", &args),
2640 Some(Action::ToggleSearchCaseSensitive)
2641 );
2642 assert_eq!(
2643 Action::from_str("toggle_search_whole_word", &args),
2644 Some(Action::ToggleSearchWholeWord)
2645 );
2646
2647 let mut args_with_char = HashMap::new();
2649 args_with_char.insert("char".to_string(), json!("5"));
2650 assert_eq!(
2651 Action::from_str("set_bookmark", &args_with_char),
2652 Some(Action::SetBookmark('5'))
2653 );
2654 assert_eq!(
2655 Action::from_str("jump_to_bookmark", &args_with_char),
2656 Some(Action::JumpToBookmark('5'))
2657 );
2658 assert_eq!(
2659 Action::from_str("clear_bookmark", &args_with_char),
2660 Some(Action::ClearBookmark('5'))
2661 );
2662 }
2663
2664 #[test]
2665 fn test_keybinding_new_defaults() {
2666 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2667
2668 let mut config = Config::default();
2672 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2673 let resolver = KeybindingResolver::new(&config);
2674
2675 let event = KeyEvent {
2677 code: KeyCode::Char('/'),
2678 modifiers: KeyModifiers::CONTROL,
2679 kind: KeyEventKind::Press,
2680 state: KeyEventState::NONE,
2681 };
2682 let action = resolver.resolve(&event, KeyContext::Normal);
2683 assert_eq!(action, Action::ToggleComment);
2684
2685 let event = KeyEvent {
2687 code: KeyCode::Char(']'),
2688 modifiers: KeyModifiers::CONTROL,
2689 kind: KeyEventKind::Press,
2690 state: KeyEventState::NONE,
2691 };
2692 let action = resolver.resolve(&event, KeyContext::Normal);
2693 assert_eq!(action, Action::GoToMatchingBracket);
2694
2695 let event = KeyEvent {
2697 code: KeyCode::Tab,
2698 modifiers: KeyModifiers::SHIFT,
2699 kind: KeyEventKind::Press,
2700 state: KeyEventState::NONE,
2701 };
2702 let action = resolver.resolve(&event, KeyContext::Normal);
2703 assert_eq!(action, Action::DedentSelection);
2704
2705 let event = KeyEvent {
2707 code: KeyCode::Char('g'),
2708 modifiers: KeyModifiers::CONTROL,
2709 kind: KeyEventKind::Press,
2710 state: KeyEventState::NONE,
2711 };
2712 let action = resolver.resolve(&event, KeyContext::Normal);
2713 assert_eq!(action, Action::GotoLine);
2714
2715 let event = KeyEvent {
2717 code: KeyCode::Char('5'),
2718 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2719 kind: KeyEventKind::Press,
2720 state: KeyEventState::NONE,
2721 };
2722 let action = resolver.resolve(&event, KeyContext::Normal);
2723 assert_eq!(action, Action::SetBookmark('5'));
2724
2725 let event = KeyEvent {
2726 code: KeyCode::Char('5'),
2727 modifiers: KeyModifiers::ALT,
2728 kind: KeyEventKind::Press,
2729 state: KeyEventState::NONE,
2730 };
2731 let action = resolver.resolve(&event, KeyContext::Normal);
2732 assert_eq!(action, Action::JumpToBookmark('5'));
2733 }
2734
2735 #[test]
2747 fn test_lsp_rename_didchange_positions_bug() {
2748 use crate::model::buffer::Buffer;
2749
2750 let config = Config::default();
2751 let (dir_context, _temp) = test_dir_context();
2752 let mut editor = Editor::new(
2753 config,
2754 80,
2755 24,
2756 dir_context,
2757 crate::view::color_support::ColorCapability::TrueColor,
2758 test_filesystem(),
2759 )
2760 .unwrap();
2761
2762 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2766 editor.active_state_mut().buffer =
2767 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2768
2769 let cursor_id = editor.active_cursors().primary_id();
2774
2775 let batch = Event::Batch {
2776 events: vec![
2777 Event::Delete {
2779 range: 23..26, deleted_text: "val".to_string(),
2781 cursor_id,
2782 },
2783 Event::Insert {
2784 position: 23,
2785 text: "value".to_string(),
2786 cursor_id,
2787 },
2788 Event::Delete {
2790 range: 7..10, deleted_text: "val".to_string(),
2792 cursor_id,
2793 },
2794 Event::Insert {
2795 position: 7,
2796 text: "value".to_string(),
2797 cursor_id,
2798 },
2799 ],
2800 description: "LSP Rename".to_string(),
2801 };
2802
2803 let lsp_changes_before = editor.collect_lsp_changes(&batch);
2805
2806 editor.apply_event_to_active_buffer(&batch);
2808
2809 let lsp_changes_after = editor.collect_lsp_changes(&batch);
2812
2813 let final_content = editor.active_state().buffer.to_string().unwrap();
2815 assert_eq!(
2816 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2817 "Buffer should have 'value' in both places"
2818 );
2819
2820 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2826
2827 let first_delete = &lsp_changes_before[0];
2828 let first_del_range = first_delete.range.unwrap();
2829 assert_eq!(
2830 first_del_range.start.line, 1,
2831 "First delete should be on line 1 (BEFORE)"
2832 );
2833 assert_eq!(
2834 first_del_range.start.character, 4,
2835 "First delete start should be at char 4 (BEFORE)"
2836 );
2837
2838 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2844
2845 let first_delete_after = &lsp_changes_after[0];
2846 let first_del_range_after = first_delete_after.range.unwrap();
2847
2848 eprintln!("BEFORE modification:");
2851 eprintln!(
2852 " Delete at line {}, char {}-{}",
2853 first_del_range.start.line,
2854 first_del_range.start.character,
2855 first_del_range.end.character
2856 );
2857 eprintln!("AFTER modification:");
2858 eprintln!(
2859 " Delete at line {}, char {}-{}",
2860 first_del_range_after.start.line,
2861 first_del_range_after.start.character,
2862 first_del_range_after.end.character
2863 );
2864
2865 assert_ne!(
2883 first_del_range_after.end.character, first_del_range.end.character,
2884 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2885 );
2886
2887 eprintln!("\n=== BUG DEMONSTRATED ===");
2888 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2889 eprintln!("the positions are WRONG because they're calculated from the");
2890 eprintln!("modified buffer, not the original buffer.");
2891 eprintln!("This causes the second rename to fail with 'content modified' error.");
2892 eprintln!("========================\n");
2893 }
2894
2895 #[test]
2896 fn test_lsp_rename_preserves_cursor_position() {
2897 use crate::model::buffer::Buffer;
2898
2899 let config = Config::default();
2900 let (dir_context, _temp) = test_dir_context();
2901 let mut editor = Editor::new(
2902 config,
2903 80,
2904 24,
2905 dir_context,
2906 crate::view::color_support::ColorCapability::TrueColor,
2907 test_filesystem(),
2908 )
2909 .unwrap();
2910
2911 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2915 editor.active_state_mut().buffer =
2916 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2917
2918 let original_cursor_pos = 23;
2920 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2921
2922 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2924 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2925 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2926
2927 let cursor_id = editor.active_cursors().primary_id();
2930 let buffer_id = editor.active_buffer();
2931
2932 let events = vec![
2933 Event::Delete {
2935 range: 23..26, deleted_text: "val".to_string(),
2937 cursor_id,
2938 },
2939 Event::Insert {
2940 position: 23,
2941 text: "value".to_string(),
2942 cursor_id,
2943 },
2944 Event::Delete {
2946 range: 7..10, deleted_text: "val".to_string(),
2948 cursor_id,
2949 },
2950 Event::Insert {
2951 position: 7,
2952 text: "value".to_string(),
2953 cursor_id,
2954 },
2955 ];
2956
2957 editor
2959 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2960 .unwrap();
2961
2962 let final_content = editor.active_state().buffer.to_string().unwrap();
2964 assert_eq!(
2965 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2966 "Buffer should have 'value' in both places"
2967 );
2968
2969 let final_cursor_pos = editor.active_cursors().primary().position;
2977 let expected_cursor_pos = 25; assert_eq!(
2980 final_cursor_pos, expected_cursor_pos,
2981 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2982 Original pos: {}, expected adjustment: +2 for first rename",
2983 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2984 );
2985
2986 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2988 assert_eq!(
2989 text_at_new_cursor, "value",
2990 "Cursor should be at the start of 'value' after rename"
2991 );
2992 }
2993
2994 #[test]
2995 fn test_lsp_rename_twice_consecutive() {
2996 use crate::model::buffer::Buffer;
2999
3000 let config = Config::default();
3001 let (dir_context, _temp) = test_dir_context();
3002 let mut editor = Editor::new(
3003 config,
3004 80,
3005 24,
3006 dir_context,
3007 crate::view::color_support::ColorCapability::TrueColor,
3008 test_filesystem(),
3009 )
3010 .unwrap();
3011
3012 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
3014 editor.active_state_mut().buffer =
3015 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
3016
3017 let cursor_id = editor.active_cursors().primary_id();
3018 let buffer_id = editor.active_buffer();
3019
3020 let events1 = vec![
3023 Event::Delete {
3025 range: 23..26,
3026 deleted_text: "val".to_string(),
3027 cursor_id,
3028 },
3029 Event::Insert {
3030 position: 23,
3031 text: "value".to_string(),
3032 cursor_id,
3033 },
3034 Event::Delete {
3036 range: 7..10,
3037 deleted_text: "val".to_string(),
3038 cursor_id,
3039 },
3040 Event::Insert {
3041 position: 7,
3042 text: "value".to_string(),
3043 cursor_id,
3044 },
3045 ];
3046
3047 let batch1 = Event::Batch {
3049 events: events1.clone(),
3050 description: "LSP Rename 1".to_string(),
3051 };
3052
3053 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
3055
3056 assert_eq!(
3058 lsp_changes1.len(),
3059 4,
3060 "First rename should have 4 LSP changes"
3061 );
3062
3063 let first_del = &lsp_changes1[0];
3065 let first_del_range = first_del.range.unwrap();
3066 assert_eq!(first_del_range.start.line, 1, "First delete line");
3067 assert_eq!(
3068 first_del_range.start.character, 4,
3069 "First delete start char"
3070 );
3071 assert_eq!(first_del_range.end.character, 7, "First delete end char");
3072
3073 editor
3075 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
3076 .unwrap();
3077
3078 let after_first = editor.active_state().buffer.to_string().unwrap();
3080 assert_eq!(
3081 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3082 "After first rename"
3083 );
3084
3085 let events2 = vec![
3095 Event::Delete {
3097 range: 25..30,
3098 deleted_text: "value".to_string(),
3099 cursor_id,
3100 },
3101 Event::Insert {
3102 position: 25,
3103 text: "x".to_string(),
3104 cursor_id,
3105 },
3106 Event::Delete {
3108 range: 7..12,
3109 deleted_text: "value".to_string(),
3110 cursor_id,
3111 },
3112 Event::Insert {
3113 position: 7,
3114 text: "x".to_string(),
3115 cursor_id,
3116 },
3117 ];
3118
3119 let batch2 = Event::Batch {
3121 events: events2.clone(),
3122 description: "LSP Rename 2".to_string(),
3123 };
3124
3125 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
3127
3128 assert_eq!(
3132 lsp_changes2.len(),
3133 4,
3134 "Second rename should have 4 LSP changes"
3135 );
3136
3137 let second_first_del = &lsp_changes2[0];
3139 let second_first_del_range = second_first_del.range.unwrap();
3140 assert_eq!(
3141 second_first_del_range.start.line, 1,
3142 "Second rename first delete should be on line 1"
3143 );
3144 assert_eq!(
3145 second_first_del_range.start.character, 4,
3146 "Second rename first delete start should be at char 4"
3147 );
3148 assert_eq!(
3149 second_first_del_range.end.character, 9,
3150 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3151 );
3152
3153 let second_third_del = &lsp_changes2[2];
3155 let second_third_del_range = second_third_del.range.unwrap();
3156 assert_eq!(
3157 second_third_del_range.start.line, 0,
3158 "Second rename third delete should be on line 0"
3159 );
3160 assert_eq!(
3161 second_third_del_range.start.character, 7,
3162 "Second rename third delete start should be at char 7"
3163 );
3164 assert_eq!(
3165 second_third_del_range.end.character, 12,
3166 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3167 );
3168
3169 editor
3171 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3172 .unwrap();
3173
3174 let after_second = editor.active_state().buffer.to_string().unwrap();
3176 assert_eq!(
3177 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3178 "After second rename"
3179 );
3180 }
3181
3182 #[test]
3183 fn test_ensure_active_tab_visible_static_offset() {
3184 let config = Config::default();
3185 let (dir_context, _temp) = test_dir_context();
3186 let mut editor = Editor::new(
3187 config,
3188 80,
3189 24,
3190 dir_context,
3191 crate::view::color_support::ColorCapability::TrueColor,
3192 test_filesystem(),
3193 )
3194 .unwrap();
3195 let split_id = editor.split_manager.active_split();
3196
3197 let buf1 = editor.new_buffer();
3199 editor
3200 .buffers
3201 .get_mut(&buf1)
3202 .unwrap()
3203 .buffer
3204 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3205 let buf2 = editor.new_buffer();
3206 editor
3207 .buffers
3208 .get_mut(&buf2)
3209 .unwrap()
3210 .buffer
3211 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3212 let buf3 = editor.new_buffer();
3213 editor
3214 .buffers
3215 .get_mut(&buf3)
3216 .unwrap()
3217 .buffer
3218 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3219
3220 {
3221 use crate::view::split::TabTarget;
3222 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
3223 view_state.open_buffers = vec![
3224 TabTarget::Buffer(buf1),
3225 TabTarget::Buffer(buf2),
3226 TabTarget::Buffer(buf3),
3227 ];
3228 view_state.tab_scroll_offset = 50;
3229 }
3230
3231 editor.ensure_active_tab_visible(split_id, buf1, 25);
3235 assert_eq!(
3236 editor
3237 .split_view_states
3238 .get(&split_id)
3239 .unwrap()
3240 .tab_scroll_offset,
3241 0
3242 );
3243
3244 editor.ensure_active_tab_visible(split_id, buf3, 25);
3246 let view_state = editor.split_view_states.get(&split_id).unwrap();
3247 assert!(view_state.tab_scroll_offset > 0);
3248 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3249 let total_width: usize = buffer_ids
3250 .iter()
3251 .enumerate()
3252 .map(|(idx, id)| {
3253 let state = editor.buffers.get(id).unwrap();
3254 let name_len = state
3255 .buffer
3256 .file_path()
3257 .and_then(|p| p.file_name())
3258 .and_then(|n| n.to_str())
3259 .map(|s| s.chars().count())
3260 .unwrap_or(0);
3261 let tab_width = 2 + name_len;
3262 if idx < buffer_ids.len() - 1 {
3263 tab_width + 1 } else {
3265 tab_width
3266 }
3267 })
3268 .sum();
3269 assert!(view_state.tab_scroll_offset <= total_width);
3270 }
3271}