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, InteractiveReplaceState, LspMessageEntry, LspProgressInfo, MouseState,
167 SearchState, TabContextMenu, DEFAULT_BACKGROUND_FILE,
168};
169use crate::config::Config;
170use crate::config_io::DirectoryContext;
171use crate::input::buffer_mode::ModeRegistry;
172use crate::input::command_registry::CommandRegistry;
173use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
174use crate::input::position_history::PositionHistory;
175use crate::input::quick_open::{
176 BufferProvider, CommandProvider, FileProvider, GotoLineProvider, QuickOpenRegistry,
177};
178use crate::model::cursor::Cursors;
179use crate::model::event::{Event, EventLog, LeafId, SplitDirection};
180use crate::model::filesystem::FileSystem;
181use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
182use crate::services::fs::FsManager;
183use crate::services::lsp::manager::LspManager;
184use crate::services::plugins::PluginManager;
185use crate::services::recovery::{RecoveryConfig, RecoveryService};
186use crate::services::time_source::{RealTimeSource, SharedTimeSource};
187use crate::state::EditorState;
188use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
189use crate::view::file_tree::{FileTree, FileTreeView};
190use crate::view::prompt::{Prompt, PromptType};
191use crate::view::scroll_sync::ScrollSyncManager;
192use crate::view::split::{SplitManager, SplitViewState};
193use crate::view::ui::{
194 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
195};
196use crossterm::event::{KeyCode, KeyModifiers};
197use ratatui::{
198 layout::{Constraint, Direction, Layout},
199 Frame,
200};
201use std::collections::{HashMap, HashSet};
202use std::ops::Range;
203use std::path::{Path, PathBuf};
204use std::sync::{Arc, RwLock};
205use std::time::Instant;
206
207pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
209pub use self::warning_domains::{
210 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
211 WarningDomainRegistry, WarningLevel, WarningPopupContent,
212};
213pub use crate::model::event::BufferId;
214
215fn uri_to_path(uri: &lsp_types::Uri) -> Result<PathBuf, String> {
217 fresh_core::file_uri::lsp_uri_to_path(uri).ok_or_else(|| "URI is not a file path".to_string())
218}
219
220#[derive(Clone, Debug)]
222pub struct PendingGrammar {
223 pub language: String,
225 pub grammar_path: String,
227 pub extensions: Vec<String>,
229}
230
231#[derive(Clone, Debug)]
233struct SemanticTokenRangeRequest {
234 buffer_id: BufferId,
235 version: u64,
236 range: Range<usize>,
237 start_line: usize,
238 end_line: usize,
239}
240
241#[derive(Clone, Copy, Debug)]
242enum SemanticTokensFullRequestKind {
243 Full,
244 FullDelta,
245}
246
247#[derive(Clone, Debug)]
248struct SemanticTokenFullRequest {
249 buffer_id: BufferId,
250 version: u64,
251 kind: SemanticTokensFullRequestKind,
252}
253
254#[derive(Clone, Debug)]
255struct FoldingRangeRequest {
256 buffer_id: BufferId,
257 version: u64,
258}
259
260#[derive(Clone, Debug)]
261struct InlayHintsRequest {
262 buffer_id: BufferId,
263 version: u64,
264}
265
266#[derive(Debug, Clone)]
272pub struct DabbrevCycleState {
273 pub original_prefix: String,
275 pub word_start: usize,
277 pub candidates: Vec<String>,
279 pub index: usize,
281}
282
283pub struct Editor {
285 buffers: HashMap<BufferId, EditorState>,
287
288 event_logs: HashMap<BufferId, EventLog>,
293
294 next_buffer_id: usize,
296
297 config: Arc<Config>,
316
317 config_snapshot_anchor: Arc<Config>,
329
330 config_cached_json: Arc<serde_json::Value>,
336
337 user_config_raw: Arc<serde_json::Value>,
342
343 dir_context: DirectoryContext,
345
346 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
348
349 pending_grammars: Vec<PendingGrammar>,
351
352 grammar_reload_pending: bool,
356
357 grammar_build_in_progress: bool,
360
361 needs_full_grammar_build: bool,
365
366 streaming_grep_cancellation: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
368
369 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
373
374 theme: crate::view::theme::Theme,
376
377 theme_registry: crate::view::theme::ThemeRegistry,
379
380 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
382
383 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
385
386 ansi_background_path: Option<PathBuf>,
388
389 background_fade: f32,
391
392 keybindings: Arc<RwLock<KeybindingResolver>>,
394
395 clipboard: crate::services::clipboard::Clipboard,
397
398 should_quit: bool,
400
401 should_detach: bool,
403
404 session_mode: bool,
406
407 software_cursor_only: bool,
409
410 session_name: Option<String>,
412
413 pending_escape_sequences: Vec<u8>,
416
417 restart_with_dir: Option<PathBuf>,
420
421 status_message: Option<String>,
423
424 plugin_status_message: Option<String>,
426
427 plugin_errors: Vec<String>,
430
431 prompt: Option<Prompt>,
433
434 terminal_width: u16,
436 terminal_height: u16,
437
438 lsp: Option<LspManager>,
440
441 buffer_metadata: HashMap<BufferId, BufferMetadata>,
443
444 mode_registry: ModeRegistry,
446
447 tokio_runtime: Option<tokio::runtime::Runtime>,
449
450 async_bridge: Option<AsyncBridge>,
452
453 split_manager: SplitManager,
455
456 split_view_states: HashMap<LeafId, SplitViewState>,
460
461 previous_viewports: HashMap<LeafId, (usize, u16, u16)>,
465
466 scroll_sync_manager: ScrollSyncManager,
469
470 file_explorer: Option<FileTreeView>,
472
473 preview: Option<(LeafId, BufferId)>,
487
488 suppress_position_history_once: bool,
493
494 fs_manager: Arc<FsManager>,
496
497 filesystem: Arc<dyn FileSystem + Send + Sync>,
499
500 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
503
504 process_spawner: Arc<dyn crate::services::remote::ProcessSpawner>,
506
507 file_explorer_visible: bool,
509
510 file_explorer_sync_in_progress: bool,
513
514 file_explorer_width_percent: f32,
517
518 pending_file_explorer_show_hidden: Option<bool>,
520
521 pending_file_explorer_show_gitignored: Option<bool>,
523
524 file_explorer_decorations: HashMap<String, Vec<crate::view::file_tree::FileExplorerDecoration>>,
526
527 file_explorer_decoration_cache: crate::view::file_tree::FileExplorerDecorationCache,
529
530 menu_bar_visible: bool,
532
533 menu_bar_auto_shown: bool,
536
537 tab_bar_visible: bool,
539
540 status_bar_visible: bool,
542
543 prompt_line_visible: bool,
545
546 mouse_enabled: bool,
548
549 same_buffer_scroll_sync: bool,
551
552 mouse_cursor_position: Option<(u16, u16)>,
556
557 gpm_active: bool,
559
560 key_context: KeyContext,
562
563 menu_state: crate::view::ui::MenuState,
565
566 menus: crate::config::MenuConfig,
568
569 working_dir: PathBuf,
571
572 pub position_history: PositionHistory,
574
575 in_navigation: bool,
577
578 next_lsp_request_id: u64,
580
581 pending_completion_requests: HashSet<u64>,
583
584 completion_items: Option<Vec<lsp_types::CompletionItem>>,
587
588 scheduled_completion_trigger: Option<Instant>,
591
592 completion_service: crate::services::completion::CompletionService,
595
596 dabbrev_state: Option<DabbrevCycleState>,
600
601 pending_goto_definition_request: Option<u64>,
603
604 pending_references_request: Option<u64>,
606
607 pending_references_symbol: String,
609
610 pending_signature_help_request: Option<u64>,
612
613 pending_code_actions_requests: HashSet<u64>,
615
616 pending_code_actions_server_names: HashMap<u64, String>,
618
619 pending_code_actions: Option<Vec<(String, lsp_types::CodeActionOrCommand)>>,
623
624 pending_inlay_hints_requests: HashMap<u64, InlayHintsRequest>,
635
636 pending_folding_range_requests: HashMap<u64, FoldingRangeRequest>,
638
639 folding_ranges_in_flight: HashMap<BufferId, (u64, u64)>,
641
642 folding_ranges_debounce: HashMap<BufferId, Instant>,
644
645 pending_semantic_token_requests: HashMap<u64, SemanticTokenFullRequest>,
647
648 semantic_tokens_in_flight: HashMap<BufferId, (u64, u64, SemanticTokensFullRequestKind)>,
650
651 pending_semantic_token_range_requests: HashMap<u64, SemanticTokenRangeRequest>,
653
654 semantic_tokens_range_in_flight: HashMap<BufferId, (u64, usize, usize, u64)>,
656
657 semantic_tokens_range_last_request: HashMap<BufferId, (usize, usize, u64, Instant)>,
659
660 semantic_tokens_range_applied: HashMap<BufferId, (usize, usize, u64)>,
662
663 semantic_tokens_full_debounce: HashMap<BufferId, Instant>,
665
666 hover: hover::HoverState,
669
670 search_state: Option<SearchState>,
672
673 search_namespace: crate::view::overlay::OverlayNamespace,
675
676 lsp_diagnostic_namespace: crate::view::overlay::OverlayNamespace,
678
679 pending_search_range: Option<Range<usize>>,
681
682 interactive_replace_state: Option<InteractiveReplaceState>,
684
685 mouse_state: MouseState,
687
688 tab_context_menu: Option<TabContextMenu>,
690
691 theme_info_popup: Option<types::ThemeInfoPopup>,
693
694 pub(crate) cached_layout: CachedLayout,
696
697 command_registry: Arc<RwLock<CommandRegistry>>,
699
700 quick_open_registry: QuickOpenRegistry,
702
703 plugin_manager: PluginManager,
705
706 plugin_dev_workspaces:
710 HashMap<BufferId, crate::services::plugins::plugin_dev_workspace::PluginDevWorkspace>,
711
712 seen_byte_ranges: HashMap<BufferId, std::collections::HashSet<(usize, usize)>>,
716
717 panel_ids: HashMap<String, BufferId>,
720
721 buffer_groups: HashMap<types::BufferGroupId, types::BufferGroup>,
723 buffer_to_group: HashMap<BufferId, types::BufferGroupId>,
725 next_buffer_group_id: usize,
727
728 pub(crate) grouped_subtrees:
736 HashMap<crate::model::event::LeafId, crate::view::split::SplitNode>,
737
738 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
741
742 prompt_histories: HashMap<String, crate::input::input_history::InputHistory>,
745
746 pending_async_prompt_callback: Option<fresh_core::api::JsCallbackId>,
750
751 lsp_progress: std::collections::HashMap<String, LspProgressInfo>,
753
754 lsp_server_statuses:
756 std::collections::HashMap<(String, String), crate::services::async_bridge::LspServerStatus>,
757
758 lsp_window_messages: Vec<LspMessageEntry>,
760
761 lsp_log_messages: Vec<LspMessageEntry>,
763
764 diagnostic_result_ids: HashMap<String, String>,
767
768 scheduled_diagnostic_pull: Option<(BufferId, Instant)>,
771
772 scheduled_inlay_hints_request: Option<(BufferId, Instant)>,
775
776 stored_push_diagnostics: HashMap<String, HashMap<String, Vec<lsp_types::Diagnostic>>>,
779
780 stored_pull_diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
782
783 stored_diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
788
789 stored_folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
792
793 event_broadcaster: crate::model::control_event::EventBroadcaster,
795
796 bookmarks: bookmarks::BookmarkState,
798
799 search_case_sensitive: bool,
801 search_whole_word: bool,
802 search_use_regex: bool,
803 search_confirm_each: bool,
805
806 macros: macros::MacroState,
809
810 #[cfg(feature = "plugins")]
812 pending_plugin_actions: Vec<(
813 String,
814 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
815 )>,
816
817 #[cfg(feature = "plugins")]
819 plugin_render_requested: bool,
820
821 chord_state: Vec<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
824
825 pending_lsp_confirmation: Option<String>,
828
829 pending_lsp_status_popup: Option<Vec<(String, String)>>,
833
834 user_dismissed_lsp_languages: std::collections::HashSet<String>,
842
843 pending_close_buffer: Option<BufferId>,
846
847 auto_revert_enabled: bool,
849
850 last_auto_revert_poll: std::time::Instant,
852
853 last_file_tree_poll: std::time::Instant,
855
856 git_index_resolved: bool,
858
859 file_mod_times: HashMap<PathBuf, std::time::SystemTime>,
862
863 dir_mod_times: HashMap<PathBuf, std::time::SystemTime>,
866
867 #[allow(clippy::type_complexity)]
871 pending_file_poll_rx:
872 Option<std::sync::mpsc::Receiver<Vec<(PathBuf, Option<std::time::SystemTime>)>>>,
873
874 #[allow(clippy::type_complexity)]
877 pending_dir_poll_rx: Option<
878 std::sync::mpsc::Receiver<(
879 Vec<(
880 crate::view::file_tree::NodeId,
881 PathBuf,
882 Option<std::time::SystemTime>,
883 )>,
884 Option<(PathBuf, std::time::SystemTime)>,
885 )>,
886 >,
887
888 file_rapid_change_counts: HashMap<PathBuf, (std::time::Instant, u32)>,
891
892 file_open_state: Option<file_open::FileOpenState>,
894
895 file_browser_layout: Option<crate::view::ui::FileBrowserLayout>,
897
898 recovery_service: RecoveryService,
900
901 full_redraw_requested: bool,
903
904 time_source: SharedTimeSource,
906
907 last_auto_recovery_save: std::time::Instant,
909
910 last_persistent_auto_save: std::time::Instant,
912
913 active_custom_contexts: HashSet<String>,
916
917 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
920
921 editor_mode: Option<String>,
924
925 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
927
928 status_log_path: Option<PathBuf>,
930
931 warning_domains: WarningDomainRegistry,
934
935 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
937
938 terminal_manager: crate::services::terminal::TerminalManager,
940
941 terminal_buffers: HashMap<BufferId, crate::services::terminal::TerminalId>,
943
944 terminal_backing_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
946
947 terminal_log_files: HashMap<crate::services::terminal::TerminalId, std::path::PathBuf>,
949
950 terminal_mode: bool,
952
953 keyboard_capture: bool,
957
958 terminal_mode_resume: std::collections::HashSet<BufferId>,
962
963 previous_click_time: Option<std::time::Instant>,
965
966 previous_click_position: Option<(u16, u16)>,
969
970 click_count: u8,
972
973 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
975
976 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
978
979 pub(crate) event_debug: Option<event_debug::EventDebug>,
981
982 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
984
985 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
987
988 color_capability: crate::view::color_support::ColorCapability,
990
991 review_hunks: Vec<fresh_core::api::ReviewHunk>,
993
994 active_action_popup: Option<(String, Vec<(String, String)>)>,
997
998 composite_buffers: HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
1001
1002 composite_view_states:
1005 HashMap<(LeafId, BufferId), crate::view::composite_view::CompositeViewState>,
1006
1007 pending_file_opens: Vec<PendingFileOpen>,
1011
1012 pending_hot_exit_recovery: bool,
1014
1015 wait_tracking: HashMap<BufferId, (u64, bool)>,
1017 completed_waits: Vec<u64>,
1019
1020 stdin_stream: stdin_stream::StdinStream,
1022
1023 line_scan: line_scan::LineScan,
1025
1026 search_scan: search_scan::SearchScan,
1028
1029 search_overlay_top_byte: Option<usize>,
1032}
1033
1034#[derive(Debug, Clone)]
1036pub struct PendingFileOpen {
1037 pub path: PathBuf,
1039 pub line: Option<usize>,
1041 pub column: Option<usize>,
1043 pub end_line: Option<usize>,
1045 pub end_column: Option<usize>,
1047 pub message: Option<String>,
1049 pub wait_id: Option<u64>,
1051}
1052
1053impl Editor {
1054 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1056 let trimmed = input.trim();
1057
1058 if trimmed.is_empty() {
1059 self.ansi_background = None;
1060 self.ansi_background_path = None;
1061 self.set_status_message(t!("status.background_cleared").to_string());
1062 return Ok(());
1063 }
1064
1065 let input_path = Path::new(trimmed);
1066 let resolved = if input_path.is_absolute() {
1067 input_path.to_path_buf()
1068 } else {
1069 self.working_dir.join(input_path)
1070 };
1071
1072 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1073
1074 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1075
1076 self.ansi_background = Some(parsed);
1077 self.ansi_background_path = Some(canonical.clone());
1078 self.set_status_message(
1079 t!(
1080 "view.background_set",
1081 path = canonical.display().to_string()
1082 )
1083 .to_string(),
1084 );
1085
1086 Ok(())
1087 }
1088
1089 fn effective_tabs_width(&self) -> u16 {
1094 if self.file_explorer_visible && self.file_explorer.is_some() {
1095 let editor_percent = 1.0 - self.file_explorer_width_percent;
1097 (self.terminal_width as f32 * editor_percent) as u16
1098 } else {
1099 self.terminal_width
1100 }
1101 }
1102
1103 pub fn active_state(&self) -> &EditorState {
1105 self.buffers.get(&self.active_buffer()).unwrap()
1106 }
1107
1108 pub fn active_state_mut(&mut self) -> &mut EditorState {
1110 self.buffers.get_mut(&self.active_buffer()).unwrap()
1111 }
1112
1113 pub fn active_cursors(&self) -> &Cursors {
1117 let split_id = self.effective_active_split();
1118 &self.split_view_states.get(&split_id).unwrap().cursors
1119 }
1120
1121 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1123 let split_id = self.effective_active_split();
1124 &mut self.split_view_states.get_mut(&split_id).unwrap().cursors
1125 }
1126
1127 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1129 self.completion_items = Some(items);
1130 }
1131
1132 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1134 let active_split = self.split_manager.active_split();
1135 &self.split_view_states.get(&active_split).unwrap().viewport
1136 }
1137
1138 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1140 let active_split = self.split_manager.active_split();
1141 &mut self
1142 .split_view_states
1143 .get_mut(&active_split)
1144 .unwrap()
1145 .viewport
1146 }
1147
1148 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1150 if let Some(composite) = self.composite_buffers.get(&buffer_id) {
1152 return composite.name.clone();
1153 }
1154
1155 self.buffer_metadata
1156 .get(&buffer_id)
1157 .map(|m| m.display_name.clone())
1158 .or_else(|| {
1159 self.buffers.get(&buffer_id).and_then(|state| {
1160 state
1161 .buffer
1162 .file_path()
1163 .and_then(|p| p.file_name())
1164 .and_then(|n| n.to_str())
1165 .map(|s| s.to_string())
1166 })
1167 })
1168 .unwrap_or_else(|| "[No Name]".to_string())
1169 }
1170
1171 pub fn active_event_log(&self) -> &EventLog {
1181 self.event_logs.get(&self.active_buffer()).unwrap()
1182 }
1183
1184 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1186 self.event_logs.get_mut(&self.active_buffer()).unwrap()
1187 }
1188
1189 pub(super) fn update_modified_from_event_log(&mut self) {
1193 let is_at_saved = self
1194 .event_logs
1195 .get(&self.active_buffer())
1196 .map(|log| log.is_at_saved_position())
1197 .unwrap_or(false);
1198
1199 if let Some(state) = self.buffers.get_mut(&self.active_buffer()) {
1200 state.buffer.set_modified(!is_at_saved);
1201 }
1202 }
1203}
1204
1205fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1214 use crossterm::event::{KeyCode, KeyModifiers};
1215
1216 let mut modifiers = KeyModifiers::NONE;
1217 let mut remaining = key_str;
1218
1219 loop {
1221 if remaining.starts_with("C-") {
1222 modifiers |= KeyModifiers::CONTROL;
1223 remaining = &remaining[2..];
1224 } else if remaining.starts_with("M-") {
1225 modifiers |= KeyModifiers::ALT;
1226 remaining = &remaining[2..];
1227 } else if remaining.starts_with("S-") {
1228 modifiers |= KeyModifiers::SHIFT;
1229 remaining = &remaining[2..];
1230 } else {
1231 break;
1232 }
1233 }
1234
1235 let upper = remaining.to_uppercase();
1238 let code = match upper.as_str() {
1239 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1240 "TAB" => KeyCode::Tab,
1241 "BACKTAB" => KeyCode::BackTab,
1242 "ESC" | "ESCAPE" => KeyCode::Esc,
1243 "SPC" | "SPACE" => KeyCode::Char(' '),
1244 "DEL" | "DELETE" => KeyCode::Delete,
1245 "BS" | "BACKSPACE" => KeyCode::Backspace,
1246 "UP" => KeyCode::Up,
1247 "DOWN" => KeyCode::Down,
1248 "LEFT" => KeyCode::Left,
1249 "RIGHT" => KeyCode::Right,
1250 "HOME" => KeyCode::Home,
1251 "END" => KeyCode::End,
1252 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1253 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1254 s if s.starts_with('F') && s.len() > 1 => {
1255 if let Ok(n) = s[1..].parse::<u8>() {
1257 KeyCode::F(n)
1258 } else {
1259 return None;
1260 }
1261 }
1262 _ if remaining.len() == 1 => {
1263 let c = remaining.chars().next()?;
1266 if c.is_ascii_uppercase() {
1267 modifiers |= KeyModifiers::SHIFT;
1268 }
1269 KeyCode::Char(c.to_ascii_lowercase())
1270 }
1271 _ => return None,
1272 };
1273
1274 Some((code, modifiers))
1275}
1276
1277#[cfg(test)]
1278mod tests {
1279 use super::*;
1280 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1281 use tempfile::TempDir;
1282
1283 fn test_dir_context() -> (DirectoryContext, TempDir) {
1285 let temp_dir = TempDir::new().unwrap();
1286 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1287 (dir_context, temp_dir)
1288 }
1289
1290 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1292 Arc::new(crate::model::filesystem::StdFileSystem)
1293 }
1294
1295 #[test]
1296 fn test_editor_new() {
1297 let config = Config::default();
1298 let (dir_context, _temp) = test_dir_context();
1299 let editor = Editor::new(
1300 config,
1301 80,
1302 24,
1303 dir_context,
1304 crate::view::color_support::ColorCapability::TrueColor,
1305 test_filesystem(),
1306 )
1307 .unwrap();
1308
1309 assert_eq!(editor.buffers.len(), 1);
1310 assert!(!editor.should_quit());
1311 }
1312
1313 #[test]
1314 fn test_new_buffer() {
1315 let config = Config::default();
1316 let (dir_context, _temp) = test_dir_context();
1317 let mut editor = Editor::new(
1318 config,
1319 80,
1320 24,
1321 dir_context,
1322 crate::view::color_support::ColorCapability::TrueColor,
1323 test_filesystem(),
1324 )
1325 .unwrap();
1326
1327 let id = editor.new_buffer();
1328 assert_eq!(editor.buffers.len(), 2);
1329 assert_eq!(editor.active_buffer(), id);
1330 }
1331
1332 #[test]
1333 #[ignore]
1334 fn test_clipboard() {
1335 let config = Config::default();
1336 let (dir_context, _temp) = test_dir_context();
1337 let mut editor = Editor::new(
1338 config,
1339 80,
1340 24,
1341 dir_context,
1342 crate::view::color_support::ColorCapability::TrueColor,
1343 test_filesystem(),
1344 )
1345 .unwrap();
1346
1347 editor.clipboard.set_internal("test".to_string());
1349
1350 editor.paste();
1352
1353 let content = editor.active_state().buffer.to_string().unwrap();
1354 assert_eq!(content, "test");
1355 }
1356
1357 #[test]
1358 fn test_action_to_events_insert_char() {
1359 let config = Config::default();
1360 let (dir_context, _temp) = test_dir_context();
1361 let mut editor = Editor::new(
1362 config,
1363 80,
1364 24,
1365 dir_context,
1366 crate::view::color_support::ColorCapability::TrueColor,
1367 test_filesystem(),
1368 )
1369 .unwrap();
1370
1371 let events = editor.action_to_events(Action::InsertChar('a'));
1372 assert!(events.is_some());
1373
1374 let events = events.unwrap();
1375 assert_eq!(events.len(), 1);
1376
1377 match &events[0] {
1378 Event::Insert { position, text, .. } => {
1379 assert_eq!(*position, 0);
1380 assert_eq!(text, "a");
1381 }
1382 _ => panic!("Expected Insert event"),
1383 }
1384 }
1385
1386 #[test]
1387 fn test_action_to_events_move_right() {
1388 let config = Config::default();
1389 let (dir_context, _temp) = test_dir_context();
1390 let mut editor = Editor::new(
1391 config,
1392 80,
1393 24,
1394 dir_context,
1395 crate::view::color_support::ColorCapability::TrueColor,
1396 test_filesystem(),
1397 )
1398 .unwrap();
1399
1400 let cursor_id = editor.active_cursors().primary_id();
1402 editor.apply_event_to_active_buffer(&Event::Insert {
1403 position: 0,
1404 text: "hello".to_string(),
1405 cursor_id,
1406 });
1407
1408 let events = editor.action_to_events(Action::MoveRight);
1409 assert!(events.is_some());
1410
1411 let events = events.unwrap();
1412 assert_eq!(events.len(), 1);
1413
1414 match &events[0] {
1415 Event::MoveCursor {
1416 new_position,
1417 new_anchor,
1418 ..
1419 } => {
1420 assert_eq!(*new_position, 5);
1422 assert_eq!(*new_anchor, None); }
1424 _ => panic!("Expected MoveCursor event"),
1425 }
1426 }
1427
1428 #[test]
1429 fn test_action_to_events_move_up_down() {
1430 let config = Config::default();
1431 let (dir_context, _temp) = test_dir_context();
1432 let mut editor = Editor::new(
1433 config,
1434 80,
1435 24,
1436 dir_context,
1437 crate::view::color_support::ColorCapability::TrueColor,
1438 test_filesystem(),
1439 )
1440 .unwrap();
1441
1442 let cursor_id = editor.active_cursors().primary_id();
1444 editor.apply_event_to_active_buffer(&Event::Insert {
1445 position: 0,
1446 text: "line1\nline2\nline3".to_string(),
1447 cursor_id,
1448 });
1449
1450 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1452 cursor_id,
1453 old_position: 0, new_position: 6,
1455 old_anchor: None, new_anchor: None,
1457 old_sticky_column: 0,
1458 new_sticky_column: 0,
1459 });
1460
1461 let events = editor.action_to_events(Action::MoveUp);
1463 assert!(events.is_some());
1464 let events = events.unwrap();
1465 assert_eq!(events.len(), 1);
1466
1467 match &events[0] {
1468 Event::MoveCursor { new_position, .. } => {
1469 assert_eq!(*new_position, 0); }
1471 _ => panic!("Expected MoveCursor event"),
1472 }
1473 }
1474
1475 #[test]
1476 fn test_action_to_events_insert_newline() {
1477 let config = Config::default();
1478 let (dir_context, _temp) = test_dir_context();
1479 let mut editor = Editor::new(
1480 config,
1481 80,
1482 24,
1483 dir_context,
1484 crate::view::color_support::ColorCapability::TrueColor,
1485 test_filesystem(),
1486 )
1487 .unwrap();
1488
1489 let events = editor.action_to_events(Action::InsertNewline);
1490 assert!(events.is_some());
1491
1492 let events = events.unwrap();
1493 assert_eq!(events.len(), 1);
1494
1495 match &events[0] {
1496 Event::Insert { text, .. } => {
1497 assert_eq!(text, "\n");
1498 }
1499 _ => panic!("Expected Insert event"),
1500 }
1501 }
1502
1503 #[test]
1504 fn test_action_to_events_unimplemented() {
1505 let config = Config::default();
1506 let (dir_context, _temp) = test_dir_context();
1507 let mut editor = Editor::new(
1508 config,
1509 80,
1510 24,
1511 dir_context,
1512 crate::view::color_support::ColorCapability::TrueColor,
1513 test_filesystem(),
1514 )
1515 .unwrap();
1516
1517 assert!(editor.action_to_events(Action::Save).is_none());
1519 assert!(editor.action_to_events(Action::Quit).is_none());
1520 assert!(editor.action_to_events(Action::Undo).is_none());
1521 }
1522
1523 #[test]
1524 fn test_action_to_events_delete_backward() {
1525 let config = Config::default();
1526 let (dir_context, _temp) = test_dir_context();
1527 let mut editor = Editor::new(
1528 config,
1529 80,
1530 24,
1531 dir_context,
1532 crate::view::color_support::ColorCapability::TrueColor,
1533 test_filesystem(),
1534 )
1535 .unwrap();
1536
1537 let cursor_id = editor.active_cursors().primary_id();
1539 editor.apply_event_to_active_buffer(&Event::Insert {
1540 position: 0,
1541 text: "hello".to_string(),
1542 cursor_id,
1543 });
1544
1545 let events = editor.action_to_events(Action::DeleteBackward);
1546 assert!(events.is_some());
1547
1548 let events = events.unwrap();
1549 assert_eq!(events.len(), 1);
1550
1551 match &events[0] {
1552 Event::Delete {
1553 range,
1554 deleted_text,
1555 ..
1556 } => {
1557 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1559 }
1560 _ => panic!("Expected Delete event"),
1561 }
1562 }
1563
1564 #[test]
1565 fn test_action_to_events_delete_forward() {
1566 let config = Config::default();
1567 let (dir_context, _temp) = test_dir_context();
1568 let mut editor = Editor::new(
1569 config,
1570 80,
1571 24,
1572 dir_context,
1573 crate::view::color_support::ColorCapability::TrueColor,
1574 test_filesystem(),
1575 )
1576 .unwrap();
1577
1578 let cursor_id = editor.active_cursors().primary_id();
1580 editor.apply_event_to_active_buffer(&Event::Insert {
1581 position: 0,
1582 text: "hello".to_string(),
1583 cursor_id,
1584 });
1585
1586 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1588 cursor_id,
1589 old_position: 0, new_position: 0,
1591 old_anchor: None, new_anchor: None,
1593 old_sticky_column: 0,
1594 new_sticky_column: 0,
1595 });
1596
1597 let events = editor.action_to_events(Action::DeleteForward);
1598 assert!(events.is_some());
1599
1600 let events = events.unwrap();
1601 assert_eq!(events.len(), 1);
1602
1603 match &events[0] {
1604 Event::Delete {
1605 range,
1606 deleted_text,
1607 ..
1608 } => {
1609 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1611 }
1612 _ => panic!("Expected Delete event"),
1613 }
1614 }
1615
1616 #[test]
1617 fn test_action_to_events_select_right() {
1618 let config = Config::default();
1619 let (dir_context, _temp) = test_dir_context();
1620 let mut editor = Editor::new(
1621 config,
1622 80,
1623 24,
1624 dir_context,
1625 crate::view::color_support::ColorCapability::TrueColor,
1626 test_filesystem(),
1627 )
1628 .unwrap();
1629
1630 let cursor_id = editor.active_cursors().primary_id();
1632 editor.apply_event_to_active_buffer(&Event::Insert {
1633 position: 0,
1634 text: "hello".to_string(),
1635 cursor_id,
1636 });
1637
1638 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1640 cursor_id,
1641 old_position: 0, new_position: 0,
1643 old_anchor: None, new_anchor: None,
1645 old_sticky_column: 0,
1646 new_sticky_column: 0,
1647 });
1648
1649 let events = editor.action_to_events(Action::SelectRight);
1650 assert!(events.is_some());
1651
1652 let events = events.unwrap();
1653 assert_eq!(events.len(), 1);
1654
1655 match &events[0] {
1656 Event::MoveCursor {
1657 new_position,
1658 new_anchor,
1659 ..
1660 } => {
1661 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1664 _ => panic!("Expected MoveCursor event"),
1665 }
1666 }
1667
1668 #[test]
1669 fn test_action_to_events_select_all() {
1670 let config = Config::default();
1671 let (dir_context, _temp) = test_dir_context();
1672 let mut editor = Editor::new(
1673 config,
1674 80,
1675 24,
1676 dir_context,
1677 crate::view::color_support::ColorCapability::TrueColor,
1678 test_filesystem(),
1679 )
1680 .unwrap();
1681
1682 let cursor_id = editor.active_cursors().primary_id();
1684 editor.apply_event_to_active_buffer(&Event::Insert {
1685 position: 0,
1686 text: "hello world".to_string(),
1687 cursor_id,
1688 });
1689
1690 let events = editor.action_to_events(Action::SelectAll);
1691 assert!(events.is_some());
1692
1693 let events = events.unwrap();
1694 assert_eq!(events.len(), 1);
1695
1696 match &events[0] {
1697 Event::MoveCursor {
1698 new_position,
1699 new_anchor,
1700 ..
1701 } => {
1702 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1705 _ => panic!("Expected MoveCursor event"),
1706 }
1707 }
1708
1709 #[test]
1710 fn test_action_to_events_document_nav() {
1711 let config = Config::default();
1712 let (dir_context, _temp) = test_dir_context();
1713 let mut editor = Editor::new(
1714 config,
1715 80,
1716 24,
1717 dir_context,
1718 crate::view::color_support::ColorCapability::TrueColor,
1719 test_filesystem(),
1720 )
1721 .unwrap();
1722
1723 let cursor_id = editor.active_cursors().primary_id();
1725 editor.apply_event_to_active_buffer(&Event::Insert {
1726 position: 0,
1727 text: "line1\nline2\nline3".to_string(),
1728 cursor_id,
1729 });
1730
1731 let events = editor.action_to_events(Action::MoveDocumentStart);
1733 assert!(events.is_some());
1734 let events = events.unwrap();
1735 match &events[0] {
1736 Event::MoveCursor { new_position, .. } => {
1737 assert_eq!(*new_position, 0);
1738 }
1739 _ => panic!("Expected MoveCursor event"),
1740 }
1741
1742 let events = editor.action_to_events(Action::MoveDocumentEnd);
1744 assert!(events.is_some());
1745 let events = events.unwrap();
1746 match &events[0] {
1747 Event::MoveCursor { new_position, .. } => {
1748 assert_eq!(*new_position, 17); }
1750 _ => panic!("Expected MoveCursor event"),
1751 }
1752 }
1753
1754 #[test]
1755 fn test_action_to_events_remove_secondary_cursors() {
1756 use crate::model::event::CursorId;
1757
1758 let config = Config::default();
1759 let (dir_context, _temp) = test_dir_context();
1760 let mut editor = Editor::new(
1761 config,
1762 80,
1763 24,
1764 dir_context,
1765 crate::view::color_support::ColorCapability::TrueColor,
1766 test_filesystem(),
1767 )
1768 .unwrap();
1769
1770 let cursor_id = editor.active_cursors().primary_id();
1772 editor.apply_event_to_active_buffer(&Event::Insert {
1773 position: 0,
1774 text: "hello world test".to_string(),
1775 cursor_id,
1776 });
1777
1778 editor.apply_event_to_active_buffer(&Event::AddCursor {
1780 cursor_id: CursorId(1),
1781 position: 5,
1782 anchor: None,
1783 });
1784 editor.apply_event_to_active_buffer(&Event::AddCursor {
1785 cursor_id: CursorId(2),
1786 position: 10,
1787 anchor: None,
1788 });
1789
1790 assert_eq!(editor.active_cursors().count(), 3);
1791
1792 let first_id = editor
1794 .active_cursors()
1795 .iter()
1796 .map(|(id, _)| id)
1797 .min_by_key(|id| id.0)
1798 .expect("Should have at least one cursor");
1799
1800 let events = editor.action_to_events(Action::RemoveSecondaryCursors);
1802 assert!(events.is_some());
1803
1804 let events = events.unwrap();
1805 let remove_cursor_events: Vec<_> = events
1808 .iter()
1809 .filter_map(|e| match e {
1810 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1811 _ => None,
1812 })
1813 .collect();
1814
1815 assert_eq!(remove_cursor_events.len(), 2);
1817
1818 for cursor_id in &remove_cursor_events {
1819 assert_ne!(*cursor_id, first_id);
1821 }
1822 }
1823
1824 #[test]
1825 fn test_action_to_events_scroll() {
1826 let config = Config::default();
1827 let (dir_context, _temp) = test_dir_context();
1828 let mut editor = Editor::new(
1829 config,
1830 80,
1831 24,
1832 dir_context,
1833 crate::view::color_support::ColorCapability::TrueColor,
1834 test_filesystem(),
1835 )
1836 .unwrap();
1837
1838 let events = editor.action_to_events(Action::ScrollUp);
1840 assert!(events.is_some());
1841 let events = events.unwrap();
1842 assert_eq!(events.len(), 1);
1843 match &events[0] {
1844 Event::Scroll { line_offset } => {
1845 assert_eq!(*line_offset, -1);
1846 }
1847 _ => panic!("Expected Scroll event"),
1848 }
1849
1850 let events = editor.action_to_events(Action::ScrollDown);
1852 assert!(events.is_some());
1853 let events = events.unwrap();
1854 assert_eq!(events.len(), 1);
1855 match &events[0] {
1856 Event::Scroll { line_offset } => {
1857 assert_eq!(*line_offset, 1);
1858 }
1859 _ => panic!("Expected Scroll event"),
1860 }
1861 }
1862
1863 #[test]
1864 fn test_action_to_events_none() {
1865 let config = Config::default();
1866 let (dir_context, _temp) = test_dir_context();
1867 let mut editor = Editor::new(
1868 config,
1869 80,
1870 24,
1871 dir_context,
1872 crate::view::color_support::ColorCapability::TrueColor,
1873 test_filesystem(),
1874 )
1875 .unwrap();
1876
1877 let events = editor.action_to_events(Action::None);
1879 assert!(events.is_none());
1880 }
1881
1882 #[test]
1883 fn test_lsp_incremental_insert_generates_correct_range() {
1884 use crate::model::buffer::Buffer;
1887
1888 let buffer = Buffer::from_str_test("hello\nworld");
1889
1890 let position = 0;
1893 let (line, character) = buffer.position_to_lsp_position(position);
1894
1895 assert_eq!(line, 0, "Insertion at start should be line 0");
1896 assert_eq!(character, 0, "Insertion at start should be char 0");
1897
1898 let lsp_pos = Position::new(line as u32, character as u32);
1900 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
1901
1902 assert_eq!(lsp_range.start.line, 0);
1903 assert_eq!(lsp_range.start.character, 0);
1904 assert_eq!(lsp_range.end.line, 0);
1905 assert_eq!(lsp_range.end.character, 0);
1906 assert_eq!(
1907 lsp_range.start, lsp_range.end,
1908 "Insert should have zero-width range"
1909 );
1910
1911 let position = 3;
1913 let (line, character) = buffer.position_to_lsp_position(position);
1914
1915 assert_eq!(line, 0);
1916 assert_eq!(character, 3);
1917
1918 let position = 6;
1920 let (line, character) = buffer.position_to_lsp_position(position);
1921
1922 assert_eq!(line, 1, "Position after newline should be line 1");
1923 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
1924 }
1925
1926 #[test]
1927 fn test_lsp_incremental_delete_generates_correct_range() {
1928 use crate::model::buffer::Buffer;
1931
1932 let buffer = Buffer::from_str_test("hello\nworld");
1933
1934 let range_start = 1;
1936 let range_end = 5;
1937
1938 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
1939 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
1940
1941 assert_eq!(start_line, 0);
1942 assert_eq!(start_char, 1);
1943 assert_eq!(end_line, 0);
1944 assert_eq!(end_char, 5);
1945
1946 let lsp_range = LspRange::new(
1947 Position::new(start_line as u32, start_char as u32),
1948 Position::new(end_line as u32, end_char as u32),
1949 );
1950
1951 assert_eq!(lsp_range.start.line, 0);
1952 assert_eq!(lsp_range.start.character, 1);
1953 assert_eq!(lsp_range.end.line, 0);
1954 assert_eq!(lsp_range.end.character, 5);
1955 assert_ne!(
1956 lsp_range.start, lsp_range.end,
1957 "Delete should have non-zero range"
1958 );
1959
1960 let range_start = 4;
1962 let range_end = 8;
1963
1964 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
1965 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
1966
1967 assert_eq!(start_line, 0, "Delete start on line 0");
1968 assert_eq!(start_char, 4, "Delete start at char 4");
1969 assert_eq!(end_line, 1, "Delete end on line 1");
1970 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
1971 }
1972
1973 #[test]
1974 fn test_lsp_incremental_utf16_encoding() {
1975 use crate::model::buffer::Buffer;
1978
1979 let buffer = Buffer::from_str_test("😀hello");
1981
1982 let (line, character) = buffer.position_to_lsp_position(4);
1984
1985 assert_eq!(line, 0);
1986 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
1987
1988 let (line, character) = buffer.position_to_lsp_position(9);
1990
1991 assert_eq!(line, 0);
1992 assert_eq!(
1993 character, 7,
1994 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
1995 );
1996
1997 let buffer = Buffer::from_str_test("café");
1999
2000 let (line, character) = buffer.position_to_lsp_position(3);
2002
2003 assert_eq!(line, 0);
2004 assert_eq!(character, 3);
2005
2006 let (line, character) = buffer.position_to_lsp_position(5);
2008
2009 assert_eq!(line, 0);
2010 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2011 }
2012
2013 #[test]
2014 fn test_lsp_content_change_event_structure() {
2015 let insert_change = TextDocumentContentChangeEvent {
2019 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2020 range_length: None,
2021 text: "NEW".to_string(),
2022 };
2023
2024 assert!(insert_change.range.is_some());
2025 assert_eq!(insert_change.text, "NEW");
2026 let range = insert_change.range.unwrap();
2027 assert_eq!(
2028 range.start, range.end,
2029 "Insert should have zero-width range"
2030 );
2031
2032 let delete_change = TextDocumentContentChangeEvent {
2034 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2035 range_length: None,
2036 text: String::new(),
2037 };
2038
2039 assert!(delete_change.range.is_some());
2040 assert_eq!(delete_change.text, "");
2041 let range = delete_change.range.unwrap();
2042 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2043 assert_eq!(range.start.line, 0);
2044 assert_eq!(range.start.character, 2);
2045 assert_eq!(range.end.line, 0);
2046 assert_eq!(range.end.character, 7);
2047 }
2048
2049 #[test]
2050 fn test_goto_matching_bracket_forward() {
2051 let config = Config::default();
2052 let (dir_context, _temp) = test_dir_context();
2053 let mut editor = Editor::new(
2054 config,
2055 80,
2056 24,
2057 dir_context,
2058 crate::view::color_support::ColorCapability::TrueColor,
2059 test_filesystem(),
2060 )
2061 .unwrap();
2062
2063 let cursor_id = editor.active_cursors().primary_id();
2065 editor.apply_event_to_active_buffer(&Event::Insert {
2066 position: 0,
2067 text: "fn main() { let x = (1 + 2); }".to_string(),
2068 cursor_id,
2069 });
2070
2071 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2073 cursor_id,
2074 old_position: 31,
2075 new_position: 10,
2076 old_anchor: None,
2077 new_anchor: None,
2078 old_sticky_column: 0,
2079 new_sticky_column: 0,
2080 });
2081
2082 assert_eq!(editor.active_cursors().primary().position, 10);
2083
2084 editor.goto_matching_bracket();
2086
2087 assert_eq!(editor.active_cursors().primary().position, 29);
2092 }
2093
2094 #[test]
2095 fn test_goto_matching_bracket_backward() {
2096 let config = Config::default();
2097 let (dir_context, _temp) = test_dir_context();
2098 let mut editor = Editor::new(
2099 config,
2100 80,
2101 24,
2102 dir_context,
2103 crate::view::color_support::ColorCapability::TrueColor,
2104 test_filesystem(),
2105 )
2106 .unwrap();
2107
2108 let cursor_id = editor.active_cursors().primary_id();
2110 editor.apply_event_to_active_buffer(&Event::Insert {
2111 position: 0,
2112 text: "fn main() { let x = (1 + 2); }".to_string(),
2113 cursor_id,
2114 });
2115
2116 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2118 cursor_id,
2119 old_position: 31,
2120 new_position: 26,
2121 old_anchor: None,
2122 new_anchor: None,
2123 old_sticky_column: 0,
2124 new_sticky_column: 0,
2125 });
2126
2127 editor.goto_matching_bracket();
2129
2130 assert_eq!(editor.active_cursors().primary().position, 20);
2132 }
2133
2134 #[test]
2135 fn test_goto_matching_bracket_nested() {
2136 let config = Config::default();
2137 let (dir_context, _temp) = test_dir_context();
2138 let mut editor = Editor::new(
2139 config,
2140 80,
2141 24,
2142 dir_context,
2143 crate::view::color_support::ColorCapability::TrueColor,
2144 test_filesystem(),
2145 )
2146 .unwrap();
2147
2148 let cursor_id = editor.active_cursors().primary_id();
2150 editor.apply_event_to_active_buffer(&Event::Insert {
2151 position: 0,
2152 text: "{a{b{c}d}e}".to_string(),
2153 cursor_id,
2154 });
2155
2156 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2158 cursor_id,
2159 old_position: 11,
2160 new_position: 0,
2161 old_anchor: None,
2162 new_anchor: None,
2163 old_sticky_column: 0,
2164 new_sticky_column: 0,
2165 });
2166
2167 editor.goto_matching_bracket();
2169
2170 assert_eq!(editor.active_cursors().primary().position, 10);
2172 }
2173
2174 #[test]
2175 fn test_search_case_sensitive() {
2176 let config = Config::default();
2177 let (dir_context, _temp) = test_dir_context();
2178 let mut editor = Editor::new(
2179 config,
2180 80,
2181 24,
2182 dir_context,
2183 crate::view::color_support::ColorCapability::TrueColor,
2184 test_filesystem(),
2185 )
2186 .unwrap();
2187
2188 let cursor_id = editor.active_cursors().primary_id();
2190 editor.apply_event_to_active_buffer(&Event::Insert {
2191 position: 0,
2192 text: "Hello hello HELLO".to_string(),
2193 cursor_id,
2194 });
2195
2196 editor.search_case_sensitive = false;
2198 editor.perform_search("hello");
2199
2200 let search_state = editor.search_state.as_ref().unwrap();
2201 assert_eq!(
2202 search_state.matches.len(),
2203 3,
2204 "Should find all 3 matches case-insensitively"
2205 );
2206
2207 editor.search_case_sensitive = true;
2209 editor.perform_search("hello");
2210
2211 let search_state = editor.search_state.as_ref().unwrap();
2212 assert_eq!(
2213 search_state.matches.len(),
2214 1,
2215 "Should find only 1 exact match"
2216 );
2217 assert_eq!(
2218 search_state.matches[0], 6,
2219 "Should find 'hello' at position 6"
2220 );
2221 }
2222
2223 #[test]
2224 fn test_search_whole_word() {
2225 let config = Config::default();
2226 let (dir_context, _temp) = test_dir_context();
2227 let mut editor = Editor::new(
2228 config,
2229 80,
2230 24,
2231 dir_context,
2232 crate::view::color_support::ColorCapability::TrueColor,
2233 test_filesystem(),
2234 )
2235 .unwrap();
2236
2237 let cursor_id = editor.active_cursors().primary_id();
2239 editor.apply_event_to_active_buffer(&Event::Insert {
2240 position: 0,
2241 text: "test testing tested attest test".to_string(),
2242 cursor_id,
2243 });
2244
2245 editor.search_whole_word = false;
2247 editor.search_case_sensitive = true;
2248 editor.perform_search("test");
2249
2250 let search_state = editor.search_state.as_ref().unwrap();
2251 assert_eq!(
2252 search_state.matches.len(),
2253 5,
2254 "Should find 'test' in all occurrences"
2255 );
2256
2257 editor.search_whole_word = true;
2259 editor.perform_search("test");
2260
2261 let search_state = editor.search_state.as_ref().unwrap();
2262 assert_eq!(
2263 search_state.matches.len(),
2264 2,
2265 "Should find only whole word 'test'"
2266 );
2267 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2268 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2269 }
2270
2271 #[test]
2272 fn test_search_scan_completes_when_capped() {
2273 let config = Config::default();
2279 let (dir_context, _temp) = test_dir_context();
2280 let mut editor = Editor::new(
2281 config,
2282 80,
2283 24,
2284 dir_context,
2285 crate::view::color_support::ColorCapability::TrueColor,
2286 test_filesystem(),
2287 )
2288 .unwrap();
2289
2290 let buffer_id = editor.active_buffer();
2293 let regex = regex::bytes::Regex::new("test").unwrap();
2294 let fake_chunks = vec![
2295 crate::model::buffer::LineScanChunk {
2296 leaf_index: 0,
2297 byte_len: 100,
2298 already_known: true,
2299 },
2300 crate::model::buffer::LineScanChunk {
2301 leaf_index: 1,
2302 byte_len: 100,
2303 already_known: true,
2304 },
2305 ];
2306
2307 let chunked = crate::model::buffer::ChunkedSearchState {
2308 chunks: fake_chunks,
2309 next_chunk: 1, next_doc_offset: 100,
2311 total_bytes: 200,
2312 scanned_bytes: 100,
2313 regex,
2314 matches: vec![
2315 crate::model::buffer::SearchMatch {
2316 byte_offset: 10,
2317 length: 4,
2318 line: 1,
2319 column: 11,
2320 context: String::new(),
2321 },
2322 crate::model::buffer::SearchMatch {
2323 byte_offset: 50,
2324 length: 4,
2325 line: 1,
2326 column: 51,
2327 context: String::new(),
2328 },
2329 ],
2330 overlap_tail: Vec::new(),
2331 overlap_doc_offset: 0,
2332 max_matches: 10_000,
2333 capped: true, query_len: 4,
2335 running_line: 1,
2336 };
2337
2338 editor.search_scan.start(
2339 buffer_id,
2340 Vec::new(),
2341 chunked,
2342 "test".to_string(),
2343 None,
2344 false,
2345 false,
2346 false,
2347 );
2348
2349 let result = editor.process_search_scan();
2351 assert!(
2352 result,
2353 "process_search_scan should return true (needs render)"
2354 );
2355
2356 assert_eq!(
2358 editor.search_scan.buffer_id(),
2359 None,
2360 "search_scan should be drained after capped scan completes"
2361 );
2362
2363 let search_state = editor
2365 .search_state
2366 .as_ref()
2367 .expect("search_state should be set after scan finishes");
2368 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2369 assert_eq!(search_state.query, "test");
2370 assert!(
2371 search_state.capped,
2372 "search_state should be marked as capped"
2373 );
2374 }
2375
2376 #[test]
2377 fn test_bookmarks() {
2378 let config = Config::default();
2379 let (dir_context, _temp) = test_dir_context();
2380 let mut editor = Editor::new(
2381 config,
2382 80,
2383 24,
2384 dir_context,
2385 crate::view::color_support::ColorCapability::TrueColor,
2386 test_filesystem(),
2387 )
2388 .unwrap();
2389
2390 let cursor_id = editor.active_cursors().primary_id();
2392 editor.apply_event_to_active_buffer(&Event::Insert {
2393 position: 0,
2394 text: "Line 1\nLine 2\nLine 3".to_string(),
2395 cursor_id,
2396 });
2397
2398 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2400 cursor_id,
2401 old_position: 21,
2402 new_position: 7,
2403 old_anchor: None,
2404 new_anchor: None,
2405 old_sticky_column: 0,
2406 new_sticky_column: 0,
2407 });
2408
2409 editor.set_bookmark('1');
2411 assert_eq!(editor.bookmarks.get('1').map(|b| b.position), Some(7));
2412
2413 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2415 cursor_id,
2416 old_position: 7,
2417 new_position: 14,
2418 old_anchor: None,
2419 new_anchor: None,
2420 old_sticky_column: 0,
2421 new_sticky_column: 0,
2422 });
2423
2424 editor.jump_to_bookmark('1');
2426 assert_eq!(editor.active_cursors().primary().position, 7);
2427
2428 editor.clear_bookmark('1');
2430 assert_eq!(editor.bookmarks.get('1'), None);
2431 }
2432
2433 #[test]
2434 fn test_action_enum_new_variants() {
2435 use serde_json::json;
2437
2438 let args = HashMap::new();
2439 assert_eq!(
2440 Action::from_str("smart_home", &args),
2441 Some(Action::SmartHome)
2442 );
2443 assert_eq!(
2444 Action::from_str("dedent_selection", &args),
2445 Some(Action::DedentSelection)
2446 );
2447 assert_eq!(
2448 Action::from_str("toggle_comment", &args),
2449 Some(Action::ToggleComment)
2450 );
2451 assert_eq!(
2452 Action::from_str("goto_matching_bracket", &args),
2453 Some(Action::GoToMatchingBracket)
2454 );
2455 assert_eq!(
2456 Action::from_str("list_bookmarks", &args),
2457 Some(Action::ListBookmarks)
2458 );
2459 assert_eq!(
2460 Action::from_str("toggle_search_case_sensitive", &args),
2461 Some(Action::ToggleSearchCaseSensitive)
2462 );
2463 assert_eq!(
2464 Action::from_str("toggle_search_whole_word", &args),
2465 Some(Action::ToggleSearchWholeWord)
2466 );
2467
2468 let mut args_with_char = HashMap::new();
2470 args_with_char.insert("char".to_string(), json!("5"));
2471 assert_eq!(
2472 Action::from_str("set_bookmark", &args_with_char),
2473 Some(Action::SetBookmark('5'))
2474 );
2475 assert_eq!(
2476 Action::from_str("jump_to_bookmark", &args_with_char),
2477 Some(Action::JumpToBookmark('5'))
2478 );
2479 assert_eq!(
2480 Action::from_str("clear_bookmark", &args_with_char),
2481 Some(Action::ClearBookmark('5'))
2482 );
2483 }
2484
2485 #[test]
2486 fn test_keybinding_new_defaults() {
2487 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2488
2489 let mut config = Config::default();
2493 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2494 let resolver = KeybindingResolver::new(&config);
2495
2496 let event = KeyEvent {
2498 code: KeyCode::Char('/'),
2499 modifiers: KeyModifiers::CONTROL,
2500 kind: KeyEventKind::Press,
2501 state: KeyEventState::NONE,
2502 };
2503 let action = resolver.resolve(&event, KeyContext::Normal);
2504 assert_eq!(action, Action::ToggleComment);
2505
2506 let event = KeyEvent {
2508 code: KeyCode::Char(']'),
2509 modifiers: KeyModifiers::CONTROL,
2510 kind: KeyEventKind::Press,
2511 state: KeyEventState::NONE,
2512 };
2513 let action = resolver.resolve(&event, KeyContext::Normal);
2514 assert_eq!(action, Action::GoToMatchingBracket);
2515
2516 let event = KeyEvent {
2518 code: KeyCode::Tab,
2519 modifiers: KeyModifiers::SHIFT,
2520 kind: KeyEventKind::Press,
2521 state: KeyEventState::NONE,
2522 };
2523 let action = resolver.resolve(&event, KeyContext::Normal);
2524 assert_eq!(action, Action::DedentSelection);
2525
2526 let event = KeyEvent {
2528 code: KeyCode::Char('g'),
2529 modifiers: KeyModifiers::CONTROL,
2530 kind: KeyEventKind::Press,
2531 state: KeyEventState::NONE,
2532 };
2533 let action = resolver.resolve(&event, KeyContext::Normal);
2534 assert_eq!(action, Action::GotoLine);
2535
2536 let event = KeyEvent {
2538 code: KeyCode::Char('5'),
2539 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2540 kind: KeyEventKind::Press,
2541 state: KeyEventState::NONE,
2542 };
2543 let action = resolver.resolve(&event, KeyContext::Normal);
2544 assert_eq!(action, Action::SetBookmark('5'));
2545
2546 let event = KeyEvent {
2547 code: KeyCode::Char('5'),
2548 modifiers: KeyModifiers::ALT,
2549 kind: KeyEventKind::Press,
2550 state: KeyEventState::NONE,
2551 };
2552 let action = resolver.resolve(&event, KeyContext::Normal);
2553 assert_eq!(action, Action::JumpToBookmark('5'));
2554 }
2555
2556 #[test]
2568 fn test_lsp_rename_didchange_positions_bug() {
2569 use crate::model::buffer::Buffer;
2570
2571 let config = Config::default();
2572 let (dir_context, _temp) = test_dir_context();
2573 let mut editor = Editor::new(
2574 config,
2575 80,
2576 24,
2577 dir_context,
2578 crate::view::color_support::ColorCapability::TrueColor,
2579 test_filesystem(),
2580 )
2581 .unwrap();
2582
2583 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2587 editor.active_state_mut().buffer =
2588 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2589
2590 let cursor_id = editor.active_cursors().primary_id();
2595
2596 let batch = Event::Batch {
2597 events: vec![
2598 Event::Delete {
2600 range: 23..26, deleted_text: "val".to_string(),
2602 cursor_id,
2603 },
2604 Event::Insert {
2605 position: 23,
2606 text: "value".to_string(),
2607 cursor_id,
2608 },
2609 Event::Delete {
2611 range: 7..10, deleted_text: "val".to_string(),
2613 cursor_id,
2614 },
2615 Event::Insert {
2616 position: 7,
2617 text: "value".to_string(),
2618 cursor_id,
2619 },
2620 ],
2621 description: "LSP Rename".to_string(),
2622 };
2623
2624 let lsp_changes_before = editor.collect_lsp_changes(&batch);
2626
2627 editor.apply_event_to_active_buffer(&batch);
2629
2630 let lsp_changes_after = editor.collect_lsp_changes(&batch);
2633
2634 let final_content = editor.active_state().buffer.to_string().unwrap();
2636 assert_eq!(
2637 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2638 "Buffer should have 'value' in both places"
2639 );
2640
2641 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2647
2648 let first_delete = &lsp_changes_before[0];
2649 let first_del_range = first_delete.range.unwrap();
2650 assert_eq!(
2651 first_del_range.start.line, 1,
2652 "First delete should be on line 1 (BEFORE)"
2653 );
2654 assert_eq!(
2655 first_del_range.start.character, 4,
2656 "First delete start should be at char 4 (BEFORE)"
2657 );
2658
2659 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2665
2666 let first_delete_after = &lsp_changes_after[0];
2667 let first_del_range_after = first_delete_after.range.unwrap();
2668
2669 eprintln!("BEFORE modification:");
2672 eprintln!(
2673 " Delete at line {}, char {}-{}",
2674 first_del_range.start.line,
2675 first_del_range.start.character,
2676 first_del_range.end.character
2677 );
2678 eprintln!("AFTER modification:");
2679 eprintln!(
2680 " Delete at line {}, char {}-{}",
2681 first_del_range_after.start.line,
2682 first_del_range_after.start.character,
2683 first_del_range_after.end.character
2684 );
2685
2686 assert_ne!(
2704 first_del_range_after.end.character, first_del_range.end.character,
2705 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2706 );
2707
2708 eprintln!("\n=== BUG DEMONSTRATED ===");
2709 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2710 eprintln!("the positions are WRONG because they're calculated from the");
2711 eprintln!("modified buffer, not the original buffer.");
2712 eprintln!("This causes the second rename to fail with 'content modified' error.");
2713 eprintln!("========================\n");
2714 }
2715
2716 #[test]
2717 fn test_lsp_rename_preserves_cursor_position() {
2718 use crate::model::buffer::Buffer;
2719
2720 let config = Config::default();
2721 let (dir_context, _temp) = test_dir_context();
2722 let mut editor = Editor::new(
2723 config,
2724 80,
2725 24,
2726 dir_context,
2727 crate::view::color_support::ColorCapability::TrueColor,
2728 test_filesystem(),
2729 )
2730 .unwrap();
2731
2732 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2736 editor.active_state_mut().buffer =
2737 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2738
2739 let original_cursor_pos = 23;
2741 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2742
2743 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2745 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2746 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2747
2748 let cursor_id = editor.active_cursors().primary_id();
2751 let buffer_id = editor.active_buffer();
2752
2753 let events = vec![
2754 Event::Delete {
2756 range: 23..26, deleted_text: "val".to_string(),
2758 cursor_id,
2759 },
2760 Event::Insert {
2761 position: 23,
2762 text: "value".to_string(),
2763 cursor_id,
2764 },
2765 Event::Delete {
2767 range: 7..10, deleted_text: "val".to_string(),
2769 cursor_id,
2770 },
2771 Event::Insert {
2772 position: 7,
2773 text: "value".to_string(),
2774 cursor_id,
2775 },
2776 ];
2777
2778 editor
2780 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2781 .unwrap();
2782
2783 let final_content = editor.active_state().buffer.to_string().unwrap();
2785 assert_eq!(
2786 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2787 "Buffer should have 'value' in both places"
2788 );
2789
2790 let final_cursor_pos = editor.active_cursors().primary().position;
2798 let expected_cursor_pos = 25; assert_eq!(
2801 final_cursor_pos, expected_cursor_pos,
2802 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2803 Original pos: {}, expected adjustment: +2 for first rename",
2804 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2805 );
2806
2807 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2809 assert_eq!(
2810 text_at_new_cursor, "value",
2811 "Cursor should be at the start of 'value' after rename"
2812 );
2813 }
2814
2815 #[test]
2816 fn test_lsp_rename_twice_consecutive() {
2817 use crate::model::buffer::Buffer;
2820
2821 let config = Config::default();
2822 let (dir_context, _temp) = test_dir_context();
2823 let mut editor = Editor::new(
2824 config,
2825 80,
2826 24,
2827 dir_context,
2828 crate::view::color_support::ColorCapability::TrueColor,
2829 test_filesystem(),
2830 )
2831 .unwrap();
2832
2833 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2835 editor.active_state_mut().buffer =
2836 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2837
2838 let cursor_id = editor.active_cursors().primary_id();
2839 let buffer_id = editor.active_buffer();
2840
2841 let events1 = vec![
2844 Event::Delete {
2846 range: 23..26,
2847 deleted_text: "val".to_string(),
2848 cursor_id,
2849 },
2850 Event::Insert {
2851 position: 23,
2852 text: "value".to_string(),
2853 cursor_id,
2854 },
2855 Event::Delete {
2857 range: 7..10,
2858 deleted_text: "val".to_string(),
2859 cursor_id,
2860 },
2861 Event::Insert {
2862 position: 7,
2863 text: "value".to_string(),
2864 cursor_id,
2865 },
2866 ];
2867
2868 let batch1 = Event::Batch {
2870 events: events1.clone(),
2871 description: "LSP Rename 1".to_string(),
2872 };
2873
2874 let lsp_changes1 = editor.collect_lsp_changes(&batch1);
2876
2877 assert_eq!(
2879 lsp_changes1.len(),
2880 4,
2881 "First rename should have 4 LSP changes"
2882 );
2883
2884 let first_del = &lsp_changes1[0];
2886 let first_del_range = first_del.range.unwrap();
2887 assert_eq!(first_del_range.start.line, 1, "First delete line");
2888 assert_eq!(
2889 first_del_range.start.character, 4,
2890 "First delete start char"
2891 );
2892 assert_eq!(first_del_range.end.character, 7, "First delete end char");
2893
2894 editor
2896 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
2897 .unwrap();
2898
2899 let after_first = editor.active_state().buffer.to_string().unwrap();
2901 assert_eq!(
2902 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
2903 "After first rename"
2904 );
2905
2906 let events2 = vec![
2916 Event::Delete {
2918 range: 25..30,
2919 deleted_text: "value".to_string(),
2920 cursor_id,
2921 },
2922 Event::Insert {
2923 position: 25,
2924 text: "x".to_string(),
2925 cursor_id,
2926 },
2927 Event::Delete {
2929 range: 7..12,
2930 deleted_text: "value".to_string(),
2931 cursor_id,
2932 },
2933 Event::Insert {
2934 position: 7,
2935 text: "x".to_string(),
2936 cursor_id,
2937 },
2938 ];
2939
2940 let batch2 = Event::Batch {
2942 events: events2.clone(),
2943 description: "LSP Rename 2".to_string(),
2944 };
2945
2946 let lsp_changes2 = editor.collect_lsp_changes(&batch2);
2948
2949 assert_eq!(
2953 lsp_changes2.len(),
2954 4,
2955 "Second rename should have 4 LSP changes"
2956 );
2957
2958 let second_first_del = &lsp_changes2[0];
2960 let second_first_del_range = second_first_del.range.unwrap();
2961 assert_eq!(
2962 second_first_del_range.start.line, 1,
2963 "Second rename first delete should be on line 1"
2964 );
2965 assert_eq!(
2966 second_first_del_range.start.character, 4,
2967 "Second rename first delete start should be at char 4"
2968 );
2969 assert_eq!(
2970 second_first_del_range.end.character, 9,
2971 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
2972 );
2973
2974 let second_third_del = &lsp_changes2[2];
2976 let second_third_del_range = second_third_del.range.unwrap();
2977 assert_eq!(
2978 second_third_del_range.start.line, 0,
2979 "Second rename third delete should be on line 0"
2980 );
2981 assert_eq!(
2982 second_third_del_range.start.character, 7,
2983 "Second rename third delete start should be at char 7"
2984 );
2985 assert_eq!(
2986 second_third_del_range.end.character, 12,
2987 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
2988 );
2989
2990 editor
2992 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
2993 .unwrap();
2994
2995 let after_second = editor.active_state().buffer.to_string().unwrap();
2997 assert_eq!(
2998 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
2999 "After second rename"
3000 );
3001 }
3002
3003 #[test]
3004 fn test_ensure_active_tab_visible_static_offset() {
3005 let config = Config::default();
3006 let (dir_context, _temp) = test_dir_context();
3007 let mut editor = Editor::new(
3008 config,
3009 80,
3010 24,
3011 dir_context,
3012 crate::view::color_support::ColorCapability::TrueColor,
3013 test_filesystem(),
3014 )
3015 .unwrap();
3016 let split_id = editor.split_manager.active_split();
3017
3018 let buf1 = editor.new_buffer();
3020 editor
3021 .buffers
3022 .get_mut(&buf1)
3023 .unwrap()
3024 .buffer
3025 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3026 let buf2 = editor.new_buffer();
3027 editor
3028 .buffers
3029 .get_mut(&buf2)
3030 .unwrap()
3031 .buffer
3032 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3033 let buf3 = editor.new_buffer();
3034 editor
3035 .buffers
3036 .get_mut(&buf3)
3037 .unwrap()
3038 .buffer
3039 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3040
3041 {
3042 use crate::view::split::TabTarget;
3043 let view_state = editor.split_view_states.get_mut(&split_id).unwrap();
3044 view_state.open_buffers = vec![
3045 TabTarget::Buffer(buf1),
3046 TabTarget::Buffer(buf2),
3047 TabTarget::Buffer(buf3),
3048 ];
3049 view_state.tab_scroll_offset = 50;
3050 }
3051
3052 editor.ensure_active_tab_visible(split_id, buf1, 25);
3056 assert_eq!(
3057 editor
3058 .split_view_states
3059 .get(&split_id)
3060 .unwrap()
3061 .tab_scroll_offset,
3062 0
3063 );
3064
3065 editor.ensure_active_tab_visible(split_id, buf3, 25);
3067 let view_state = editor.split_view_states.get(&split_id).unwrap();
3068 assert!(view_state.tab_scroll_offset > 0);
3069 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3070 let total_width: usize = buffer_ids
3071 .iter()
3072 .enumerate()
3073 .map(|(idx, id)| {
3074 let state = editor.buffers.get(id).unwrap();
3075 let name_len = state
3076 .buffer
3077 .file_path()
3078 .and_then(|p| p.file_name())
3079 .and_then(|n| n.to_str())
3080 .map(|s| s.chars().count())
3081 .unwrap_or(0);
3082 let tab_width = 2 + name_len;
3083 if idx < buffer_ids.len() - 1 {
3084 tab_width + 1 } else {
3086 tab_width
3087 }
3088 })
3089 .sum();
3090 assert!(view_state.tab_scroll_offset <= total_width);
3091 }
3092}