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: crate::view::theme::ThemeRegistry,
405
406 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
408
409 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
411
412 ansi_background_path: Option<PathBuf>,
414
415 background_fade: f32,
417
418 keybindings: Arc<RwLock<KeybindingResolver>>,
420
421 clipboard: crate::services::clipboard::Clipboard,
423
424 should_quit: bool,
426
427 should_detach: bool,
429
430 session_mode: bool,
432
433 software_cursor_only: bool,
435
436 session_name: Option<String>,
438
439 pending_escape_sequences: Vec<u8>,
442
443 restart_with_dir: Option<PathBuf>,
446
447 status_message: Option<String>,
449
450 plugin_status_message: Option<String>,
452
453 last_window_title: Option<String>,
457
458 plugin_errors: Vec<String>,
461
462 prompt: Option<Prompt>,
464
465 terminal_width: u16,
467 terminal_height: u16,
468
469 lsp: Option<LspManager>,
471
472 buffer_metadata: HashMap<BufferId, BufferMetadata>,
474
475 mode_registry: ModeRegistry,
477
478 tokio_runtime: Option<tokio::runtime::Runtime>,
480
481 async_bridge: Option<AsyncBridge>,
483
484 split_manager: SplitManager,
486
487 split_view_states: HashMap<LeafId, SplitViewState>,
491
492 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
496
497 scroll_sync_manager: ScrollSyncManager,
500
501 file_explorer: Option<FileTreeView>,
503
504 preview: Option<(LeafId, BufferId)>,
518
519 suppress_position_history_once: bool,
524
525 fs_manager: Arc<FsManager>,
527
528 authority: crate::services::authority::Authority,
538
539 pending_authority: Option<crate::services::authority::Authority>,
545
546 pub remote_indicator_override: Option<crate::view::ui::status_bar::RemoteIndicatorOverride>,
552
553 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
558
559 file_explorer_visible: bool,
561
562 file_explorer_sync_in_progress: bool,
565
566 file_explorer_width: crate::config::ExplorerWidth,
570
571 pending_file_explorer_show_hidden: Option<bool>,
573
574 pending_file_explorer_show_gitignored: Option<bool>,
576
577 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
579
580 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
582
583 pub(crate) file_explorer_clipboard: Option<crate::app::file_explorer::FileExplorerClipboard>,
585
586 menu_bar_visible: bool,
588
589 menu_bar_auto_shown: bool,
592
593 tab_bar_visible: bool,
595
596 status_bar_visible: bool,
598
599 prompt_line_visible: bool,
601
602 mouse_enabled: bool,
604
605 same_buffer_scroll_sync: bool,
607
608 mouse_cursor_position: Option<(u16, u16)>,
612
613 gpm_active: bool,
615
616 key_context: KeyContext,
618
619 menu_state: crate::view::ui::MenuState,
621
622 menus: crate::config::MenuConfig,
624
625 working_dir: PathBuf,
627
628 pub position_history: PositionHistory,
630
631 in_navigation: bool,
633
634 next_lsp_request_id: u64,
636
637 pending_completion_requests: HashSet<u64>,
639
640 completion_items: Option<Vec<lsp_types::CompletionItem>>,
643
644 scheduled_completion_trigger: Option<Instant>,
647
648 completion_service: crate::services::completion::CompletionService,
651
652 dabbrev_state: Option<DabbrevCycleState>,
656
657 pending_goto_definition_request: Option<u64>,
659
660 pending_references_request: Option<u64>,
662
663 pending_references_symbol: String,
665
666 pending_signature_help_request: Option<u64>,
668
669 pending_code_actions_requests: HashSet<u64>,
671
672 pending_code_actions_server_names: HashMap<u64, String>,
674
675 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
679
680 pending_inlay_hints_requests: HashMap<u64, InlayHintsRequest>,
691
692 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
694
695 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
697
698 folding_ranges_debounce: HashMap<BufferId, Instant>,
700
701 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
703
704 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
706
707 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
709
710 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
712
713 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
715
716 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
718
719 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
721
722 hover: hover::HoverState,
725
726 search_state: Option<SearchState>,
728
729 search_namespace: crate::view::overlay::OverlayNamespace,
731
732 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
734
735 pending_search_range: Option<Range<usize>>,
737
738 interactive_replace_state: Option<InteractiveReplaceState>,
740
741 mouse_state: MouseState,
743
744 tab_context_menu: Option<TabContextMenu>,
746
747 file_explorer_context_menu: Option<FileExplorerContextMenu>,
749
750 theme_info_popup: Option<types::ThemeInfoPopup>,
752
753 pub(crate) cached_layout: CachedLayout,
755
756 command_registry: Arc<RwLock<CommandRegistry>>,
758
759 quick_open_registry: QuickOpenRegistry,
761
762 plugin_manager: PluginManager,
764
765 plugin_dev_workspaces:
769 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
770
771 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
775
776 panel_ids: HashMap<String, BufferId>,
779
780 buffer_groups: HashMap<types::BufferGroupId, types::BufferGroup>,
782 buffer_to_group: HashMap<BufferId, types::BufferGroupId>,
784 next_buffer_group_id: usize,
786
787 pub(crate) grouped_subtrees:
795 HashMap<crate::model::event::LeafId, crate::view::split::SplitNode>,
796
797 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
800
801 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
807
808 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
811
812 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
816
817 pending_next_key_callbacks: std::collections::VecDeque<fresh_core::api::JsCallbackId>,
822
823 key_capture_active: bool,
830
831 pending_key_capture_buffer: std::collections::VecDeque<fresh_core::api::KeyEventPayload>,
836
837 goto_line_preview: Option<GotoLinePreviewSnapshot>,
842
843 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
845
846 lsp_server_statuses:
848 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
849
850 lsp_window_messages: Vec<LspMessageEntry>,
852
853 lsp_log_messages: Vec<LspMessageEntry>,
855
856 diagnostic_result_ids: HashMap<String, String>,
859
860 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
863
864 scheduled_inlay_hints_request: Option<(BufferId, Instant)>,
867
868 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
871
872 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
874
875 stored_diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
880
881 stored_folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
884
885 event_broadcaster: crate::model::control_event::EventBroadcaster,
887
888 bookmarks: bookmarks::BookmarkState,
890
891 search_case_sensitive: bool,
893 search_whole_word: bool,
894 search_use_regex: bool,
895 search_confirm_each: bool,
897
898 macros: macros::MacroState,
901
902 #[cfg(feature = "plugins")]
904 pending_plugin_actions: Vec<(
905 String,
906 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
907 )>,
908
909 #[cfg(feature = "plugins")]
911 plugin_render_requested: bool,
912
913 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
916
917 user_dismissed_lsp_languages: std::collections::HashSet<String>,
931
932 auto_start_prompted_languages: std::collections::HashSet<String>,
938
939 pending_auto_start_prompts: std::collections::HashSet<String>,
947
948 lsp_auto_prompt_enabled: bool,
956
957 pending_close_buffer: Option<BufferId>,
960
961 auto_revert_enabled: bool,
963
964 last_auto_revert_poll: std::time::Instant,
966
967 last_file_tree_poll: std::time::Instant,
969
970 git_index_resolved: bool,
972
973 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
976
977 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
980
981 #[allow(clippy::type_complexity)]
985 pending_file_poll_rx:
986 Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
987
988 #[allow(clippy::type_complexity)]
991 pending_dir_poll_rx: Option<
992 std::sync::mpsc::Receiver<(
993 Vec<(
994 crate::view::file_tree::NodeId,
995 PathBuf,
996 Option<std::time::SystemTime>,
997 )>,
998 Option<(PathBuf, std::time::SystemTime)>,
999 )>,
1000 >,
1001
1002 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
1005
1006 file_open_state: Option<file_open::FileOpenState>,
1008
1009 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
1011
1012 recovery_service: RecoveryService,
1014
1015 full_redraw_requested: bool,
1017
1018 suspend_requested: bool,
1021
1022 time_source: SharedTimeSource,
1024
1025 last_auto_recovery_save: std::time::Instant,
1027
1028 last_persistent_auto_save: std::time::Instant,
1030
1031 active_custom_contexts: HashSet<String>,
1034
1035 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
1038
1039 editor_mode: Option<String>,
1042
1043 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
1045
1046 status_log_path: Option<PathBuf>,
1048
1049 warning_domains: WarningDomainRegistry,
1052
1053 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
1055
1056 terminal_manager: crate::services::terminal::TerminalManager,
1058
1059 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
1061
1062 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
1064
1065 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
1067
1068 ephemeral_terminals: std::collections::HashSet<crate::services::terminal::TerminalId>,
1074
1075 terminal_mode: bool,
1077
1078 keyboard_capture: bool,
1082
1083 terminal_mode_resume: std::collections::HashSet<BufferId>,
1087
1088 previous_click_time: Option<std::time::Instant>,
1090
1091 previous_click_position: Option<(u16, u16)>,
1094
1095 click_count: u8,
1097
1098 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
1100
1101 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
1103
1104 pub(crate) event_debug: Option<event_debug::EventDebug>,
1106
1107 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
1109
1110 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
1112
1113 color_capability: crate::view::color_support::ColorCapability,
1115
1116 review_hunks: Vec<fresh_core::api::ReviewHunk>,
1118
1119 pub(crate) global_popups: crate::view::popup::PopupManager,
1127
1128 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
1131
1132 composite_view_states:
1135 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
1136
1137 pending_file_opens: Vec<PendingFileOpen>,
1141
1142 pending_hot_exit_recovery: bool,
1144
1145 wait_tracking: HashMap<BufferId, (u64, bool)>,
1147 completed_waits: Vec<u64>,
1149
1150 stdin_stream: stdin_stream::StdinStream,
1152
1153 line_scan: line_scan::LineScan,
1155
1156 search_scan: search_scan::SearchScan,
1158
1159 search_overlay_top_byte: Option<usize>,
1162
1163 pub animations: crate::view::animation::AnimationRunner,
1167
1168 pub(crate) previous_cursor_screen_pos: Option<((u16, u16), LeafId)>,
1176 pub(crate) cursor_jump_animation: Option<crate::view::animation::AnimationId>,
1179
1180 pub(crate) pending_vb_animations: Vec<(u64, BufferId, fresh_core::api::PluginAnimationKind)>,
1186}
1187
1188#[derive(Debug, Clone)]
1190pub struct PendingFileOpen {
1191 pub path: PathBuf,
1193 pub line: Option<usize>,
1195 pub column: Option<usize>,
1197 pub end_line: Option<usize>,
1199 pub end_column: Option<usize>,
1201 pub message: Option<String>,
1203 pub wait_id: Option<u64>,
1205}
1206
1207impl Editor {
1208 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1210 let trimmed = input.trim();
1211
1212 if trimmed.is_empty() {
1213 self.ansi_background = None;
1214 self.ansi_background_path = None;
1215 self.set_status_message(t!("status.background_cleared").to_string());
1216 return Ok(());
1217 }
1218
1219 let input_path = Path::new(trimmed);
1220 let resolved = if input_path.is_absolute() {
1221 input_path.to_path_buf()
1222 } else {
1223 self.working_dir.join(input_path)
1224 };
1225
1226 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1227
1228 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1229
1230 self.ansi_background = Some(parsed);
1231 self.ansi_background_path = Some(canonical.clone());
1232 self.set_status_message(
1233 t!(
1234 "view.background_set",
1235 path = canonical.display().to_string()
1236 )
1237 .to_string(),
1238 );
1239
1240 Ok(())
1241 }
1242
1243 fn effective_tabs_width(&self) -> u16 {
1248 if self.file_explorer_visible && self.file_explorer.is_some() {
1249 let explorer = self.file_explorer_width.to_cols(self.terminal_width);
1250 self.terminal_width.saturating_sub(explorer)
1251 } else {
1252 self.terminal_width
1253 }
1254 }
1255
1256 pub fn active_state(&self) -> &EditorState {
1258 self.buffers.get(&self.active_buffer()).unwrap()
1259 }
1260
1261 pub fn active_state_mut(&mut self) -> &mut EditorState {
1263 self.buffers.get_mut(&self.active_buffer()).unwrap()
1264 }
1265
1266 pub fn active_cursors(&self) -> &Cursors {
1270 let split_id = self.effective_active_split();
1271 &self.split_view_states.get(&split_id).unwrap().cursors
1272 }
1273
1274 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1276 let split_id = self.effective_active_split();
1277 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
1278 }
1279
1280 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1282 self.completion_items = Some(items);
1283 }
1284
1285 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1287 let active_split = self.split_manager.active_split();
1288 &self.split_view_states.get(&active_split).unwrap().viewport
1289 }
1290
1291 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1293 let active_split = self.split_manager.active_split();
1294 &mut self
1295 .split_view_states
1296 .get_mut(&active_split)
1297 .unwrap()
1298 .viewport
1299 }
1300
1301 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1303 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1305 return composite.name.clone();
1306 }
1307
1308 self.buffer_metadata
1309 .get(&buffer_id)
1310 .map(|m| m.display_name.clone())
1311 .or_else(|| {
1312 self.buffers.get(&buffer_id).and_then(|state| {
1313 state
1314 .buffer
1315 .file_path()
1316 .and_then(|p| p.file_name())
1317 .and_then(|n| n.to_str())
1318 .map(|s| s.to_string())
1319 })
1320 })
1321 .unwrap_or_else(|| "[No Name]".to_string())
1322 }
1323
1324 pub fn active_event_log(&self) -> &EventLog {
1334 self.event_logs.get(&self.active_buffer()).unwrap()
1335 }
1336
1337 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1339 self.event_logs.get_mut(&self.active_buffer()).unwrap()
1340 }
1341
1342 pub(super) fn update_modified_from_event_log(&mut self) {
1346 let is_at_saved = self
1347 .event_logs
1348 .get(&self.active_buffer())
1349 .map(|log| log.is_at_saved_position())
1350 .unwrap_or(false);
1351
1352 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1353 state.buffer.set_modified(!is_at_saved);
1354 }
1355 }
1356}
1357
1358fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1367 use crossterm::event::{KeyCode, KeyModifiers};
1368
1369 let mut modifiers = KeyModifiers::NONE;
1370 let mut remaining = key_str;
1371
1372 loop {
1374 if remaining.starts_with("C-") {
1375 modifiers |= KeyModifiers::CONTROL;
1376 remaining = &remaining[2..];
1377 } else if remaining.starts_with("M-") {
1378 modifiers |= KeyModifiers::ALT;
1379 remaining = &remaining[2..];
1380 } else if remaining.starts_with("S-") {
1381 modifiers |= KeyModifiers::SHIFT;
1382 remaining = &remaining[2..];
1383 } else {
1384 break;
1385 }
1386 }
1387
1388 let upper = remaining.to_uppercase();
1391 let code = match upper.as_str() {
1392 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1393 "TAB" => KeyCode::Tab,
1394 "BACKTAB" => KeyCode::BackTab,
1395 "ESC" | "ESCAPE" => KeyCode::Esc,
1396 "SPC" | "SPACE" => KeyCode::Char(' '),
1397 "DEL" | "DELETE" => KeyCode::Delete,
1398 "BS" | "BACKSPACE" => KeyCode::Backspace,
1399 "UP" => KeyCode::Up,
1400 "DOWN" => KeyCode::Down,
1401 "LEFT" => KeyCode::Left,
1402 "RIGHT" => KeyCode::Right,
1403 "HOME" => KeyCode::Home,
1404 "END" => KeyCode::End,
1405 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1406 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1407 s if s.starts_with('F') && s.len() > 1 => {
1408 if let Ok(n) = s[1..].parse::<u8>() {
1410 KeyCode::F(n)
1411 } else {
1412 return None;
1413 }
1414 }
1415 _ if remaining.len() == 1 => {
1416 let c = remaining.chars().next()?;
1419 if c.is_ascii_uppercase() {
1420 modifiers |= KeyModifiers::SHIFT;
1421 }
1422 KeyCode::Char(c.to_ascii_lowercase())
1423 }
1424 _ => return None,
1425 };
1426
1427 Some((code, modifiers))
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432 use super::*;
1433 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1434 use tempfile::TempDir;
1435
1436 fn test_dir_context() -> (DirectoryContext, TempDir) {
1438 let temp_dir = TempDir::new().unwrap();
1439 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1440 (dir_context, temp_dir)
1441 }
1442
1443 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1445 Arc::new(crate::model::filesystem::StdFileSystem)
1446 }
1447
1448 #[test]
1449 fn test_editor_new() {
1450 let config = Config::default();
1451 let (dir_context, _temp) = test_dir_context();
1452 let editor = Editor::new(
1453 config,
1454 80,
1455 24,
1456 dir_context,
1457 crate::view::color_support::ColorCapability::TrueColor,
1458 test_filesystem(),
1459 )
1460 .unwrap();
1461
1462 assert_eq!(editor.buffers.len(), 1);
1463 assert!(!editor.should_quit());
1464 }
1465
1466 #[test]
1467 fn test_new_buffer() {
1468 let config = Config::default();
1469 let (dir_context, _temp) = test_dir_context();
1470 let mut editor = Editor::new(
1471 config,
1472 80,
1473 24,
1474 dir_context,
1475 crate::view::color_support::ColorCapability::TrueColor,
1476 test_filesystem(),
1477 )
1478 .unwrap();
1479
1480 let id = editor.new_buffer();
1481 assert_eq!(editor.buffers.len(), 2);
1482 assert_eq!(editor.active_buffer(), id);
1483 }
1484
1485 #[test]
1486 #[ignore]
1487 fn test_clipboard() {
1488 let config = Config::default();
1489 let (dir_context, _temp) = test_dir_context();
1490 let mut editor = Editor::new(
1491 config,
1492 80,
1493 24,
1494 dir_context,
1495 crate::view::color_support::ColorCapability::TrueColor,
1496 test_filesystem(),
1497 )
1498 .unwrap();
1499
1500 editor.clipboard.set_internal("test".to_string());
1502
1503 editor.paste();
1505
1506 let content = editor.active_state().buffer.to_string().unwrap();
1507 assert_eq!(content, "test");
1508 }
1509
1510 #[test]
1511 fn test_action_to_events_insert_char() {
1512 let config = Config::default();
1513 let (dir_context, _temp) = test_dir_context();
1514 let mut editor = Editor::new(
1515 config,
1516 80,
1517 24,
1518 dir_context,
1519 crate::view::color_support::ColorCapability::TrueColor,
1520 test_filesystem(),
1521 )
1522 .unwrap();
1523
1524 let events = editor.action_to_events(Action::InsertChar('a'));
1525 assert!(events.is_some());
1526
1527 let events = events.unwrap();
1528 assert_eq!(events.len(), 1);
1529
1530 match &events[0] {
1531 Event::Insert { position, text, .. } => {
1532 assert_eq!(*position, 0);
1533 assert_eq!(text, "a");
1534 }
1535 _ => panic!("Expected Insert event"),
1536 }
1537 }
1538
1539 #[test]
1540 fn test_action_to_events_move_right() {
1541 let config = Config::default();
1542 let (dir_context, _temp) = test_dir_context();
1543 let mut editor = Editor::new(
1544 config,
1545 80,
1546 24,
1547 dir_context,
1548 crate::view::color_support::ColorCapability::TrueColor,
1549 test_filesystem(),
1550 )
1551 .unwrap();
1552
1553 let cursor_id = editor.active_cursors().primary_id();
1555 editor.apply_event_to_active_buffer(&Event::Insert {
1556 position: 0,
1557 text: "hello".to_string(),
1558 cursor_id,
1559 });
1560
1561 let events = editor.action_to_events(Action::MoveRight);
1562 assert!(events.is_some());
1563
1564 let events = events.unwrap();
1565 assert_eq!(events.len(), 1);
1566
1567 match &events[0] {
1568 Event::MoveCursor {
1569 new_position,
1570 new_anchor,
1571 ..
1572 } => {
1573 assert_eq!(*new_position, 5);
1575 assert_eq!(*new_anchor, None); }
1577 _ => panic!("Expected MoveCursor event"),
1578 }
1579 }
1580
1581 #[test]
1582 fn test_action_to_events_move_up_down() {
1583 let config = Config::default();
1584 let (dir_context, _temp) = test_dir_context();
1585 let mut editor = Editor::new(
1586 config,
1587 80,
1588 24,
1589 dir_context,
1590 crate::view::color_support::ColorCapability::TrueColor,
1591 test_filesystem(),
1592 )
1593 .unwrap();
1594
1595 let cursor_id = editor.active_cursors().primary_id();
1597 editor.apply_event_to_active_buffer(&Event::Insert {
1598 position: 0,
1599 text: "line1\nline2\nline3".to_string(),
1600 cursor_id,
1601 });
1602
1603 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1605 cursor_id,
1606 old_position: 0, new_position: 6,
1608 old_anchor: None, new_anchor: None,
1610 old_sticky_column: 0,
1611 new_sticky_column: 0,
1612 });
1613
1614 let events = editor.action_to_events(Action::MoveUp);
1616 assert!(events.is_some());
1617 let events = events.unwrap();
1618 assert_eq!(events.len(), 1);
1619
1620 match &events[0] {
1621 Event::MoveCursor { new_position, .. } => {
1622 assert_eq!(*new_position, 0); }
1624 _ => panic!("Expected MoveCursor event"),
1625 }
1626 }
1627
1628 #[test]
1629 fn test_action_to_events_insert_newline() {
1630 let config = Config::default();
1631 let (dir_context, _temp) = test_dir_context();
1632 let mut editor = Editor::new(
1633 config,
1634 80,
1635 24,
1636 dir_context,
1637 crate::view::color_support::ColorCapability::TrueColor,
1638 test_filesystem(),
1639 )
1640 .unwrap();
1641
1642 let events = editor.action_to_events(Action::InsertNewline);
1643 assert!(events.is_some());
1644
1645 let events = events.unwrap();
1646 assert_eq!(events.len(), 1);
1647
1648 match &events[0] {
1649 Event::Insert { text, .. } => {
1650 assert_eq!(text, "\n");
1651 }
1652 _ => panic!("Expected Insert event"),
1653 }
1654 }
1655
1656 #[test]
1657 fn test_action_to_events_unimplemented() {
1658 let config = Config::default();
1659 let (dir_context, _temp) = test_dir_context();
1660 let mut editor = Editor::new(
1661 config,
1662 80,
1663 24,
1664 dir_context,
1665 crate::view::color_support::ColorCapability::TrueColor,
1666 test_filesystem(),
1667 )
1668 .unwrap();
1669
1670 assert!(editor.action_to_events(Action::Save).is_none());
1672 assert!(editor.action_to_events(Action::Quit).is_none());
1673 assert!(editor.action_to_events(Action::Undo).is_none());
1674 }
1675
1676 #[test]
1677 fn test_action_to_events_delete_backward() {
1678 let config = Config::default();
1679 let (dir_context, _temp) = test_dir_context();
1680 let mut editor = Editor::new(
1681 config,
1682 80,
1683 24,
1684 dir_context,
1685 crate::view::color_support::ColorCapability::TrueColor,
1686 test_filesystem(),
1687 )
1688 .unwrap();
1689
1690 let cursor_id = editor.active_cursors().primary_id();
1692 editor.apply_event_to_active_buffer(&Event::Insert {
1693 position: 0,
1694 text: "hello".to_string(),
1695 cursor_id,
1696 });
1697
1698 let events = editor.action_to_events(Action::DeleteBackward);
1699 assert!(events.is_some());
1700
1701 let events = events.unwrap();
1702 assert_eq!(events.len(), 1);
1703
1704 match &events[0] {
1705 Event::Delete {
1706 range,
1707 deleted_text,
1708 ..
1709 } => {
1710 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1712 }
1713 _ => panic!("Expected Delete event"),
1714 }
1715 }
1716
1717 #[test]
1718 fn test_action_to_events_delete_forward() {
1719 let config = Config::default();
1720 let (dir_context, _temp) = test_dir_context();
1721 let mut editor = Editor::new(
1722 config,
1723 80,
1724 24,
1725 dir_context,
1726 crate::view::color_support::ColorCapability::TrueColor,
1727 test_filesystem(),
1728 )
1729 .unwrap();
1730
1731 let cursor_id = editor.active_cursors().primary_id();
1733 editor.apply_event_to_active_buffer(&Event::Insert {
1734 position: 0,
1735 text: "hello".to_string(),
1736 cursor_id,
1737 });
1738
1739 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1741 cursor_id,
1742 old_position: 0, new_position: 0,
1744 old_anchor: None, new_anchor: None,
1746 old_sticky_column: 0,
1747 new_sticky_column: 0,
1748 });
1749
1750 let events = editor.action_to_events(Action::DeleteForward);
1751 assert!(events.is_some());
1752
1753 let events = events.unwrap();
1754 assert_eq!(events.len(), 1);
1755
1756 match &events[0] {
1757 Event::Delete {
1758 range,
1759 deleted_text,
1760 ..
1761 } => {
1762 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1764 }
1765 _ => panic!("Expected Delete event"),
1766 }
1767 }
1768
1769 #[test]
1770 fn test_action_to_events_select_right() {
1771 let config = Config::default();
1772 let (dir_context, _temp) = test_dir_context();
1773 let mut editor = Editor::new(
1774 config,
1775 80,
1776 24,
1777 dir_context,
1778 crate::view::color_support::ColorCapability::TrueColor,
1779 test_filesystem(),
1780 )
1781 .unwrap();
1782
1783 let cursor_id = editor.active_cursors().primary_id();
1785 editor.apply_event_to_active_buffer(&Event::Insert {
1786 position: 0,
1787 text: "hello".to_string(),
1788 cursor_id,
1789 });
1790
1791 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1793 cursor_id,
1794 old_position: 0, new_position: 0,
1796 old_anchor: None, new_anchor: None,
1798 old_sticky_column: 0,
1799 new_sticky_column: 0,
1800 });
1801
1802 let events = editor.action_to_events(Action::SelectRight);
1803 assert!(events.is_some());
1804
1805 let events = events.unwrap();
1806 assert_eq!(events.len(), 1);
1807
1808 match &events[0] {
1809 Event::MoveCursor {
1810 new_position,
1811 new_anchor,
1812 ..
1813 } => {
1814 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1817 _ => panic!("Expected MoveCursor event"),
1818 }
1819 }
1820
1821 #[test]
1822 fn test_action_to_events_select_all() {
1823 let config = Config::default();
1824 let (dir_context, _temp) = test_dir_context();
1825 let mut editor = Editor::new(
1826 config,
1827 80,
1828 24,
1829 dir_context,
1830 crate::view::color_support::ColorCapability::TrueColor,
1831 test_filesystem(),
1832 )
1833 .unwrap();
1834
1835 let cursor_id = editor.active_cursors().primary_id();
1837 editor.apply_event_to_active_buffer(&Event::Insert {
1838 position: 0,
1839 text: "hello world".to_string(),
1840 cursor_id,
1841 });
1842
1843 let events = editor.action_to_events(Action::SelectAll);
1844 assert!(events.is_some());
1845
1846 let events = events.unwrap();
1847 assert_eq!(events.len(), 1);
1848
1849 match &events[0] {
1850 Event::MoveCursor {
1851 new_position,
1852 new_anchor,
1853 ..
1854 } => {
1855 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1858 _ => panic!("Expected MoveCursor event"),
1859 }
1860 }
1861
1862 #[test]
1863 fn test_action_to_events_document_nav() {
1864 let config = Config::default();
1865 let (dir_context, _temp) = test_dir_context();
1866 let mut editor = Editor::new(
1867 config,
1868 80,
1869 24,
1870 dir_context,
1871 crate::view::color_support::ColorCapability::TrueColor,
1872 test_filesystem(),
1873 )
1874 .unwrap();
1875
1876 let cursor_id = editor.active_cursors().primary_id();
1878 editor.apply_event_to_active_buffer(&Event::Insert {
1879 position: 0,
1880 text: "line1\nline2\nline3".to_string(),
1881 cursor_id,
1882 });
1883
1884 let events = editor.action_to_events(Action::MoveDocumentStart);
1886 assert!(events.is_some());
1887 let events = events.unwrap();
1888 match &events[0] {
1889 Event::MoveCursor { new_position, .. } => {
1890 assert_eq!(*new_position, 0);
1891 }
1892 _ => panic!("Expected MoveCursor event"),
1893 }
1894
1895 let events = editor.action_to_events(Action::MoveDocumentEnd);
1897 assert!(events.is_some());
1898 let events = events.unwrap();
1899 match &events[0] {
1900 Event::MoveCursor { new_position, .. } => {
1901 assert_eq!(*new_position, 17); }
1903 _ => panic!("Expected MoveCursor event"),
1904 }
1905 }
1906
1907 #[test]
1908 fn test_action_to_events_remove_secondary_cursors() {
1909 use crate::model::event::CursorId;
1910
1911 let config = Config::default();
1912 let (dir_context, _temp) = test_dir_context();
1913 let mut editor = Editor::new(
1914 config,
1915 80,
1916 24,
1917 dir_context,
1918 crate::view::color_support::ColorCapability::TrueColor,
1919 test_filesystem(),
1920 )
1921 .unwrap();
1922
1923 let cursor_id = editor.active_cursors().primary_id();
1925 editor.apply_event_to_active_buffer(&Event::Insert {
1926 position: 0,
1927 text: "hello world test".to_string(),
1928 cursor_id,
1929 });
1930
1931 editor.apply_event_to_active_buffer(&Event::AddCursor {
1933 cursor_id: CursorId(1),
1934 position: 5,
1935 anchor: None,
1936 });
1937 editor.apply_event_to_active_buffer(&Event::AddCursor {
1938 cursor_id: CursorId(2),
1939 position: 10,
1940 anchor: None,
1941 });
1942
1943 assert_eq!(editor.active_cursors().count(), 3);
1944
1945 let first_id = editor
1947 .active_cursors()
1948 .iter()
1949 .map(|(id, _)| id)
1950 .min_by_key(|id| id.0)
1951 .expect("Should have at least one cursor");
1952
1953 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
1955 assert!(events.is_some());
1956
1957 let events = events.unwrap();
1958 let remove_cursor_events: Vec<_> = events
1961 .iter()
1962 .filter_map(|e| match e {
1963 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1964 _ => None,
1965 })
1966 .collect();
1967
1968 assert_eq!(remove_cursor_events.len(), 2);
1970
1971 for cursor_id in &remove_cursor_events {
1972 assert_ne!(*cursor_id, first_id);
1974 }
1975 }
1976
1977 #[test]
1978 fn test_action_to_events_scroll() {
1979 let config = Config::default();
1980 let (dir_context, _temp) = test_dir_context();
1981 let mut editor = Editor::new(
1982 config,
1983 80,
1984 24,
1985 dir_context,
1986 crate::view::color_support::ColorCapability::TrueColor,
1987 test_filesystem(),
1988 )
1989 .unwrap();
1990
1991 let events = editor.action_to_events(Action::ScrollUp);
1993 assert!(events.is_some());
1994 let events = events.unwrap();
1995 assert_eq!(events.len(), 1);
1996 match &events[0] {
1997 Event::Scroll { line_offset } => {
1998 assert_eq!(*line_offset, -1);
1999 }
2000 _ => panic!("Expected Scroll event"),
2001 }
2002
2003 let events = editor.action_to_events(Action::ScrollDown);
2005 assert!(events.is_some());
2006 let events = events.unwrap();
2007 assert_eq!(events.len(), 1);
2008 match &events[0] {
2009 Event::Scroll { line_offset } => {
2010 assert_eq!(*line_offset, 1);
2011 }
2012 _ => panic!("Expected Scroll event"),
2013 }
2014 }
2015
2016 #[test]
2017 fn test_action_to_events_none() {
2018 let config = Config::default();
2019 let (dir_context, _temp) = test_dir_context();
2020 let mut editor = Editor::new(
2021 config,
2022 80,
2023 24,
2024 dir_context,
2025 crate::view::color_support::ColorCapability::TrueColor,
2026 test_filesystem(),
2027 )
2028 .unwrap();
2029
2030 let events = editor.action_to_events(Action::None);
2032 assert!(events.is_none());
2033 }
2034
2035 #[test]
2036 fn test_lsp_incremental_insert_generates_correct_range() {
2037 use crate::model::buffer::Buffer;
2040
2041 let buffer = Buffer::from_str_test("hello\nworld");
2042
2043 let position = 0;
2046 let (line, character) = buffer.position_to_lsp_position(position);
2047
2048 assert_eq!(line, 0, "Insertion at start should be line 0");
2049 assert_eq!(character, 0, "Insertion at start should be char 0");
2050
2051 let lsp_pos = Position::new(line as u32, character as u32);
2053 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2054
2055 assert_eq!(lsp_range.start.line, 0);
2056 assert_eq!(lsp_range.start.character, 0);
2057 assert_eq!(lsp_range.end.line, 0);
2058 assert_eq!(lsp_range.end.character, 0);
2059 assert_eq!(
2060 lsp_range.start, lsp_range.end,
2061 "Insert should have zero-width range"
2062 );
2063
2064 let position = 3;
2066 let (line, character) = buffer.position_to_lsp_position(position);
2067
2068 assert_eq!(line, 0);
2069 assert_eq!(character, 3);
2070
2071 let position = 6;
2073 let (line, character) = buffer.position_to_lsp_position(position);
2074
2075 assert_eq!(line, 1, "Position after newline should be line 1");
2076 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2077 }
2078
2079 #[test]
2080 fn test_lsp_incremental_delete_generates_correct_range() {
2081 use crate::model::buffer::Buffer;
2084
2085 let buffer = Buffer::from_str_test("hello\nworld");
2086
2087 let range_start = 1;
2089 let range_end = 5;
2090
2091 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2092 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2093
2094 assert_eq!(start_line, 0);
2095 assert_eq!(start_char, 1);
2096 assert_eq!(end_line, 0);
2097 assert_eq!(end_char, 5);
2098
2099 let lsp_range = LspRange::new(
2100 Position::new(start_line as u32, start_char as u32),
2101 Position::new(end_line as u32, end_char as u32),
2102 );
2103
2104 assert_eq!(lsp_range.start.line, 0);
2105 assert_eq!(lsp_range.start.character, 1);
2106 assert_eq!(lsp_range.end.line, 0);
2107 assert_eq!(lsp_range.end.character, 5);
2108 assert_ne!(
2109 lsp_range.start, lsp_range.end,
2110 "Delete should have non-zero range"
2111 );
2112
2113 let range_start = 4;
2115 let range_end = 8;
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, "Delete start on line 0");
2121 assert_eq!(start_char, 4, "Delete start at char 4");
2122 assert_eq!(end_line, 1, "Delete end on line 1");
2123 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2124 }
2125
2126 #[test]
2127 fn test_lsp_incremental_utf16_encoding() {
2128 use crate::model::buffer::Buffer;
2131
2132 let buffer = Buffer::from_str_test("😀hello");
2134
2135 let (line, character) = buffer.position_to_lsp_position(4);
2137
2138 assert_eq!(line, 0);
2139 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2140
2141 let (line, character) = buffer.position_to_lsp_position(9);
2143
2144 assert_eq!(line, 0);
2145 assert_eq!(
2146 character, 7,
2147 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2148 );
2149
2150 let buffer = Buffer::from_str_test("café");
2152
2153 let (line, character) = buffer.position_to_lsp_position(3);
2155
2156 assert_eq!(line, 0);
2157 assert_eq!(character, 3);
2158
2159 let (line, character) = buffer.position_to_lsp_position(5);
2161
2162 assert_eq!(line, 0);
2163 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2164 }
2165
2166 #[test]
2167 fn test_lsp_content_change_event_structure() {
2168 let insert_change = TextDocumentContentChangeEvent {
2172 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2173 range_length: None,
2174 text: "NEW".to_string(),
2175 };
2176
2177 assert!(insert_change.range.is_some());
2178 assert_eq!(insert_change.text, "NEW");
2179 let range = insert_change.range.unwrap();
2180 assert_eq!(
2181 range.start, range.end,
2182 "Insert should have zero-width range"
2183 );
2184
2185 let delete_change = TextDocumentContentChangeEvent {
2187 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2188 range_length: None,
2189 text: String::new(),
2190 };
2191
2192 assert!(delete_change.range.is_some());
2193 assert_eq!(delete_change.text, "");
2194 let range = delete_change.range.unwrap();
2195 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2196 assert_eq!(range.start.line, 0);
2197 assert_eq!(range.start.character, 2);
2198 assert_eq!(range.end.line, 0);
2199 assert_eq!(range.end.character, 7);
2200 }
2201
2202 #[test]
2203 fn test_goto_matching_bracket_forward() {
2204 let config = Config::default();
2205 let (dir_context, _temp) = test_dir_context();
2206 let mut editor = Editor::new(
2207 config,
2208 80,
2209 24,
2210 dir_context,
2211 crate::view::color_support::ColorCapability::TrueColor,
2212 test_filesystem(),
2213 )
2214 .unwrap();
2215
2216 let cursor_id = editor.active_cursors().primary_id();
2218 editor.apply_event_to_active_buffer(&Event::Insert {
2219 position: 0,
2220 text: "fn main() { let x = (1 + 2); }".to_string(),
2221 cursor_id,
2222 });
2223
2224 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2226 cursor_id,
2227 old_position: 31,
2228 new_position: 10,
2229 old_anchor: None,
2230 new_anchor: None,
2231 old_sticky_column: 0,
2232 new_sticky_column: 0,
2233 });
2234
2235 assert_eq!(editor.active_cursors().primary().position, 10);
2236
2237 editor.goto_matching_bracket();
2239
2240 assert_eq!(editor.active_cursors().primary().position, 29);
2245 }
2246
2247 #[test]
2248 fn test_goto_matching_bracket_backward() {
2249 let config = Config::default();
2250 let (dir_context, _temp) = test_dir_context();
2251 let mut editor = Editor::new(
2252 config,
2253 80,
2254 24,
2255 dir_context,
2256 crate::view::color_support::ColorCapability::TrueColor,
2257 test_filesystem(),
2258 )
2259 .unwrap();
2260
2261 let cursor_id = editor.active_cursors().primary_id();
2263 editor.apply_event_to_active_buffer(&Event::Insert {
2264 position: 0,
2265 text: "fn main() { let x = (1 + 2); }".to_string(),
2266 cursor_id,
2267 });
2268
2269 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2271 cursor_id,
2272 old_position: 31,
2273 new_position: 26,
2274 old_anchor: None,
2275 new_anchor: None,
2276 old_sticky_column: 0,
2277 new_sticky_column: 0,
2278 });
2279
2280 editor.goto_matching_bracket();
2282
2283 assert_eq!(editor.active_cursors().primary().position, 20);
2285 }
2286
2287 #[test]
2288 fn test_goto_matching_bracket_nested() {
2289 let config = Config::default();
2290 let (dir_context, _temp) = test_dir_context();
2291 let mut editor = Editor::new(
2292 config,
2293 80,
2294 24,
2295 dir_context,
2296 crate::view::color_support::ColorCapability::TrueColor,
2297 test_filesystem(),
2298 )
2299 .unwrap();
2300
2301 let cursor_id = editor.active_cursors().primary_id();
2303 editor.apply_event_to_active_buffer(&Event::Insert {
2304 position: 0,
2305 text: "{a{b{c}d}e}".to_string(),
2306 cursor_id,
2307 });
2308
2309 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2311 cursor_id,
2312 old_position: 11,
2313 new_position: 0,
2314 old_anchor: None,
2315 new_anchor: None,
2316 old_sticky_column: 0,
2317 new_sticky_column: 0,
2318 });
2319
2320 editor.goto_matching_bracket();
2322
2323 assert_eq!(editor.active_cursors().primary().position, 10);
2325 }
2326
2327 #[test]
2328 fn test_search_case_sensitive() {
2329 let config = Config::default();
2330 let (dir_context, _temp) = test_dir_context();
2331 let mut editor = Editor::new(
2332 config,
2333 80,
2334 24,
2335 dir_context,
2336 crate::view::color_support::ColorCapability::TrueColor,
2337 test_filesystem(),
2338 )
2339 .unwrap();
2340
2341 let cursor_id = editor.active_cursors().primary_id();
2343 editor.apply_event_to_active_buffer(&Event::Insert {
2344 position: 0,
2345 text: "Hello hello HELLO".to_string(),
2346 cursor_id,
2347 });
2348
2349 editor.search_case_sensitive = false;
2351 editor.perform_search("hello");
2352
2353 let search_state = editor.search_state.as_ref().unwrap();
2354 assert_eq!(
2355 search_state.matches.len(),
2356 3,
2357 "Should find all 3 matches case-insensitively"
2358 );
2359
2360 editor.search_case_sensitive = true;
2362 editor.perform_search("hello");
2363
2364 let search_state = editor.search_state.as_ref().unwrap();
2365 assert_eq!(
2366 search_state.matches.len(),
2367 1,
2368 "Should find only 1 exact match"
2369 );
2370 assert_eq!(
2371 search_state.matches[0], 6,
2372 "Should find 'hello' at position 6"
2373 );
2374 }
2375
2376 #[test]
2377 fn test_search_whole_word() {
2378 let config = Config::default();
2379 let (dir_context, _temp) = test_dir_context();
2380 let mut editor = Editor::new(
2381 config,
2382 80,
2383 24,
2384 dir_context,
2385 crate::view::color_support::ColorCapability::TrueColor,
2386 test_filesystem(),
2387 )
2388 .unwrap();
2389
2390 let cursor_id = editor.active_cursors().primary_id();
2392 editor.apply_event_to_active_buffer(&Event::Insert {
2393 position: 0,
2394 text: "test testing tested attest test".to_string(),
2395 cursor_id,
2396 });
2397
2398 editor.search_whole_word = false;
2400 editor.search_case_sensitive = true;
2401 editor.perform_search("test");
2402
2403 let search_state = editor.search_state.as_ref().unwrap();
2404 assert_eq!(
2405 search_state.matches.len(),
2406 5,
2407 "Should find 'test' in all occurrences"
2408 );
2409
2410 editor.search_whole_word = true;
2412 editor.perform_search("test");
2413
2414 let search_state = editor.search_state.as_ref().unwrap();
2415 assert_eq!(
2416 search_state.matches.len(),
2417 2,
2418 "Should find only whole word 'test'"
2419 );
2420 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2421 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2422 }
2423
2424 #[test]
2425 fn test_search_scan_completes_when_capped() {
2426 let config = Config::default();
2432 let (dir_context, _temp) = test_dir_context();
2433 let mut editor = Editor::new(
2434 config,
2435 80,
2436 24,
2437 dir_context,
2438 crate::view::color_support::ColorCapability::TrueColor,
2439 test_filesystem(),
2440 )
2441 .unwrap();
2442
2443 let buffer_id = editor.active_buffer();
2446 let regex = regex::bytes::Regex::new("test").unwrap();
2447 let fake_chunks = vec![
2448 crate::model::buffer::LineScanChunk {
2449 leaf_index: 0,
2450 byte_len: 100,
2451 already_known: true,
2452 },
2453 crate::model::buffer::LineScanChunk {
2454 leaf_index: 1,
2455 byte_len: 100,
2456 already_known: true,
2457 },
2458 ];
2459
2460 let chunked = crate::model::buffer::ChunkedSearchState {
2461 chunks: fake_chunks,
2462 next_chunk: 1, next_doc_offset: 100,
2464 total_bytes: 200,
2465 scanned_bytes: 100,
2466 regex,
2467 matches: vec![
2468 crate::model::buffer::SearchMatch {
2469 byte_offset: 10,
2470 length: 4,
2471 line: 1,
2472 column: 11,
2473 context: String::new(),
2474 },
2475 crate::model::buffer::SearchMatch {
2476 byte_offset: 50,
2477 length: 4,
2478 line: 1,
2479 column: 51,
2480 context: String::new(),
2481 },
2482 ],
2483 overlap_tail: Vec::new(),
2484 overlap_doc_offset: 0,
2485 max_matches: 10_000,
2486 capped: true, query_len: 4,
2488 running_line: 1,
2489 };
2490
2491 editor.search_scan.start(
2492 buffer_id,
2493 Vec::new(),
2494 chunked,
2495 "test".to_string(),
2496 None,
2497 false,
2498 false,
2499 false,
2500 );
2501
2502 let result = editor.process_search_scan();
2504 assert!(
2505 result,
2506 "process_search_scan should return true (needs render)"
2507 );
2508
2509 assert_eq!(
2511 editor.search_scan.buffer_id(),
2512 None,
2513 "search_scan should be drained after capped scan completes"
2514 );
2515
2516 let search_state = editor
2518 .search_state
2519 .as_ref()
2520 .expect("search_state should be set after scan finishes");
2521 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2522 assert_eq!(search_state.query, "test");
2523 assert!(
2524 search_state.capped,
2525 "search_state should be marked as capped"
2526 );
2527 }
2528
2529 #[test]
2530 fn test_bookmarks() {
2531 let config = Config::default();
2532 let (dir_context, _temp) = test_dir_context();
2533 let mut editor = Editor::new(
2534 config,
2535 80,
2536 24,
2537 dir_context,
2538 crate::view::color_support::ColorCapability::TrueColor,
2539 test_filesystem(),
2540 )
2541 .unwrap();
2542
2543 let cursor_id = editor.active_cursors().primary_id();
2545 editor.apply_event_to_active_buffer(&Event::Insert {
2546 position: 0,
2547 text: "Line 1\nLine 2\nLine 3".to_string(),
2548 cursor_id,
2549 });
2550
2551 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2553 cursor_id,
2554 old_position: 21,
2555 new_position: 7,
2556 old_anchor: None,
2557 new_anchor: None,
2558 old_sticky_column: 0,
2559 new_sticky_column: 0,
2560 });
2561
2562 editor.set_bookmark('1');
2564 assert_eq!(editor.bookmarks.get('1').map(|b| b.position), Some(7));
2565
2566 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2568 cursor_id,
2569 old_position: 7,
2570 new_position: 14,
2571 old_anchor: None,
2572 new_anchor: None,
2573 old_sticky_column: 0,
2574 new_sticky_column: 0,
2575 });
2576
2577 editor.jump_to_bookmark('1');
2579 assert_eq!(editor.active_cursors().primary().position, 7);
2580
2581 editor.clear_bookmark('1');
2583 assert_eq!(editor.bookmarks.get('1'), None);
2584 }
2585
2586 #[test]
2587 fn test_action_enum_new_variants() {
2588 use serde_json::json;
2590
2591 let args = HashMap::new();
2592 assert_eq!(
2593 Action::from_str("smart_home", &args),
2594 Some(Action::SmartHome)
2595 );
2596 assert_eq!(
2597 Action::from_str("dedent_selection", &args),
2598 Some(Action::DedentSelection)
2599 );
2600 assert_eq!(
2601 Action::from_str("toggle_comment", &args),
2602 Some(Action::ToggleComment)
2603 );
2604 assert_eq!(
2605 Action::from_str("goto_matching_bracket", &args),
2606 Some(Action::GoToMatchingBracket)
2607 );
2608 assert_eq!(
2609 Action::from_str("list_bookmarks", &args),
2610 Some(Action::ListBookmarks)
2611 );
2612 assert_eq!(
2613 Action::from_str("toggle_search_case_sensitive", &args),
2614 Some(Action::ToggleSearchCaseSensitive)
2615 );
2616 assert_eq!(
2617 Action::from_str("toggle_search_whole_word", &args),
2618 Some(Action::ToggleSearchWholeWord)
2619 );
2620
2621 let mut args_with_char = HashMap::new();
2623 args_with_char.insert("char".to_string(), json!("5"));
2624 assert_eq!(
2625 Action::from_str("set_bookmark", &args_with_char),
2626 Some(Action::SetBookmark('5'))
2627 );
2628 assert_eq!(
2629 Action::from_str("jump_to_bookmark", &args_with_char),
2630 Some(Action::JumpToBookmark('5'))
2631 );
2632 assert_eq!(
2633 Action::from_str("clear_bookmark", &args_with_char),
2634 Some(Action::ClearBookmark('5'))
2635 );
2636 }
2637
2638 #[test]
2639 fn test_keybinding_new_defaults() {
2640 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2641
2642 let mut config = Config::default();
2646 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2647 let resolver = KeybindingResolver::new(&config);
2648
2649 let event = KeyEvent {
2651 code: KeyCode::Char('/'),
2652 modifiers: KeyModifiers::CONTROL,
2653 kind: KeyEventKind::Press,
2654 state: KeyEventState::NONE,
2655 };
2656 let action = resolver.resolve(&event, KeyContext::Normal);
2657 assert_eq!(action, Action::ToggleComment);
2658
2659 let event = KeyEvent {
2661 code: KeyCode::Char(']'),
2662 modifiers: KeyModifiers::CONTROL,
2663 kind: KeyEventKind::Press,
2664 state: KeyEventState::NONE,
2665 };
2666 let action = resolver.resolve(&event, KeyContext::Normal);
2667 assert_eq!(action, Action::GoToMatchingBracket);
2668
2669 let event = KeyEvent {
2671 code: KeyCode::Tab,
2672 modifiers: KeyModifiers::SHIFT,
2673 kind: KeyEventKind::Press,
2674 state: KeyEventState::NONE,
2675 };
2676 let action = resolver.resolve(&event, KeyContext::Normal);
2677 assert_eq!(action, Action::DedentSelection);
2678
2679 let event = KeyEvent {
2681 code: KeyCode::Char('g'),
2682 modifiers: KeyModifiers::CONTROL,
2683 kind: KeyEventKind::Press,
2684 state: KeyEventState::NONE,
2685 };
2686 let action = resolver.resolve(&event, KeyContext::Normal);
2687 assert_eq!(action, Action::GotoLine);
2688
2689 let event = KeyEvent {
2691 code: KeyCode::Char('5'),
2692 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2693 kind: KeyEventKind::Press,
2694 state: KeyEventState::NONE,
2695 };
2696 let action = resolver.resolve(&event, KeyContext::Normal);
2697 assert_eq!(action, Action::SetBookmark('5'));
2698
2699 let event = KeyEvent {
2700 code: KeyCode::Char('5'),
2701 modifiers: KeyModifiers::ALT,
2702 kind: KeyEventKind::Press,
2703 state: KeyEventState::NONE,
2704 };
2705 let action = resolver.resolve(&event, KeyContext::Normal);
2706 assert_eq!(action, Action::JumpToBookmark('5'));
2707 }
2708
2709 #[test]
2721 fn test_lsp_rename_didchange_positions_bug() {
2722 use crate::model::buffer::Buffer;
2723
2724 let config = Config::default();
2725 let (dir_context, _temp) = test_dir_context();
2726 let mut editor = Editor::new(
2727 config,
2728 80,
2729 24,
2730 dir_context,
2731 crate::view::color_support::ColorCapability::TrueColor,
2732 test_filesystem(),
2733 )
2734 .unwrap();
2735
2736 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2740 editor.active_state_mut().buffer =
2741 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2742
2743 let cursor_id = editor.active_cursors().primary_id();
2748
2749 let batch = Event::Batch {
2750 events: vec![
2751 Event::Delete {
2753 range: 23..26, deleted_text: "val".to_string(),
2755 cursor_id,
2756 },
2757 Event::Insert {
2758 position: 23,
2759 text: "value".to_string(),
2760 cursor_id,
2761 },
2762 Event::Delete {
2764 range: 7..10, deleted_text: "val".to_string(),
2766 cursor_id,
2767 },
2768 Event::Insert {
2769 position: 7,
2770 text: "value".to_string(),
2771 cursor_id,
2772 },
2773 ],
2774 description: "LSP Rename".to_string(),
2775 };
2776
2777 let lsp_changes_before = editor.collect_lsp_changes(&batch);
2779
2780 editor.apply_event_to_active_buffer(&batch);
2782
2783 let lsp_changes_after = editor.collect_lsp_changes(&batch);
2786
2787 let final_content = editor.active_state().buffer.to_string().unwrap();
2789 assert_eq!(
2790 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2791 "Buffer should have 'value' in both places"
2792 );
2793
2794 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2800
2801 let first_delete = &lsp_changes_before[0];
2802 let first_del_range = first_delete.range.unwrap();
2803 assert_eq!(
2804 first_del_range.start.line, 1,
2805 "First delete should be on line 1 (BEFORE)"
2806 );
2807 assert_eq!(
2808 first_del_range.start.character, 4,
2809 "First delete start should be at char 4 (BEFORE)"
2810 );
2811
2812 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2818
2819 let first_delete_after = &lsp_changes_after[0];
2820 let first_del_range_after = first_delete_after.range.unwrap();
2821
2822 eprintln!("BEFORE modification:");
2825 eprintln!(
2826 " Delete at line {}, char {}-{}",
2827 first_del_range.start.line,
2828 first_del_range.start.character,
2829 first_del_range.end.character
2830 );
2831 eprintln!("AFTER modification:");
2832 eprintln!(
2833 " Delete at line {}, char {}-{}",
2834 first_del_range_after.start.line,
2835 first_del_range_after.start.character,
2836 first_del_range_after.end.character
2837 );
2838
2839 assert_ne!(
2857 first_del_range_after.end.character, first_del_range.end.character,
2858 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2859 );
2860
2861 eprintln!("\n=== BUG DEMONSTRATED ===");
2862 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2863 eprintln!("the positions are WRONG because they're calculated from the");
2864 eprintln!("modified buffer, not the original buffer.");
2865 eprintln!("This causes the second rename to fail with 'content modified' error.");
2866 eprintln!("========================\n");
2867 }
2868
2869 #[test]
2870 fn test_lsp_rename_preserves_cursor_position() {
2871 use crate::model::buffer::Buffer;
2872
2873 let config = Config::default();
2874 let (dir_context, _temp) = test_dir_context();
2875 let mut editor = Editor::new(
2876 config,
2877 80,
2878 24,
2879 dir_context,
2880 crate::view::color_support::ColorCapability::TrueColor,
2881 test_filesystem(),
2882 )
2883 .unwrap();
2884
2885 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2889 editor.active_state_mut().buffer =
2890 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2891
2892 let original_cursor_pos = 23;
2894 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2895
2896 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2898 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2899 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2900
2901 let cursor_id = editor.active_cursors().primary_id();
2904 let buffer_id = editor.active_buffer();
2905
2906 let events = vec![
2907 Event::Delete {
2909 range: 23..26, deleted_text: "val".to_string(),
2911 cursor_id,
2912 },
2913 Event::Insert {
2914 position: 23,
2915 text: "value".to_string(),
2916 cursor_id,
2917 },
2918 Event::Delete {
2920 range: 7..10, deleted_text: "val".to_string(),
2922 cursor_id,
2923 },
2924 Event::Insert {
2925 position: 7,
2926 text: "value".to_string(),
2927 cursor_id,
2928 },
2929 ];
2930
2931 editor
2933 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2934 .unwrap();
2935
2936 let final_content = editor.active_state().buffer.to_string().unwrap();
2938 assert_eq!(
2939 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2940 "Buffer should have 'value' in both places"
2941 );
2942
2943 let final_cursor_pos = editor.active_cursors().primary().position;
2951 let expected_cursor_pos = 25; assert_eq!(
2954 final_cursor_pos, expected_cursor_pos,
2955 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2956 Original pos: {}, expected adjustment: +2 for first rename",
2957 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2958 );
2959
2960 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2962 assert_eq!(
2963 text_at_new_cursor, "value",
2964 "Cursor should be at the start of 'value' after rename"
2965 );
2966 }
2967
2968 #[test]
2969 fn test_lsp_rename_twice_consecutive() {
2970 use crate::model::buffer::Buffer;
2973
2974 let config = Config::default();
2975 let (dir_context, _temp) = test_dir_context();
2976 let mut editor = Editor::new(
2977 config,
2978 80,
2979 24,
2980 dir_context,
2981 crate::view::color_support::ColorCapability::TrueColor,
2982 test_filesystem(),
2983 )
2984 .unwrap();
2985
2986 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2988 editor.active_state_mut().buffer =
2989 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2990
2991 let cursor_id = editor.active_cursors().primary_id();
2992 let buffer_id = editor.active_buffer();
2993
2994 let events1 = vec![
2997 Event::Delete {
2999 range: 23..26,
3000 deleted_text: "val".to_string(),
3001 cursor_id,
3002 },
3003 Event::Insert {
3004 position: 23,
3005 text: "value".to_string(),
3006 cursor_id,
3007 },
3008 Event::Delete {
3010 range: 7..10,
3011 deleted_text: "val".to_string(),
3012 cursor_id,
3013 },
3014 Event::Insert {
3015 position: 7,
3016 text: "value".to_string(),
3017 cursor_id,
3018 },
3019 ];
3020
3021 let batch1 = Event::Batch {
3023 events: events1.clone(),
3024 description: "LSP Rename 1".to_string(),
3025 };
3026
3027 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
3029
3030 assert_eq!(
3032 lsp_changes1.len(),
3033 4,
3034 "First rename should have 4 LSP changes"
3035 );
3036
3037 let first_del = &lsp_changes1[0];
3039 let first_del_range = first_del.range.unwrap();
3040 assert_eq!(first_del_range.start.line, 1, "First delete line");
3041 assert_eq!(
3042 first_del_range.start.character, 4,
3043 "First delete start char"
3044 );
3045 assert_eq!(first_del_range.end.character, 7, "First delete end char");
3046
3047 editor
3049 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
3050 .unwrap();
3051
3052 let after_first = editor.active_state().buffer.to_string().unwrap();
3054 assert_eq!(
3055 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3056 "After first rename"
3057 );
3058
3059 let events2 = vec![
3069 Event::Delete {
3071 range: 25..30,
3072 deleted_text: "value".to_string(),
3073 cursor_id,
3074 },
3075 Event::Insert {
3076 position: 25,
3077 text: "x".to_string(),
3078 cursor_id,
3079 },
3080 Event::Delete {
3082 range: 7..12,
3083 deleted_text: "value".to_string(),
3084 cursor_id,
3085 },
3086 Event::Insert {
3087 position: 7,
3088 text: "x".to_string(),
3089 cursor_id,
3090 },
3091 ];
3092
3093 let batch2 = Event::Batch {
3095 events: events2.clone(),
3096 description: "LSP Rename 2".to_string(),
3097 };
3098
3099 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
3101
3102 assert_eq!(
3106 lsp_changes2.len(),
3107 4,
3108 "Second rename should have 4 LSP changes"
3109 );
3110
3111 let second_first_del = &lsp_changes2[0];
3113 let second_first_del_range = second_first_del.range.unwrap();
3114 assert_eq!(
3115 second_first_del_range.start.line, 1,
3116 "Second rename first delete should be on line 1"
3117 );
3118 assert_eq!(
3119 second_first_del_range.start.character, 4,
3120 "Second rename first delete start should be at char 4"
3121 );
3122 assert_eq!(
3123 second_first_del_range.end.character, 9,
3124 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3125 );
3126
3127 let second_third_del = &lsp_changes2[2];
3129 let second_third_del_range = second_third_del.range.unwrap();
3130 assert_eq!(
3131 second_third_del_range.start.line, 0,
3132 "Second rename third delete should be on line 0"
3133 );
3134 assert_eq!(
3135 second_third_del_range.start.character, 7,
3136 "Second rename third delete start should be at char 7"
3137 );
3138 assert_eq!(
3139 second_third_del_range.end.character, 12,
3140 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3141 );
3142
3143 editor
3145 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3146 .unwrap();
3147
3148 let after_second = editor.active_state().buffer.to_string().unwrap();
3150 assert_eq!(
3151 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3152 "After second rename"
3153 );
3154 }
3155
3156 #[test]
3157 fn test_ensure_active_tab_visible_static_offset() {
3158 let config = Config::default();
3159 let (dir_context, _temp) = test_dir_context();
3160 let mut editor = Editor::new(
3161 config,
3162 80,
3163 24,
3164 dir_context,
3165 crate::view::color_support::ColorCapability::TrueColor,
3166 test_filesystem(),
3167 )
3168 .unwrap();
3169 let split_id = editor.split_manager.active_split();
3170
3171 let buf1 = editor.new_buffer();
3173 editor
3174 .buffers
3175 .get_mut(&buf1)
3176 .unwrap()
3177 .buffer
3178 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3179 let buf2 = editor.new_buffer();
3180 editor
3181 .buffers
3182 .get_mut(&buf2)
3183 .unwrap()
3184 .buffer
3185 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3186 let buf3 = editor.new_buffer();
3187 editor
3188 .buffers
3189 .get_mut(&buf3)
3190 .unwrap()
3191 .buffer
3192 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3193
3194 {
3195 use crate::view::split::TabTarget;
3196 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
3197 view_state.open_buffers = vec![
3198 TabTarget::Buffer(buf1),
3199 TabTarget::Buffer(buf2),
3200 TabTarget::Buffer(buf3),
3201 ];
3202 view_state.tab_scroll_offset = 50;
3203 }
3204
3205 editor.ensure_active_tab_visible(split_id, buf1, 25);
3209 assert_eq!(
3210 editor
3211 .split_view_states
3212 .get(&split_id)
3213 .unwrap()
3214 .tab_scroll_offset,
3215 0
3216 );
3217
3218 editor.ensure_active_tab_visible(split_id, buf3, 25);
3220 let view_state = editor.split_view_states.get(&split_id).unwrap();
3221 assert!(view_state.tab_scroll_offset > 0);
3222 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3223 let total_width: usize = buffer_ids
3224 .iter()
3225 .enumerate()
3226 .map(|(idx, id)| {
3227 let state = editor.buffers.get(id).unwrap();
3228 let name_len = state
3229 .buffer
3230 .file_path()
3231 .and_then(|p| p.file_name())
3232 .and_then(|n| n.to_str())
3233 .map(|s| s.chars().count())
3234 .unwrap_or(0);
3235 let tab_width = 2 + name_len;
3236 if idx < buffer_ids.len() - 1 {
3237 tab_width + 1 } else {
3239 tab_width
3240 }
3241 })
3242 .sum();
3243 assert!(view_state.tab_scroll_offset <= total_width);
3244 }
3245}