1mod action_events;
2mod active_focus;
3mod async_dispatch;
4mod async_messages;
5mod bookmark_actions;
6mod bookmarks;
7mod buffer_close;
8mod buffer_config_resolve;
9mod buffer_groups;
10mod buffer_management;
11mod calibration_actions;
12pub mod calibration_wizard;
13mod click_geometry;
14mod click_handlers;
15mod clipboard;
16mod composite_buffer_actions;
17mod dabbrev_actions;
18mod diagnostic_jumps;
19mod editor_accessors;
20mod editor_init;
21mod event_apply;
22pub mod event_debug;
23mod event_debug_actions;
24mod file_explorer;
25pub mod file_open;
26mod file_open_input;
27mod file_open_orchestrators;
28mod file_open_queue;
29mod file_operations;
30mod help;
31mod help_actions;
32mod hover;
33mod input;
34mod input_dispatch;
35mod input_helpers;
36pub mod keybinding_editor;
37mod keybinding_editor_actions;
38mod lifecycle;
39mod line_scan;
40mod lsp_actions;
41mod lsp_event_notify;
42mod lsp_requests;
43mod lsp_status;
44mod macro_actions;
45mod macros;
46mod menu_actions;
47mod menu_context;
48mod mouse_input;
49mod on_save_actions;
50mod path_utils;
51mod plugin_commands;
52mod plugin_dispatch;
53mod popup_actions;
54mod popup_dialogs;
55mod popup_overlay_actions;
56mod prompt_actions;
57mod prompt_lifecycle;
58mod recovery_actions;
59mod regex_replace;
60mod render;
61mod scan_orchestrators;
62mod scroll_sync;
63mod scrollbar_input;
64mod scrollbar_math;
65mod search_ops;
66mod search_scan;
67mod settings_actions;
68mod settings_prompts;
69mod shell_command;
70mod smart_home;
71mod split_actions;
72mod stdin_stream;
73mod tab_drag;
74mod terminal;
75mod terminal_input;
76mod terminal_mouse;
77mod text_ops;
78mod theme_inspect;
79mod toggle_actions;
80pub mod types;
81mod undo_actions;
82mod view_actions;
83mod virtual_buffers;
84pub mod warning_domains;
85pub mod workspace;
86
87use anyhow::Result as AnyhowResult;
88use rust_i18n::t;
89
90pub fn editor_tick(
95 editor: &mut Editor,
96 mut clear_terminal: impl FnMut() -> AnyhowResult<()>,
97) -> AnyhowResult<bool> {
98 let mut needs_render = false;
99
100 let async_messages = {
101 let _s = tracing::info_span!("process_async_messages").entered();
102 editor.process_async_messages()
103 };
104 if async_messages {
105 needs_render = true;
106 }
107 let pending_file_opens = {
108 let _s = tracing::info_span!("process_pending_file_opens").entered();
109 editor.process_pending_file_opens()
110 };
111 if pending_file_opens {
112 needs_render = true;
113 }
114 if editor.process_line_scan() {
115 needs_render = true;
116 }
117 let search_scan = {
118 let _s = tracing::info_span!("process_search_scan").entered();
119 editor.process_search_scan()
120 };
121 if search_scan {
122 needs_render = true;
123 }
124 let search_overlay_refresh = {
125 let _s = tracing::info_span!("check_search_overlay_refresh").entered();
126 editor.check_search_overlay_refresh()
127 };
128 if search_overlay_refresh {
129 needs_render = true;
130 }
131 if editor.check_mouse_hover_timer() {
132 needs_render = true;
133 }
134 if editor.check_semantic_highlight_timer() {
135 needs_render = true;
136 }
137 if editor.check_completion_trigger_timer() {
138 needs_render = true;
139 }
140 editor.check_diagnostic_pull_timer();
141 if editor.check_warning_log() {
142 needs_render = true;
143 }
144 if editor.poll_stdin_streaming() {
145 needs_render = true;
146 }
147
148 if let Err(e) = editor.auto_recovery_save_dirty_buffers() {
149 tracing::debug!("Auto-recovery-save error: {}", e);
150 }
151 if let Err(e) = editor.auto_save_persistent_buffers() {
152 tracing::debug!("Auto-save (disk) error: {}", e);
153 }
154
155 if editor.take_full_redraw_request() {
156 clear_terminal()?;
157 needs_render = true;
158 }
159
160 Ok(needs_render)
161}
162
163pub(crate) use path_utils::normalize_path;
164
165use self::types::{
166 CachedLayout, EventLineInfo, InteractiveReplaceState, LspMessageEntry, LspProgressInfo,
167 MouseState, SearchState, TabContextMenu, DEFAULT_BACKGROUND_FILE,
168};
169use crate::config::Config;
170use crate::config_io::{ConfigLayer, ConfigResolver, DirectoryContext};
171use crate::input::actions::action_to_events as convert_action_to_events;
172use crate::input::buffer_mode::ModeRegistry;
173use crate::input::command_registry::CommandRegistry;
174use crate::input::commands::Suggestion;
175use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
176use crate::input::position_history::PositionHistory;
177use crate::input::quick_open::{
178 BufferInfo, BufferProvider, CommandProvider, FileProvider, GotoLineProvider, QuickOpenContext,
179 QuickOpenRegistry,
180};
181use crate::model::cursor::Cursors;
182use crate::model::event::{Event, EventLog, LeafId, SplitDirection, SplitId};
183use crate::model::filesystem::FileSystem;
184use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
185use crate::services::fs::FsManager;
186use crate::services::lsp::manager::LspManager;
187use crate::services::plugins::PluginManager;
188use crate::services::recovery::{RecoveryConfig, RecoveryService};
189use crate::services::time_source::{RealTimeSource, SharedTimeSource};
190use crate::state::EditorState;
191use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
192use crate::view::file_tree::{FileTree, FileTreeView};
193use crate::view::prompt::{Prompt, PromptType};
194use crate::view::scroll_sync::ScrollSyncManager;
195use crate::view::split::{SplitManager, SplitViewState};
196use crate::view::ui::{
197 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
198};
199use crossterm::event::{KeyCode, KeyModifiers};
200#[cfg(feature = "plugins")]
201use fresh_core::api::BufferSavedDiff;
202#[cfg(feature = "plugins")]
203use fresh_core::api::JsCallbackId;
204use fresh_core::api::PluginCommand;
205use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
206use ratatui::{
207 layout::{Constraint, Direction, Layout},
208 Frame,
209};
210use std::collections::{HashMap, HashSet};
211use std::ops::Range;
212use std::path::{Path, PathBuf};
213use std::sync::{Arc, RwLock};
214use std::time::Instant;
215
216pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
218pub use self::warning_domains::{
219 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
220 WarningDomainRegistry, WarningLevel, WarningPopupContent,
221};
222pub use crate::model::event::BufferId;
223
224fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
226 fresh_core::file_uri::lsp_uri_to_path(uri).ok_or_else(|| "URI is not a file path".to_string())
227}
228
229#[derive(Clone, Debug)]
231pub struct PendingGrammar {
232 pub language: String,
234 pub grammar_path: String,
236 pub extensions: Vec<String>,
238}
239
240#[derive(Clone, Debug)]
242struct SemanticTokenRangeRequest {
243 buffer_id: BufferId,
244 version: u64,
245 range: Range<usize>,
246 start_line: usize,
247 end_line: usize,
248}
249
250#[derive(Clone, Copy, Debug)]
251enum SemanticTokensFullRequestKind {
252 Full,
253 FullDelta,
254}
255
256#[derive(Clone, Debug)]
257struct SemanticTokenFullRequest {
258 buffer_id: BufferId,
259 version: u64,
260 kind: SemanticTokensFullRequestKind,
261}
262
263#[derive(Clone, Debug)]
264struct FoldingRangeRequest {
265 buffer_id: BufferId,
266 version: u64,
267}
268
269#[derive(Clone, Debug)]
270struct InlayHintsRequest {
271 buffer_id: BufferId,
272 version: u64,
273}
274
275#[derive(Debug, Clone)]
281pub struct DabbrevCycleState {
282 pub original_prefix: String,
284 pub word_start: usize,
286 pub candidates: Vec<String>,
288 pub index: usize,
290}
291
292pub struct Editor {
294 buffers: HashMap<BufferId, EditorState>,
296
297 event_logs: HashMap<BufferId, EventLog>,
302
303 next_buffer_id: usize,
305
306 config: Config,
308
309 user_config_raw: serde_json::Value,
311
312 dir_context: DirectoryContext,
314
315 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
317
318 pending_grammars: Vec<PendingGrammar>,
320
321 grammar_reload_pending: bool,
325
326 grammar_build_in_progress: bool,
329
330 needs_full_grammar_build: bool,
334
335 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
337
338 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
342
343 theme: crate::view::theme::Theme,
345
346 theme_registry: crate::view::theme::ThemeRegistry,
348
349 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
351
352 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
354
355 ansi_background_path: Option<PathBuf>,
357
358 background_fade: f32,
360
361 keybindings: Arc<RwLock<KeybindingResolver>>,
363
364 clipboard: crate::services::clipboard::Clipboard,
366
367 should_quit: bool,
369
370 should_detach: bool,
372
373 session_mode: bool,
375
376 software_cursor_only: bool,
378
379 session_name: Option<String>,
381
382 pending_escape_sequences: Vec<u8>,
385
386 restart_with_dir: Option<PathBuf>,
389
390 status_message: Option<String>,
392
393 plugin_status_message: Option<String>,
395
396 plugin_errors: Vec<String>,
399
400 prompt: Option<Prompt>,
402
403 terminal_width: u16,
405 terminal_height: u16,
406
407 lsp: Option<LspManager>,
409
410 buffer_metadata: HashMap<BufferId, BufferMetadata>,
412
413 mode_registry: ModeRegistry,
415
416 tokio_runtime: Option<tokio::runtime::Runtime>,
418
419 async_bridge: Option<AsyncBridge>,
421
422 split_manager: SplitManager,
424
425 split_view_states: HashMap<LeafId, SplitViewState>,
429
430 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
434
435 scroll_sync_manager: ScrollSyncManager,
438
439 file_explorer: Option<FileTreeView>,
441
442 preview: Option<(LeafId, BufferId)>,
456
457 suppress_position_history_once: bool,
462
463 fs_manager: Arc<FsManager>,
465
466 filesystem: Arc<dyn FileSystem + Send + Sync>,
468
469 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
472
473 process_spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
475
476 file_explorer_visible: bool,
478
479 file_explorer_sync_in_progress: bool,
482
483 file_explorer_width_percent: f32,
486
487 pending_file_explorer_show_hidden: Option<bool>,
489
490 pending_file_explorer_show_gitignored: Option<bool>,
492
493 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
495
496 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
498
499 menu_bar_visible: bool,
501
502 menu_bar_auto_shown: bool,
505
506 tab_bar_visible: bool,
508
509 status_bar_visible: bool,
511
512 prompt_line_visible: bool,
514
515 mouse_enabled: bool,
517
518 same_buffer_scroll_sync: bool,
520
521 mouse_cursor_position: Option<(u16, u16)>,
525
526 gpm_active: bool,
528
529 key_context: KeyContext,
531
532 menu_state: crate::view::ui::MenuState,
534
535 menus: crate::config::MenuConfig,
537
538 working_dir: PathBuf,
540
541 pub position_history: PositionHistory,
543
544 in_navigation: bool,
546
547 next_lsp_request_id: u64,
549
550 pending_completion_requests: HashSet<u64>,
552
553 completion_items: Option<Vec<lsp_types::CompletionItem>>,
556
557 scheduled_completion_trigger: Option<Instant>,
560
561 completion_service: crate::services::completion::CompletionService,
564
565 dabbrev_state: Option<DabbrevCycleState>,
569
570 pending_goto_definition_request: Option<u64>,
572
573 pending_references_request: Option<u64>,
575
576 pending_references_symbol: String,
578
579 pending_signature_help_request: Option<u64>,
581
582 pending_code_actions_requests: HashSet<u64>,
584
585 pending_code_actions_server_names: HashMap<u64, String>,
587
588 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
592
593 pending_inlay_hints_requests: HashMap<u64, InlayHintsRequest>,
604
605 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
607
608 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
610
611 folding_ranges_debounce: HashMap<BufferId, Instant>,
613
614 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
616
617 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
619
620 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
622
623 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
625
626 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
628
629 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
631
632 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
634
635 hover: hover::HoverState,
638
639 search_state: Option<SearchState>,
641
642 search_namespace: crate::view::overlay::OverlayNamespace,
644
645 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
647
648 pending_search_range: Option<Range<usize>>,
650
651 interactive_replace_state: Option<InteractiveReplaceState>,
653
654 mouse_state: MouseState,
656
657 tab_context_menu: Option<TabContextMenu>,
659
660 theme_info_popup: Option<types::ThemeInfoPopup>,
662
663 pub(crate) cached_layout: CachedLayout,
665
666 command_registry: Arc<RwLock<CommandRegistry>>,
668
669 quick_open_registry: QuickOpenRegistry,
671
672 plugin_manager: PluginManager,
674
675 plugin_dev_workspaces:
679 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
680
681 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
685
686 panel_ids: HashMap<String, BufferId>,
689
690 buffer_groups: HashMap<types::BufferGroupId, types::BufferGroup>,
692 buffer_to_group: HashMap<BufferId, types::BufferGroupId>,
694 next_buffer_group_id: usize,
696
697 pub(crate) grouped_subtrees:
705 HashMap<crate::model::event::LeafId, crate::view::split::SplitNode>,
706
707 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
710
711 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
714
715 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
719
720 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
722
723 lsp_server_statuses:
725 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
726
727 lsp_window_messages: Vec<LspMessageEntry>,
729
730 lsp_log_messages: Vec<LspMessageEntry>,
732
733 diagnostic_result_ids: HashMap<String, String>,
736
737 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
740
741 scheduled_inlay_hints_request: Option<(BufferId, Instant)>,
744
745 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
748
749 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
751
752 stored_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
754
755 stored_folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
758
759 event_broadcaster: crate::model::control_event::EventBroadcaster,
761
762 bookmarks: bookmarks::BookmarkState,
764
765 search_case_sensitive: bool,
767 search_whole_word: bool,
768 search_use_regex: bool,
769 search_confirm_each: bool,
771
772 macros: macros::MacroState,
775
776 #[cfg(feature = "plugins")]
778 pending_plugin_actions: Vec<(
779 String,
780 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
781 )>,
782
783 #[cfg(feature = "plugins")]
785 plugin_render_requested: bool,
786
787 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
790
791 pending_lsp_confirmation: Option<String>,
794
795 pending_lsp_status_popup: Option<Vec<(String, String)>>,
799
800 user_dismissed_lsp_languages: std::collections::HashSet<String>,
808
809 pending_close_buffer: Option<BufferId>,
812
813 auto_revert_enabled: bool,
815
816 last_auto_revert_poll: std::time::Instant,
818
819 last_file_tree_poll: std::time::Instant,
821
822 git_index_resolved: bool,
824
825 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
828
829 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
832
833 #[allow(clippy::type_complexity)]
837 pending_file_poll_rx:
838 Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
839
840 #[allow(clippy::type_complexity)]
843 pending_dir_poll_rx: Option<
844 std::sync::mpsc::Receiver<(
845 Vec<(
846 crate::view::file_tree::NodeId,
847 PathBuf,
848 Option<std::time::SystemTime>,
849 )>,
850 Option<(PathBuf, std::time::SystemTime)>,
851 )>,
852 >,
853
854 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
857
858 file_open_state: Option<file_open::FileOpenState>,
860
861 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
863
864 recovery_service: RecoveryService,
866
867 full_redraw_requested: bool,
869
870 time_source: SharedTimeSource,
872
873 last_auto_recovery_save: std::time::Instant,
875
876 last_persistent_auto_save: std::time::Instant,
878
879 active_custom_contexts: HashSet<String>,
882
883 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
886
887 editor_mode: Option<String>,
890
891 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
893
894 status_log_path: Option<PathBuf>,
896
897 warning_domains: WarningDomainRegistry,
900
901 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
903
904 terminal_manager: crate::services::terminal::TerminalManager,
906
907 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
909
910 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
912
913 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
915
916 terminal_mode: bool,
918
919 keyboard_capture: bool,
923
924 terminal_mode_resume: std::collections::HashSet<BufferId>,
928
929 previous_click_time: Option<std::time::Instant>,
931
932 previous_click_position: Option<(u16, u16)>,
935
936 click_count: u8,
938
939 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
941
942 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
944
945 pub(crate) event_debug: Option<event_debug::EventDebug>,
947
948 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
950
951 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
953
954 color_capability: crate::view::color_support::ColorCapability,
956
957 review_hunks: Vec<fresh_core::api::ReviewHunk>,
959
960 active_action_popup: Option<(String, Vec<(String, String)>)>,
963
964 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
967
968 composite_view_states:
971 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
972
973 pending_file_opens: Vec<PendingFileOpen>,
977
978 pending_hot_exit_recovery: bool,
980
981 wait_tracking: HashMap<BufferId, (u64, bool)>,
983 completed_waits: Vec<u64>,
985
986 stdin_stream: stdin_stream::StdinStream,
988
989 line_scan: line_scan::LineScan,
991
992 search_scan: search_scan::SearchScan,
994
995 search_overlay_top_byte: Option<usize>,
998}
999
1000#[derive(Debug, Clone)]
1002pub struct PendingFileOpen {
1003 pub path: PathBuf,
1005 pub line: Option<usize>,
1007 pub column: Option<usize>,
1009 pub end_line: Option<usize>,
1011 pub end_column: Option<usize>,
1013 pub message: Option<String>,
1015 pub wait_id: Option<u64>,
1017}
1018
1019impl Editor {
1020 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1022 let trimmed = input.trim();
1023
1024 if trimmed.is_empty() {
1025 self.ansi_background = None;
1026 self.ansi_background_path = None;
1027 self.set_status_message(t!("status.background_cleared").to_string());
1028 return Ok(());
1029 }
1030
1031 let input_path = Path::new(trimmed);
1032 let resolved = if input_path.is_absolute() {
1033 input_path.to_path_buf()
1034 } else {
1035 self.working_dir.join(input_path)
1036 };
1037
1038 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1039
1040 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1041
1042 self.ansi_background = Some(parsed);
1043 self.ansi_background_path = Some(canonical.clone());
1044 self.set_status_message(
1045 t!(
1046 "view.background_set",
1047 path = canonical.display().to_string()
1048 )
1049 .to_string(),
1050 );
1051
1052 Ok(())
1053 }
1054
1055 fn effective_tabs_width(&self) -> u16 {
1060 if self.file_explorer_visible && self.file_explorer.is_some() {
1061 let editor_percent = 1.0 - self.file_explorer_width_percent;
1063 (self.terminal_width as f32 * editor_percent) as u16
1064 } else {
1065 self.terminal_width
1066 }
1067 }
1068
1069 pub fn active_state(&self) -> &EditorState {
1071 self.buffers.get(&self.active_buffer()).unwrap()
1072 }
1073
1074 pub fn active_state_mut(&mut self) -> &mut EditorState {
1076 self.buffers.get_mut(&self.active_buffer()).unwrap()
1077 }
1078
1079 pub fn active_cursors(&self) -> &Cursors {
1083 let split_id = self.effective_active_split();
1084 &self.split_view_states.get(&split_id).unwrap().cursors
1085 }
1086
1087 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1089 let split_id = self.effective_active_split();
1090 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
1091 }
1092
1093 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1095 self.completion_items = Some(items);
1096 }
1097
1098 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1100 let active_split = self.split_manager.active_split();
1101 &self.split_view_states.get(&active_split).unwrap().viewport
1102 }
1103
1104 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1106 let active_split = self.split_manager.active_split();
1107 &mut self
1108 .split_view_states
1109 .get_mut(&active_split)
1110 .unwrap()
1111 .viewport
1112 }
1113
1114 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1116 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1118 return composite.name.clone();
1119 }
1120
1121 self.buffer_metadata
1122 .get(&buffer_id)
1123 .map(|m| m.display_name.clone())
1124 .or_else(|| {
1125 self.buffers.get(&buffer_id).and_then(|state| {
1126 state
1127 .buffer
1128 .file_path()
1129 .and_then(|p| p.file_name())
1130 .and_then(|n| n.to_str())
1131 .map(|s| s.to_string())
1132 })
1133 })
1134 .unwrap_or_else(|| "[No Name]".to_string())
1135 }
1136
1137 pub fn active_event_log(&self) -> &EventLog {
1147 self.event_logs.get(&self.active_buffer()).unwrap()
1148 }
1149
1150 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1152 self.event_logs.get_mut(&self.active_buffer()).unwrap()
1153 }
1154
1155 pub(super) fn update_modified_from_event_log(&mut self) {
1159 let is_at_saved = self
1160 .event_logs
1161 .get(&self.active_buffer())
1162 .map(|log| log.is_at_saved_position())
1163 .unwrap_or(false);
1164
1165 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1166 state.buffer.set_modified(!is_at_saved);
1167 }
1168 }
1169}
1170
1171fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1180 use crossterm::event::{KeyCode, KeyModifiers};
1181
1182 let mut modifiers = KeyModifiers::NONE;
1183 let mut remaining = key_str;
1184
1185 loop {
1187 if remaining.starts_with("C-") {
1188 modifiers |= KeyModifiers::CONTROL;
1189 remaining = &remaining[2..];
1190 } else if remaining.starts_with("M-") {
1191 modifiers |= KeyModifiers::ALT;
1192 remaining = &remaining[2..];
1193 } else if remaining.starts_with("S-") {
1194 modifiers |= KeyModifiers::SHIFT;
1195 remaining = &remaining[2..];
1196 } else {
1197 break;
1198 }
1199 }
1200
1201 let upper = remaining.to_uppercase();
1204 let code = match upper.as_str() {
1205 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1206 "TAB" => KeyCode::Tab,
1207 "BACKTAB" => KeyCode::BackTab,
1208 "ESC" | "ESCAPE" => KeyCode::Esc,
1209 "SPC" | "SPACE" => KeyCode::Char(' '),
1210 "DEL" | "DELETE" => KeyCode::Delete,
1211 "BS" | "BACKSPACE" => KeyCode::Backspace,
1212 "UP" => KeyCode::Up,
1213 "DOWN" => KeyCode::Down,
1214 "LEFT" => KeyCode::Left,
1215 "RIGHT" => KeyCode::Right,
1216 "HOME" => KeyCode::Home,
1217 "END" => KeyCode::End,
1218 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1219 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1220 s if s.starts_with('F') && s.len() > 1 => {
1221 if let Ok(n) = s[1..].parse::<u8>() {
1223 KeyCode::F(n)
1224 } else {
1225 return None;
1226 }
1227 }
1228 _ if remaining.len() == 1 => {
1229 let c = remaining.chars().next()?;
1232 if c.is_ascii_uppercase() {
1233 modifiers |= KeyModifiers::SHIFT;
1234 }
1235 KeyCode::Char(c.to_ascii_lowercase())
1236 }
1237 _ => return None,
1238 };
1239
1240 Some((code, modifiers))
1241}
1242
1243#[cfg(test)]
1244mod tests {
1245 use super::*;
1246 use tempfile::TempDir;
1247
1248 fn test_dir_context() -> (DirectoryContext, TempDir) {
1250 let temp_dir = TempDir::new().unwrap();
1251 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1252 (dir_context, temp_dir)
1253 }
1254
1255 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1257 Arc::new(crate::model::filesystem::StdFileSystem)
1258 }
1259
1260 #[test]
1261 fn test_editor_new() {
1262 let config = Config::default();
1263 let (dir_context, _temp) = test_dir_context();
1264 let editor = Editor::new(
1265 config,
1266 80,
1267 24,
1268 dir_context,
1269 crate::view::color_support::ColorCapability::TrueColor,
1270 test_filesystem(),
1271 )
1272 .unwrap();
1273
1274 assert_eq!(editor.buffers.len(), 1);
1275 assert!(!editor.should_quit());
1276 }
1277
1278 #[test]
1279 fn test_new_buffer() {
1280 let config = Config::default();
1281 let (dir_context, _temp) = test_dir_context();
1282 let mut editor = Editor::new(
1283 config,
1284 80,
1285 24,
1286 dir_context,
1287 crate::view::color_support::ColorCapability::TrueColor,
1288 test_filesystem(),
1289 )
1290 .unwrap();
1291
1292 let id = editor.new_buffer();
1293 assert_eq!(editor.buffers.len(), 2);
1294 assert_eq!(editor.active_buffer(), id);
1295 }
1296
1297 #[test]
1298 #[ignore]
1299 fn test_clipboard() {
1300 let config = Config::default();
1301 let (dir_context, _temp) = test_dir_context();
1302 let mut editor = Editor::new(
1303 config,
1304 80,
1305 24,
1306 dir_context,
1307 crate::view::color_support::ColorCapability::TrueColor,
1308 test_filesystem(),
1309 )
1310 .unwrap();
1311
1312 editor.clipboard.set_internal("test".to_string());
1314
1315 editor.paste();
1317
1318 let content = editor.active_state().buffer.to_string().unwrap();
1319 assert_eq!(content, "test");
1320 }
1321
1322 #[test]
1323 fn test_action_to_events_insert_char() {
1324 let config = Config::default();
1325 let (dir_context, _temp) = test_dir_context();
1326 let mut editor = Editor::new(
1327 config,
1328 80,
1329 24,
1330 dir_context,
1331 crate::view::color_support::ColorCapability::TrueColor,
1332 test_filesystem(),
1333 )
1334 .unwrap();
1335
1336 let events = editor.action_to_events(Action::InsertChar('a'));
1337 assert!(events.is_some());
1338
1339 let events = events.unwrap();
1340 assert_eq!(events.len(), 1);
1341
1342 match &events[0] {
1343 Event::Insert { position, text, .. } => {
1344 assert_eq!(*position, 0);
1345 assert_eq!(text, "a");
1346 }
1347 _ => panic!("Expected Insert event"),
1348 }
1349 }
1350
1351 #[test]
1352 fn test_action_to_events_move_right() {
1353 let config = Config::default();
1354 let (dir_context, _temp) = test_dir_context();
1355 let mut editor = Editor::new(
1356 config,
1357 80,
1358 24,
1359 dir_context,
1360 crate::view::color_support::ColorCapability::TrueColor,
1361 test_filesystem(),
1362 )
1363 .unwrap();
1364
1365 let cursor_id = editor.active_cursors().primary_id();
1367 editor.apply_event_to_active_buffer(&Event::Insert {
1368 position: 0,
1369 text: "hello".to_string(),
1370 cursor_id,
1371 });
1372
1373 let events = editor.action_to_events(Action::MoveRight);
1374 assert!(events.is_some());
1375
1376 let events = events.unwrap();
1377 assert_eq!(events.len(), 1);
1378
1379 match &events[0] {
1380 Event::MoveCursor {
1381 new_position,
1382 new_anchor,
1383 ..
1384 } => {
1385 assert_eq!(*new_position, 5);
1387 assert_eq!(*new_anchor, None); }
1389 _ => panic!("Expected MoveCursor event"),
1390 }
1391 }
1392
1393 #[test]
1394 fn test_action_to_events_move_up_down() {
1395 let config = Config::default();
1396 let (dir_context, _temp) = test_dir_context();
1397 let mut 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 let cursor_id = editor.active_cursors().primary_id();
1409 editor.apply_event_to_active_buffer(&Event::Insert {
1410 position: 0,
1411 text: "line1\nline2\nline3".to_string(),
1412 cursor_id,
1413 });
1414
1415 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1417 cursor_id,
1418 old_position: 0, new_position: 6,
1420 old_anchor: None, new_anchor: None,
1422 old_sticky_column: 0,
1423 new_sticky_column: 0,
1424 });
1425
1426 let events = editor.action_to_events(Action::MoveUp);
1428 assert!(events.is_some());
1429 let events = events.unwrap();
1430 assert_eq!(events.len(), 1);
1431
1432 match &events[0] {
1433 Event::MoveCursor { new_position, .. } => {
1434 assert_eq!(*new_position, 0); }
1436 _ => panic!("Expected MoveCursor event"),
1437 }
1438 }
1439
1440 #[test]
1441 fn test_action_to_events_insert_newline() {
1442 let config = Config::default();
1443 let (dir_context, _temp) = test_dir_context();
1444 let mut editor = Editor::new(
1445 config,
1446 80,
1447 24,
1448 dir_context,
1449 crate::view::color_support::ColorCapability::TrueColor,
1450 test_filesystem(),
1451 )
1452 .unwrap();
1453
1454 let events = editor.action_to_events(Action::InsertNewline);
1455 assert!(events.is_some());
1456
1457 let events = events.unwrap();
1458 assert_eq!(events.len(), 1);
1459
1460 match &events[0] {
1461 Event::Insert { text, .. } => {
1462 assert_eq!(text, "\n");
1463 }
1464 _ => panic!("Expected Insert event"),
1465 }
1466 }
1467
1468 #[test]
1469 fn test_action_to_events_unimplemented() {
1470 let config = Config::default();
1471 let (dir_context, _temp) = test_dir_context();
1472 let mut editor = Editor::new(
1473 config,
1474 80,
1475 24,
1476 dir_context,
1477 crate::view::color_support::ColorCapability::TrueColor,
1478 test_filesystem(),
1479 )
1480 .unwrap();
1481
1482 assert!(editor.action_to_events(Action::Save).is_none());
1484 assert!(editor.action_to_events(Action::Quit).is_none());
1485 assert!(editor.action_to_events(Action::Undo).is_none());
1486 }
1487
1488 #[test]
1489 fn test_action_to_events_delete_backward() {
1490 let config = Config::default();
1491 let (dir_context, _temp) = test_dir_context();
1492 let mut editor = Editor::new(
1493 config,
1494 80,
1495 24,
1496 dir_context,
1497 crate::view::color_support::ColorCapability::TrueColor,
1498 test_filesystem(),
1499 )
1500 .unwrap();
1501
1502 let cursor_id = editor.active_cursors().primary_id();
1504 editor.apply_event_to_active_buffer(&Event::Insert {
1505 position: 0,
1506 text: "hello".to_string(),
1507 cursor_id,
1508 });
1509
1510 let events = editor.action_to_events(Action::DeleteBackward);
1511 assert!(events.is_some());
1512
1513 let events = events.unwrap();
1514 assert_eq!(events.len(), 1);
1515
1516 match &events[0] {
1517 Event::Delete {
1518 range,
1519 deleted_text,
1520 ..
1521 } => {
1522 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1524 }
1525 _ => panic!("Expected Delete event"),
1526 }
1527 }
1528
1529 #[test]
1530 fn test_action_to_events_delete_forward() {
1531 let config = Config::default();
1532 let (dir_context, _temp) = test_dir_context();
1533 let mut editor = Editor::new(
1534 config,
1535 80,
1536 24,
1537 dir_context,
1538 crate::view::color_support::ColorCapability::TrueColor,
1539 test_filesystem(),
1540 )
1541 .unwrap();
1542
1543 let cursor_id = editor.active_cursors().primary_id();
1545 editor.apply_event_to_active_buffer(&Event::Insert {
1546 position: 0,
1547 text: "hello".to_string(),
1548 cursor_id,
1549 });
1550
1551 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1553 cursor_id,
1554 old_position: 0, new_position: 0,
1556 old_anchor: None, new_anchor: None,
1558 old_sticky_column: 0,
1559 new_sticky_column: 0,
1560 });
1561
1562 let events = editor.action_to_events(Action::DeleteForward);
1563 assert!(events.is_some());
1564
1565 let events = events.unwrap();
1566 assert_eq!(events.len(), 1);
1567
1568 match &events[0] {
1569 Event::Delete {
1570 range,
1571 deleted_text,
1572 ..
1573 } => {
1574 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1576 }
1577 _ => panic!("Expected Delete event"),
1578 }
1579 }
1580
1581 #[test]
1582 fn test_action_to_events_select_right() {
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: "hello".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: 0,
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::SelectRight);
1615 assert!(events.is_some());
1616
1617 let events = events.unwrap();
1618 assert_eq!(events.len(), 1);
1619
1620 match &events[0] {
1621 Event::MoveCursor {
1622 new_position,
1623 new_anchor,
1624 ..
1625 } => {
1626 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1629 _ => panic!("Expected MoveCursor event"),
1630 }
1631 }
1632
1633 #[test]
1634 fn test_action_to_events_select_all() {
1635 let config = Config::default();
1636 let (dir_context, _temp) = test_dir_context();
1637 let mut editor = Editor::new(
1638 config,
1639 80,
1640 24,
1641 dir_context,
1642 crate::view::color_support::ColorCapability::TrueColor,
1643 test_filesystem(),
1644 )
1645 .unwrap();
1646
1647 let cursor_id = editor.active_cursors().primary_id();
1649 editor.apply_event_to_active_buffer(&Event::Insert {
1650 position: 0,
1651 text: "hello world".to_string(),
1652 cursor_id,
1653 });
1654
1655 let events = editor.action_to_events(Action::SelectAll);
1656 assert!(events.is_some());
1657
1658 let events = events.unwrap();
1659 assert_eq!(events.len(), 1);
1660
1661 match &events[0] {
1662 Event::MoveCursor {
1663 new_position,
1664 new_anchor,
1665 ..
1666 } => {
1667 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1670 _ => panic!("Expected MoveCursor event"),
1671 }
1672 }
1673
1674 #[test]
1675 fn test_action_to_events_document_nav() {
1676 let config = Config::default();
1677 let (dir_context, _temp) = test_dir_context();
1678 let mut editor = Editor::new(
1679 config,
1680 80,
1681 24,
1682 dir_context,
1683 crate::view::color_support::ColorCapability::TrueColor,
1684 test_filesystem(),
1685 )
1686 .unwrap();
1687
1688 let cursor_id = editor.active_cursors().primary_id();
1690 editor.apply_event_to_active_buffer(&Event::Insert {
1691 position: 0,
1692 text: "line1\nline2\nline3".to_string(),
1693 cursor_id,
1694 });
1695
1696 let events = editor.action_to_events(Action::MoveDocumentStart);
1698 assert!(events.is_some());
1699 let events = events.unwrap();
1700 match &events[0] {
1701 Event::MoveCursor { new_position, .. } => {
1702 assert_eq!(*new_position, 0);
1703 }
1704 _ => panic!("Expected MoveCursor event"),
1705 }
1706
1707 let events = editor.action_to_events(Action::MoveDocumentEnd);
1709 assert!(events.is_some());
1710 let events = events.unwrap();
1711 match &events[0] {
1712 Event::MoveCursor { new_position, .. } => {
1713 assert_eq!(*new_position, 17); }
1715 _ => panic!("Expected MoveCursor event"),
1716 }
1717 }
1718
1719 #[test]
1720 fn test_action_to_events_remove_secondary_cursors() {
1721 use crate::model::event::CursorId;
1722
1723 let config = Config::default();
1724 let (dir_context, _temp) = test_dir_context();
1725 let mut editor = Editor::new(
1726 config,
1727 80,
1728 24,
1729 dir_context,
1730 crate::view::color_support::ColorCapability::TrueColor,
1731 test_filesystem(),
1732 )
1733 .unwrap();
1734
1735 let cursor_id = editor.active_cursors().primary_id();
1737 editor.apply_event_to_active_buffer(&Event::Insert {
1738 position: 0,
1739 text: "hello world test".to_string(),
1740 cursor_id,
1741 });
1742
1743 editor.apply_event_to_active_buffer(&Event::AddCursor {
1745 cursor_id: CursorId(1),
1746 position: 5,
1747 anchor: None,
1748 });
1749 editor.apply_event_to_active_buffer(&Event::AddCursor {
1750 cursor_id: CursorId(2),
1751 position: 10,
1752 anchor: None,
1753 });
1754
1755 assert_eq!(editor.active_cursors().count(), 3);
1756
1757 let first_id = editor
1759 .active_cursors()
1760 .iter()
1761 .map(|(id, _)| id)
1762 .min_by_key(|id| id.0)
1763 .expect("Should have at least one cursor");
1764
1765 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
1767 assert!(events.is_some());
1768
1769 let events = events.unwrap();
1770 let remove_cursor_events: Vec<_> = events
1773 .iter()
1774 .filter_map(|e| match e {
1775 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1776 _ => None,
1777 })
1778 .collect();
1779
1780 assert_eq!(remove_cursor_events.len(), 2);
1782
1783 for cursor_id in &remove_cursor_events {
1784 assert_ne!(*cursor_id, first_id);
1786 }
1787 }
1788
1789 #[test]
1790 fn test_action_to_events_scroll() {
1791 let config = Config::default();
1792 let (dir_context, _temp) = test_dir_context();
1793 let mut editor = Editor::new(
1794 config,
1795 80,
1796 24,
1797 dir_context,
1798 crate::view::color_support::ColorCapability::TrueColor,
1799 test_filesystem(),
1800 )
1801 .unwrap();
1802
1803 let events = editor.action_to_events(Action::ScrollUp);
1805 assert!(events.is_some());
1806 let events = events.unwrap();
1807 assert_eq!(events.len(), 1);
1808 match &events[0] {
1809 Event::Scroll { line_offset } => {
1810 assert_eq!(*line_offset, -1);
1811 }
1812 _ => panic!("Expected Scroll event"),
1813 }
1814
1815 let events = editor.action_to_events(Action::ScrollDown);
1817 assert!(events.is_some());
1818 let events = events.unwrap();
1819 assert_eq!(events.len(), 1);
1820 match &events[0] {
1821 Event::Scroll { line_offset } => {
1822 assert_eq!(*line_offset, 1);
1823 }
1824 _ => panic!("Expected Scroll event"),
1825 }
1826 }
1827
1828 #[test]
1829 fn test_action_to_events_none() {
1830 let config = Config::default();
1831 let (dir_context, _temp) = test_dir_context();
1832 let mut editor = Editor::new(
1833 config,
1834 80,
1835 24,
1836 dir_context,
1837 crate::view::color_support::ColorCapability::TrueColor,
1838 test_filesystem(),
1839 )
1840 .unwrap();
1841
1842 let events = editor.action_to_events(Action::None);
1844 assert!(events.is_none());
1845 }
1846
1847 #[test]
1848 fn test_lsp_incremental_insert_generates_correct_range() {
1849 use crate::model::buffer::Buffer;
1852
1853 let buffer = Buffer::from_str_test("hello\nworld");
1854
1855 let position = 0;
1858 let (line, character) = buffer.position_to_lsp_position(position);
1859
1860 assert_eq!(line, 0, "Insertion at start should be line 0");
1861 assert_eq!(character, 0, "Insertion at start should be char 0");
1862
1863 let lsp_pos = Position::new(line as u32, character as u32);
1865 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
1866
1867 assert_eq!(lsp_range.start.line, 0);
1868 assert_eq!(lsp_range.start.character, 0);
1869 assert_eq!(lsp_range.end.line, 0);
1870 assert_eq!(lsp_range.end.character, 0);
1871 assert_eq!(
1872 lsp_range.start, lsp_range.end,
1873 "Insert should have zero-width range"
1874 );
1875
1876 let position = 3;
1878 let (line, character) = buffer.position_to_lsp_position(position);
1879
1880 assert_eq!(line, 0);
1881 assert_eq!(character, 3);
1882
1883 let position = 6;
1885 let (line, character) = buffer.position_to_lsp_position(position);
1886
1887 assert_eq!(line, 1, "Position after newline should be line 1");
1888 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
1889 }
1890
1891 #[test]
1892 fn test_lsp_incremental_delete_generates_correct_range() {
1893 use crate::model::buffer::Buffer;
1896
1897 let buffer = Buffer::from_str_test("hello\nworld");
1898
1899 let range_start = 1;
1901 let range_end = 5;
1902
1903 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
1904 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
1905
1906 assert_eq!(start_line, 0);
1907 assert_eq!(start_char, 1);
1908 assert_eq!(end_line, 0);
1909 assert_eq!(end_char, 5);
1910
1911 let lsp_range = LspRange::new(
1912 Position::new(start_line as u32, start_char as u32),
1913 Position::new(end_line as u32, end_char as u32),
1914 );
1915
1916 assert_eq!(lsp_range.start.line, 0);
1917 assert_eq!(lsp_range.start.character, 1);
1918 assert_eq!(lsp_range.end.line, 0);
1919 assert_eq!(lsp_range.end.character, 5);
1920 assert_ne!(
1921 lsp_range.start, lsp_range.end,
1922 "Delete should have non-zero range"
1923 );
1924
1925 let range_start = 4;
1927 let range_end = 8;
1928
1929 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
1930 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
1931
1932 assert_eq!(start_line, 0, "Delete start on line 0");
1933 assert_eq!(start_char, 4, "Delete start at char 4");
1934 assert_eq!(end_line, 1, "Delete end on line 1");
1935 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
1936 }
1937
1938 #[test]
1939 fn test_lsp_incremental_utf16_encoding() {
1940 use crate::model::buffer::Buffer;
1943
1944 let buffer = Buffer::from_str_test("😀hello");
1946
1947 let (line, character) = buffer.position_to_lsp_position(4);
1949
1950 assert_eq!(line, 0);
1951 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
1952
1953 let (line, character) = buffer.position_to_lsp_position(9);
1955
1956 assert_eq!(line, 0);
1957 assert_eq!(
1958 character, 7,
1959 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
1960 );
1961
1962 let buffer = Buffer::from_str_test("café");
1964
1965 let (line, character) = buffer.position_to_lsp_position(3);
1967
1968 assert_eq!(line, 0);
1969 assert_eq!(character, 3);
1970
1971 let (line, character) = buffer.position_to_lsp_position(5);
1973
1974 assert_eq!(line, 0);
1975 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
1976 }
1977
1978 #[test]
1979 fn test_lsp_content_change_event_structure() {
1980 let insert_change = TextDocumentContentChangeEvent {
1984 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
1985 range_length: None,
1986 text: "NEW".to_string(),
1987 };
1988
1989 assert!(insert_change.range.is_some());
1990 assert_eq!(insert_change.text, "NEW");
1991 let range = insert_change.range.unwrap();
1992 assert_eq!(
1993 range.start, range.end,
1994 "Insert should have zero-width range"
1995 );
1996
1997 let delete_change = TextDocumentContentChangeEvent {
1999 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2000 range_length: None,
2001 text: String::new(),
2002 };
2003
2004 assert!(delete_change.range.is_some());
2005 assert_eq!(delete_change.text, "");
2006 let range = delete_change.range.unwrap();
2007 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2008 assert_eq!(range.start.line, 0);
2009 assert_eq!(range.start.character, 2);
2010 assert_eq!(range.end.line, 0);
2011 assert_eq!(range.end.character, 7);
2012 }
2013
2014 #[test]
2015 fn test_goto_matching_bracket_forward() {
2016 let config = Config::default();
2017 let (dir_context, _temp) = test_dir_context();
2018 let mut editor = Editor::new(
2019 config,
2020 80,
2021 24,
2022 dir_context,
2023 crate::view::color_support::ColorCapability::TrueColor,
2024 test_filesystem(),
2025 )
2026 .unwrap();
2027
2028 let cursor_id = editor.active_cursors().primary_id();
2030 editor.apply_event_to_active_buffer(&Event::Insert {
2031 position: 0,
2032 text: "fn main() { let x = (1 + 2); }".to_string(),
2033 cursor_id,
2034 });
2035
2036 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2038 cursor_id,
2039 old_position: 31,
2040 new_position: 10,
2041 old_anchor: None,
2042 new_anchor: None,
2043 old_sticky_column: 0,
2044 new_sticky_column: 0,
2045 });
2046
2047 assert_eq!(editor.active_cursors().primary().position, 10);
2048
2049 editor.goto_matching_bracket();
2051
2052 assert_eq!(editor.active_cursors().primary().position, 29);
2057 }
2058
2059 #[test]
2060 fn test_goto_matching_bracket_backward() {
2061 let config = Config::default();
2062 let (dir_context, _temp) = test_dir_context();
2063 let mut editor = Editor::new(
2064 config,
2065 80,
2066 24,
2067 dir_context,
2068 crate::view::color_support::ColorCapability::TrueColor,
2069 test_filesystem(),
2070 )
2071 .unwrap();
2072
2073 let cursor_id = editor.active_cursors().primary_id();
2075 editor.apply_event_to_active_buffer(&Event::Insert {
2076 position: 0,
2077 text: "fn main() { let x = (1 + 2); }".to_string(),
2078 cursor_id,
2079 });
2080
2081 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2083 cursor_id,
2084 old_position: 31,
2085 new_position: 26,
2086 old_anchor: None,
2087 new_anchor: None,
2088 old_sticky_column: 0,
2089 new_sticky_column: 0,
2090 });
2091
2092 editor.goto_matching_bracket();
2094
2095 assert_eq!(editor.active_cursors().primary().position, 20);
2097 }
2098
2099 #[test]
2100 fn test_goto_matching_bracket_nested() {
2101 let config = Config::default();
2102 let (dir_context, _temp) = test_dir_context();
2103 let mut editor = Editor::new(
2104 config,
2105 80,
2106 24,
2107 dir_context,
2108 crate::view::color_support::ColorCapability::TrueColor,
2109 test_filesystem(),
2110 )
2111 .unwrap();
2112
2113 let cursor_id = editor.active_cursors().primary_id();
2115 editor.apply_event_to_active_buffer(&Event::Insert {
2116 position: 0,
2117 text: "{a{b{c}d}e}".to_string(),
2118 cursor_id,
2119 });
2120
2121 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2123 cursor_id,
2124 old_position: 11,
2125 new_position: 0,
2126 old_anchor: None,
2127 new_anchor: None,
2128 old_sticky_column: 0,
2129 new_sticky_column: 0,
2130 });
2131
2132 editor.goto_matching_bracket();
2134
2135 assert_eq!(editor.active_cursors().primary().position, 10);
2137 }
2138
2139 #[test]
2140 fn test_search_case_sensitive() {
2141 let config = Config::default();
2142 let (dir_context, _temp) = test_dir_context();
2143 let mut editor = Editor::new(
2144 config,
2145 80,
2146 24,
2147 dir_context,
2148 crate::view::color_support::ColorCapability::TrueColor,
2149 test_filesystem(),
2150 )
2151 .unwrap();
2152
2153 let cursor_id = editor.active_cursors().primary_id();
2155 editor.apply_event_to_active_buffer(&Event::Insert {
2156 position: 0,
2157 text: "Hello hello HELLO".to_string(),
2158 cursor_id,
2159 });
2160
2161 editor.search_case_sensitive = false;
2163 editor.perform_search("hello");
2164
2165 let search_state = editor.search_state.as_ref().unwrap();
2166 assert_eq!(
2167 search_state.matches.len(),
2168 3,
2169 "Should find all 3 matches case-insensitively"
2170 );
2171
2172 editor.search_case_sensitive = true;
2174 editor.perform_search("hello");
2175
2176 let search_state = editor.search_state.as_ref().unwrap();
2177 assert_eq!(
2178 search_state.matches.len(),
2179 1,
2180 "Should find only 1 exact match"
2181 );
2182 assert_eq!(
2183 search_state.matches[0], 6,
2184 "Should find 'hello' at position 6"
2185 );
2186 }
2187
2188 #[test]
2189 fn test_search_whole_word() {
2190 let config = Config::default();
2191 let (dir_context, _temp) = test_dir_context();
2192 let mut editor = Editor::new(
2193 config,
2194 80,
2195 24,
2196 dir_context,
2197 crate::view::color_support::ColorCapability::TrueColor,
2198 test_filesystem(),
2199 )
2200 .unwrap();
2201
2202 let cursor_id = editor.active_cursors().primary_id();
2204 editor.apply_event_to_active_buffer(&Event::Insert {
2205 position: 0,
2206 text: "test testing tested attest test".to_string(),
2207 cursor_id,
2208 });
2209
2210 editor.search_whole_word = false;
2212 editor.search_case_sensitive = true;
2213 editor.perform_search("test");
2214
2215 let search_state = editor.search_state.as_ref().unwrap();
2216 assert_eq!(
2217 search_state.matches.len(),
2218 5,
2219 "Should find 'test' in all occurrences"
2220 );
2221
2222 editor.search_whole_word = true;
2224 editor.perform_search("test");
2225
2226 let search_state = editor.search_state.as_ref().unwrap();
2227 assert_eq!(
2228 search_state.matches.len(),
2229 2,
2230 "Should find only whole word 'test'"
2231 );
2232 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2233 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2234 }
2235
2236 #[test]
2237 fn test_search_scan_completes_when_capped() {
2238 let config = Config::default();
2244 let (dir_context, _temp) = test_dir_context();
2245 let mut editor = Editor::new(
2246 config,
2247 80,
2248 24,
2249 dir_context,
2250 crate::view::color_support::ColorCapability::TrueColor,
2251 test_filesystem(),
2252 )
2253 .unwrap();
2254
2255 let buffer_id = editor.active_buffer();
2258 let regex = regex::bytes::Regex::new("test").unwrap();
2259 let fake_chunks = vec![
2260 crate::model::buffer::LineScanChunk {
2261 leaf_index: 0,
2262 byte_len: 100,
2263 already_known: true,
2264 },
2265 crate::model::buffer::LineScanChunk {
2266 leaf_index: 1,
2267 byte_len: 100,
2268 already_known: true,
2269 },
2270 ];
2271
2272 let chunked = crate::model::buffer::ChunkedSearchState {
2273 chunks: fake_chunks,
2274 next_chunk: 1, next_doc_offset: 100,
2276 total_bytes: 200,
2277 scanned_bytes: 100,
2278 regex,
2279 matches: vec![
2280 crate::model::buffer::SearchMatch {
2281 byte_offset: 10,
2282 length: 4,
2283 line: 1,
2284 column: 11,
2285 context: String::new(),
2286 },
2287 crate::model::buffer::SearchMatch {
2288 byte_offset: 50,
2289 length: 4,
2290 line: 1,
2291 column: 51,
2292 context: String::new(),
2293 },
2294 ],
2295 overlap_tail: Vec::new(),
2296 overlap_doc_offset: 0,
2297 max_matches: 10_000,
2298 capped: true, query_len: 4,
2300 running_line: 1,
2301 };
2302
2303 editor.search_scan.start(
2304 buffer_id,
2305 Vec::new(),
2306 chunked,
2307 "test".to_string(),
2308 None,
2309 false,
2310 false,
2311 false,
2312 );
2313
2314 let result = editor.process_search_scan();
2316 assert!(
2317 result,
2318 "process_search_scan should return true (needs render)"
2319 );
2320
2321 assert_eq!(
2323 editor.search_scan.buffer_id(),
2324 None,
2325 "search_scan should be drained after capped scan completes"
2326 );
2327
2328 let search_state = editor
2330 .search_state
2331 .as_ref()
2332 .expect("search_state should be set after scan finishes");
2333 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2334 assert_eq!(search_state.query, "test");
2335 assert!(
2336 search_state.capped,
2337 "search_state should be marked as capped"
2338 );
2339 }
2340
2341 #[test]
2342 fn test_bookmarks() {
2343 let config = Config::default();
2344 let (dir_context, _temp) = test_dir_context();
2345 let mut editor = Editor::new(
2346 config,
2347 80,
2348 24,
2349 dir_context,
2350 crate::view::color_support::ColorCapability::TrueColor,
2351 test_filesystem(),
2352 )
2353 .unwrap();
2354
2355 let cursor_id = editor.active_cursors().primary_id();
2357 editor.apply_event_to_active_buffer(&Event::Insert {
2358 position: 0,
2359 text: "Line 1\nLine 2\nLine 3".to_string(),
2360 cursor_id,
2361 });
2362
2363 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2365 cursor_id,
2366 old_position: 21,
2367 new_position: 7,
2368 old_anchor: None,
2369 new_anchor: None,
2370 old_sticky_column: 0,
2371 new_sticky_column: 0,
2372 });
2373
2374 editor.set_bookmark('1');
2376 assert_eq!(editor.bookmarks.get('1').map(|b| b.position), Some(7));
2377
2378 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2380 cursor_id,
2381 old_position: 7,
2382 new_position: 14,
2383 old_anchor: None,
2384 new_anchor: None,
2385 old_sticky_column: 0,
2386 new_sticky_column: 0,
2387 });
2388
2389 editor.jump_to_bookmark('1');
2391 assert_eq!(editor.active_cursors().primary().position, 7);
2392
2393 editor.clear_bookmark('1');
2395 assert_eq!(editor.bookmarks.get('1'), None);
2396 }
2397
2398 #[test]
2399 fn test_action_enum_new_variants() {
2400 use serde_json::json;
2402
2403 let args = HashMap::new();
2404 assert_eq!(
2405 Action::from_str("smart_home", &args),
2406 Some(Action::SmartHome)
2407 );
2408 assert_eq!(
2409 Action::from_str("dedent_selection", &args),
2410 Some(Action::DedentSelection)
2411 );
2412 assert_eq!(
2413 Action::from_str("toggle_comment", &args),
2414 Some(Action::ToggleComment)
2415 );
2416 assert_eq!(
2417 Action::from_str("goto_matching_bracket", &args),
2418 Some(Action::GoToMatchingBracket)
2419 );
2420 assert_eq!(
2421 Action::from_str("list_bookmarks", &args),
2422 Some(Action::ListBookmarks)
2423 );
2424 assert_eq!(
2425 Action::from_str("toggle_search_case_sensitive", &args),
2426 Some(Action::ToggleSearchCaseSensitive)
2427 );
2428 assert_eq!(
2429 Action::from_str("toggle_search_whole_word", &args),
2430 Some(Action::ToggleSearchWholeWord)
2431 );
2432
2433 let mut args_with_char = HashMap::new();
2435 args_with_char.insert("char".to_string(), json!("5"));
2436 assert_eq!(
2437 Action::from_str("set_bookmark", &args_with_char),
2438 Some(Action::SetBookmark('5'))
2439 );
2440 assert_eq!(
2441 Action::from_str("jump_to_bookmark", &args_with_char),
2442 Some(Action::JumpToBookmark('5'))
2443 );
2444 assert_eq!(
2445 Action::from_str("clear_bookmark", &args_with_char),
2446 Some(Action::ClearBookmark('5'))
2447 );
2448 }
2449
2450 #[test]
2451 fn test_keybinding_new_defaults() {
2452 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2453
2454 let mut config = Config::default();
2458 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2459 let resolver = KeybindingResolver::new(&config);
2460
2461 let event = KeyEvent {
2463 code: KeyCode::Char('/'),
2464 modifiers: KeyModifiers::CONTROL,
2465 kind: KeyEventKind::Press,
2466 state: KeyEventState::NONE,
2467 };
2468 let action = resolver.resolve(&event, KeyContext::Normal);
2469 assert_eq!(action, Action::ToggleComment);
2470
2471 let event = KeyEvent {
2473 code: KeyCode::Char(']'),
2474 modifiers: KeyModifiers::CONTROL,
2475 kind: KeyEventKind::Press,
2476 state: KeyEventState::NONE,
2477 };
2478 let action = resolver.resolve(&event, KeyContext::Normal);
2479 assert_eq!(action, Action::GoToMatchingBracket);
2480
2481 let event = KeyEvent {
2483 code: KeyCode::Tab,
2484 modifiers: KeyModifiers::SHIFT,
2485 kind: KeyEventKind::Press,
2486 state: KeyEventState::NONE,
2487 };
2488 let action = resolver.resolve(&event, KeyContext::Normal);
2489 assert_eq!(action, Action::DedentSelection);
2490
2491 let event = KeyEvent {
2493 code: KeyCode::Char('g'),
2494 modifiers: KeyModifiers::CONTROL,
2495 kind: KeyEventKind::Press,
2496 state: KeyEventState::NONE,
2497 };
2498 let action = resolver.resolve(&event, KeyContext::Normal);
2499 assert_eq!(action, Action::GotoLine);
2500
2501 let event = KeyEvent {
2503 code: KeyCode::Char('5'),
2504 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2505 kind: KeyEventKind::Press,
2506 state: KeyEventState::NONE,
2507 };
2508 let action = resolver.resolve(&event, KeyContext::Normal);
2509 assert_eq!(action, Action::SetBookmark('5'));
2510
2511 let event = KeyEvent {
2512 code: KeyCode::Char('5'),
2513 modifiers: KeyModifiers::ALT,
2514 kind: KeyEventKind::Press,
2515 state: KeyEventState::NONE,
2516 };
2517 let action = resolver.resolve(&event, KeyContext::Normal);
2518 assert_eq!(action, Action::JumpToBookmark('5'));
2519 }
2520
2521 #[test]
2533 fn test_lsp_rename_didchange_positions_bug() {
2534 use crate::model::buffer::Buffer;
2535
2536 let config = Config::default();
2537 let (dir_context, _temp) = test_dir_context();
2538 let mut editor = Editor::new(
2539 config,
2540 80,
2541 24,
2542 dir_context,
2543 crate::view::color_support::ColorCapability::TrueColor,
2544 test_filesystem(),
2545 )
2546 .unwrap();
2547
2548 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2552 editor.active_state_mut().buffer =
2553 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2554
2555 let cursor_id = editor.active_cursors().primary_id();
2560
2561 let batch = Event::Batch {
2562 events: vec![
2563 Event::Delete {
2565 range: 23..26, deleted_text: "val".to_string(),
2567 cursor_id,
2568 },
2569 Event::Insert {
2570 position: 23,
2571 text: "value".to_string(),
2572 cursor_id,
2573 },
2574 Event::Delete {
2576 range: 7..10, deleted_text: "val".to_string(),
2578 cursor_id,
2579 },
2580 Event::Insert {
2581 position: 7,
2582 text: "value".to_string(),
2583 cursor_id,
2584 },
2585 ],
2586 description: "LSP Rename".to_string(),
2587 };
2588
2589 let lsp_changes_before = editor.collect_lsp_changes(&batch);
2591
2592 editor.apply_event_to_active_buffer(&batch);
2594
2595 let lsp_changes_after = editor.collect_lsp_changes(&batch);
2598
2599 let final_content = editor.active_state().buffer.to_string().unwrap();
2601 assert_eq!(
2602 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2603 "Buffer should have 'value' in both places"
2604 );
2605
2606 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2612
2613 let first_delete = &lsp_changes_before[0];
2614 let first_del_range = first_delete.range.unwrap();
2615 assert_eq!(
2616 first_del_range.start.line, 1,
2617 "First delete should be on line 1 (BEFORE)"
2618 );
2619 assert_eq!(
2620 first_del_range.start.character, 4,
2621 "First delete start should be at char 4 (BEFORE)"
2622 );
2623
2624 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2630
2631 let first_delete_after = &lsp_changes_after[0];
2632 let first_del_range_after = first_delete_after.range.unwrap();
2633
2634 eprintln!("BEFORE modification:");
2637 eprintln!(
2638 " Delete at line {}, char {}-{}",
2639 first_del_range.start.line,
2640 first_del_range.start.character,
2641 first_del_range.end.character
2642 );
2643 eprintln!("AFTER modification:");
2644 eprintln!(
2645 " Delete at line {}, char {}-{}",
2646 first_del_range_after.start.line,
2647 first_del_range_after.start.character,
2648 first_del_range_after.end.character
2649 );
2650
2651 assert_ne!(
2669 first_del_range_after.end.character, first_del_range.end.character,
2670 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2671 );
2672
2673 eprintln!("\n=== BUG DEMONSTRATED ===");
2674 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2675 eprintln!("the positions are WRONG because they're calculated from the");
2676 eprintln!("modified buffer, not the original buffer.");
2677 eprintln!("This causes the second rename to fail with 'content modified' error.");
2678 eprintln!("========================\n");
2679 }
2680
2681 #[test]
2682 fn test_lsp_rename_preserves_cursor_position() {
2683 use crate::model::buffer::Buffer;
2684
2685 let config = Config::default();
2686 let (dir_context, _temp) = test_dir_context();
2687 let mut editor = Editor::new(
2688 config,
2689 80,
2690 24,
2691 dir_context,
2692 crate::view::color_support::ColorCapability::TrueColor,
2693 test_filesystem(),
2694 )
2695 .unwrap();
2696
2697 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2701 editor.active_state_mut().buffer =
2702 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2703
2704 let original_cursor_pos = 23;
2706 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2707
2708 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2710 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2711 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2712
2713 let cursor_id = editor.active_cursors().primary_id();
2716 let buffer_id = editor.active_buffer();
2717
2718 let events = vec![
2719 Event::Delete {
2721 range: 23..26, deleted_text: "val".to_string(),
2723 cursor_id,
2724 },
2725 Event::Insert {
2726 position: 23,
2727 text: "value".to_string(),
2728 cursor_id,
2729 },
2730 Event::Delete {
2732 range: 7..10, deleted_text: "val".to_string(),
2734 cursor_id,
2735 },
2736 Event::Insert {
2737 position: 7,
2738 text: "value".to_string(),
2739 cursor_id,
2740 },
2741 ];
2742
2743 editor
2745 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2746 .unwrap();
2747
2748 let final_content = editor.active_state().buffer.to_string().unwrap();
2750 assert_eq!(
2751 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2752 "Buffer should have 'value' in both places"
2753 );
2754
2755 let final_cursor_pos = editor.active_cursors().primary().position;
2763 let expected_cursor_pos = 25; assert_eq!(
2766 final_cursor_pos, expected_cursor_pos,
2767 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2768 Original pos: {}, expected adjustment: +2 for first rename",
2769 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2770 );
2771
2772 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2774 assert_eq!(
2775 text_at_new_cursor, "value",
2776 "Cursor should be at the start of 'value' after rename"
2777 );
2778 }
2779
2780 #[test]
2781 fn test_lsp_rename_twice_consecutive() {
2782 use crate::model::buffer::Buffer;
2785
2786 let config = Config::default();
2787 let (dir_context, _temp) = test_dir_context();
2788 let mut editor = Editor::new(
2789 config,
2790 80,
2791 24,
2792 dir_context,
2793 crate::view::color_support::ColorCapability::TrueColor,
2794 test_filesystem(),
2795 )
2796 .unwrap();
2797
2798 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2800 editor.active_state_mut().buffer =
2801 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2802
2803 let cursor_id = editor.active_cursors().primary_id();
2804 let buffer_id = editor.active_buffer();
2805
2806 let events1 = vec![
2809 Event::Delete {
2811 range: 23..26,
2812 deleted_text: "val".to_string(),
2813 cursor_id,
2814 },
2815 Event::Insert {
2816 position: 23,
2817 text: "value".to_string(),
2818 cursor_id,
2819 },
2820 Event::Delete {
2822 range: 7..10,
2823 deleted_text: "val".to_string(),
2824 cursor_id,
2825 },
2826 Event::Insert {
2827 position: 7,
2828 text: "value".to_string(),
2829 cursor_id,
2830 },
2831 ];
2832
2833 let batch1 = Event::Batch {
2835 events: events1.clone(),
2836 description: "LSP Rename 1".to_string(),
2837 };
2838
2839 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
2841
2842 assert_eq!(
2844 lsp_changes1.len(),
2845 4,
2846 "First rename should have 4 LSP changes"
2847 );
2848
2849 let first_del = &lsp_changes1[0];
2851 let first_del_range = first_del.range.unwrap();
2852 assert_eq!(first_del_range.start.line, 1, "First delete line");
2853 assert_eq!(
2854 first_del_range.start.character, 4,
2855 "First delete start char"
2856 );
2857 assert_eq!(first_del_range.end.character, 7, "First delete end char");
2858
2859 editor
2861 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
2862 .unwrap();
2863
2864 let after_first = editor.active_state().buffer.to_string().unwrap();
2866 assert_eq!(
2867 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
2868 "After first rename"
2869 );
2870
2871 let events2 = vec![
2881 Event::Delete {
2883 range: 25..30,
2884 deleted_text: "value".to_string(),
2885 cursor_id,
2886 },
2887 Event::Insert {
2888 position: 25,
2889 text: "x".to_string(),
2890 cursor_id,
2891 },
2892 Event::Delete {
2894 range: 7..12,
2895 deleted_text: "value".to_string(),
2896 cursor_id,
2897 },
2898 Event::Insert {
2899 position: 7,
2900 text: "x".to_string(),
2901 cursor_id,
2902 },
2903 ];
2904
2905 let batch2 = Event::Batch {
2907 events: events2.clone(),
2908 description: "LSP Rename 2".to_string(),
2909 };
2910
2911 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
2913
2914 assert_eq!(
2918 lsp_changes2.len(),
2919 4,
2920 "Second rename should have 4 LSP changes"
2921 );
2922
2923 let second_first_del = &lsp_changes2[0];
2925 let second_first_del_range = second_first_del.range.unwrap();
2926 assert_eq!(
2927 second_first_del_range.start.line, 1,
2928 "Second rename first delete should be on line 1"
2929 );
2930 assert_eq!(
2931 second_first_del_range.start.character, 4,
2932 "Second rename first delete start should be at char 4"
2933 );
2934 assert_eq!(
2935 second_first_del_range.end.character, 9,
2936 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
2937 );
2938
2939 let second_third_del = &lsp_changes2[2];
2941 let second_third_del_range = second_third_del.range.unwrap();
2942 assert_eq!(
2943 second_third_del_range.start.line, 0,
2944 "Second rename third delete should be on line 0"
2945 );
2946 assert_eq!(
2947 second_third_del_range.start.character, 7,
2948 "Second rename third delete start should be at char 7"
2949 );
2950 assert_eq!(
2951 second_third_del_range.end.character, 12,
2952 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
2953 );
2954
2955 editor
2957 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
2958 .unwrap();
2959
2960 let after_second = editor.active_state().buffer.to_string().unwrap();
2962 assert_eq!(
2963 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
2964 "After second rename"
2965 );
2966 }
2967
2968 #[test]
2969 fn test_ensure_active_tab_visible_static_offset() {
2970 let config = Config::default();
2971 let (dir_context, _temp) = test_dir_context();
2972 let mut editor = Editor::new(
2973 config,
2974 80,
2975 24,
2976 dir_context,
2977 crate::view::color_support::ColorCapability::TrueColor,
2978 test_filesystem(),
2979 )
2980 .unwrap();
2981 let split_id = editor.split_manager.active_split();
2982
2983 let buf1 = editor.new_buffer();
2985 editor
2986 .buffers
2987 .get_mut(&buf1)
2988 .unwrap()
2989 .buffer
2990 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
2991 let buf2 = editor.new_buffer();
2992 editor
2993 .buffers
2994 .get_mut(&buf2)
2995 .unwrap()
2996 .buffer
2997 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
2998 let buf3 = editor.new_buffer();
2999 editor
3000 .buffers
3001 .get_mut(&buf3)
3002 .unwrap()
3003 .buffer
3004 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3005
3006 {
3007 use crate::view::split::TabTarget;
3008 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
3009 view_state.open_buffers = vec![
3010 TabTarget::Buffer(buf1),
3011 TabTarget::Buffer(buf2),
3012 TabTarget::Buffer(buf3),
3013 ];
3014 view_state.tab_scroll_offset = 50;
3015 }
3016
3017 editor.ensure_active_tab_visible(split_id, buf1, 25);
3021 assert_eq!(
3022 editor
3023 .split_view_states
3024 .get(&split_id)
3025 .unwrap()
3026 .tab_scroll_offset,
3027 0
3028 );
3029
3030 editor.ensure_active_tab_visible(split_id, buf3, 25);
3032 let view_state = editor.split_view_states.get(&split_id).unwrap();
3033 assert!(view_state.tab_scroll_offset > 0);
3034 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3035 let total_width: usize = buffer_ids
3036 .iter()
3037 .enumerate()
3038 .map(|(idx, id)| {
3039 let state = editor.buffers.get(id).unwrap();
3040 let name_len = state
3041 .buffer
3042 .file_path()
3043 .and_then(|p| p.file_name())
3044 .and_then(|n| n.to_str())
3045 .map(|s| s.chars().count())
3046 .unwrap_or(0);
3047 let tab_width = 2 + name_len;
3048 if idx < buffer_ids.len() - 1 {
3049 tab_width + 1 } else {
3051 tab_width
3052 }
3053 })
3054 .sum();
3055 assert!(view_state.tab_scroll_offset <= total_width);
3056 }
3057}