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 uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
219 fresh_core::file_uri::lsp_uri_to_path(uri).ok_or_else(|| "URI is not a file path".to_string())
220}
221
222#[derive(Clone, Debug)]
224pub struct PendingGrammar {
225 pub language: String,
227 pub grammar_path: String,
229 pub extensions: Vec<String>,
231}
232
233#[derive(Clone, Debug)]
235struct SemanticTokenRangeRequest {
236 buffer_id: BufferId,
237 version: u64,
238 range: Range<usize>,
239 start_line: usize,
240 end_line: usize,
241}
242
243#[derive(Clone, Copy, Debug)]
244enum SemanticTokensFullRequestKind {
245 Full,
246 FullDelta,
247}
248
249#[derive(Clone, Debug)]
250struct SemanticTokenFullRequest {
251 buffer_id: BufferId,
252 version: u64,
253 kind: SemanticTokensFullRequestKind,
254}
255
256#[derive(Clone, Debug)]
257struct FoldingRangeRequest {
258 buffer_id: BufferId,
259 version: u64,
260}
261
262#[derive(Clone, Debug)]
263struct InlayHintsRequest {
264 buffer_id: BufferId,
265 version: u64,
266}
267
268#[derive(Debug, Clone)]
274pub struct DabbrevCycleState {
275 pub original_prefix: String,
277 pub word_start: usize,
279 pub candidates: Vec<String>,
281 pub index: usize,
283}
284
285#[derive(Debug, Clone)]
300pub(crate) struct GotoLinePreviewSnapshot {
301 pub buffer_id: BufferId,
302 pub split_id: LeafId,
303 pub cursor_id: crate::model::event::CursorId,
304 pub position: usize,
305 pub anchor: Option<usize>,
306 pub sticky_column: usize,
307 pub viewport_top_byte: usize,
308 pub viewport_top_view_line_offset: usize,
309 pub viewport_left_column: usize,
310 pub last_jump_position: usize,
311}
312
313pub struct Editor {
315 buffers: HashMap<BufferId, EditorState>,
317
318 event_logs: HashMap<BufferId, EventLog>,
323
324 next_buffer_id: usize,
326
327 config: Arc<Config>,
347
348 config_snapshot_anchor: Arc<Config>,
350
351 config_cached_json: Arc<serde_json::Value>,
354
355 user_config_raw: Arc<serde_json::Value>,
357
358 dir_context: DirectoryContext,
360
361 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
363
364 pending_grammars: Vec<PendingGrammar>,
366
367 grammar_reload_pending: bool,
371
372 grammar_build_in_progress: bool,
375
376 needs_full_grammar_build: bool,
380
381 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
383
384 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
388
389 theme: crate::view::theme::Theme,
391
392 theme_registry: crate::view::theme::ThemeRegistry,
394
395 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
397
398 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
400
401 ansi_background_path: Option<PathBuf>,
403
404 background_fade: f32,
406
407 keybindings: Arc<RwLock<KeybindingResolver>>,
409
410 clipboard: crate::services::clipboard::Clipboard,
412
413 should_quit: bool,
415
416 should_detach: bool,
418
419 session_mode: bool,
421
422 software_cursor_only: bool,
424
425 session_name: Option<String>,
427
428 pending_escape_sequences: Vec<u8>,
431
432 restart_with_dir: Option<PathBuf>,
435
436 status_message: Option<String>,
438
439 plugin_status_message: Option<String>,
441
442 last_window_title: Option<String>,
446
447 plugin_errors: Vec<String>,
450
451 prompt: Option<Prompt>,
453
454 terminal_width: u16,
456 terminal_height: u16,
457
458 lsp: Option<LspManager>,
460
461 buffer_metadata: HashMap<BufferId, BufferMetadata>,
463
464 mode_registry: ModeRegistry,
466
467 tokio_runtime: Option<tokio::runtime::Runtime>,
469
470 async_bridge: Option<AsyncBridge>,
472
473 split_manager: SplitManager,
475
476 split_view_states: HashMap<LeafId, SplitViewState>,
480
481 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
485
486 scroll_sync_manager: ScrollSyncManager,
489
490 file_explorer: Option<FileTreeView>,
492
493 preview: Option<(LeafId, BufferId)>,
507
508 suppress_position_history_once: bool,
513
514 fs_manager: Arc<FsManager>,
516
517 authority: crate::services::authority::Authority,
527
528 pending_authority: Option<crate::services::authority::Authority>,
534
535 pub remote_indicator_override: Option<crate::view::ui::status_bar::RemoteIndicatorOverride>,
541
542 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
547
548 file_explorer_visible: bool,
550
551 file_explorer_sync_in_progress: bool,
554
555 file_explorer_width: crate::config::ExplorerWidth,
559
560 pending_file_explorer_show_hidden: Option<bool>,
562
563 pending_file_explorer_show_gitignored: Option<bool>,
565
566 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
568
569 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
571
572 pub(crate) file_explorer_clipboard: Option<crate::app::file_explorer::FileExplorerClipboard>,
574
575 menu_bar_visible: bool,
577
578 menu_bar_auto_shown: bool,
581
582 tab_bar_visible: bool,
584
585 status_bar_visible: bool,
587
588 prompt_line_visible: bool,
590
591 mouse_enabled: bool,
593
594 same_buffer_scroll_sync: bool,
596
597 mouse_cursor_position: Option<(u16, u16)>,
601
602 gpm_active: bool,
604
605 key_context: KeyContext,
607
608 menu_state: crate::view::ui::MenuState,
610
611 menus: crate::config::MenuConfig,
613
614 working_dir: PathBuf,
616
617 pub position_history: PositionHistory,
619
620 in_navigation: bool,
622
623 next_lsp_request_id: u64,
625
626 pending_completion_requests: HashSet<u64>,
628
629 completion_items: Option<Vec<lsp_types::CompletionItem>>,
632
633 scheduled_completion_trigger: Option<Instant>,
636
637 completion_service: crate::services::completion::CompletionService,
640
641 dabbrev_state: Option<DabbrevCycleState>,
645
646 pending_goto_definition_request: Option<u64>,
648
649 pending_references_request: Option<u64>,
651
652 pending_references_symbol: String,
654
655 pending_signature_help_request: Option<u64>,
657
658 pending_code_actions_requests: HashSet<u64>,
660
661 pending_code_actions_server_names: HashMap<u64, String>,
663
664 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
668
669 pending_inlay_hints_requests: HashMap<u64, InlayHintsRequest>,
680
681 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
683
684 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
686
687 folding_ranges_debounce: HashMap<BufferId, Instant>,
689
690 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
692
693 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
695
696 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
698
699 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
701
702 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
704
705 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
707
708 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
710
711 hover: hover::HoverState,
714
715 search_state: Option<SearchState>,
717
718 search_namespace: crate::view::overlay::OverlayNamespace,
720
721 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
723
724 pending_search_range: Option<Range<usize>>,
726
727 interactive_replace_state: Option<InteractiveReplaceState>,
729
730 mouse_state: MouseState,
732
733 tab_context_menu: Option<TabContextMenu>,
735
736 file_explorer_context_menu: Option<FileExplorerContextMenu>,
738
739 theme_info_popup: Option<types::ThemeInfoPopup>,
741
742 pub(crate) cached_layout: CachedLayout,
744
745 command_registry: Arc<RwLock<CommandRegistry>>,
747
748 quick_open_registry: QuickOpenRegistry,
750
751 plugin_manager: PluginManager,
753
754 plugin_dev_workspaces:
758 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
759
760 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
764
765 panel_ids: HashMap<String, BufferId>,
768
769 buffer_groups: HashMap<types::BufferGroupId, types::BufferGroup>,
771 buffer_to_group: HashMap<BufferId, types::BufferGroupId>,
773 next_buffer_group_id: usize,
775
776 pub(crate) grouped_subtrees:
784 HashMap<crate::model::event::LeafId, crate::view::split::SplitNode>,
785
786 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
789
790 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
796
797 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
800
801 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
805
806 goto_line_preview: Option<GotoLinePreviewSnapshot>,
811
812 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
814
815 lsp_server_statuses:
817 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
818
819 lsp_window_messages: Vec<LspMessageEntry>,
821
822 lsp_log_messages: Vec<LspMessageEntry>,
824
825 diagnostic_result_ids: HashMap<String, String>,
828
829 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
832
833 scheduled_inlay_hints_request: Option<(BufferId, Instant)>,
836
837 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
840
841 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
843
844 stored_diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
849
850 stored_folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
853
854 event_broadcaster: crate::model::control_event::EventBroadcaster,
856
857 bookmarks: bookmarks::BookmarkState,
859
860 search_case_sensitive: bool,
862 search_whole_word: bool,
863 search_use_regex: bool,
864 search_confirm_each: bool,
866
867 macros: macros::MacroState,
870
871 #[cfg(feature = "plugins")]
873 pending_plugin_actions: Vec<(
874 String,
875 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
876 )>,
877
878 #[cfg(feature = "plugins")]
880 plugin_render_requested: bool,
881
882 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
885
886 user_dismissed_lsp_languages: std::collections::HashSet<String>,
900
901 auto_start_prompted_languages: std::collections::HashSet<String>,
907
908 pending_auto_start_prompts: std::collections::HashSet<String>,
916
917 lsp_auto_prompt_enabled: bool,
925
926 pending_close_buffer: Option<BufferId>,
929
930 auto_revert_enabled: bool,
932
933 last_auto_revert_poll: std::time::Instant,
935
936 last_file_tree_poll: std::time::Instant,
938
939 git_index_resolved: bool,
941
942 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
945
946 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
949
950 #[allow(clippy::type_complexity)]
954 pending_file_poll_rx:
955 Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
956
957 #[allow(clippy::type_complexity)]
960 pending_dir_poll_rx: Option<
961 std::sync::mpsc::Receiver<(
962 Vec<(
963 crate::view::file_tree::NodeId,
964 PathBuf,
965 Option<std::time::SystemTime>,
966 )>,
967 Option<(PathBuf, std::time::SystemTime)>,
968 )>,
969 >,
970
971 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
974
975 file_open_state: Option<file_open::FileOpenState>,
977
978 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
980
981 recovery_service: RecoveryService,
983
984 full_redraw_requested: bool,
986
987 suspend_requested: bool,
990
991 time_source: SharedTimeSource,
993
994 last_auto_recovery_save: std::time::Instant,
996
997 last_persistent_auto_save: std::time::Instant,
999
1000 active_custom_contexts: HashSet<String>,
1003
1004 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
1007
1008 editor_mode: Option<String>,
1011
1012 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
1014
1015 status_log_path: Option<PathBuf>,
1017
1018 warning_domains: WarningDomainRegistry,
1021
1022 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
1024
1025 terminal_manager: crate::services::terminal::TerminalManager,
1027
1028 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
1030
1031 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
1033
1034 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
1036
1037 ephemeral_terminals: std::collections::HashSet<crate::services::terminal::TerminalId>,
1043
1044 terminal_mode: bool,
1046
1047 keyboard_capture: bool,
1051
1052 terminal_mode_resume: std::collections::HashSet<BufferId>,
1056
1057 previous_click_time: Option<std::time::Instant>,
1059
1060 previous_click_position: Option<(u16, u16)>,
1063
1064 click_count: u8,
1066
1067 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
1069
1070 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
1072
1073 pub(crate) event_debug: Option<event_debug::EventDebug>,
1075
1076 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
1078
1079 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
1081
1082 color_capability: crate::view::color_support::ColorCapability,
1084
1085 review_hunks: Vec<fresh_core::api::ReviewHunk>,
1087
1088 pub(crate) global_popups: crate::view::popup::PopupManager,
1096
1097 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
1100
1101 composite_view_states:
1104 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
1105
1106 pending_file_opens: Vec<PendingFileOpen>,
1110
1111 pending_hot_exit_recovery: bool,
1113
1114 wait_tracking: HashMap<BufferId, (u64, bool)>,
1116 completed_waits: Vec<u64>,
1118
1119 stdin_stream: stdin_stream::StdinStream,
1121
1122 line_scan: line_scan::LineScan,
1124
1125 search_scan: search_scan::SearchScan,
1127
1128 search_overlay_top_byte: Option<usize>,
1131}
1132
1133#[derive(Debug, Clone)]
1135pub struct PendingFileOpen {
1136 pub path: PathBuf,
1138 pub line: Option<usize>,
1140 pub column: Option<usize>,
1142 pub end_line: Option<usize>,
1144 pub end_column: Option<usize>,
1146 pub message: Option<String>,
1148 pub wait_id: Option<u64>,
1150}
1151
1152impl Editor {
1153 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1155 let trimmed = input.trim();
1156
1157 if trimmed.is_empty() {
1158 self.ansi_background = None;
1159 self.ansi_background_path = None;
1160 self.set_status_message(t!("status.background_cleared").to_string());
1161 return Ok(());
1162 }
1163
1164 let input_path = Path::new(trimmed);
1165 let resolved = if input_path.is_absolute() {
1166 input_path.to_path_buf()
1167 } else {
1168 self.working_dir.join(input_path)
1169 };
1170
1171 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1172
1173 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1174
1175 self.ansi_background = Some(parsed);
1176 self.ansi_background_path = Some(canonical.clone());
1177 self.set_status_message(
1178 t!(
1179 "view.background_set",
1180 path = canonical.display().to_string()
1181 )
1182 .to_string(),
1183 );
1184
1185 Ok(())
1186 }
1187
1188 fn effective_tabs_width(&self) -> u16 {
1193 if self.file_explorer_visible && self.file_explorer.is_some() {
1194 let explorer = self.file_explorer_width.to_cols(self.terminal_width);
1195 self.terminal_width.saturating_sub(explorer)
1196 } else {
1197 self.terminal_width
1198 }
1199 }
1200
1201 pub fn active_state(&self) -> &EditorState {
1203 self.buffers.get(&self.active_buffer()).unwrap()
1204 }
1205
1206 pub fn active_state_mut(&mut self) -> &mut EditorState {
1208 self.buffers.get_mut(&self.active_buffer()).unwrap()
1209 }
1210
1211 pub fn active_cursors(&self) -> &Cursors {
1215 let split_id = self.effective_active_split();
1216 &self.split_view_states.get(&split_id).unwrap().cursors
1217 }
1218
1219 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1221 let split_id = self.effective_active_split();
1222 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
1223 }
1224
1225 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1227 self.completion_items = Some(items);
1228 }
1229
1230 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1232 let active_split = self.split_manager.active_split();
1233 &self.split_view_states.get(&active_split).unwrap().viewport
1234 }
1235
1236 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1238 let active_split = self.split_manager.active_split();
1239 &mut self
1240 .split_view_states
1241 .get_mut(&active_split)
1242 .unwrap()
1243 .viewport
1244 }
1245
1246 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1248 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1250 return composite.name.clone();
1251 }
1252
1253 self.buffer_metadata
1254 .get(&buffer_id)
1255 .map(|m| m.display_name.clone())
1256 .or_else(|| {
1257 self.buffers.get(&buffer_id).and_then(|state| {
1258 state
1259 .buffer
1260 .file_path()
1261 .and_then(|p| p.file_name())
1262 .and_then(|n| n.to_str())
1263 .map(|s| s.to_string())
1264 })
1265 })
1266 .unwrap_or_else(|| "[No Name]".to_string())
1267 }
1268
1269 pub fn active_event_log(&self) -> &EventLog {
1279 self.event_logs.get(&self.active_buffer()).unwrap()
1280 }
1281
1282 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1284 self.event_logs.get_mut(&self.active_buffer()).unwrap()
1285 }
1286
1287 pub(super) fn update_modified_from_event_log(&mut self) {
1291 let is_at_saved = self
1292 .event_logs
1293 .get(&self.active_buffer())
1294 .map(|log| log.is_at_saved_position())
1295 .unwrap_or(false);
1296
1297 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1298 state.buffer.set_modified(!is_at_saved);
1299 }
1300 }
1301}
1302
1303fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1312 use crossterm::event::{KeyCode, KeyModifiers};
1313
1314 let mut modifiers = KeyModifiers::NONE;
1315 let mut remaining = key_str;
1316
1317 loop {
1319 if remaining.starts_with("C-") {
1320 modifiers |= KeyModifiers::CONTROL;
1321 remaining = &remaining[2..];
1322 } else if remaining.starts_with("M-") {
1323 modifiers |= KeyModifiers::ALT;
1324 remaining = &remaining[2..];
1325 } else if remaining.starts_with("S-") {
1326 modifiers |= KeyModifiers::SHIFT;
1327 remaining = &remaining[2..];
1328 } else {
1329 break;
1330 }
1331 }
1332
1333 let upper = remaining.to_uppercase();
1336 let code = match upper.as_str() {
1337 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1338 "TAB" => KeyCode::Tab,
1339 "BACKTAB" => KeyCode::BackTab,
1340 "ESC" | "ESCAPE" => KeyCode::Esc,
1341 "SPC" | "SPACE" => KeyCode::Char(' '),
1342 "DEL" | "DELETE" => KeyCode::Delete,
1343 "BS" | "BACKSPACE" => KeyCode::Backspace,
1344 "UP" => KeyCode::Up,
1345 "DOWN" => KeyCode::Down,
1346 "LEFT" => KeyCode::Left,
1347 "RIGHT" => KeyCode::Right,
1348 "HOME" => KeyCode::Home,
1349 "END" => KeyCode::End,
1350 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1351 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1352 s if s.starts_with('F') && s.len() > 1 => {
1353 if let Ok(n) = s[1..].parse::<u8>() {
1355 KeyCode::F(n)
1356 } else {
1357 return None;
1358 }
1359 }
1360 _ if remaining.len() == 1 => {
1361 let c = remaining.chars().next()?;
1364 if c.is_ascii_uppercase() {
1365 modifiers |= KeyModifiers::SHIFT;
1366 }
1367 KeyCode::Char(c.to_ascii_lowercase())
1368 }
1369 _ => return None,
1370 };
1371
1372 Some((code, modifiers))
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use super::*;
1378 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1379 use tempfile::TempDir;
1380
1381 fn test_dir_context() -> (DirectoryContext, TempDir) {
1383 let temp_dir = TempDir::new().unwrap();
1384 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1385 (dir_context, temp_dir)
1386 }
1387
1388 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1390 Arc::new(crate::model::filesystem::StdFileSystem)
1391 }
1392
1393 #[test]
1394 fn test_editor_new() {
1395 let config = Config::default();
1396 let (dir_context, _temp) = test_dir_context();
1397 let editor = Editor::new(
1398 config,
1399 80,
1400 24,
1401 dir_context,
1402 crate::view::color_support::ColorCapability::TrueColor,
1403 test_filesystem(),
1404 )
1405 .unwrap();
1406
1407 assert_eq!(editor.buffers.len(), 1);
1408 assert!(!editor.should_quit());
1409 }
1410
1411 #[test]
1412 fn test_new_buffer() {
1413 let config = Config::default();
1414 let (dir_context, _temp) = test_dir_context();
1415 let mut editor = Editor::new(
1416 config,
1417 80,
1418 24,
1419 dir_context,
1420 crate::view::color_support::ColorCapability::TrueColor,
1421 test_filesystem(),
1422 )
1423 .unwrap();
1424
1425 let id = editor.new_buffer();
1426 assert_eq!(editor.buffers.len(), 2);
1427 assert_eq!(editor.active_buffer(), id);
1428 }
1429
1430 #[test]
1431 #[ignore]
1432 fn test_clipboard() {
1433 let config = Config::default();
1434 let (dir_context, _temp) = test_dir_context();
1435 let mut editor = Editor::new(
1436 config,
1437 80,
1438 24,
1439 dir_context,
1440 crate::view::color_support::ColorCapability::TrueColor,
1441 test_filesystem(),
1442 )
1443 .unwrap();
1444
1445 editor.clipboard.set_internal("test".to_string());
1447
1448 editor.paste();
1450
1451 let content = editor.active_state().buffer.to_string().unwrap();
1452 assert_eq!(content, "test");
1453 }
1454
1455 #[test]
1456 fn test_action_to_events_insert_char() {
1457 let config = Config::default();
1458 let (dir_context, _temp) = test_dir_context();
1459 let mut editor = Editor::new(
1460 config,
1461 80,
1462 24,
1463 dir_context,
1464 crate::view::color_support::ColorCapability::TrueColor,
1465 test_filesystem(),
1466 )
1467 .unwrap();
1468
1469 let events = editor.action_to_events(Action::InsertChar('a'));
1470 assert!(events.is_some());
1471
1472 let events = events.unwrap();
1473 assert_eq!(events.len(), 1);
1474
1475 match &events[0] {
1476 Event::Insert { position, text, .. } => {
1477 assert_eq!(*position, 0);
1478 assert_eq!(text, "a");
1479 }
1480 _ => panic!("Expected Insert event"),
1481 }
1482 }
1483
1484 #[test]
1485 fn test_action_to_events_move_right() {
1486 let config = Config::default();
1487 let (dir_context, _temp) = test_dir_context();
1488 let mut editor = Editor::new(
1489 config,
1490 80,
1491 24,
1492 dir_context,
1493 crate::view::color_support::ColorCapability::TrueColor,
1494 test_filesystem(),
1495 )
1496 .unwrap();
1497
1498 let cursor_id = editor.active_cursors().primary_id();
1500 editor.apply_event_to_active_buffer(&Event::Insert {
1501 position: 0,
1502 text: "hello".to_string(),
1503 cursor_id,
1504 });
1505
1506 let events = editor.action_to_events(Action::MoveRight);
1507 assert!(events.is_some());
1508
1509 let events = events.unwrap();
1510 assert_eq!(events.len(), 1);
1511
1512 match &events[0] {
1513 Event::MoveCursor {
1514 new_position,
1515 new_anchor,
1516 ..
1517 } => {
1518 assert_eq!(*new_position, 5);
1520 assert_eq!(*new_anchor, None); }
1522 _ => panic!("Expected MoveCursor event"),
1523 }
1524 }
1525
1526 #[test]
1527 fn test_action_to_events_move_up_down() {
1528 let config = Config::default();
1529 let (dir_context, _temp) = test_dir_context();
1530 let mut editor = Editor::new(
1531 config,
1532 80,
1533 24,
1534 dir_context,
1535 crate::view::color_support::ColorCapability::TrueColor,
1536 test_filesystem(),
1537 )
1538 .unwrap();
1539
1540 let cursor_id = editor.active_cursors().primary_id();
1542 editor.apply_event_to_active_buffer(&Event::Insert {
1543 position: 0,
1544 text: "line1\nline2\nline3".to_string(),
1545 cursor_id,
1546 });
1547
1548 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1550 cursor_id,
1551 old_position: 0, new_position: 6,
1553 old_anchor: None, new_anchor: None,
1555 old_sticky_column: 0,
1556 new_sticky_column: 0,
1557 });
1558
1559 let events = editor.action_to_events(Action::MoveUp);
1561 assert!(events.is_some());
1562 let events = events.unwrap();
1563 assert_eq!(events.len(), 1);
1564
1565 match &events[0] {
1566 Event::MoveCursor { new_position, .. } => {
1567 assert_eq!(*new_position, 0); }
1569 _ => panic!("Expected MoveCursor event"),
1570 }
1571 }
1572
1573 #[test]
1574 fn test_action_to_events_insert_newline() {
1575 let config = Config::default();
1576 let (dir_context, _temp) = test_dir_context();
1577 let mut editor = Editor::new(
1578 config,
1579 80,
1580 24,
1581 dir_context,
1582 crate::view::color_support::ColorCapability::TrueColor,
1583 test_filesystem(),
1584 )
1585 .unwrap();
1586
1587 let events = editor.action_to_events(Action::InsertNewline);
1588 assert!(events.is_some());
1589
1590 let events = events.unwrap();
1591 assert_eq!(events.len(), 1);
1592
1593 match &events[0] {
1594 Event::Insert { text, .. } => {
1595 assert_eq!(text, "\n");
1596 }
1597 _ => panic!("Expected Insert event"),
1598 }
1599 }
1600
1601 #[test]
1602 fn test_action_to_events_unimplemented() {
1603 let config = Config::default();
1604 let (dir_context, _temp) = test_dir_context();
1605 let mut editor = Editor::new(
1606 config,
1607 80,
1608 24,
1609 dir_context,
1610 crate::view::color_support::ColorCapability::TrueColor,
1611 test_filesystem(),
1612 )
1613 .unwrap();
1614
1615 assert!(editor.action_to_events(Action::Save).is_none());
1617 assert!(editor.action_to_events(Action::Quit).is_none());
1618 assert!(editor.action_to_events(Action::Undo).is_none());
1619 }
1620
1621 #[test]
1622 fn test_action_to_events_delete_backward() {
1623 let config = Config::default();
1624 let (dir_context, _temp) = test_dir_context();
1625 let mut editor = Editor::new(
1626 config,
1627 80,
1628 24,
1629 dir_context,
1630 crate::view::color_support::ColorCapability::TrueColor,
1631 test_filesystem(),
1632 )
1633 .unwrap();
1634
1635 let cursor_id = editor.active_cursors().primary_id();
1637 editor.apply_event_to_active_buffer(&Event::Insert {
1638 position: 0,
1639 text: "hello".to_string(),
1640 cursor_id,
1641 });
1642
1643 let events = editor.action_to_events(Action::DeleteBackward);
1644 assert!(events.is_some());
1645
1646 let events = events.unwrap();
1647 assert_eq!(events.len(), 1);
1648
1649 match &events[0] {
1650 Event::Delete {
1651 range,
1652 deleted_text,
1653 ..
1654 } => {
1655 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1657 }
1658 _ => panic!("Expected Delete event"),
1659 }
1660 }
1661
1662 #[test]
1663 fn test_action_to_events_delete_forward() {
1664 let config = Config::default();
1665 let (dir_context, _temp) = test_dir_context();
1666 let mut editor = Editor::new(
1667 config,
1668 80,
1669 24,
1670 dir_context,
1671 crate::view::color_support::ColorCapability::TrueColor,
1672 test_filesystem(),
1673 )
1674 .unwrap();
1675
1676 let cursor_id = editor.active_cursors().primary_id();
1678 editor.apply_event_to_active_buffer(&Event::Insert {
1679 position: 0,
1680 text: "hello".to_string(),
1681 cursor_id,
1682 });
1683
1684 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1686 cursor_id,
1687 old_position: 0, new_position: 0,
1689 old_anchor: None, new_anchor: None,
1691 old_sticky_column: 0,
1692 new_sticky_column: 0,
1693 });
1694
1695 let events = editor.action_to_events(Action::DeleteForward);
1696 assert!(events.is_some());
1697
1698 let events = events.unwrap();
1699 assert_eq!(events.len(), 1);
1700
1701 match &events[0] {
1702 Event::Delete {
1703 range,
1704 deleted_text,
1705 ..
1706 } => {
1707 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1709 }
1710 _ => panic!("Expected Delete event"),
1711 }
1712 }
1713
1714 #[test]
1715 fn test_action_to_events_select_right() {
1716 let config = Config::default();
1717 let (dir_context, _temp) = test_dir_context();
1718 let mut editor = Editor::new(
1719 config,
1720 80,
1721 24,
1722 dir_context,
1723 crate::view::color_support::ColorCapability::TrueColor,
1724 test_filesystem(),
1725 )
1726 .unwrap();
1727
1728 let cursor_id = editor.active_cursors().primary_id();
1730 editor.apply_event_to_active_buffer(&Event::Insert {
1731 position: 0,
1732 text: "hello".to_string(),
1733 cursor_id,
1734 });
1735
1736 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1738 cursor_id,
1739 old_position: 0, new_position: 0,
1741 old_anchor: None, new_anchor: None,
1743 old_sticky_column: 0,
1744 new_sticky_column: 0,
1745 });
1746
1747 let events = editor.action_to_events(Action::SelectRight);
1748 assert!(events.is_some());
1749
1750 let events = events.unwrap();
1751 assert_eq!(events.len(), 1);
1752
1753 match &events[0] {
1754 Event::MoveCursor {
1755 new_position,
1756 new_anchor,
1757 ..
1758 } => {
1759 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1762 _ => panic!("Expected MoveCursor event"),
1763 }
1764 }
1765
1766 #[test]
1767 fn test_action_to_events_select_all() {
1768 let config = Config::default();
1769 let (dir_context, _temp) = test_dir_context();
1770 let mut editor = Editor::new(
1771 config,
1772 80,
1773 24,
1774 dir_context,
1775 crate::view::color_support::ColorCapability::TrueColor,
1776 test_filesystem(),
1777 )
1778 .unwrap();
1779
1780 let cursor_id = editor.active_cursors().primary_id();
1782 editor.apply_event_to_active_buffer(&Event::Insert {
1783 position: 0,
1784 text: "hello world".to_string(),
1785 cursor_id,
1786 });
1787
1788 let events = editor.action_to_events(Action::SelectAll);
1789 assert!(events.is_some());
1790
1791 let events = events.unwrap();
1792 assert_eq!(events.len(), 1);
1793
1794 match &events[0] {
1795 Event::MoveCursor {
1796 new_position,
1797 new_anchor,
1798 ..
1799 } => {
1800 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1803 _ => panic!("Expected MoveCursor event"),
1804 }
1805 }
1806
1807 #[test]
1808 fn test_action_to_events_document_nav() {
1809 let config = Config::default();
1810 let (dir_context, _temp) = test_dir_context();
1811 let mut editor = Editor::new(
1812 config,
1813 80,
1814 24,
1815 dir_context,
1816 crate::view::color_support::ColorCapability::TrueColor,
1817 test_filesystem(),
1818 )
1819 .unwrap();
1820
1821 let cursor_id = editor.active_cursors().primary_id();
1823 editor.apply_event_to_active_buffer(&Event::Insert {
1824 position: 0,
1825 text: "line1\nline2\nline3".to_string(),
1826 cursor_id,
1827 });
1828
1829 let events = editor.action_to_events(Action::MoveDocumentStart);
1831 assert!(events.is_some());
1832 let events = events.unwrap();
1833 match &events[0] {
1834 Event::MoveCursor { new_position, .. } => {
1835 assert_eq!(*new_position, 0);
1836 }
1837 _ => panic!("Expected MoveCursor event"),
1838 }
1839
1840 let events = editor.action_to_events(Action::MoveDocumentEnd);
1842 assert!(events.is_some());
1843 let events = events.unwrap();
1844 match &events[0] {
1845 Event::MoveCursor { new_position, .. } => {
1846 assert_eq!(*new_position, 17); }
1848 _ => panic!("Expected MoveCursor event"),
1849 }
1850 }
1851
1852 #[test]
1853 fn test_action_to_events_remove_secondary_cursors() {
1854 use crate::model::event::CursorId;
1855
1856 let config = Config::default();
1857 let (dir_context, _temp) = test_dir_context();
1858 let mut editor = Editor::new(
1859 config,
1860 80,
1861 24,
1862 dir_context,
1863 crate::view::color_support::ColorCapability::TrueColor,
1864 test_filesystem(),
1865 )
1866 .unwrap();
1867
1868 let cursor_id = editor.active_cursors().primary_id();
1870 editor.apply_event_to_active_buffer(&Event::Insert {
1871 position: 0,
1872 text: "hello world test".to_string(),
1873 cursor_id,
1874 });
1875
1876 editor.apply_event_to_active_buffer(&Event::AddCursor {
1878 cursor_id: CursorId(1),
1879 position: 5,
1880 anchor: None,
1881 });
1882 editor.apply_event_to_active_buffer(&Event::AddCursor {
1883 cursor_id: CursorId(2),
1884 position: 10,
1885 anchor: None,
1886 });
1887
1888 assert_eq!(editor.active_cursors().count(), 3);
1889
1890 let first_id = editor
1892 .active_cursors()
1893 .iter()
1894 .map(|(id, _)| id)
1895 .min_by_key(|id| id.0)
1896 .expect("Should have at least one cursor");
1897
1898 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
1900 assert!(events.is_some());
1901
1902 let events = events.unwrap();
1903 let remove_cursor_events: Vec<_> = events
1906 .iter()
1907 .filter_map(|e| match e {
1908 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1909 _ => None,
1910 })
1911 .collect();
1912
1913 assert_eq!(remove_cursor_events.len(), 2);
1915
1916 for cursor_id in &remove_cursor_events {
1917 assert_ne!(*cursor_id, first_id);
1919 }
1920 }
1921
1922 #[test]
1923 fn test_action_to_events_scroll() {
1924 let config = Config::default();
1925 let (dir_context, _temp) = test_dir_context();
1926 let mut editor = Editor::new(
1927 config,
1928 80,
1929 24,
1930 dir_context,
1931 crate::view::color_support::ColorCapability::TrueColor,
1932 test_filesystem(),
1933 )
1934 .unwrap();
1935
1936 let events = editor.action_to_events(Action::ScrollUp);
1938 assert!(events.is_some());
1939 let events = events.unwrap();
1940 assert_eq!(events.len(), 1);
1941 match &events[0] {
1942 Event::Scroll { line_offset } => {
1943 assert_eq!(*line_offset, -1);
1944 }
1945 _ => panic!("Expected Scroll event"),
1946 }
1947
1948 let events = editor.action_to_events(Action::ScrollDown);
1950 assert!(events.is_some());
1951 let events = events.unwrap();
1952 assert_eq!(events.len(), 1);
1953 match &events[0] {
1954 Event::Scroll { line_offset } => {
1955 assert_eq!(*line_offset, 1);
1956 }
1957 _ => panic!("Expected Scroll event"),
1958 }
1959 }
1960
1961 #[test]
1962 fn test_action_to_events_none() {
1963 let config = Config::default();
1964 let (dir_context, _temp) = test_dir_context();
1965 let mut editor = Editor::new(
1966 config,
1967 80,
1968 24,
1969 dir_context,
1970 crate::view::color_support::ColorCapability::TrueColor,
1971 test_filesystem(),
1972 )
1973 .unwrap();
1974
1975 let events = editor.action_to_events(Action::None);
1977 assert!(events.is_none());
1978 }
1979
1980 #[test]
1981 fn test_lsp_incremental_insert_generates_correct_range() {
1982 use crate::model::buffer::Buffer;
1985
1986 let buffer = Buffer::from_str_test("hello\nworld");
1987
1988 let position = 0;
1991 let (line, character) = buffer.position_to_lsp_position(position);
1992
1993 assert_eq!(line, 0, "Insertion at start should be line 0");
1994 assert_eq!(character, 0, "Insertion at start should be char 0");
1995
1996 let lsp_pos = Position::new(line as u32, character as u32);
1998 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
1999
2000 assert_eq!(lsp_range.start.line, 0);
2001 assert_eq!(lsp_range.start.character, 0);
2002 assert_eq!(lsp_range.end.line, 0);
2003 assert_eq!(lsp_range.end.character, 0);
2004 assert_eq!(
2005 lsp_range.start, lsp_range.end,
2006 "Insert should have zero-width range"
2007 );
2008
2009 let position = 3;
2011 let (line, character) = buffer.position_to_lsp_position(position);
2012
2013 assert_eq!(line, 0);
2014 assert_eq!(character, 3);
2015
2016 let position = 6;
2018 let (line, character) = buffer.position_to_lsp_position(position);
2019
2020 assert_eq!(line, 1, "Position after newline should be line 1");
2021 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2022 }
2023
2024 #[test]
2025 fn test_lsp_incremental_delete_generates_correct_range() {
2026 use crate::model::buffer::Buffer;
2029
2030 let buffer = Buffer::from_str_test("hello\nworld");
2031
2032 let range_start = 1;
2034 let range_end = 5;
2035
2036 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2037 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2038
2039 assert_eq!(start_line, 0);
2040 assert_eq!(start_char, 1);
2041 assert_eq!(end_line, 0);
2042 assert_eq!(end_char, 5);
2043
2044 let lsp_range = LspRange::new(
2045 Position::new(start_line as u32, start_char as u32),
2046 Position::new(end_line as u32, end_char as u32),
2047 );
2048
2049 assert_eq!(lsp_range.start.line, 0);
2050 assert_eq!(lsp_range.start.character, 1);
2051 assert_eq!(lsp_range.end.line, 0);
2052 assert_eq!(lsp_range.end.character, 5);
2053 assert_ne!(
2054 lsp_range.start, lsp_range.end,
2055 "Delete should have non-zero range"
2056 );
2057
2058 let range_start = 4;
2060 let range_end = 8;
2061
2062 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2063 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2064
2065 assert_eq!(start_line, 0, "Delete start on line 0");
2066 assert_eq!(start_char, 4, "Delete start at char 4");
2067 assert_eq!(end_line, 1, "Delete end on line 1");
2068 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2069 }
2070
2071 #[test]
2072 fn test_lsp_incremental_utf16_encoding() {
2073 use crate::model::buffer::Buffer;
2076
2077 let buffer = Buffer::from_str_test("😀hello");
2079
2080 let (line, character) = buffer.position_to_lsp_position(4);
2082
2083 assert_eq!(line, 0);
2084 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2085
2086 let (line, character) = buffer.position_to_lsp_position(9);
2088
2089 assert_eq!(line, 0);
2090 assert_eq!(
2091 character, 7,
2092 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2093 );
2094
2095 let buffer = Buffer::from_str_test("café");
2097
2098 let (line, character) = buffer.position_to_lsp_position(3);
2100
2101 assert_eq!(line, 0);
2102 assert_eq!(character, 3);
2103
2104 let (line, character) = buffer.position_to_lsp_position(5);
2106
2107 assert_eq!(line, 0);
2108 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2109 }
2110
2111 #[test]
2112 fn test_lsp_content_change_event_structure() {
2113 let insert_change = TextDocumentContentChangeEvent {
2117 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2118 range_length: None,
2119 text: "NEW".to_string(),
2120 };
2121
2122 assert!(insert_change.range.is_some());
2123 assert_eq!(insert_change.text, "NEW");
2124 let range = insert_change.range.unwrap();
2125 assert_eq!(
2126 range.start, range.end,
2127 "Insert should have zero-width range"
2128 );
2129
2130 let delete_change = TextDocumentContentChangeEvent {
2132 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2133 range_length: None,
2134 text: String::new(),
2135 };
2136
2137 assert!(delete_change.range.is_some());
2138 assert_eq!(delete_change.text, "");
2139 let range = delete_change.range.unwrap();
2140 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2141 assert_eq!(range.start.line, 0);
2142 assert_eq!(range.start.character, 2);
2143 assert_eq!(range.end.line, 0);
2144 assert_eq!(range.end.character, 7);
2145 }
2146
2147 #[test]
2148 fn test_goto_matching_bracket_forward() {
2149 let config = Config::default();
2150 let (dir_context, _temp) = test_dir_context();
2151 let mut editor = Editor::new(
2152 config,
2153 80,
2154 24,
2155 dir_context,
2156 crate::view::color_support::ColorCapability::TrueColor,
2157 test_filesystem(),
2158 )
2159 .unwrap();
2160
2161 let cursor_id = editor.active_cursors().primary_id();
2163 editor.apply_event_to_active_buffer(&Event::Insert {
2164 position: 0,
2165 text: "fn main() { let x = (1 + 2); }".to_string(),
2166 cursor_id,
2167 });
2168
2169 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2171 cursor_id,
2172 old_position: 31,
2173 new_position: 10,
2174 old_anchor: None,
2175 new_anchor: None,
2176 old_sticky_column: 0,
2177 new_sticky_column: 0,
2178 });
2179
2180 assert_eq!(editor.active_cursors().primary().position, 10);
2181
2182 editor.goto_matching_bracket();
2184
2185 assert_eq!(editor.active_cursors().primary().position, 29);
2190 }
2191
2192 #[test]
2193 fn test_goto_matching_bracket_backward() {
2194 let config = Config::default();
2195 let (dir_context, _temp) = test_dir_context();
2196 let mut editor = Editor::new(
2197 config,
2198 80,
2199 24,
2200 dir_context,
2201 crate::view::color_support::ColorCapability::TrueColor,
2202 test_filesystem(),
2203 )
2204 .unwrap();
2205
2206 let cursor_id = editor.active_cursors().primary_id();
2208 editor.apply_event_to_active_buffer(&Event::Insert {
2209 position: 0,
2210 text: "fn main() { let x = (1 + 2); }".to_string(),
2211 cursor_id,
2212 });
2213
2214 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2216 cursor_id,
2217 old_position: 31,
2218 new_position: 26,
2219 old_anchor: None,
2220 new_anchor: None,
2221 old_sticky_column: 0,
2222 new_sticky_column: 0,
2223 });
2224
2225 editor.goto_matching_bracket();
2227
2228 assert_eq!(editor.active_cursors().primary().position, 20);
2230 }
2231
2232 #[test]
2233 fn test_goto_matching_bracket_nested() {
2234 let config = Config::default();
2235 let (dir_context, _temp) = test_dir_context();
2236 let mut editor = Editor::new(
2237 config,
2238 80,
2239 24,
2240 dir_context,
2241 crate::view::color_support::ColorCapability::TrueColor,
2242 test_filesystem(),
2243 )
2244 .unwrap();
2245
2246 let cursor_id = editor.active_cursors().primary_id();
2248 editor.apply_event_to_active_buffer(&Event::Insert {
2249 position: 0,
2250 text: "{a{b{c}d}e}".to_string(),
2251 cursor_id,
2252 });
2253
2254 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2256 cursor_id,
2257 old_position: 11,
2258 new_position: 0,
2259 old_anchor: None,
2260 new_anchor: None,
2261 old_sticky_column: 0,
2262 new_sticky_column: 0,
2263 });
2264
2265 editor.goto_matching_bracket();
2267
2268 assert_eq!(editor.active_cursors().primary().position, 10);
2270 }
2271
2272 #[test]
2273 fn test_search_case_sensitive() {
2274 let config = Config::default();
2275 let (dir_context, _temp) = test_dir_context();
2276 let mut editor = Editor::new(
2277 config,
2278 80,
2279 24,
2280 dir_context,
2281 crate::view::color_support::ColorCapability::TrueColor,
2282 test_filesystem(),
2283 )
2284 .unwrap();
2285
2286 let cursor_id = editor.active_cursors().primary_id();
2288 editor.apply_event_to_active_buffer(&Event::Insert {
2289 position: 0,
2290 text: "Hello hello HELLO".to_string(),
2291 cursor_id,
2292 });
2293
2294 editor.search_case_sensitive = false;
2296 editor.perform_search("hello");
2297
2298 let search_state = editor.search_state.as_ref().unwrap();
2299 assert_eq!(
2300 search_state.matches.len(),
2301 3,
2302 "Should find all 3 matches case-insensitively"
2303 );
2304
2305 editor.search_case_sensitive = true;
2307 editor.perform_search("hello");
2308
2309 let search_state = editor.search_state.as_ref().unwrap();
2310 assert_eq!(
2311 search_state.matches.len(),
2312 1,
2313 "Should find only 1 exact match"
2314 );
2315 assert_eq!(
2316 search_state.matches[0], 6,
2317 "Should find 'hello' at position 6"
2318 );
2319 }
2320
2321 #[test]
2322 fn test_search_whole_word() {
2323 let config = Config::default();
2324 let (dir_context, _temp) = test_dir_context();
2325 let mut editor = Editor::new(
2326 config,
2327 80,
2328 24,
2329 dir_context,
2330 crate::view::color_support::ColorCapability::TrueColor,
2331 test_filesystem(),
2332 )
2333 .unwrap();
2334
2335 let cursor_id = editor.active_cursors().primary_id();
2337 editor.apply_event_to_active_buffer(&Event::Insert {
2338 position: 0,
2339 text: "test testing tested attest test".to_string(),
2340 cursor_id,
2341 });
2342
2343 editor.search_whole_word = false;
2345 editor.search_case_sensitive = true;
2346 editor.perform_search("test");
2347
2348 let search_state = editor.search_state.as_ref().unwrap();
2349 assert_eq!(
2350 search_state.matches.len(),
2351 5,
2352 "Should find 'test' in all occurrences"
2353 );
2354
2355 editor.search_whole_word = true;
2357 editor.perform_search("test");
2358
2359 let search_state = editor.search_state.as_ref().unwrap();
2360 assert_eq!(
2361 search_state.matches.len(),
2362 2,
2363 "Should find only whole word 'test'"
2364 );
2365 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2366 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2367 }
2368
2369 #[test]
2370 fn test_search_scan_completes_when_capped() {
2371 let config = Config::default();
2377 let (dir_context, _temp) = test_dir_context();
2378 let mut editor = Editor::new(
2379 config,
2380 80,
2381 24,
2382 dir_context,
2383 crate::view::color_support::ColorCapability::TrueColor,
2384 test_filesystem(),
2385 )
2386 .unwrap();
2387
2388 let buffer_id = editor.active_buffer();
2391 let regex = regex::bytes::Regex::new("test").unwrap();
2392 let fake_chunks = vec![
2393 crate::model::buffer::LineScanChunk {
2394 leaf_index: 0,
2395 byte_len: 100,
2396 already_known: true,
2397 },
2398 crate::model::buffer::LineScanChunk {
2399 leaf_index: 1,
2400 byte_len: 100,
2401 already_known: true,
2402 },
2403 ];
2404
2405 let chunked = crate::model::buffer::ChunkedSearchState {
2406 chunks: fake_chunks,
2407 next_chunk: 1, next_doc_offset: 100,
2409 total_bytes: 200,
2410 scanned_bytes: 100,
2411 regex,
2412 matches: vec![
2413 crate::model::buffer::SearchMatch {
2414 byte_offset: 10,
2415 length: 4,
2416 line: 1,
2417 column: 11,
2418 context: String::new(),
2419 },
2420 crate::model::buffer::SearchMatch {
2421 byte_offset: 50,
2422 length: 4,
2423 line: 1,
2424 column: 51,
2425 context: String::new(),
2426 },
2427 ],
2428 overlap_tail: Vec::new(),
2429 overlap_doc_offset: 0,
2430 max_matches: 10_000,
2431 capped: true, query_len: 4,
2433 running_line: 1,
2434 };
2435
2436 editor.search_scan.start(
2437 buffer_id,
2438 Vec::new(),
2439 chunked,
2440 "test".to_string(),
2441 None,
2442 false,
2443 false,
2444 false,
2445 );
2446
2447 let result = editor.process_search_scan();
2449 assert!(
2450 result,
2451 "process_search_scan should return true (needs render)"
2452 );
2453
2454 assert_eq!(
2456 editor.search_scan.buffer_id(),
2457 None,
2458 "search_scan should be drained after capped scan completes"
2459 );
2460
2461 let search_state = editor
2463 .search_state
2464 .as_ref()
2465 .expect("search_state should be set after scan finishes");
2466 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2467 assert_eq!(search_state.query, "test");
2468 assert!(
2469 search_state.capped,
2470 "search_state should be marked as capped"
2471 );
2472 }
2473
2474 #[test]
2475 fn test_bookmarks() {
2476 let config = Config::default();
2477 let (dir_context, _temp) = test_dir_context();
2478 let mut editor = Editor::new(
2479 config,
2480 80,
2481 24,
2482 dir_context,
2483 crate::view::color_support::ColorCapability::TrueColor,
2484 test_filesystem(),
2485 )
2486 .unwrap();
2487
2488 let cursor_id = editor.active_cursors().primary_id();
2490 editor.apply_event_to_active_buffer(&Event::Insert {
2491 position: 0,
2492 text: "Line 1\nLine 2\nLine 3".to_string(),
2493 cursor_id,
2494 });
2495
2496 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2498 cursor_id,
2499 old_position: 21,
2500 new_position: 7,
2501 old_anchor: None,
2502 new_anchor: None,
2503 old_sticky_column: 0,
2504 new_sticky_column: 0,
2505 });
2506
2507 editor.set_bookmark('1');
2509 assert_eq!(editor.bookmarks.get('1').map(|b| b.position), Some(7));
2510
2511 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2513 cursor_id,
2514 old_position: 7,
2515 new_position: 14,
2516 old_anchor: None,
2517 new_anchor: None,
2518 old_sticky_column: 0,
2519 new_sticky_column: 0,
2520 });
2521
2522 editor.jump_to_bookmark('1');
2524 assert_eq!(editor.active_cursors().primary().position, 7);
2525
2526 editor.clear_bookmark('1');
2528 assert_eq!(editor.bookmarks.get('1'), None);
2529 }
2530
2531 #[test]
2532 fn test_action_enum_new_variants() {
2533 use serde_json::json;
2535
2536 let args = HashMap::new();
2537 assert_eq!(
2538 Action::from_str("smart_home", &args),
2539 Some(Action::SmartHome)
2540 );
2541 assert_eq!(
2542 Action::from_str("dedent_selection", &args),
2543 Some(Action::DedentSelection)
2544 );
2545 assert_eq!(
2546 Action::from_str("toggle_comment", &args),
2547 Some(Action::ToggleComment)
2548 );
2549 assert_eq!(
2550 Action::from_str("goto_matching_bracket", &args),
2551 Some(Action::GoToMatchingBracket)
2552 );
2553 assert_eq!(
2554 Action::from_str("list_bookmarks", &args),
2555 Some(Action::ListBookmarks)
2556 );
2557 assert_eq!(
2558 Action::from_str("toggle_search_case_sensitive", &args),
2559 Some(Action::ToggleSearchCaseSensitive)
2560 );
2561 assert_eq!(
2562 Action::from_str("toggle_search_whole_word", &args),
2563 Some(Action::ToggleSearchWholeWord)
2564 );
2565
2566 let mut args_with_char = HashMap::new();
2568 args_with_char.insert("char".to_string(), json!("5"));
2569 assert_eq!(
2570 Action::from_str("set_bookmark", &args_with_char),
2571 Some(Action::SetBookmark('5'))
2572 );
2573 assert_eq!(
2574 Action::from_str("jump_to_bookmark", &args_with_char),
2575 Some(Action::JumpToBookmark('5'))
2576 );
2577 assert_eq!(
2578 Action::from_str("clear_bookmark", &args_with_char),
2579 Some(Action::ClearBookmark('5'))
2580 );
2581 }
2582
2583 #[test]
2584 fn test_keybinding_new_defaults() {
2585 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2586
2587 let mut config = Config::default();
2591 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2592 let resolver = KeybindingResolver::new(&config);
2593
2594 let event = KeyEvent {
2596 code: KeyCode::Char('/'),
2597 modifiers: KeyModifiers::CONTROL,
2598 kind: KeyEventKind::Press,
2599 state: KeyEventState::NONE,
2600 };
2601 let action = resolver.resolve(&event, KeyContext::Normal);
2602 assert_eq!(action, Action::ToggleComment);
2603
2604 let event = KeyEvent {
2606 code: KeyCode::Char(']'),
2607 modifiers: KeyModifiers::CONTROL,
2608 kind: KeyEventKind::Press,
2609 state: KeyEventState::NONE,
2610 };
2611 let action = resolver.resolve(&event, KeyContext::Normal);
2612 assert_eq!(action, Action::GoToMatchingBracket);
2613
2614 let event = KeyEvent {
2616 code: KeyCode::Tab,
2617 modifiers: KeyModifiers::SHIFT,
2618 kind: KeyEventKind::Press,
2619 state: KeyEventState::NONE,
2620 };
2621 let action = resolver.resolve(&event, KeyContext::Normal);
2622 assert_eq!(action, Action::DedentSelection);
2623
2624 let event = KeyEvent {
2626 code: KeyCode::Char('g'),
2627 modifiers: KeyModifiers::CONTROL,
2628 kind: KeyEventKind::Press,
2629 state: KeyEventState::NONE,
2630 };
2631 let action = resolver.resolve(&event, KeyContext::Normal);
2632 assert_eq!(action, Action::GotoLine);
2633
2634 let event = KeyEvent {
2636 code: KeyCode::Char('5'),
2637 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2638 kind: KeyEventKind::Press,
2639 state: KeyEventState::NONE,
2640 };
2641 let action = resolver.resolve(&event, KeyContext::Normal);
2642 assert_eq!(action, Action::SetBookmark('5'));
2643
2644 let event = KeyEvent {
2645 code: KeyCode::Char('5'),
2646 modifiers: KeyModifiers::ALT,
2647 kind: KeyEventKind::Press,
2648 state: KeyEventState::NONE,
2649 };
2650 let action = resolver.resolve(&event, KeyContext::Normal);
2651 assert_eq!(action, Action::JumpToBookmark('5'));
2652 }
2653
2654 #[test]
2666 fn test_lsp_rename_didchange_positions_bug() {
2667 use crate::model::buffer::Buffer;
2668
2669 let config = Config::default();
2670 let (dir_context, _temp) = test_dir_context();
2671 let mut editor = Editor::new(
2672 config,
2673 80,
2674 24,
2675 dir_context,
2676 crate::view::color_support::ColorCapability::TrueColor,
2677 test_filesystem(),
2678 )
2679 .unwrap();
2680
2681 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2685 editor.active_state_mut().buffer =
2686 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2687
2688 let cursor_id = editor.active_cursors().primary_id();
2693
2694 let batch = Event::Batch {
2695 events: vec![
2696 Event::Delete {
2698 range: 23..26, deleted_text: "val".to_string(),
2700 cursor_id,
2701 },
2702 Event::Insert {
2703 position: 23,
2704 text: "value".to_string(),
2705 cursor_id,
2706 },
2707 Event::Delete {
2709 range: 7..10, deleted_text: "val".to_string(),
2711 cursor_id,
2712 },
2713 Event::Insert {
2714 position: 7,
2715 text: "value".to_string(),
2716 cursor_id,
2717 },
2718 ],
2719 description: "LSP Rename".to_string(),
2720 };
2721
2722 let lsp_changes_before = editor.collect_lsp_changes(&batch);
2724
2725 editor.apply_event_to_active_buffer(&batch);
2727
2728 let lsp_changes_after = editor.collect_lsp_changes(&batch);
2731
2732 let final_content = editor.active_state().buffer.to_string().unwrap();
2734 assert_eq!(
2735 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2736 "Buffer should have 'value' in both places"
2737 );
2738
2739 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2745
2746 let first_delete = &lsp_changes_before[0];
2747 let first_del_range = first_delete.range.unwrap();
2748 assert_eq!(
2749 first_del_range.start.line, 1,
2750 "First delete should be on line 1 (BEFORE)"
2751 );
2752 assert_eq!(
2753 first_del_range.start.character, 4,
2754 "First delete start should be at char 4 (BEFORE)"
2755 );
2756
2757 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2763
2764 let first_delete_after = &lsp_changes_after[0];
2765 let first_del_range_after = first_delete_after.range.unwrap();
2766
2767 eprintln!("BEFORE modification:");
2770 eprintln!(
2771 " Delete at line {}, char {}-{}",
2772 first_del_range.start.line,
2773 first_del_range.start.character,
2774 first_del_range.end.character
2775 );
2776 eprintln!("AFTER modification:");
2777 eprintln!(
2778 " Delete at line {}, char {}-{}",
2779 first_del_range_after.start.line,
2780 first_del_range_after.start.character,
2781 first_del_range_after.end.character
2782 );
2783
2784 assert_ne!(
2802 first_del_range_after.end.character, first_del_range.end.character,
2803 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2804 );
2805
2806 eprintln!("\n=== BUG DEMONSTRATED ===");
2807 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2808 eprintln!("the positions are WRONG because they're calculated from the");
2809 eprintln!("modified buffer, not the original buffer.");
2810 eprintln!("This causes the second rename to fail with 'content modified' error.");
2811 eprintln!("========================\n");
2812 }
2813
2814 #[test]
2815 fn test_lsp_rename_preserves_cursor_position() {
2816 use crate::model::buffer::Buffer;
2817
2818 let config = Config::default();
2819 let (dir_context, _temp) = test_dir_context();
2820 let mut editor = Editor::new(
2821 config,
2822 80,
2823 24,
2824 dir_context,
2825 crate::view::color_support::ColorCapability::TrueColor,
2826 test_filesystem(),
2827 )
2828 .unwrap();
2829
2830 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2834 editor.active_state_mut().buffer =
2835 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2836
2837 let original_cursor_pos = 23;
2839 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2840
2841 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2843 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2844 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2845
2846 let cursor_id = editor.active_cursors().primary_id();
2849 let buffer_id = editor.active_buffer();
2850
2851 let events = vec![
2852 Event::Delete {
2854 range: 23..26, deleted_text: "val".to_string(),
2856 cursor_id,
2857 },
2858 Event::Insert {
2859 position: 23,
2860 text: "value".to_string(),
2861 cursor_id,
2862 },
2863 Event::Delete {
2865 range: 7..10, deleted_text: "val".to_string(),
2867 cursor_id,
2868 },
2869 Event::Insert {
2870 position: 7,
2871 text: "value".to_string(),
2872 cursor_id,
2873 },
2874 ];
2875
2876 editor
2878 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2879 .unwrap();
2880
2881 let final_content = editor.active_state().buffer.to_string().unwrap();
2883 assert_eq!(
2884 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2885 "Buffer should have 'value' in both places"
2886 );
2887
2888 let final_cursor_pos = editor.active_cursors().primary().position;
2896 let expected_cursor_pos = 25; assert_eq!(
2899 final_cursor_pos, expected_cursor_pos,
2900 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2901 Original pos: {}, expected adjustment: +2 for first rename",
2902 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2903 );
2904
2905 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2907 assert_eq!(
2908 text_at_new_cursor, "value",
2909 "Cursor should be at the start of 'value' after rename"
2910 );
2911 }
2912
2913 #[test]
2914 fn test_lsp_rename_twice_consecutive() {
2915 use crate::model::buffer::Buffer;
2918
2919 let config = Config::default();
2920 let (dir_context, _temp) = test_dir_context();
2921 let mut editor = Editor::new(
2922 config,
2923 80,
2924 24,
2925 dir_context,
2926 crate::view::color_support::ColorCapability::TrueColor,
2927 test_filesystem(),
2928 )
2929 .unwrap();
2930
2931 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2933 editor.active_state_mut().buffer =
2934 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2935
2936 let cursor_id = editor.active_cursors().primary_id();
2937 let buffer_id = editor.active_buffer();
2938
2939 let events1 = vec![
2942 Event::Delete {
2944 range: 23..26,
2945 deleted_text: "val".to_string(),
2946 cursor_id,
2947 },
2948 Event::Insert {
2949 position: 23,
2950 text: "value".to_string(),
2951 cursor_id,
2952 },
2953 Event::Delete {
2955 range: 7..10,
2956 deleted_text: "val".to_string(),
2957 cursor_id,
2958 },
2959 Event::Insert {
2960 position: 7,
2961 text: "value".to_string(),
2962 cursor_id,
2963 },
2964 ];
2965
2966 let batch1 = Event::Batch {
2968 events: events1.clone(),
2969 description: "LSP Rename 1".to_string(),
2970 };
2971
2972 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
2974
2975 assert_eq!(
2977 lsp_changes1.len(),
2978 4,
2979 "First rename should have 4 LSP changes"
2980 );
2981
2982 let first_del = &lsp_changes1[0];
2984 let first_del_range = first_del.range.unwrap();
2985 assert_eq!(first_del_range.start.line, 1, "First delete line");
2986 assert_eq!(
2987 first_del_range.start.character, 4,
2988 "First delete start char"
2989 );
2990 assert_eq!(first_del_range.end.character, 7, "First delete end char");
2991
2992 editor
2994 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
2995 .unwrap();
2996
2997 let after_first = editor.active_state().buffer.to_string().unwrap();
2999 assert_eq!(
3000 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3001 "After first rename"
3002 );
3003
3004 let events2 = vec![
3014 Event::Delete {
3016 range: 25..30,
3017 deleted_text: "value".to_string(),
3018 cursor_id,
3019 },
3020 Event::Insert {
3021 position: 25,
3022 text: "x".to_string(),
3023 cursor_id,
3024 },
3025 Event::Delete {
3027 range: 7..12,
3028 deleted_text: "value".to_string(),
3029 cursor_id,
3030 },
3031 Event::Insert {
3032 position: 7,
3033 text: "x".to_string(),
3034 cursor_id,
3035 },
3036 ];
3037
3038 let batch2 = Event::Batch {
3040 events: events2.clone(),
3041 description: "LSP Rename 2".to_string(),
3042 };
3043
3044 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
3046
3047 assert_eq!(
3051 lsp_changes2.len(),
3052 4,
3053 "Second rename should have 4 LSP changes"
3054 );
3055
3056 let second_first_del = &lsp_changes2[0];
3058 let second_first_del_range = second_first_del.range.unwrap();
3059 assert_eq!(
3060 second_first_del_range.start.line, 1,
3061 "Second rename first delete should be on line 1"
3062 );
3063 assert_eq!(
3064 second_first_del_range.start.character, 4,
3065 "Second rename first delete start should be at char 4"
3066 );
3067 assert_eq!(
3068 second_first_del_range.end.character, 9,
3069 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3070 );
3071
3072 let second_third_del = &lsp_changes2[2];
3074 let second_third_del_range = second_third_del.range.unwrap();
3075 assert_eq!(
3076 second_third_del_range.start.line, 0,
3077 "Second rename third delete should be on line 0"
3078 );
3079 assert_eq!(
3080 second_third_del_range.start.character, 7,
3081 "Second rename third delete start should be at char 7"
3082 );
3083 assert_eq!(
3084 second_third_del_range.end.character, 12,
3085 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3086 );
3087
3088 editor
3090 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3091 .unwrap();
3092
3093 let after_second = editor.active_state().buffer.to_string().unwrap();
3095 assert_eq!(
3096 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3097 "After second rename"
3098 );
3099 }
3100
3101 #[test]
3102 fn test_ensure_active_tab_visible_static_offset() {
3103 let config = Config::default();
3104 let (dir_context, _temp) = test_dir_context();
3105 let mut editor = Editor::new(
3106 config,
3107 80,
3108 24,
3109 dir_context,
3110 crate::view::color_support::ColorCapability::TrueColor,
3111 test_filesystem(),
3112 )
3113 .unwrap();
3114 let split_id = editor.split_manager.active_split();
3115
3116 let buf1 = editor.new_buffer();
3118 editor
3119 .buffers
3120 .get_mut(&buf1)
3121 .unwrap()
3122 .buffer
3123 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3124 let buf2 = editor.new_buffer();
3125 editor
3126 .buffers
3127 .get_mut(&buf2)
3128 .unwrap()
3129 .buffer
3130 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3131 let buf3 = editor.new_buffer();
3132 editor
3133 .buffers
3134 .get_mut(&buf3)
3135 .unwrap()
3136 .buffer
3137 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3138
3139 {
3140 use crate::view::split::TabTarget;
3141 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
3142 view_state.open_buffers = vec![
3143 TabTarget::Buffer(buf1),
3144 TabTarget::Buffer(buf2),
3145 TabTarget::Buffer(buf3),
3146 ];
3147 view_state.tab_scroll_offset = 50;
3148 }
3149
3150 editor.ensure_active_tab_visible(split_id, buf1, 25);
3154 assert_eq!(
3155 editor
3156 .split_view_states
3157 .get(&split_id)
3158 .unwrap()
3159 .tab_scroll_offset,
3160 0
3161 );
3162
3163 editor.ensure_active_tab_visible(split_id, buf3, 25);
3165 let view_state = editor.split_view_states.get(&split_id).unwrap();
3166 assert!(view_state.tab_scroll_offset > 0);
3167 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3168 let total_width: usize = buffer_ids
3169 .iter()
3170 .enumerate()
3171 .map(|(idx, id)| {
3172 let state = editor.buffers.get(id).unwrap();
3173 let name_len = state
3174 .buffer
3175 .file_path()
3176 .and_then(|p| p.file_name())
3177 .and_then(|n| n.to_str())
3178 .map(|s| s.chars().count())
3179 .unwrap_or(0);
3180 let tab_width = 2 + name_len;
3181 if idx < buffer_ids.len() - 1 {
3182 tab_width + 1 } else {
3184 tab_width
3185 }
3186 })
3187 .sum();
3188 assert!(view_state.tab_scroll_offset <= total_width);
3189 }
3190}