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;
41pub mod lsp_auto_prompt;
42mod lsp_event_notify;
43mod lsp_requests;
44mod lsp_status;
45mod macro_actions;
46mod macros;
47mod menu_actions;
48mod menu_context;
49mod mouse_input;
50mod navigation;
51mod on_save_actions;
52mod path_utils;
53mod plugin_commands;
54mod plugin_dispatch;
55mod popup_actions;
56mod popup_dialogs;
57mod popup_overlay_actions;
58mod prompt_actions;
59mod prompt_lifecycle;
60mod recovery_actions;
61mod regex_replace;
62mod render;
63mod scan_orchestrators;
64mod scroll_sync;
65mod scrollbar_input;
66mod scrollbar_math;
67mod search_ops;
68mod search_scan;
69mod settings_actions;
70mod settings_prompts;
71mod shell_command;
72mod smart_home;
73mod split_actions;
74mod stdin_stream;
75mod tab_drag;
76mod terminal;
77mod terminal_input;
78mod terminal_mouse;
79mod text_ops;
80mod theme_inspect;
81mod toggle_actions;
82pub mod types;
83mod undo_actions;
84mod view_actions;
85mod virtual_buffers;
86pub mod warning_domains;
87pub mod workspace;
88
89use anyhow::Result as AnyhowResult;
90use rust_i18n::t;
91
92pub fn editor_tick(
97 editor: &mut Editor,
98 mut clear_terminal: impl FnMut() -> AnyhowResult<()>,
99) -> AnyhowResult<bool> {
100 let mut needs_render = false;
101
102 let async_messages = {
103 let _s = tracing::info_span!("process_async_messages").entered();
104 editor.process_async_messages()
105 };
106 if async_messages {
107 needs_render = true;
108 }
109 let pending_file_opens = {
110 let _s = tracing::info_span!("process_pending_file_opens").entered();
111 editor.process_pending_file_opens()
112 };
113 if pending_file_opens {
114 needs_render = true;
115 }
116 if editor.process_line_scan() {
117 needs_render = true;
118 }
119 let search_scan = {
120 let _s = tracing::info_span!("process_search_scan").entered();
121 editor.process_search_scan()
122 };
123 if search_scan {
124 needs_render = true;
125 }
126 let search_overlay_refresh = {
127 let _s = tracing::info_span!("check_search_overlay_refresh").entered();
128 editor.check_search_overlay_refresh()
129 };
130 if search_overlay_refresh {
131 needs_render = true;
132 }
133 if editor.check_mouse_hover_timer() {
134 needs_render = true;
135 }
136 if editor.check_semantic_highlight_timer() {
137 needs_render = true;
138 }
139 if editor.check_completion_trigger_timer() {
140 needs_render = true;
141 }
142 editor.check_diagnostic_pull_timer();
143 if editor.check_warning_log() {
144 needs_render = true;
145 }
146 if editor.poll_stdin_streaming() {
147 needs_render = true;
148 }
149
150 if let Err(e) = editor.auto_recovery_save_dirty_buffers() {
151 tracing::debug!("Auto-recovery-save error: {}", e);
152 }
153 if let Err(e) = editor.auto_save_persistent_buffers() {
154 tracing::debug!("Auto-save (disk) error: {}", e);
155 }
156
157 if editor.take_full_redraw_request() {
158 clear_terminal()?;
159 needs_render = true;
160 }
161
162 Ok(needs_render)
163}
164
165pub(crate) use path_utils::normalize_path;
166
167use self::types::{
168 CachedLayout, FileExplorerContextMenu, InteractiveReplaceState, LspMessageEntry,
169 LspProgressInfo, MouseState, SearchState, TabContextMenu, DEFAULT_BACKGROUND_FILE,
170};
171use crate::config::Config;
172use crate::config_io::DirectoryContext;
173use crate::input::buffer_mode::ModeRegistry;
174use crate::input::command_registry::CommandRegistry;
175use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
176use crate::input::position_history::PositionHistory;
177use crate::input::quick_open::{
178 BufferProvider, CommandProvider, FileProvider, GotoLineProvider, QuickOpenRegistry,
179};
180use crate::model::cursor::Cursors;
181use crate::model::event::{Event, EventLog, LeafId, SplitDirection};
182use crate::model::filesystem::FileSystem;
183use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
184use crate::services::fs::FsManager;
185use crate::services::lsp::manager::LspManager;
186use crate::services::plugins::PluginManager;
187use crate::services::recovery::{RecoveryConfig, RecoveryService};
188use crate::services::time_source::{RealTimeSource, SharedTimeSource};
189use crate::state::EditorState;
190use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
191use crate::view::file_tree::{FileTree, FileTreeView};
192use crate::view::prompt::{Prompt, PromptType};
193use crate::view::scroll_sync::ScrollSyncManager;
194use crate::view::split::{SplitManager, SplitViewState};
195use crate::view::ui::{
196 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
197};
198use crossterm::event::{KeyCode, KeyModifiers};
199use ratatui::{
200 layout::{Constraint, Direction, Layout},
201 Frame,
202};
203use std::collections::{HashMap, HashSet};
204use std::ops::Range;
205use std::path::{Path, PathBuf};
206use std::sync::{Arc, RwLock};
207use std::time::Instant;
208
209pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
211pub use self::warning_domains::{
212 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
213 WarningDomainRegistry, WarningLevel, WarningPopupContent,
214};
215pub use crate::model::event::BufferId;
216
217fn lsp_uri_to_host_path(
226 uri: &crate::app::types::LspUri,
227 translation: Option<&crate::services::authority::PathTranslation>,
228) -> Result<PathBuf, String> {
229 uri.to_host_path(translation)
230 .ok_or_else(|| "URI is not a file path".to_string())
231}
232
233#[derive(Clone, Debug)]
235pub struct PendingGrammar {
236 pub language: String,
238 pub grammar_path: String,
240 pub extensions: Vec<String>,
242}
243
244#[derive(Clone, Debug)]
246struct SemanticTokenRangeRequest {
247 buffer_id: BufferId,
248 version: u64,
249 range: Range<usize>,
250 start_line: usize,
251 end_line: usize,
252}
253
254#[derive(Clone, Copy, Debug)]
255enum SemanticTokensFullRequestKind {
256 Full,
257 FullDelta,
258}
259
260#[derive(Clone, Debug)]
261struct SemanticTokenFullRequest {
262 buffer_id: BufferId,
263 version: u64,
264 kind: SemanticTokensFullRequestKind,
265}
266
267#[derive(Clone, Debug)]
268struct FoldingRangeRequest {
269 buffer_id: BufferId,
270 version: u64,
271}
272
273#[derive(Clone, Debug)]
274struct InlayHintsRequest {
275 buffer_id: BufferId,
276 version: u64,
277}
278
279#[derive(Debug, Clone)]
285pub struct DabbrevCycleState {
286 pub original_prefix: String,
288 pub word_start: usize,
290 pub candidates: Vec<String>,
292 pub index: usize,
294}
295
296#[derive(Debug, Clone)]
311pub(crate) struct GotoLinePreviewSnapshot {
312 pub buffer_id: BufferId,
313 pub split_id: LeafId,
314 pub cursor_id: crate::model::event::CursorId,
315 pub position: usize,
316 pub anchor: Option<usize>,
317 pub sticky_column: usize,
318 pub viewport_top_byte: usize,
319 pub viewport_top_view_line_offset: usize,
320 pub viewport_left_column: usize,
321 pub last_jump_position: usize,
322}
323
324pub struct Editor {
326 buffers: HashMap<BufferId, EditorState>,
328
329 event_logs: HashMap<BufferId, EventLog>,
334
335 next_buffer_id: usize,
337
338 config: Arc<Config>,
358
359 config_snapshot_anchor: Arc<Config>,
361
362 config_cached_json: Arc<serde_json::Value>,
365
366 user_config_raw: Arc<serde_json::Value>,
368
369 dir_context: DirectoryContext,
371
372 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
374
375 pending_grammars: Vec<PendingGrammar>,
377
378 grammar_reload_pending: bool,
382
383 grammar_build_in_progress: bool,
386
387 needs_full_grammar_build: bool,
391
392 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
394
395 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
399
400 theme: crate::view::theme::Theme,
402
403 theme_registry: Arc<crate::view::theme::ThemeRegistry>,
406
407 expanded_menus_cache: crate::view::ui::ExpandedMenusCache,
410
411 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
413
414 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
416
417 ansi_background_path: Option<PathBuf>,
419
420 background_fade: f32,
422
423 keybindings: Arc<RwLock<KeybindingResolver>>,
425
426 clipboard: crate::services::clipboard::Clipboard,
428
429 should_quit: bool,
431
432 should_detach: bool,
434
435 session_mode: bool,
437
438 software_cursor_only: bool,
440
441 session_name: Option<String>,
443
444 pending_escape_sequences: Vec<u8>,
447
448 restart_with_dir: Option<PathBuf>,
451
452 status_message: Option<String>,
454
455 plugin_status_message: Option<String>,
457
458 last_window_title: Option<String>,
462
463 plugin_errors: Vec<String>,
466
467 prompt: Option<Prompt>,
469
470 terminal_width: u16,
472 terminal_height: u16,
473
474 lsp: Option<LspManager>,
476
477 buffer_metadata: HashMap<BufferId, BufferMetadata>,
479
480 mode_registry: ModeRegistry,
482
483 tokio_runtime: Option<tokio::runtime::Runtime>,
485
486 async_bridge: Option<AsyncBridge>,
488
489 split_manager: SplitManager,
491
492 split_view_states: HashMap<LeafId, SplitViewState>,
496
497 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
501
502 scroll_sync_manager: ScrollSyncManager,
505
506 file_explorer: Option<FileTreeView>,
508
509 preview: Option<(LeafId, BufferId)>,
523
524 suppress_position_history_once: bool,
529
530 fs_manager: Arc<FsManager>,
532
533 authority: crate::services::authority::Authority,
543
544 pending_authority: Option<crate::services::authority::Authority>,
550
551 pub remote_indicator_override: Option<crate::view::ui::status_bar::RemoteIndicatorOverride>,
557
558 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
563
564 file_explorer_visible: bool,
566
567 file_explorer_sync_in_progress: bool,
570
571 file_explorer_width: crate::config::ExplorerWidth,
575
576 file_explorer_side: crate::config::FileExplorerSide,
578
579 pending_file_explorer_show_hidden: Option<bool>,
581
582 pending_file_explorer_show_gitignored: Option<bool>,
584
585 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
587
588 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
590
591 pub(crate) file_explorer_clipboard: Option<crate::app::file_explorer::FileExplorerClipboard>,
593
594 menu_bar_visible: bool,
596
597 menu_bar_auto_shown: bool,
600
601 tab_bar_visible: bool,
603
604 status_bar_visible: bool,
606
607 prompt_line_visible: bool,
609
610 mouse_enabled: bool,
612
613 same_buffer_scroll_sync: bool,
615
616 mouse_cursor_position: Option<(u16, u16)>,
620
621 gpm_active: bool,
623
624 key_context: KeyContext,
626
627 menu_state: crate::view::ui::MenuState,
629
630 menus: crate::config::MenuConfig,
632
633 working_dir: PathBuf,
635
636 pub position_history: PositionHistory,
638
639 in_navigation: bool,
641
642 next_lsp_request_id: u64,
644
645 pending_completion_requests: HashSet<u64>,
647
648 completion_items: Option<Vec<lsp_types::CompletionItem>>,
651
652 scheduled_completion_trigger: Option<Instant>,
655
656 completion_service: crate::services::completion::CompletionService,
659
660 dabbrev_state: Option<DabbrevCycleState>,
664
665 pending_goto_definition_request: Option<u64>,
667
668 pending_references_request: Option<u64>,
670
671 pending_references_symbol: String,
673
674 pending_signature_help_request: Option<u64>,
676
677 pending_code_actions_requests: HashSet<u64>,
679
680 pending_code_actions_server_names: HashMap<u64, String>,
682
683 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
687
688 pending_inlay_hints_requests: HashMap<u64, InlayHintsRequest>,
699
700 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
702
703 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
705
706 folding_ranges_debounce: HashMap<BufferId, Instant>,
708
709 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
711
712 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
714
715 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
717
718 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
720
721 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
723
724 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
726
727 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
729
730 hover: hover::HoverState,
733
734 search_state: Option<SearchState>,
736
737 search_namespace: crate::view::overlay::OverlayNamespace,
739
740 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
742
743 pending_search_range: Option<Range<usize>>,
745
746 interactive_replace_state: Option<InteractiveReplaceState>,
748
749 mouse_state: MouseState,
751
752 tab_context_menu: Option<TabContextMenu>,
754
755 file_explorer_context_menu: Option<FileExplorerContextMenu>,
757
758 theme_info_popup: Option<types::ThemeInfoPopup>,
760
761 pub(crate) cached_layout: CachedLayout,
763
764 command_registry: Arc<RwLock<CommandRegistry>>,
766
767 quick_open_registry: QuickOpenRegistry,
769
770 plugin_manager: PluginManager,
772
773 plugin_dev_workspaces:
777 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
778
779 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
783
784 panel_ids: HashMap<String, BufferId>,
787
788 buffer_groups: HashMap<types::BufferGroupId, types::BufferGroup>,
790 buffer_to_group: HashMap<BufferId, types::BufferGroupId>,
792 next_buffer_group_id: usize,
794
795 pub(crate) grouped_subtrees:
803 HashMap<crate::model::event::LeafId, crate::view::split::SplitNode>,
804
805 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
808
809 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
815
816 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
819
820 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
824
825 pending_next_key_callbacks: std::collections::VecDeque<fresh_core::api::JsCallbackId>,
830
831 key_capture_active: bool,
838
839 pending_key_capture_buffer: std::collections::VecDeque<fresh_core::api::KeyEventPayload>,
844
845 goto_line_preview: Option<GotoLinePreviewSnapshot>,
850
851 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
853
854 lsp_server_statuses:
856 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
857
858 lsp_window_messages: Vec<LspMessageEntry>,
860
861 lsp_log_messages: Vec<LspMessageEntry>,
863
864 diagnostic_result_ids: HashMap<String, String>,
867
868 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
871
872 scheduled_inlay_hints_request: Option<(BufferId, Instant)>,
875
876 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
879
880 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
882
883 stored_diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
888
889 stored_folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
892
893 event_broadcaster: crate::model::control_event::EventBroadcaster,
895
896 bookmarks: bookmarks::BookmarkState,
898
899 search_case_sensitive: bool,
901 search_whole_word: bool,
902 search_use_regex: bool,
903 search_confirm_each: bool,
905
906 macros: macros::MacroState,
909
910 #[cfg(feature = "plugins")]
912 pending_plugin_actions: Vec<(
913 String,
914 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
915 )>,
916
917 #[cfg(feature = "plugins")]
919 plugin_render_requested: bool,
920
921 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
924
925 user_dismissed_lsp_languages: std::collections::HashSet<String>,
939
940 auto_start_prompted_languages: std::collections::HashSet<String>,
946
947 pending_auto_start_prompts: std::collections::HashSet<String>,
955
956 lsp_auto_prompt_enabled: bool,
964
965 pending_close_buffer: Option<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 pub fn active_state(&self) -> &EditorState {
1266 self.buffers.get(&self.active_buffer()).unwrap()
1267 }
1268
1269 pub fn active_state_mut(&mut self) -> &mut EditorState {
1271 self.buffers.get_mut(&self.active_buffer()).unwrap()
1272 }
1273
1274 pub fn active_cursors(&self) -> &Cursors {
1278 let split_id = self.effective_active_split();
1279 &self.split_view_states.get(&split_id).unwrap().cursors
1280 }
1281
1282 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1284 let split_id = self.effective_active_split();
1285 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
1286 }
1287
1288 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1290 self.completion_items = Some(items);
1291 }
1292
1293 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1295 let active_split = self.split_manager.active_split();
1296 &self.split_view_states.get(&active_split).unwrap().viewport
1297 }
1298
1299 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1301 let active_split = self.split_manager.active_split();
1302 &mut self
1303 .split_view_states
1304 .get_mut(&active_split)
1305 .unwrap()
1306 .viewport
1307 }
1308
1309 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1311 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1313 return composite.name.clone();
1314 }
1315
1316 self.buffer_metadata
1317 .get(&buffer_id)
1318 .map(|m| m.display_name.clone())
1319 .or_else(|| {
1320 self.buffers.get(&buffer_id).and_then(|state| {
1321 state
1322 .buffer
1323 .file_path()
1324 .and_then(|p| p.file_name())
1325 .and_then(|n| n.to_str())
1326 .map(|s| s.to_string())
1327 })
1328 })
1329 .unwrap_or_else(|| "[No Name]".to_string())
1330 }
1331
1332 pub fn active_event_log(&self) -> &EventLog {
1342 self.event_logs.get(&self.active_buffer()).unwrap()
1343 }
1344
1345 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1347 self.event_logs.get_mut(&self.active_buffer()).unwrap()
1348 }
1349
1350 pub(super) fn update_modified_from_event_log(&mut self) {
1354 let is_at_saved = self
1355 .event_logs
1356 .get(&self.active_buffer())
1357 .map(|log| log.is_at_saved_position())
1358 .unwrap_or(false);
1359
1360 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1361 state.buffer.set_modified(!is_at_saved);
1362 }
1363 }
1364}
1365
1366fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1375 use crossterm::event::{KeyCode, KeyModifiers};
1376
1377 let mut modifiers = KeyModifiers::NONE;
1378 let mut remaining = key_str;
1379
1380 loop {
1382 if remaining.starts_with("C-") {
1383 modifiers |= KeyModifiers::CONTROL;
1384 remaining = &remaining[2..];
1385 } else if remaining.starts_with("M-") {
1386 modifiers |= KeyModifiers::ALT;
1387 remaining = &remaining[2..];
1388 } else if remaining.starts_with("S-") {
1389 modifiers |= KeyModifiers::SHIFT;
1390 remaining = &remaining[2..];
1391 } else {
1392 break;
1393 }
1394 }
1395
1396 let upper = remaining.to_uppercase();
1399 let code = match upper.as_str() {
1400 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1401 "TAB" => KeyCode::Tab,
1402 "BACKTAB" => KeyCode::BackTab,
1403 "ESC" | "ESCAPE" => KeyCode::Esc,
1404 "SPC" | "SPACE" => KeyCode::Char(' '),
1405 "DEL" | "DELETE" => KeyCode::Delete,
1406 "BS" | "BACKSPACE" => KeyCode::Backspace,
1407 "UP" => KeyCode::Up,
1408 "DOWN" => KeyCode::Down,
1409 "LEFT" => KeyCode::Left,
1410 "RIGHT" => KeyCode::Right,
1411 "HOME" => KeyCode::Home,
1412 "END" => KeyCode::End,
1413 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1414 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1415 s if s.starts_with('F') && s.len() > 1 => {
1416 if let Ok(n) = s[1..].parse::<u8>() {
1418 KeyCode::F(n)
1419 } else {
1420 return None;
1421 }
1422 }
1423 _ if remaining.len() == 1 => {
1424 let c = remaining.chars().next()?;
1427 if c.is_ascii_uppercase() {
1428 modifiers |= KeyModifiers::SHIFT;
1429 }
1430 KeyCode::Char(c.to_ascii_lowercase())
1431 }
1432 _ => return None,
1433 };
1434
1435 Some((code, modifiers))
1436}
1437
1438#[cfg(test)]
1439mod tests {
1440 use super::*;
1441 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1442 use tempfile::TempDir;
1443
1444 fn test_dir_context() -> (DirectoryContext, TempDir) {
1446 let temp_dir = TempDir::new().unwrap();
1447 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1448 (dir_context, temp_dir)
1449 }
1450
1451 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1453 Arc::new(crate::model::filesystem::StdFileSystem)
1454 }
1455
1456 #[test]
1457 fn test_editor_new() {
1458 let config = Config::default();
1459 let (dir_context, _temp) = test_dir_context();
1460 let editor = Editor::new(
1461 config,
1462 80,
1463 24,
1464 dir_context,
1465 crate::view::color_support::ColorCapability::TrueColor,
1466 test_filesystem(),
1467 )
1468 .unwrap();
1469
1470 assert_eq!(editor.buffers.len(), 1);
1471 assert!(!editor.should_quit());
1472 }
1473
1474 #[test]
1475 fn test_new_buffer() {
1476 let config = Config::default();
1477 let (dir_context, _temp) = test_dir_context();
1478 let mut 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 let id = editor.new_buffer();
1489 assert_eq!(editor.buffers.len(), 2);
1490 assert_eq!(editor.active_buffer(), id);
1491 }
1492
1493 #[test]
1494 #[ignore]
1495 fn test_clipboard() {
1496 let config = Config::default();
1497 let (dir_context, _temp) = test_dir_context();
1498 let mut editor = Editor::new(
1499 config,
1500 80,
1501 24,
1502 dir_context,
1503 crate::view::color_support::ColorCapability::TrueColor,
1504 test_filesystem(),
1505 )
1506 .unwrap();
1507
1508 editor.clipboard.set_internal("test".to_string());
1510
1511 editor.paste();
1513
1514 let content = editor.active_state().buffer.to_string().unwrap();
1515 assert_eq!(content, "test");
1516 }
1517
1518 #[test]
1519 fn test_action_to_events_insert_char() {
1520 let config = Config::default();
1521 let (dir_context, _temp) = test_dir_context();
1522 let mut editor = Editor::new(
1523 config,
1524 80,
1525 24,
1526 dir_context,
1527 crate::view::color_support::ColorCapability::TrueColor,
1528 test_filesystem(),
1529 )
1530 .unwrap();
1531
1532 let events = editor.action_to_events(Action::InsertChar('a'));
1533 assert!(events.is_some());
1534
1535 let events = events.unwrap();
1536 assert_eq!(events.len(), 1);
1537
1538 match &events[0] {
1539 Event::Insert { position, text, .. } => {
1540 assert_eq!(*position, 0);
1541 assert_eq!(text, "a");
1542 }
1543 _ => panic!("Expected Insert event"),
1544 }
1545 }
1546
1547 #[test]
1548 fn test_action_to_events_move_right() {
1549 let config = Config::default();
1550 let (dir_context, _temp) = test_dir_context();
1551 let mut editor = Editor::new(
1552 config,
1553 80,
1554 24,
1555 dir_context,
1556 crate::view::color_support::ColorCapability::TrueColor,
1557 test_filesystem(),
1558 )
1559 .unwrap();
1560
1561 let cursor_id = editor.active_cursors().primary_id();
1563 editor.apply_event_to_active_buffer(&Event::Insert {
1564 position: 0,
1565 text: "hello".to_string(),
1566 cursor_id,
1567 });
1568
1569 let events = editor.action_to_events(Action::MoveRight);
1570 assert!(events.is_some());
1571
1572 let events = events.unwrap();
1573 assert_eq!(events.len(), 1);
1574
1575 match &events[0] {
1576 Event::MoveCursor {
1577 new_position,
1578 new_anchor,
1579 ..
1580 } => {
1581 assert_eq!(*new_position, 5);
1583 assert_eq!(*new_anchor, None); }
1585 _ => panic!("Expected MoveCursor event"),
1586 }
1587 }
1588
1589 #[test]
1590 fn test_action_to_events_move_up_down() {
1591 let config = Config::default();
1592 let (dir_context, _temp) = test_dir_context();
1593 let mut editor = Editor::new(
1594 config,
1595 80,
1596 24,
1597 dir_context,
1598 crate::view::color_support::ColorCapability::TrueColor,
1599 test_filesystem(),
1600 )
1601 .unwrap();
1602
1603 let cursor_id = editor.active_cursors().primary_id();
1605 editor.apply_event_to_active_buffer(&Event::Insert {
1606 position: 0,
1607 text: "line1\nline2\nline3".to_string(),
1608 cursor_id,
1609 });
1610
1611 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1613 cursor_id,
1614 old_position: 0, new_position: 6,
1616 old_anchor: None, new_anchor: None,
1618 old_sticky_column: 0,
1619 new_sticky_column: 0,
1620 });
1621
1622 let events = editor.action_to_events(Action::MoveUp);
1624 assert!(events.is_some());
1625 let events = events.unwrap();
1626 assert_eq!(events.len(), 1);
1627
1628 match &events[0] {
1629 Event::MoveCursor { new_position, .. } => {
1630 assert_eq!(*new_position, 0); }
1632 _ => panic!("Expected MoveCursor event"),
1633 }
1634 }
1635
1636 #[test]
1637 fn test_action_to_events_insert_newline() {
1638 let config = Config::default();
1639 let (dir_context, _temp) = test_dir_context();
1640 let mut editor = Editor::new(
1641 config,
1642 80,
1643 24,
1644 dir_context,
1645 crate::view::color_support::ColorCapability::TrueColor,
1646 test_filesystem(),
1647 )
1648 .unwrap();
1649
1650 let events = editor.action_to_events(Action::InsertNewline);
1651 assert!(events.is_some());
1652
1653 let events = events.unwrap();
1654 assert_eq!(events.len(), 1);
1655
1656 match &events[0] {
1657 Event::Insert { text, .. } => {
1658 assert_eq!(text, "\n");
1659 }
1660 _ => panic!("Expected Insert event"),
1661 }
1662 }
1663
1664 #[test]
1665 fn test_action_to_events_unimplemented() {
1666 let config = Config::default();
1667 let (dir_context, _temp) = test_dir_context();
1668 let mut editor = Editor::new(
1669 config,
1670 80,
1671 24,
1672 dir_context,
1673 crate::view::color_support::ColorCapability::TrueColor,
1674 test_filesystem(),
1675 )
1676 .unwrap();
1677
1678 assert!(editor.action_to_events(Action::Save).is_none());
1680 assert!(editor.action_to_events(Action::Quit).is_none());
1681 assert!(editor.action_to_events(Action::Undo).is_none());
1682 }
1683
1684 #[test]
1685 fn test_action_to_events_delete_backward() {
1686 let config = Config::default();
1687 let (dir_context, _temp) = test_dir_context();
1688 let mut editor = Editor::new(
1689 config,
1690 80,
1691 24,
1692 dir_context,
1693 crate::view::color_support::ColorCapability::TrueColor,
1694 test_filesystem(),
1695 )
1696 .unwrap();
1697
1698 let cursor_id = editor.active_cursors().primary_id();
1700 editor.apply_event_to_active_buffer(&Event::Insert {
1701 position: 0,
1702 text: "hello".to_string(),
1703 cursor_id,
1704 });
1705
1706 let events = editor.action_to_events(Action::DeleteBackward);
1707 assert!(events.is_some());
1708
1709 let events = events.unwrap();
1710 assert_eq!(events.len(), 1);
1711
1712 match &events[0] {
1713 Event::Delete {
1714 range,
1715 deleted_text,
1716 ..
1717 } => {
1718 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1720 }
1721 _ => panic!("Expected Delete event"),
1722 }
1723 }
1724
1725 #[test]
1726 fn test_action_to_events_delete_forward() {
1727 let config = Config::default();
1728 let (dir_context, _temp) = test_dir_context();
1729 let mut editor = Editor::new(
1730 config,
1731 80,
1732 24,
1733 dir_context,
1734 crate::view::color_support::ColorCapability::TrueColor,
1735 test_filesystem(),
1736 )
1737 .unwrap();
1738
1739 let cursor_id = editor.active_cursors().primary_id();
1741 editor.apply_event_to_active_buffer(&Event::Insert {
1742 position: 0,
1743 text: "hello".to_string(),
1744 cursor_id,
1745 });
1746
1747 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1749 cursor_id,
1750 old_position: 0, new_position: 0,
1752 old_anchor: None, new_anchor: None,
1754 old_sticky_column: 0,
1755 new_sticky_column: 0,
1756 });
1757
1758 let events = editor.action_to_events(Action::DeleteForward);
1759 assert!(events.is_some());
1760
1761 let events = events.unwrap();
1762 assert_eq!(events.len(), 1);
1763
1764 match &events[0] {
1765 Event::Delete {
1766 range,
1767 deleted_text,
1768 ..
1769 } => {
1770 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1772 }
1773 _ => panic!("Expected Delete event"),
1774 }
1775 }
1776
1777 #[test]
1778 fn test_action_to_events_select_right() {
1779 let config = Config::default();
1780 let (dir_context, _temp) = test_dir_context();
1781 let mut editor = Editor::new(
1782 config,
1783 80,
1784 24,
1785 dir_context,
1786 crate::view::color_support::ColorCapability::TrueColor,
1787 test_filesystem(),
1788 )
1789 .unwrap();
1790
1791 let cursor_id = editor.active_cursors().primary_id();
1793 editor.apply_event_to_active_buffer(&Event::Insert {
1794 position: 0,
1795 text: "hello".to_string(),
1796 cursor_id,
1797 });
1798
1799 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1801 cursor_id,
1802 old_position: 0, new_position: 0,
1804 old_anchor: None, new_anchor: None,
1806 old_sticky_column: 0,
1807 new_sticky_column: 0,
1808 });
1809
1810 let events = editor.action_to_events(Action::SelectRight);
1811 assert!(events.is_some());
1812
1813 let events = events.unwrap();
1814 assert_eq!(events.len(), 1);
1815
1816 match &events[0] {
1817 Event::MoveCursor {
1818 new_position,
1819 new_anchor,
1820 ..
1821 } => {
1822 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1825 _ => panic!("Expected MoveCursor event"),
1826 }
1827 }
1828
1829 #[test]
1830 fn test_action_to_events_select_all() {
1831 let config = Config::default();
1832 let (dir_context, _temp) = test_dir_context();
1833 let mut editor = Editor::new(
1834 config,
1835 80,
1836 24,
1837 dir_context,
1838 crate::view::color_support::ColorCapability::TrueColor,
1839 test_filesystem(),
1840 )
1841 .unwrap();
1842
1843 let cursor_id = editor.active_cursors().primary_id();
1845 editor.apply_event_to_active_buffer(&Event::Insert {
1846 position: 0,
1847 text: "hello world".to_string(),
1848 cursor_id,
1849 });
1850
1851 let events = editor.action_to_events(Action::SelectAll);
1852 assert!(events.is_some());
1853
1854 let events = events.unwrap();
1855 assert_eq!(events.len(), 1);
1856
1857 match &events[0] {
1858 Event::MoveCursor {
1859 new_position,
1860 new_anchor,
1861 ..
1862 } => {
1863 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1866 _ => panic!("Expected MoveCursor event"),
1867 }
1868 }
1869
1870 #[test]
1871 fn test_action_to_events_document_nav() {
1872 let config = Config::default();
1873 let (dir_context, _temp) = test_dir_context();
1874 let mut editor = Editor::new(
1875 config,
1876 80,
1877 24,
1878 dir_context,
1879 crate::view::color_support::ColorCapability::TrueColor,
1880 test_filesystem(),
1881 )
1882 .unwrap();
1883
1884 let cursor_id = editor.active_cursors().primary_id();
1886 editor.apply_event_to_active_buffer(&Event::Insert {
1887 position: 0,
1888 text: "line1\nline2\nline3".to_string(),
1889 cursor_id,
1890 });
1891
1892 let events = editor.action_to_events(Action::MoveDocumentStart);
1894 assert!(events.is_some());
1895 let events = events.unwrap();
1896 match &events[0] {
1897 Event::MoveCursor { new_position, .. } => {
1898 assert_eq!(*new_position, 0);
1899 }
1900 _ => panic!("Expected MoveCursor event"),
1901 }
1902
1903 let events = editor.action_to_events(Action::MoveDocumentEnd);
1905 assert!(events.is_some());
1906 let events = events.unwrap();
1907 match &events[0] {
1908 Event::MoveCursor { new_position, .. } => {
1909 assert_eq!(*new_position, 17); }
1911 _ => panic!("Expected MoveCursor event"),
1912 }
1913 }
1914
1915 #[test]
1916 fn test_action_to_events_remove_secondary_cursors() {
1917 use crate::model::event::CursorId;
1918
1919 let config = Config::default();
1920 let (dir_context, _temp) = test_dir_context();
1921 let mut editor = Editor::new(
1922 config,
1923 80,
1924 24,
1925 dir_context,
1926 crate::view::color_support::ColorCapability::TrueColor,
1927 test_filesystem(),
1928 )
1929 .unwrap();
1930
1931 let cursor_id = editor.active_cursors().primary_id();
1933 editor.apply_event_to_active_buffer(&Event::Insert {
1934 position: 0,
1935 text: "hello world test".to_string(),
1936 cursor_id,
1937 });
1938
1939 editor.apply_event_to_active_buffer(&Event::AddCursor {
1941 cursor_id: CursorId(1),
1942 position: 5,
1943 anchor: None,
1944 });
1945 editor.apply_event_to_active_buffer(&Event::AddCursor {
1946 cursor_id: CursorId(2),
1947 position: 10,
1948 anchor: None,
1949 });
1950
1951 assert_eq!(editor.active_cursors().count(), 3);
1952
1953 let first_id = editor
1955 .active_cursors()
1956 .iter()
1957 .map(|(id, _)| id)
1958 .min_by_key(|id| id.0)
1959 .expect("Should have at least one cursor");
1960
1961 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
1963 assert!(events.is_some());
1964
1965 let events = events.unwrap();
1966 let remove_cursor_events: Vec<_> = events
1969 .iter()
1970 .filter_map(|e| match e {
1971 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1972 _ => None,
1973 })
1974 .collect();
1975
1976 assert_eq!(remove_cursor_events.len(), 2);
1978
1979 for cursor_id in &remove_cursor_events {
1980 assert_ne!(*cursor_id, first_id);
1982 }
1983 }
1984
1985 #[test]
1986 fn test_action_to_events_scroll() {
1987 let config = Config::default();
1988 let (dir_context, _temp) = test_dir_context();
1989 let mut editor = Editor::new(
1990 config,
1991 80,
1992 24,
1993 dir_context,
1994 crate::view::color_support::ColorCapability::TrueColor,
1995 test_filesystem(),
1996 )
1997 .unwrap();
1998
1999 let events = editor.action_to_events(Action::ScrollUp);
2001 assert!(events.is_some());
2002 let events = events.unwrap();
2003 assert_eq!(events.len(), 1);
2004 match &events[0] {
2005 Event::Scroll { line_offset } => {
2006 assert_eq!(*line_offset, -1);
2007 }
2008 _ => panic!("Expected Scroll event"),
2009 }
2010
2011 let events = editor.action_to_events(Action::ScrollDown);
2013 assert!(events.is_some());
2014 let events = events.unwrap();
2015 assert_eq!(events.len(), 1);
2016 match &events[0] {
2017 Event::Scroll { line_offset } => {
2018 assert_eq!(*line_offset, 1);
2019 }
2020 _ => panic!("Expected Scroll event"),
2021 }
2022 }
2023
2024 #[test]
2025 fn test_action_to_events_none() {
2026 let config = Config::default();
2027 let (dir_context, _temp) = test_dir_context();
2028 let mut editor = Editor::new(
2029 config,
2030 80,
2031 24,
2032 dir_context,
2033 crate::view::color_support::ColorCapability::TrueColor,
2034 test_filesystem(),
2035 )
2036 .unwrap();
2037
2038 let events = editor.action_to_events(Action::None);
2040 assert!(events.is_none());
2041 }
2042
2043 #[test]
2044 fn test_lsp_incremental_insert_generates_correct_range() {
2045 use crate::model::buffer::Buffer;
2048
2049 let buffer = Buffer::from_str_test("hello\nworld");
2050
2051 let position = 0;
2054 let (line, character) = buffer.position_to_lsp_position(position);
2055
2056 assert_eq!(line, 0, "Insertion at start should be line 0");
2057 assert_eq!(character, 0, "Insertion at start should be char 0");
2058
2059 let lsp_pos = Position::new(line as u32, character as u32);
2061 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2062
2063 assert_eq!(lsp_range.start.line, 0);
2064 assert_eq!(lsp_range.start.character, 0);
2065 assert_eq!(lsp_range.end.line, 0);
2066 assert_eq!(lsp_range.end.character, 0);
2067 assert_eq!(
2068 lsp_range.start, lsp_range.end,
2069 "Insert should have zero-width range"
2070 );
2071
2072 let position = 3;
2074 let (line, character) = buffer.position_to_lsp_position(position);
2075
2076 assert_eq!(line, 0);
2077 assert_eq!(character, 3);
2078
2079 let position = 6;
2081 let (line, character) = buffer.position_to_lsp_position(position);
2082
2083 assert_eq!(line, 1, "Position after newline should be line 1");
2084 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2085 }
2086
2087 #[test]
2088 fn test_lsp_incremental_delete_generates_correct_range() {
2089 use crate::model::buffer::Buffer;
2092
2093 let buffer = Buffer::from_str_test("hello\nworld");
2094
2095 let range_start = 1;
2097 let range_end = 5;
2098
2099 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2100 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2101
2102 assert_eq!(start_line, 0);
2103 assert_eq!(start_char, 1);
2104 assert_eq!(end_line, 0);
2105 assert_eq!(end_char, 5);
2106
2107 let lsp_range = LspRange::new(
2108 Position::new(start_line as u32, start_char as u32),
2109 Position::new(end_line as u32, end_char as u32),
2110 );
2111
2112 assert_eq!(lsp_range.start.line, 0);
2113 assert_eq!(lsp_range.start.character, 1);
2114 assert_eq!(lsp_range.end.line, 0);
2115 assert_eq!(lsp_range.end.character, 5);
2116 assert_ne!(
2117 lsp_range.start, lsp_range.end,
2118 "Delete should have non-zero range"
2119 );
2120
2121 let range_start = 4;
2123 let range_end = 8;
2124
2125 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2126 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2127
2128 assert_eq!(start_line, 0, "Delete start on line 0");
2129 assert_eq!(start_char, 4, "Delete start at char 4");
2130 assert_eq!(end_line, 1, "Delete end on line 1");
2131 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2132 }
2133
2134 #[test]
2135 fn test_lsp_incremental_utf16_encoding() {
2136 use crate::model::buffer::Buffer;
2139
2140 let buffer = Buffer::from_str_test("😀hello");
2142
2143 let (line, character) = buffer.position_to_lsp_position(4);
2145
2146 assert_eq!(line, 0);
2147 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2148
2149 let (line, character) = buffer.position_to_lsp_position(9);
2151
2152 assert_eq!(line, 0);
2153 assert_eq!(
2154 character, 7,
2155 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2156 );
2157
2158 let buffer = Buffer::from_str_test("café");
2160
2161 let (line, character) = buffer.position_to_lsp_position(3);
2163
2164 assert_eq!(line, 0);
2165 assert_eq!(character, 3);
2166
2167 let (line, character) = buffer.position_to_lsp_position(5);
2169
2170 assert_eq!(line, 0);
2171 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2172 }
2173
2174 #[test]
2175 fn test_lsp_content_change_event_structure() {
2176 let insert_change = TextDocumentContentChangeEvent {
2180 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2181 range_length: None,
2182 text: "NEW".to_string(),
2183 };
2184
2185 assert!(insert_change.range.is_some());
2186 assert_eq!(insert_change.text, "NEW");
2187 let range = insert_change.range.unwrap();
2188 assert_eq!(
2189 range.start, range.end,
2190 "Insert should have zero-width range"
2191 );
2192
2193 let delete_change = TextDocumentContentChangeEvent {
2195 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2196 range_length: None,
2197 text: String::new(),
2198 };
2199
2200 assert!(delete_change.range.is_some());
2201 assert_eq!(delete_change.text, "");
2202 let range = delete_change.range.unwrap();
2203 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2204 assert_eq!(range.start.line, 0);
2205 assert_eq!(range.start.character, 2);
2206 assert_eq!(range.end.line, 0);
2207 assert_eq!(range.end.character, 7);
2208 }
2209
2210 #[test]
2211 fn test_goto_matching_bracket_forward() {
2212 let config = Config::default();
2213 let (dir_context, _temp) = test_dir_context();
2214 let mut editor = Editor::new(
2215 config,
2216 80,
2217 24,
2218 dir_context,
2219 crate::view::color_support::ColorCapability::TrueColor,
2220 test_filesystem(),
2221 )
2222 .unwrap();
2223
2224 let cursor_id = editor.active_cursors().primary_id();
2226 editor.apply_event_to_active_buffer(&Event::Insert {
2227 position: 0,
2228 text: "fn main() { let x = (1 + 2); }".to_string(),
2229 cursor_id,
2230 });
2231
2232 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2234 cursor_id,
2235 old_position: 31,
2236 new_position: 10,
2237 old_anchor: None,
2238 new_anchor: None,
2239 old_sticky_column: 0,
2240 new_sticky_column: 0,
2241 });
2242
2243 assert_eq!(editor.active_cursors().primary().position, 10);
2244
2245 editor.goto_matching_bracket();
2247
2248 assert_eq!(editor.active_cursors().primary().position, 29);
2253 }
2254
2255 #[test]
2256 fn test_goto_matching_bracket_backward() {
2257 let config = Config::default();
2258 let (dir_context, _temp) = test_dir_context();
2259 let mut editor = Editor::new(
2260 config,
2261 80,
2262 24,
2263 dir_context,
2264 crate::view::color_support::ColorCapability::TrueColor,
2265 test_filesystem(),
2266 )
2267 .unwrap();
2268
2269 let cursor_id = editor.active_cursors().primary_id();
2271 editor.apply_event_to_active_buffer(&Event::Insert {
2272 position: 0,
2273 text: "fn main() { let x = (1 + 2); }".to_string(),
2274 cursor_id,
2275 });
2276
2277 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2279 cursor_id,
2280 old_position: 31,
2281 new_position: 26,
2282 old_anchor: None,
2283 new_anchor: None,
2284 old_sticky_column: 0,
2285 new_sticky_column: 0,
2286 });
2287
2288 editor.goto_matching_bracket();
2290
2291 assert_eq!(editor.active_cursors().primary().position, 20);
2293 }
2294
2295 #[test]
2296 fn test_goto_matching_bracket_nested() {
2297 let config = Config::default();
2298 let (dir_context, _temp) = test_dir_context();
2299 let mut editor = Editor::new(
2300 config,
2301 80,
2302 24,
2303 dir_context,
2304 crate::view::color_support::ColorCapability::TrueColor,
2305 test_filesystem(),
2306 )
2307 .unwrap();
2308
2309 let cursor_id = editor.active_cursors().primary_id();
2311 editor.apply_event_to_active_buffer(&Event::Insert {
2312 position: 0,
2313 text: "{a{b{c}d}e}".to_string(),
2314 cursor_id,
2315 });
2316
2317 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2319 cursor_id,
2320 old_position: 11,
2321 new_position: 0,
2322 old_anchor: None,
2323 new_anchor: None,
2324 old_sticky_column: 0,
2325 new_sticky_column: 0,
2326 });
2327
2328 editor.goto_matching_bracket();
2330
2331 assert_eq!(editor.active_cursors().primary().position, 10);
2333 }
2334
2335 #[test]
2336 fn test_search_case_sensitive() {
2337 let config = Config::default();
2338 let (dir_context, _temp) = test_dir_context();
2339 let mut editor = Editor::new(
2340 config,
2341 80,
2342 24,
2343 dir_context,
2344 crate::view::color_support::ColorCapability::TrueColor,
2345 test_filesystem(),
2346 )
2347 .unwrap();
2348
2349 let cursor_id = editor.active_cursors().primary_id();
2351 editor.apply_event_to_active_buffer(&Event::Insert {
2352 position: 0,
2353 text: "Hello hello HELLO".to_string(),
2354 cursor_id,
2355 });
2356
2357 editor.search_case_sensitive = false;
2359 editor.perform_search("hello");
2360
2361 let search_state = editor.search_state.as_ref().unwrap();
2362 assert_eq!(
2363 search_state.matches.len(),
2364 3,
2365 "Should find all 3 matches case-insensitively"
2366 );
2367
2368 editor.search_case_sensitive = true;
2370 editor.perform_search("hello");
2371
2372 let search_state = editor.search_state.as_ref().unwrap();
2373 assert_eq!(
2374 search_state.matches.len(),
2375 1,
2376 "Should find only 1 exact match"
2377 );
2378 assert_eq!(
2379 search_state.matches[0], 6,
2380 "Should find 'hello' at position 6"
2381 );
2382 }
2383
2384 #[test]
2385 fn test_search_whole_word() {
2386 let config = Config::default();
2387 let (dir_context, _temp) = test_dir_context();
2388 let mut editor = Editor::new(
2389 config,
2390 80,
2391 24,
2392 dir_context,
2393 crate::view::color_support::ColorCapability::TrueColor,
2394 test_filesystem(),
2395 )
2396 .unwrap();
2397
2398 let cursor_id = editor.active_cursors().primary_id();
2400 editor.apply_event_to_active_buffer(&Event::Insert {
2401 position: 0,
2402 text: "test testing tested attest test".to_string(),
2403 cursor_id,
2404 });
2405
2406 editor.search_whole_word = false;
2408 editor.search_case_sensitive = true;
2409 editor.perform_search("test");
2410
2411 let search_state = editor.search_state.as_ref().unwrap();
2412 assert_eq!(
2413 search_state.matches.len(),
2414 5,
2415 "Should find 'test' in all occurrences"
2416 );
2417
2418 editor.search_whole_word = true;
2420 editor.perform_search("test");
2421
2422 let search_state = editor.search_state.as_ref().unwrap();
2423 assert_eq!(
2424 search_state.matches.len(),
2425 2,
2426 "Should find only whole word 'test'"
2427 );
2428 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2429 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2430 }
2431
2432 #[test]
2433 fn test_search_scan_completes_when_capped() {
2434 let config = Config::default();
2440 let (dir_context, _temp) = test_dir_context();
2441 let mut editor = Editor::new(
2442 config,
2443 80,
2444 24,
2445 dir_context,
2446 crate::view::color_support::ColorCapability::TrueColor,
2447 test_filesystem(),
2448 )
2449 .unwrap();
2450
2451 let buffer_id = editor.active_buffer();
2454 let regex = regex::bytes::Regex::new("test").unwrap();
2455 let fake_chunks = vec![
2456 crate::model::buffer::LineScanChunk {
2457 leaf_index: 0,
2458 byte_len: 100,
2459 already_known: true,
2460 },
2461 crate::model::buffer::LineScanChunk {
2462 leaf_index: 1,
2463 byte_len: 100,
2464 already_known: true,
2465 },
2466 ];
2467
2468 let chunked = crate::model::buffer::ChunkedSearchState {
2469 chunks: fake_chunks,
2470 next_chunk: 1, next_doc_offset: 100,
2472 total_bytes: 200,
2473 scanned_bytes: 100,
2474 regex,
2475 matches: vec![
2476 crate::model::buffer::SearchMatch {
2477 byte_offset: 10,
2478 length: 4,
2479 line: 1,
2480 column: 11,
2481 context: String::new(),
2482 },
2483 crate::model::buffer::SearchMatch {
2484 byte_offset: 50,
2485 length: 4,
2486 line: 1,
2487 column: 51,
2488 context: String::new(),
2489 },
2490 ],
2491 overlap_tail: Vec::new(),
2492 overlap_doc_offset: 0,
2493 max_matches: 10_000,
2494 capped: true, query_len: 4,
2496 running_line: 1,
2497 };
2498
2499 editor.search_scan.start(
2500 buffer_id,
2501 Vec::new(),
2502 chunked,
2503 "test".to_string(),
2504 None,
2505 false,
2506 false,
2507 false,
2508 );
2509
2510 let result = editor.process_search_scan();
2512 assert!(
2513 result,
2514 "process_search_scan should return true (needs render)"
2515 );
2516
2517 assert_eq!(
2519 editor.search_scan.buffer_id(),
2520 None,
2521 "search_scan should be drained after capped scan completes"
2522 );
2523
2524 let search_state = editor
2526 .search_state
2527 .as_ref()
2528 .expect("search_state should be set after scan finishes");
2529 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2530 assert_eq!(search_state.query, "test");
2531 assert!(
2532 search_state.capped,
2533 "search_state should be marked as capped"
2534 );
2535 }
2536
2537 #[test]
2538 fn test_bookmarks() {
2539 let config = Config::default();
2540 let (dir_context, _temp) = test_dir_context();
2541 let mut editor = Editor::new(
2542 config,
2543 80,
2544 24,
2545 dir_context,
2546 crate::view::color_support::ColorCapability::TrueColor,
2547 test_filesystem(),
2548 )
2549 .unwrap();
2550
2551 let cursor_id = editor.active_cursors().primary_id();
2553 editor.apply_event_to_active_buffer(&Event::Insert {
2554 position: 0,
2555 text: "Line 1\nLine 2\nLine 3".to_string(),
2556 cursor_id,
2557 });
2558
2559 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2561 cursor_id,
2562 old_position: 21,
2563 new_position: 7,
2564 old_anchor: None,
2565 new_anchor: None,
2566 old_sticky_column: 0,
2567 new_sticky_column: 0,
2568 });
2569
2570 editor.set_bookmark('1');
2572 assert_eq!(editor.bookmarks.get('1').map(|b| b.position), Some(7));
2573
2574 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2576 cursor_id,
2577 old_position: 7,
2578 new_position: 14,
2579 old_anchor: None,
2580 new_anchor: None,
2581 old_sticky_column: 0,
2582 new_sticky_column: 0,
2583 });
2584
2585 editor.jump_to_bookmark('1');
2587 assert_eq!(editor.active_cursors().primary().position, 7);
2588
2589 editor.clear_bookmark('1');
2591 assert_eq!(editor.bookmarks.get('1'), None);
2592 }
2593
2594 #[test]
2595 fn test_action_enum_new_variants() {
2596 use serde_json::json;
2598
2599 let args = HashMap::new();
2600 assert_eq!(
2601 Action::from_str("smart_home", &args),
2602 Some(Action::SmartHome)
2603 );
2604 assert_eq!(
2605 Action::from_str("dedent_selection", &args),
2606 Some(Action::DedentSelection)
2607 );
2608 assert_eq!(
2609 Action::from_str("toggle_comment", &args),
2610 Some(Action::ToggleComment)
2611 );
2612 assert_eq!(
2613 Action::from_str("goto_matching_bracket", &args),
2614 Some(Action::GoToMatchingBracket)
2615 );
2616 assert_eq!(
2617 Action::from_str("list_bookmarks", &args),
2618 Some(Action::ListBookmarks)
2619 );
2620 assert_eq!(
2621 Action::from_str("toggle_search_case_sensitive", &args),
2622 Some(Action::ToggleSearchCaseSensitive)
2623 );
2624 assert_eq!(
2625 Action::from_str("toggle_search_whole_word", &args),
2626 Some(Action::ToggleSearchWholeWord)
2627 );
2628
2629 let mut args_with_char = HashMap::new();
2631 args_with_char.insert("char".to_string(), json!("5"));
2632 assert_eq!(
2633 Action::from_str("set_bookmark", &args_with_char),
2634 Some(Action::SetBookmark('5'))
2635 );
2636 assert_eq!(
2637 Action::from_str("jump_to_bookmark", &args_with_char),
2638 Some(Action::JumpToBookmark('5'))
2639 );
2640 assert_eq!(
2641 Action::from_str("clear_bookmark", &args_with_char),
2642 Some(Action::ClearBookmark('5'))
2643 );
2644 }
2645
2646 #[test]
2647 fn test_keybinding_new_defaults() {
2648 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2649
2650 let mut config = Config::default();
2654 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2655 let resolver = KeybindingResolver::new(&config);
2656
2657 let event = KeyEvent {
2659 code: KeyCode::Char('/'),
2660 modifiers: KeyModifiers::CONTROL,
2661 kind: KeyEventKind::Press,
2662 state: KeyEventState::NONE,
2663 };
2664 let action = resolver.resolve(&event, KeyContext::Normal);
2665 assert_eq!(action, Action::ToggleComment);
2666
2667 let event = KeyEvent {
2669 code: KeyCode::Char(']'),
2670 modifiers: KeyModifiers::CONTROL,
2671 kind: KeyEventKind::Press,
2672 state: KeyEventState::NONE,
2673 };
2674 let action = resolver.resolve(&event, KeyContext::Normal);
2675 assert_eq!(action, Action::GoToMatchingBracket);
2676
2677 let event = KeyEvent {
2679 code: KeyCode::Tab,
2680 modifiers: KeyModifiers::SHIFT,
2681 kind: KeyEventKind::Press,
2682 state: KeyEventState::NONE,
2683 };
2684 let action = resolver.resolve(&event, KeyContext::Normal);
2685 assert_eq!(action, Action::DedentSelection);
2686
2687 let event = KeyEvent {
2689 code: KeyCode::Char('g'),
2690 modifiers: KeyModifiers::CONTROL,
2691 kind: KeyEventKind::Press,
2692 state: KeyEventState::NONE,
2693 };
2694 let action = resolver.resolve(&event, KeyContext::Normal);
2695 assert_eq!(action, Action::GotoLine);
2696
2697 let event = KeyEvent {
2699 code: KeyCode::Char('5'),
2700 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2701 kind: KeyEventKind::Press,
2702 state: KeyEventState::NONE,
2703 };
2704 let action = resolver.resolve(&event, KeyContext::Normal);
2705 assert_eq!(action, Action::SetBookmark('5'));
2706
2707 let event = KeyEvent {
2708 code: KeyCode::Char('5'),
2709 modifiers: KeyModifiers::ALT,
2710 kind: KeyEventKind::Press,
2711 state: KeyEventState::NONE,
2712 };
2713 let action = resolver.resolve(&event, KeyContext::Normal);
2714 assert_eq!(action, Action::JumpToBookmark('5'));
2715 }
2716
2717 #[test]
2729 fn test_lsp_rename_didchange_positions_bug() {
2730 use crate::model::buffer::Buffer;
2731
2732 let config = Config::default();
2733 let (dir_context, _temp) = test_dir_context();
2734 let mut editor = Editor::new(
2735 config,
2736 80,
2737 24,
2738 dir_context,
2739 crate::view::color_support::ColorCapability::TrueColor,
2740 test_filesystem(),
2741 )
2742 .unwrap();
2743
2744 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2748 editor.active_state_mut().buffer =
2749 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2750
2751 let cursor_id = editor.active_cursors().primary_id();
2756
2757 let batch = Event::Batch {
2758 events: vec![
2759 Event::Delete {
2761 range: 23..26, deleted_text: "val".to_string(),
2763 cursor_id,
2764 },
2765 Event::Insert {
2766 position: 23,
2767 text: "value".to_string(),
2768 cursor_id,
2769 },
2770 Event::Delete {
2772 range: 7..10, deleted_text: "val".to_string(),
2774 cursor_id,
2775 },
2776 Event::Insert {
2777 position: 7,
2778 text: "value".to_string(),
2779 cursor_id,
2780 },
2781 ],
2782 description: "LSP Rename".to_string(),
2783 };
2784
2785 let lsp_changes_before = editor.collect_lsp_changes(&batch);
2787
2788 editor.apply_event_to_active_buffer(&batch);
2790
2791 let lsp_changes_after = editor.collect_lsp_changes(&batch);
2794
2795 let final_content = editor.active_state().buffer.to_string().unwrap();
2797 assert_eq!(
2798 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2799 "Buffer should have 'value' in both places"
2800 );
2801
2802 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2808
2809 let first_delete = &lsp_changes_before[0];
2810 let first_del_range = first_delete.range.unwrap();
2811 assert_eq!(
2812 first_del_range.start.line, 1,
2813 "First delete should be on line 1 (BEFORE)"
2814 );
2815 assert_eq!(
2816 first_del_range.start.character, 4,
2817 "First delete start should be at char 4 (BEFORE)"
2818 );
2819
2820 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2826
2827 let first_delete_after = &lsp_changes_after[0];
2828 let first_del_range_after = first_delete_after.range.unwrap();
2829
2830 eprintln!("BEFORE modification:");
2833 eprintln!(
2834 " Delete at line {}, char {}-{}",
2835 first_del_range.start.line,
2836 first_del_range.start.character,
2837 first_del_range.end.character
2838 );
2839 eprintln!("AFTER modification:");
2840 eprintln!(
2841 " Delete at line {}, char {}-{}",
2842 first_del_range_after.start.line,
2843 first_del_range_after.start.character,
2844 first_del_range_after.end.character
2845 );
2846
2847 assert_ne!(
2865 first_del_range_after.end.character, first_del_range.end.character,
2866 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2867 );
2868
2869 eprintln!("\n=== BUG DEMONSTRATED ===");
2870 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2871 eprintln!("the positions are WRONG because they're calculated from the");
2872 eprintln!("modified buffer, not the original buffer.");
2873 eprintln!("This causes the second rename to fail with 'content modified' error.");
2874 eprintln!("========================\n");
2875 }
2876
2877 #[test]
2878 fn test_lsp_rename_preserves_cursor_position() {
2879 use crate::model::buffer::Buffer;
2880
2881 let config = Config::default();
2882 let (dir_context, _temp) = test_dir_context();
2883 let mut editor = Editor::new(
2884 config,
2885 80,
2886 24,
2887 dir_context,
2888 crate::view::color_support::ColorCapability::TrueColor,
2889 test_filesystem(),
2890 )
2891 .unwrap();
2892
2893 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2897 editor.active_state_mut().buffer =
2898 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2899
2900 let original_cursor_pos = 23;
2902 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2903
2904 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2906 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2907 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2908
2909 let cursor_id = editor.active_cursors().primary_id();
2912 let buffer_id = editor.active_buffer();
2913
2914 let events = vec![
2915 Event::Delete {
2917 range: 23..26, deleted_text: "val".to_string(),
2919 cursor_id,
2920 },
2921 Event::Insert {
2922 position: 23,
2923 text: "value".to_string(),
2924 cursor_id,
2925 },
2926 Event::Delete {
2928 range: 7..10, deleted_text: "val".to_string(),
2930 cursor_id,
2931 },
2932 Event::Insert {
2933 position: 7,
2934 text: "value".to_string(),
2935 cursor_id,
2936 },
2937 ];
2938
2939 editor
2941 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2942 .unwrap();
2943
2944 let final_content = editor.active_state().buffer.to_string().unwrap();
2946 assert_eq!(
2947 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2948 "Buffer should have 'value' in both places"
2949 );
2950
2951 let final_cursor_pos = editor.active_cursors().primary().position;
2959 let expected_cursor_pos = 25; assert_eq!(
2962 final_cursor_pos, expected_cursor_pos,
2963 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2964 Original pos: {}, expected adjustment: +2 for first rename",
2965 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2966 );
2967
2968 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2970 assert_eq!(
2971 text_at_new_cursor, "value",
2972 "Cursor should be at the start of 'value' after rename"
2973 );
2974 }
2975
2976 #[test]
2977 fn test_lsp_rename_twice_consecutive() {
2978 use crate::model::buffer::Buffer;
2981
2982 let config = Config::default();
2983 let (dir_context, _temp) = test_dir_context();
2984 let mut editor = Editor::new(
2985 config,
2986 80,
2987 24,
2988 dir_context,
2989 crate::view::color_support::ColorCapability::TrueColor,
2990 test_filesystem(),
2991 )
2992 .unwrap();
2993
2994 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2996 editor.active_state_mut().buffer =
2997 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2998
2999 let cursor_id = editor.active_cursors().primary_id();
3000 let buffer_id = editor.active_buffer();
3001
3002 let events1 = vec![
3005 Event::Delete {
3007 range: 23..26,
3008 deleted_text: "val".to_string(),
3009 cursor_id,
3010 },
3011 Event::Insert {
3012 position: 23,
3013 text: "value".to_string(),
3014 cursor_id,
3015 },
3016 Event::Delete {
3018 range: 7..10,
3019 deleted_text: "val".to_string(),
3020 cursor_id,
3021 },
3022 Event::Insert {
3023 position: 7,
3024 text: "value".to_string(),
3025 cursor_id,
3026 },
3027 ];
3028
3029 let batch1 = Event::Batch {
3031 events: events1.clone(),
3032 description: "LSP Rename 1".to_string(),
3033 };
3034
3035 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
3037
3038 assert_eq!(
3040 lsp_changes1.len(),
3041 4,
3042 "First rename should have 4 LSP changes"
3043 );
3044
3045 let first_del = &lsp_changes1[0];
3047 let first_del_range = first_del.range.unwrap();
3048 assert_eq!(first_del_range.start.line, 1, "First delete line");
3049 assert_eq!(
3050 first_del_range.start.character, 4,
3051 "First delete start char"
3052 );
3053 assert_eq!(first_del_range.end.character, 7, "First delete end char");
3054
3055 editor
3057 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
3058 .unwrap();
3059
3060 let after_first = editor.active_state().buffer.to_string().unwrap();
3062 assert_eq!(
3063 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3064 "After first rename"
3065 );
3066
3067 let events2 = vec![
3077 Event::Delete {
3079 range: 25..30,
3080 deleted_text: "value".to_string(),
3081 cursor_id,
3082 },
3083 Event::Insert {
3084 position: 25,
3085 text: "x".to_string(),
3086 cursor_id,
3087 },
3088 Event::Delete {
3090 range: 7..12,
3091 deleted_text: "value".to_string(),
3092 cursor_id,
3093 },
3094 Event::Insert {
3095 position: 7,
3096 text: "x".to_string(),
3097 cursor_id,
3098 },
3099 ];
3100
3101 let batch2 = Event::Batch {
3103 events: events2.clone(),
3104 description: "LSP Rename 2".to_string(),
3105 };
3106
3107 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
3109
3110 assert_eq!(
3114 lsp_changes2.len(),
3115 4,
3116 "Second rename should have 4 LSP changes"
3117 );
3118
3119 let second_first_del = &lsp_changes2[0];
3121 let second_first_del_range = second_first_del.range.unwrap();
3122 assert_eq!(
3123 second_first_del_range.start.line, 1,
3124 "Second rename first delete should be on line 1"
3125 );
3126 assert_eq!(
3127 second_first_del_range.start.character, 4,
3128 "Second rename first delete start should be at char 4"
3129 );
3130 assert_eq!(
3131 second_first_del_range.end.character, 9,
3132 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3133 );
3134
3135 let second_third_del = &lsp_changes2[2];
3137 let second_third_del_range = second_third_del.range.unwrap();
3138 assert_eq!(
3139 second_third_del_range.start.line, 0,
3140 "Second rename third delete should be on line 0"
3141 );
3142 assert_eq!(
3143 second_third_del_range.start.character, 7,
3144 "Second rename third delete start should be at char 7"
3145 );
3146 assert_eq!(
3147 second_third_del_range.end.character, 12,
3148 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3149 );
3150
3151 editor
3153 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3154 .unwrap();
3155
3156 let after_second = editor.active_state().buffer.to_string().unwrap();
3158 assert_eq!(
3159 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3160 "After second rename"
3161 );
3162 }
3163
3164 #[test]
3165 fn test_ensure_active_tab_visible_static_offset() {
3166 let config = Config::default();
3167 let (dir_context, _temp) = test_dir_context();
3168 let mut editor = Editor::new(
3169 config,
3170 80,
3171 24,
3172 dir_context,
3173 crate::view::color_support::ColorCapability::TrueColor,
3174 test_filesystem(),
3175 )
3176 .unwrap();
3177 let split_id = editor.split_manager.active_split();
3178
3179 let buf1 = editor.new_buffer();
3181 editor
3182 .buffers
3183 .get_mut(&buf1)
3184 .unwrap()
3185 .buffer
3186 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3187 let buf2 = editor.new_buffer();
3188 editor
3189 .buffers
3190 .get_mut(&buf2)
3191 .unwrap()
3192 .buffer
3193 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3194 let buf3 = editor.new_buffer();
3195 editor
3196 .buffers
3197 .get_mut(&buf3)
3198 .unwrap()
3199 .buffer
3200 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3201
3202 {
3203 use crate::view::split::TabTarget;
3204 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
3205 view_state.open_buffers = vec![
3206 TabTarget::Buffer(buf1),
3207 TabTarget::Buffer(buf2),
3208 TabTarget::Buffer(buf3),
3209 ];
3210 view_state.tab_scroll_offset = 50;
3211 }
3212
3213 editor.ensure_active_tab_visible(split_id, buf1, 25);
3217 assert_eq!(
3218 editor
3219 .split_view_states
3220 .get(&split_id)
3221 .unwrap()
3222 .tab_scroll_offset,
3223 0
3224 );
3225
3226 editor.ensure_active_tab_visible(split_id, buf3, 25);
3228 let view_state = editor.split_view_states.get(&split_id).unwrap();
3229 assert!(view_state.tab_scroll_offset > 0);
3230 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3231 let total_width: usize = buffer_ids
3232 .iter()
3233 .enumerate()
3234 .map(|(idx, id)| {
3235 let state = editor.buffers.get(id).unwrap();
3236 let name_len = state
3237 .buffer
3238 .file_path()
3239 .and_then(|p| p.file_name())
3240 .and_then(|n| n.to_str())
3241 .map(|s| s.chars().count())
3242 .unwrap_or(0);
3243 let tab_width = 2 + name_len;
3244 if idx < buffer_ids.len() - 1 {
3245 tab_width + 1 } else {
3247 tab_width
3248 }
3249 })
3250 .sum();
3251 assert!(view_state.tab_scroll_offset <= total_width);
3252 }
3253}