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 navigation;
50mod on_save_actions;
51mod orchestrator_persistence;
52mod path_utils;
53mod plugin_commands;
54mod plugin_dispatch;
55mod popup_actions;
56mod popup_dialogs;
57mod popup_overlay_actions;
58mod prompt_actions;
59mod prompt_lifecycle;
60mod recovery_actions;
61mod regex_replace;
62mod render;
63mod scan_orchestrators;
64mod scroll_sync;
65mod scrollbar_input;
66mod scrollbar_math;
67mod search_ops;
68mod search_scan;
69mod settings_actions;
70mod settings_prompts;
71mod shell_command;
72mod smart_home;
73mod split_actions;
74mod stdin_stream;
75mod tab_drag;
76mod terminal;
77mod terminal_input;
78mod terminal_mouse;
79mod text_ops;
80mod theme_inspect;
81mod toggle_actions;
82pub mod types;
83mod undo_actions;
84mod view_actions;
85mod virtual_buffers;
86pub mod warning_domains;
87pub mod window;
88mod window_actions;
89pub mod window_resources;
90pub mod workspace;
91
92use anyhow::Result as AnyhowResult;
93use rust_i18n::t;
94
95pub fn editor_tick(
100 editor: &mut Editor,
101 mut clear_terminal: impl FnMut() -> AnyhowResult<()>,
102) -> AnyhowResult<bool> {
103 let mut needs_render = false;
104
105 let async_messages = {
106 let _s = tracing::info_span!("process_async_messages").entered();
107 editor.process_async_messages()
108 };
109 if async_messages {
110 needs_render = true;
111 }
112 let pending_file_opens = {
113 let _s = tracing::info_span!("process_pending_file_opens").entered();
114 editor.process_pending_file_opens()
115 };
116 if pending_file_opens {
117 needs_render = true;
118 }
119 if editor.process_line_scan() {
120 needs_render = true;
121 }
122 let search_scan = {
123 let _s = tracing::info_span!("process_search_scan").entered();
124 editor.process_search_scan()
125 };
126 if search_scan {
127 needs_render = true;
128 }
129 let search_overlay_refresh = {
130 let _s = tracing::info_span!("check_search_overlay_refresh").entered();
131 editor.check_search_overlay_refresh()
132 };
133 if search_overlay_refresh {
134 needs_render = true;
135 }
136 if editor.check_mouse_hover_timer() {
137 needs_render = true;
138 }
139 if editor.active_window().check_semantic_highlight_timer() {
140 needs_render = true;
141 }
142 if editor.check_completion_trigger_timer() {
143 needs_render = true;
144 }
145 editor.active_window_mut().check_diagnostic_pull_timer();
146 if editor.check_warning_log() {
147 needs_render = true;
148 }
149 if editor.poll_stdin_streaming() {
150 needs_render = true;
151 }
152
153 if let Err(e) = editor.auto_recovery_save_dirty_buffers() {
154 tracing::debug!("Auto-recovery-save error: {}", e);
155 }
156 if let Err(e) = editor.auto_save_persistent_buffers() {
157 tracing::debug!("Auto-save (disk) error: {}", e);
158 }
159
160 if editor.take_full_redraw_request() {
161 clear_terminal()?;
162 needs_render = true;
163 }
164
165 Ok(needs_render)
166}
167
168pub(crate) use path_utils::normalize_path;
169
170use self::types::{
171 LspMenuItem, LspMessageEntry, LspProgressInfo, SearchState, TabContextMenu,
172 DEFAULT_BACKGROUND_FILE,
173};
174use crate::config::Config;
175use crate::config_io::DirectoryContext;
176use crate::input::buffer_mode::ModeRegistry;
177use crate::input::command_registry::CommandRegistry;
178use crate::input::keybindings::{Action, KeyContext, KeybindingResolver};
179use crate::input::quick_open::{
180 BufferProvider, CommandProvider, FileProvider, GotoLineProvider, QuickOpenRegistry,
181};
182use crate::model::cursor::Cursors;
183use crate::model::event::{Event, EventLog, LeafId, SplitDirection};
184use crate::model::filesystem::FileSystem;
185use crate::services::async_bridge::{AsyncBridge, AsyncMessage};
186use crate::services::fs::FsManager;
187use crate::services::lsp::manager::LspManager;
188use crate::services::plugins::PluginManager;
189use crate::services::recovery::{RecoveryConfig, RecoveryService};
190use crate::services::time_source::{RealTimeSource, SharedTimeSource};
191use crate::state::EditorState;
192use crate::types::{LspLanguageConfig, LspServerConfig, ProcessLimits};
193use crate::view::file_tree::{FileTree, FileTreeView};
194use crate::view::prompt::PromptType;
195use crate::view::split::{SplitManager, SplitViewState};
196use crate::view::ui::{
197 FileExplorerRenderer, SplitRenderer, StatusBarRenderer, SuggestionsRenderer,
198};
199use crossterm::event::{KeyCode, KeyModifiers};
200use ratatui::{
201 layout::{Constraint, Direction, Layout},
202 Frame,
203};
204use std::collections::HashMap;
205use std::ops::Range;
206use std::path::{Path, PathBuf};
207use std::sync::{Arc, Mutex, RwLock};
208use std::time::Instant;
209
210pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
212pub use self::warning_domains::{
213 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
214 WarningDomainRegistry, WarningLevel, WarningPopupContent,
215};
216pub use crate::model::event::BufferId;
217
218fn lsp_uri_to_host_path(
227 uri: &crate::app::types::LspUri,
228 translation: Option<&crate::services::authority::PathTranslation>,
229) -> Result<PathBuf, String> {
230 uri.to_host_path(translation)
231 .ok_or_else(|| "URI is not a file path".to_string())
232}
233
234#[derive(Clone, Debug)]
236pub struct PendingGrammar {
237 pub language: String,
239 pub grammar_path: String,
241 pub extensions: Vec<String>,
243}
244
245#[derive(Clone, Debug)]
247pub(crate) struct SemanticTokenRangeRequest {
248 pub(crate) buffer_id: BufferId,
249 pub(crate) version: u64,
250 pub(crate) range: Range<usize>,
251 pub(crate) start_line: usize,
252 pub(crate) end_line: usize,
253}
254
255#[derive(Clone, Copy, Debug)]
256pub(crate) enum SemanticTokensFullRequestKind {
257 Full,
258 FullDelta,
259}
260
261#[derive(Clone, Debug)]
262pub(crate) struct SemanticTokenFullRequest {
263 pub(crate) buffer_id: BufferId,
264 pub(crate) version: u64,
265 pub(crate) kind: SemanticTokensFullRequestKind,
266}
267
268#[derive(Clone, Debug)]
269pub(crate) struct FoldingRangeRequest {
270 pub(crate) buffer_id: BufferId,
271 pub(crate) version: u64,
272}
273
274#[derive(Clone, Debug)]
275pub(crate) struct InlayHintsRequest {
276 pub(crate) buffer_id: BufferId,
277 pub(crate) version: u64,
278}
279
280#[derive(Debug, Clone)]
286pub struct DabbrevCycleState {
287 pub original_prefix: String,
289 pub word_start: usize,
291 pub candidates: Vec<String>,
293 pub index: usize,
295}
296
297#[derive(Debug, Clone)]
312pub(crate) struct GotoLinePreviewSnapshot {
313 pub buffer_id: BufferId,
314 pub split_id: LeafId,
315 pub cursor_id: crate::model::event::CursorId,
316 pub position: usize,
317 pub anchor: Option<usize>,
318 pub sticky_column: usize,
319 pub viewport_top_byte: usize,
320 pub viewport_top_view_line_offset: usize,
321 pub viewport_left_column: usize,
322 pub last_jump_position: usize,
323}
324
325pub struct Editor {
327 next_buffer_id: usize,
349
350 pub(crate) buffer_id_alloc: crate::app::window_resources::BufferIdAllocator,
357
358 config: Arc<Config>,
378
379 config_snapshot_anchor: Arc<Config>,
381
382 config_cached_json: Arc<serde_json::Value>,
385
386 user_config_raw: Arc<serde_json::Value>,
388
389 dir_context: DirectoryContext,
391
392 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
394
395 pending_grammars: Vec<PendingGrammar>,
397
398 grammar_reload_pending: bool,
402
403 grammar_build_in_progress: bool,
406
407 needs_full_grammar_build: bool,
411
412 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
416
417 pub(crate) theme: Arc<RwLock<crate::view::theme::Theme>>,
422
423 theme_registry: Arc<crate::view::theme::ThemeRegistry>,
426
427 expanded_menus_cache: crate::view::ui::ExpandedMenusCache,
430
431 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
433
434 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
436
437 ansi_background_path: Option<PathBuf>,
439
440 background_fade: f32,
442
443 keybindings: Arc<RwLock<KeybindingResolver>>,
445
446 clipboard: crate::services::clipboard::Clipboard,
448
449 should_quit: bool,
451
452 should_detach: bool,
454
455 session_mode: bool,
457
458 software_cursor_only: bool,
460
461 session_name: Option<String>,
463
464 pending_escape_sequences: Vec<u8>,
467
468 restart_with_dir: Option<PathBuf>,
471
472 last_window_title: Option<String>,
479
480 terminal_width: u16,
483 terminal_height: u16,
484
485 mode_registry: ModeRegistry,
494
495 tokio_runtime: Option<Arc<tokio::runtime::Runtime>>,
497
498 async_bridge: Option<AsyncBridge>,
500
501 fs_manager: Arc<FsManager>,
519
520 authority: crate::services::authority::Authority,
530
531 pending_authority: Option<crate::services::authority::Authority>,
537
538 pub remote_indicator_override: Option<crate::view::ui::status_bar::RemoteIndicatorOverride>,
544
545 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
550
551 menu_state: crate::view::ui::MenuState,
573
574 menus: crate::config::MenuConfig,
576
577 working_dir: PathBuf,
585
586 pub(crate) windows: HashMap<fresh_core::WindowId, crate::app::window::Window>,
591
592 pub(crate) active_window: fresh_core::WindowId,
595
596 #[allow(dead_code)]
601 pub(crate) next_window_id: u64,
602
603 command_registry: Arc<RwLock<CommandRegistry>>,
621
622 quick_open_registry: QuickOpenRegistry,
624
625 plugin_manager: Arc<RwLock<PluginManager>>,
633
634 status_bar_token_registry: Mutex<HashMap<String, String>>,
640
641 pub(crate) plugin_schemas:
648 std::sync::Arc<std::sync::RwLock<HashMap<String, serde_json::Value>>>,
649
650 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
666
667 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
673 event_broadcaster: crate::model::control_event::EventBroadcaster,
692
693 #[cfg(feature = "plugins")]
700 pending_plugin_actions: Vec<(
701 String,
702 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
703 )>,
704
705 #[cfg(feature = "plugins")]
707 plugin_render_requested: bool,
708
709 recovery_service: RecoveryService,
736
737 full_redraw_requested: bool,
739
740 suspend_requested: bool,
743
744 time_source: SharedTimeSource,
746
747 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
757 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
759
760 status_log_path: Option<PathBuf>,
762
763 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
769
770 file_watcher_manager: crate::services::file_watcher::FileWatcherManager,
780
781 pub(crate) last_path_change_for_test: Option<(u64, std::path::PathBuf, &'static str)>,
787
788 pub(crate) last_watch_response_for_test: Option<(u64, Result<u64, String>)>,
792
793 pub(crate) preview_window_id: Option<fresh_core::WindowId>,
800
801 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
821
822 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
824
825 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
830
831 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
833
834 color_capability: crate::view::color_support::ColorCapability,
836
837 pub(crate) global_popups: crate::view::popup::PopupManager,
848
849 stdin_stream: stdin_stream::StdinStream,
867
868 pub(crate) previous_cursor_screen_pos: Option<((u16, u16), LeafId)>,
891 pub(crate) cursor_jump_animation: Option<crate::view::animation::AnimationId>,
894
895 pub(crate) pending_vb_animations: Vec<(u64, BufferId, fresh_core::api::PluginAnimationKind)>,
901
902 pub(crate) widget_registry: crate::widgets::WidgetRegistry,
910
911 pub(crate) floating_widget_panel: Option<FloatingWidgetState>,
918}
919
920pub(crate) const FLOATING_PANEL_BUFFER_ID: BufferId = BufferId(usize::MAX);
926
927#[derive(Debug, Clone)]
934pub(crate) struct FloatingWidgetState {
935 pub panel_id: crate::widgets::PanelId,
936 pub width_pct: u8,
937 pub height_pct: u8,
938 pub entries: Vec<fresh_core::text_property::TextPropertyEntry>,
942 pub focus_cursor: Option<crate::widgets::FocusCursor>,
944 pub embeds: Vec<crate::widgets::EmbedRect>,
951 pub overlays: Vec<crate::widgets::OverlayRow>,
958 pub last_inner_rect: Option<ratatui::layout::Rect>,
961}
962
963#[derive(Debug, Clone)]
965pub struct PendingFileOpen {
966 pub path: PathBuf,
968 pub line: Option<usize>,
970 pub column: Option<usize>,
972 pub end_line: Option<usize>,
974 pub end_column: Option<usize>,
976 pub message: Option<String>,
978 pub wait_id: Option<u64>,
980}
981
982impl Editor {
983 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
985 let trimmed = input.trim();
986
987 if trimmed.is_empty() {
988 self.ansi_background = None;
989 self.ansi_background_path = None;
990 self.set_status_message(t!("status.background_cleared").to_string());
991 return Ok(());
992 }
993
994 let input_path = Path::new(trimmed);
995 let resolved = if input_path.is_absolute() {
996 input_path.to_path_buf()
997 } else {
998 self.working_dir.join(input_path)
999 };
1000
1001 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1002
1003 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1004
1005 self.ansi_background = Some(parsed);
1006 self.ansi_background_path = Some(canonical.clone());
1007 self.set_status_message(
1008 t!(
1009 "view.background_set",
1010 path = canonical.display().to_string()
1011 )
1012 .to_string(),
1013 );
1014
1015 Ok(())
1016 }
1017
1018 #[doc(hidden)]
1022 pub fn buffer_count_for_tests(&self) -> usize {
1023 self.windows
1024 .get(&self.active_window)
1025 .map(|w| &w.buffers)
1026 .expect("active window present")
1027 .len()
1028 }
1029
1030 #[doc(hidden)]
1034 pub fn all_buffer_ids_for_tests(&self) -> Vec<BufferId> {
1035 let mut ids: Vec<BufferId> = self
1036 .windows
1037 .get(&self.active_window)
1038 .map(|w| &w.buffers)
1039 .expect("active window present")
1040 .ids();
1041 ids.sort_by_key(|id| id.0);
1042 ids
1043 }
1044
1045 pub fn active_state(&self) -> &EditorState {
1047 self.windows
1048 .get(&self.active_window)
1049 .map(|w| &w.buffers)
1050 .expect("active window present")
1051 .get(&self.active_buffer())
1052 .unwrap()
1053 }
1054
1055 pub fn active_state_mut(&mut self) -> &mut EditorState {
1057 let __buffer_id = self.active_buffer();
1058 self.windows
1059 .get_mut(&self.active_window)
1060 .map(|w| &mut w.buffers)
1061 .expect("active window present")
1062 .get_mut(&__buffer_id)
1063 .unwrap()
1064 }
1065
1066 pub fn active_cursors(&self) -> &Cursors {
1070 let split_id = self.effective_active_split();
1071 &self
1072 .windows
1073 .get(&self.active_window)
1074 .and_then(|w| w.buffers.splits())
1075 .map(|(_, vs)| vs)
1076 .expect("active window must have a populated split layout")
1077 .get(&split_id)
1078 .unwrap()
1079 .cursors
1080 }
1081
1082 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1084 let split_id = self.effective_active_split();
1085 &mut self
1086 .windows
1087 .get_mut(&self.active_window)
1088 .and_then(|w| w.split_view_states_mut())
1089 .expect("active window must have a populated split layout")
1090 .get_mut(&split_id)
1091 .unwrap()
1092 .cursors
1093 }
1094
1095 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1097 self.active_window_mut().completion_items = Some(items);
1098 }
1099
1100 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1102 let active_split = self
1103 .windows
1104 .get(&self.active_window)
1105 .and_then(|w| w.buffers.splits())
1106 .map(|(mgr, _)| mgr)
1107 .expect("active window must have a populated split layout")
1108 .active_split();
1109 &self
1110 .windows
1111 .get(&self.active_window)
1112 .and_then(|w| w.buffers.splits())
1113 .map(|(_, vs)| vs)
1114 .expect("active window must have a populated split layout")
1115 .get(&active_split)
1116 .unwrap()
1117 .viewport
1118 }
1119
1120 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1122 let active_split = self
1123 .windows
1124 .get(&self.active_window)
1125 .and_then(|w| w.buffers.splits())
1126 .map(|(mgr, _)| mgr)
1127 .expect("active window must have a populated split layout")
1128 .active_split();
1129 &mut self
1130 .windows
1131 .get_mut(&self.active_window)
1132 .and_then(|w| w.split_view_states_mut())
1133 .expect("active window must have a populated split layout")
1134 .get_mut(&active_split)
1135 .unwrap()
1136 .viewport
1137 }
1138
1139 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1141 if let Some(composite) = self.active_window().composite_buffers.get(&buffer_id) {
1143 return composite.name.clone();
1144 }
1145
1146 self.active_window()
1147 .buffer_metadata
1148 .get(&buffer_id)
1149 .map(|m| m.display_name.clone())
1150 .or_else(|| {
1151 self.windows
1152 .get(&self.active_window)
1153 .map(|w| &w.buffers)
1154 .expect("active window present")
1155 .get(&buffer_id)
1156 .and_then(|state| {
1157 state
1158 .buffer
1159 .file_path()
1160 .and_then(|p| p.file_name())
1161 .and_then(|n| n.to_str())
1162 .map(|s| s.to_string())
1163 })
1164 })
1165 .unwrap_or_else(|| "[No Name]".to_string())
1166 }
1167
1168 pub fn active_event_log(&self) -> &EventLog {
1178 self.active_window()
1179 .event_logs
1180 .get(&self.active_buffer())
1181 .unwrap()
1182 }
1183
1184 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1186 let buffer_id = self.active_buffer();
1187 self.active_window_mut()
1188 .event_logs
1189 .get_mut(&buffer_id)
1190 .unwrap()
1191 }
1192
1193 pub fn register_status_bar_element(
1197 &self,
1198 plugin_name: &str,
1199 token_name: &str,
1200 title: &str,
1201 ) -> Result<(), String> {
1202 if plugin_name.is_empty() {
1203 return Err("Plugin name cannot be empty".to_string());
1204 }
1205 if token_name.is_empty() {
1206 return Err("Token name cannot be empty".to_string());
1207 }
1208
1209 let key = format!("{}:{}", plugin_name, token_name);
1210 let mut registry = self.status_bar_token_registry.lock().unwrap();
1211
1212 if registry.contains_key(&key) {
1213 return Err(format!("Token '{}' already registered", key));
1214 }
1215
1216 registry.insert(key, title.to_string());
1217 Ok(())
1218 }
1219
1220 pub fn set_status_bar_value(
1225 &mut self,
1226 buffer_id: BufferId,
1227 key: &str,
1228 value: String,
1229 ) -> Result<(), String> {
1230 for window in self.windows.values_mut() {
1231 if window.buffers.contains_key(&buffer_id) {
1232 window
1233 .status_bar_values
1234 .entry(buffer_id)
1235 .or_default()
1236 .insert(key.to_string(), value);
1237 return Ok(());
1238 }
1239 }
1240 Err(format!("Buffer {:?} not found", buffer_id))
1241 }
1242
1243 pub fn get_status_bar_elements(&self) -> Vec<(String, String)> {
1246 self.status_bar_token_registry
1247 .lock()
1248 .unwrap()
1249 .iter()
1250 .map(|(k, title)| (format!("{{{}}}", k), title.clone()))
1251 .collect()
1252 }
1253
1254 pub fn get_status_bar_element_values(&self, buffer_id: BufferId) -> HashMap<String, String> {
1256 for window in self.windows.values() {
1257 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1258 return values.clone();
1259 }
1260 }
1261 HashMap::new()
1262 }
1263
1264 pub fn current_status_bar_value(&self, buffer_id: BufferId, key: &str) -> Option<&str> {
1268 for window in self.windows.values() {
1269 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1270 if let Some(v) = values.get(key) {
1271 return Some(v.as_str());
1272 }
1273 return None;
1274 }
1275 }
1276 None
1277 }
1278
1279 fn remove_plugin_status_bar_elements(&mut self, plugin_name: &str) {
1282 let prefix = format!("{}:", plugin_name);
1283 self.status_bar_token_registry
1284 .lock()
1285 .unwrap()
1286 .retain(|k, _| !k.starts_with(&prefix));
1287 for window in self.windows.values_mut() {
1288 for values in window.status_bar_values.values_mut() {
1289 values.retain(|k, _| !k.starts_with(&prefix));
1290 }
1291 }
1292 }
1293}
1294
1295fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1304 use crossterm::event::{KeyCode, KeyModifiers};
1305
1306 let mut modifiers = KeyModifiers::NONE;
1307 let mut remaining = key_str;
1308
1309 loop {
1311 if remaining.starts_with("C-") {
1312 modifiers |= KeyModifiers::CONTROL;
1313 remaining = &remaining[2..];
1314 } else if remaining.starts_with("M-") {
1315 modifiers |= KeyModifiers::ALT;
1316 remaining = &remaining[2..];
1317 } else if remaining.starts_with("S-") {
1318 modifiers |= KeyModifiers::SHIFT;
1319 remaining = &remaining[2..];
1320 } else {
1321 break;
1322 }
1323 }
1324
1325 let upper = remaining.to_uppercase();
1328 let code = match upper.as_str() {
1329 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1330 "TAB" => KeyCode::Tab,
1331 "BACKTAB" => KeyCode::BackTab,
1332 "ESC" | "ESCAPE" => KeyCode::Esc,
1333 "SPC" | "SPACE" => KeyCode::Char(' '),
1334 "DEL" | "DELETE" => KeyCode::Delete,
1335 "BS" | "BACKSPACE" => KeyCode::Backspace,
1336 "UP" => KeyCode::Up,
1337 "DOWN" => KeyCode::Down,
1338 "LEFT" => KeyCode::Left,
1339 "RIGHT" => KeyCode::Right,
1340 "HOME" => KeyCode::Home,
1341 "END" => KeyCode::End,
1342 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1343 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1344 s if s.starts_with('F') && s.len() > 1 => {
1345 if let Ok(n) = s[1..].parse::<u8>() {
1347 KeyCode::F(n)
1348 } else {
1349 return None;
1350 }
1351 }
1352 _ if remaining.len() == 1 => {
1353 let c = remaining.chars().next()?;
1356 if c.is_ascii_uppercase() {
1357 modifiers |= KeyModifiers::SHIFT;
1358 }
1359 KeyCode::Char(c.to_ascii_lowercase())
1360 }
1361 _ => return None,
1362 };
1363
1364 if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
1369 return Some((KeyCode::BackTab, modifiers.difference(KeyModifiers::SHIFT)));
1370 }
1371
1372 Some((code, modifiers))
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use super::*;
1378 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1379 use tempfile::TempDir;
1380
1381 fn test_dir_context() -> (DirectoryContext, TempDir) {
1383 let temp_dir = TempDir::new().unwrap();
1384 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1385 (dir_context, temp_dir)
1386 }
1387
1388 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1390 Arc::new(crate::model::filesystem::StdFileSystem)
1391 }
1392
1393 #[test]
1394 fn parse_key_string_shift_tab_normalizes_to_backtab() {
1395 use crossterm::event::{KeyCode, KeyModifiers};
1396 assert_eq!(
1401 parse_key_string("S-Tab"),
1402 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1403 );
1404 assert_eq!(
1405 parse_key_string("BackTab"),
1406 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1407 );
1408 assert_eq!(
1410 parse_key_string("Tab"),
1411 Some((KeyCode::Tab, KeyModifiers::NONE)),
1412 );
1413 }
1414
1415 #[test]
1416 fn test_editor_new() {
1417 let config = Config::default();
1418 let (dir_context, _temp) = test_dir_context();
1419 let editor = Editor::new(
1420 config,
1421 80,
1422 24,
1423 dir_context,
1424 crate::view::color_support::ColorCapability::TrueColor,
1425 test_filesystem(),
1426 )
1427 .unwrap();
1428
1429 assert_eq!(editor.buffers().len(), 1);
1430 assert!(!editor.should_quit());
1431 }
1432
1433 #[test]
1434 fn test_new_buffer() {
1435 let config = Config::default();
1436 let (dir_context, _temp) = test_dir_context();
1437 let mut editor = Editor::new(
1438 config,
1439 80,
1440 24,
1441 dir_context,
1442 crate::view::color_support::ColorCapability::TrueColor,
1443 test_filesystem(),
1444 )
1445 .unwrap();
1446
1447 let id = editor.new_buffer();
1448 assert_eq!(editor.buffers().len(), 2);
1449 assert_eq!(editor.active_buffer(), id);
1450 }
1451
1452 #[test]
1453 #[ignore]
1454 fn test_clipboard() {
1455 let config = Config::default();
1456 let (dir_context, _temp) = test_dir_context();
1457 let mut editor = Editor::new(
1458 config,
1459 80,
1460 24,
1461 dir_context,
1462 crate::view::color_support::ColorCapability::TrueColor,
1463 test_filesystem(),
1464 )
1465 .unwrap();
1466
1467 editor.clipboard.set_internal("test".to_string());
1469
1470 editor.paste();
1472
1473 let content = editor.active_state().buffer.to_string().unwrap();
1474 assert_eq!(content, "test");
1475 }
1476
1477 #[test]
1478 fn test_action_to_events_insert_char() {
1479 let config = Config::default();
1480 let (dir_context, _temp) = test_dir_context();
1481 let mut editor = Editor::new(
1482 config,
1483 80,
1484 24,
1485 dir_context,
1486 crate::view::color_support::ColorCapability::TrueColor,
1487 test_filesystem(),
1488 )
1489 .unwrap();
1490
1491 let events = editor
1492 .active_window_mut()
1493 .action_to_events(Action::InsertChar('a'));
1494 assert!(events.is_some());
1495
1496 let events = events.unwrap();
1497 assert_eq!(events.len(), 1);
1498
1499 match &events[0] {
1500 Event::Insert { position, text, .. } => {
1501 assert_eq!(*position, 0);
1502 assert_eq!(text, "a");
1503 }
1504 _ => panic!("Expected Insert event"),
1505 }
1506 }
1507
1508 #[test]
1509 fn test_action_to_events_move_right() {
1510 let config = Config::default();
1511 let (dir_context, _temp) = test_dir_context();
1512 let mut editor = Editor::new(
1513 config,
1514 80,
1515 24,
1516 dir_context,
1517 crate::view::color_support::ColorCapability::TrueColor,
1518 test_filesystem(),
1519 )
1520 .unwrap();
1521
1522 let cursor_id = editor.active_cursors().primary_id();
1524 editor.apply_event_to_active_buffer(&Event::Insert {
1525 position: 0,
1526 text: "hello".to_string(),
1527 cursor_id,
1528 });
1529
1530 let events = editor
1531 .active_window_mut()
1532 .action_to_events(Action::MoveRight);
1533 assert!(events.is_some());
1534
1535 let events = events.unwrap();
1536 assert_eq!(events.len(), 1);
1537
1538 match &events[0] {
1539 Event::MoveCursor {
1540 new_position,
1541 new_anchor,
1542 ..
1543 } => {
1544 assert_eq!(*new_position, 5);
1546 assert_eq!(*new_anchor, None); }
1548 _ => panic!("Expected MoveCursor event"),
1549 }
1550 }
1551
1552 #[test]
1553 fn test_action_to_events_move_up_down() {
1554 let config = Config::default();
1555 let (dir_context, _temp) = test_dir_context();
1556 let mut editor = Editor::new(
1557 config,
1558 80,
1559 24,
1560 dir_context,
1561 crate::view::color_support::ColorCapability::TrueColor,
1562 test_filesystem(),
1563 )
1564 .unwrap();
1565
1566 let cursor_id = editor.active_cursors().primary_id();
1568 editor.apply_event_to_active_buffer(&Event::Insert {
1569 position: 0,
1570 text: "line1\nline2\nline3".to_string(),
1571 cursor_id,
1572 });
1573
1574 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1576 cursor_id,
1577 old_position: 0, new_position: 6,
1579 old_anchor: None, new_anchor: None,
1581 old_sticky_column: 0,
1582 new_sticky_column: 0,
1583 });
1584
1585 let events = editor.active_window_mut().action_to_events(Action::MoveUp);
1587 assert!(events.is_some());
1588 let events = events.unwrap();
1589 assert_eq!(events.len(), 1);
1590
1591 match &events[0] {
1592 Event::MoveCursor { new_position, .. } => {
1593 assert_eq!(*new_position, 0); }
1595 _ => panic!("Expected MoveCursor event"),
1596 }
1597 }
1598
1599 #[test]
1600 fn test_action_to_events_insert_newline() {
1601 let config = Config::default();
1602 let (dir_context, _temp) = test_dir_context();
1603 let mut editor = Editor::new(
1604 config,
1605 80,
1606 24,
1607 dir_context,
1608 crate::view::color_support::ColorCapability::TrueColor,
1609 test_filesystem(),
1610 )
1611 .unwrap();
1612
1613 let events = editor
1614 .active_window_mut()
1615 .action_to_events(Action::InsertNewline);
1616 assert!(events.is_some());
1617
1618 let events = events.unwrap();
1619 assert_eq!(events.len(), 1);
1620
1621 match &events[0] {
1622 Event::Insert { text, .. } => {
1623 assert_eq!(text, "\n");
1624 }
1625 _ => panic!("Expected Insert event"),
1626 }
1627 }
1628
1629 #[test]
1630 fn test_action_to_events_unimplemented() {
1631 let config = Config::default();
1632 let (dir_context, _temp) = test_dir_context();
1633 let mut editor = Editor::new(
1634 config,
1635 80,
1636 24,
1637 dir_context,
1638 crate::view::color_support::ColorCapability::TrueColor,
1639 test_filesystem(),
1640 )
1641 .unwrap();
1642
1643 assert!(editor
1645 .active_window_mut()
1646 .action_to_events(Action::Save)
1647 .is_none());
1648 assert!(editor
1649 .active_window_mut()
1650 .action_to_events(Action::Quit)
1651 .is_none());
1652 assert!(editor
1653 .active_window_mut()
1654 .action_to_events(Action::Undo)
1655 .is_none());
1656 }
1657
1658 #[test]
1659 fn test_action_to_events_delete_backward() {
1660 let config = Config::default();
1661 let (dir_context, _temp) = test_dir_context();
1662 let mut editor = Editor::new(
1663 config,
1664 80,
1665 24,
1666 dir_context,
1667 crate::view::color_support::ColorCapability::TrueColor,
1668 test_filesystem(),
1669 )
1670 .unwrap();
1671
1672 let cursor_id = editor.active_cursors().primary_id();
1674 editor.apply_event_to_active_buffer(&Event::Insert {
1675 position: 0,
1676 text: "hello".to_string(),
1677 cursor_id,
1678 });
1679
1680 let events = editor
1681 .active_window_mut()
1682 .action_to_events(Action::DeleteBackward);
1683 assert!(events.is_some());
1684
1685 let events = events.unwrap();
1686 assert_eq!(events.len(), 1);
1687
1688 match &events[0] {
1689 Event::Delete {
1690 range,
1691 deleted_text,
1692 ..
1693 } => {
1694 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1696 }
1697 _ => panic!("Expected Delete event"),
1698 }
1699 }
1700
1701 #[test]
1702 fn test_action_to_events_delete_forward() {
1703 let config = Config::default();
1704 let (dir_context, _temp) = test_dir_context();
1705 let mut editor = Editor::new(
1706 config,
1707 80,
1708 24,
1709 dir_context,
1710 crate::view::color_support::ColorCapability::TrueColor,
1711 test_filesystem(),
1712 )
1713 .unwrap();
1714
1715 let cursor_id = editor.active_cursors().primary_id();
1717 editor.apply_event_to_active_buffer(&Event::Insert {
1718 position: 0,
1719 text: "hello".to_string(),
1720 cursor_id,
1721 });
1722
1723 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1725 cursor_id,
1726 old_position: 0, new_position: 0,
1728 old_anchor: None, new_anchor: None,
1730 old_sticky_column: 0,
1731 new_sticky_column: 0,
1732 });
1733
1734 let events = editor
1735 .active_window_mut()
1736 .action_to_events(Action::DeleteForward);
1737 assert!(events.is_some());
1738
1739 let events = events.unwrap();
1740 assert_eq!(events.len(), 1);
1741
1742 match &events[0] {
1743 Event::Delete {
1744 range,
1745 deleted_text,
1746 ..
1747 } => {
1748 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1750 }
1751 _ => panic!("Expected Delete event"),
1752 }
1753 }
1754
1755 #[test]
1756 fn test_action_to_events_select_right() {
1757 let config = Config::default();
1758 let (dir_context, _temp) = test_dir_context();
1759 let mut editor = Editor::new(
1760 config,
1761 80,
1762 24,
1763 dir_context,
1764 crate::view::color_support::ColorCapability::TrueColor,
1765 test_filesystem(),
1766 )
1767 .unwrap();
1768
1769 let cursor_id = editor.active_cursors().primary_id();
1771 editor.apply_event_to_active_buffer(&Event::Insert {
1772 position: 0,
1773 text: "hello".to_string(),
1774 cursor_id,
1775 });
1776
1777 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1779 cursor_id,
1780 old_position: 0, new_position: 0,
1782 old_anchor: None, new_anchor: None,
1784 old_sticky_column: 0,
1785 new_sticky_column: 0,
1786 });
1787
1788 let events = editor
1789 .active_window_mut()
1790 .action_to_events(Action::SelectRight);
1791 assert!(events.is_some());
1792
1793 let events = events.unwrap();
1794 assert_eq!(events.len(), 1);
1795
1796 match &events[0] {
1797 Event::MoveCursor {
1798 new_position,
1799 new_anchor,
1800 ..
1801 } => {
1802 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1805 _ => panic!("Expected MoveCursor event"),
1806 }
1807 }
1808
1809 #[test]
1810 fn test_action_to_events_select_all() {
1811 let config = Config::default();
1812 let (dir_context, _temp) = test_dir_context();
1813 let mut editor = Editor::new(
1814 config,
1815 80,
1816 24,
1817 dir_context,
1818 crate::view::color_support::ColorCapability::TrueColor,
1819 test_filesystem(),
1820 )
1821 .unwrap();
1822
1823 let cursor_id = editor.active_cursors().primary_id();
1825 editor.apply_event_to_active_buffer(&Event::Insert {
1826 position: 0,
1827 text: "hello world".to_string(),
1828 cursor_id,
1829 });
1830
1831 let events = editor
1832 .active_window_mut()
1833 .action_to_events(Action::SelectAll);
1834 assert!(events.is_some());
1835
1836 let events = events.unwrap();
1837 assert_eq!(events.len(), 1);
1838
1839 match &events[0] {
1840 Event::MoveCursor {
1841 new_position,
1842 new_anchor,
1843 ..
1844 } => {
1845 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1848 _ => panic!("Expected MoveCursor event"),
1849 }
1850 }
1851
1852 #[test]
1853 fn test_action_to_events_document_nav() {
1854 let config = Config::default();
1855 let (dir_context, _temp) = test_dir_context();
1856 let mut editor = Editor::new(
1857 config,
1858 80,
1859 24,
1860 dir_context,
1861 crate::view::color_support::ColorCapability::TrueColor,
1862 test_filesystem(),
1863 )
1864 .unwrap();
1865
1866 let cursor_id = editor.active_cursors().primary_id();
1868 editor.apply_event_to_active_buffer(&Event::Insert {
1869 position: 0,
1870 text: "line1\nline2\nline3".to_string(),
1871 cursor_id,
1872 });
1873
1874 let events = editor
1876 .active_window_mut()
1877 .action_to_events(Action::MoveDocumentStart);
1878 assert!(events.is_some());
1879 let events = events.unwrap();
1880 match &events[0] {
1881 Event::MoveCursor { new_position, .. } => {
1882 assert_eq!(*new_position, 0);
1883 }
1884 _ => panic!("Expected MoveCursor event"),
1885 }
1886
1887 let events = editor
1889 .active_window_mut()
1890 .action_to_events(Action::MoveDocumentEnd);
1891 assert!(events.is_some());
1892 let events = events.unwrap();
1893 match &events[0] {
1894 Event::MoveCursor { new_position, .. } => {
1895 assert_eq!(*new_position, 17); }
1897 _ => panic!("Expected MoveCursor event"),
1898 }
1899 }
1900
1901 #[test]
1902 fn test_action_to_events_remove_secondary_cursors() {
1903 use crate::model::event::CursorId;
1904
1905 let config = Config::default();
1906 let (dir_context, _temp) = test_dir_context();
1907 let mut editor = Editor::new(
1908 config,
1909 80,
1910 24,
1911 dir_context,
1912 crate::view::color_support::ColorCapability::TrueColor,
1913 test_filesystem(),
1914 )
1915 .unwrap();
1916
1917 let cursor_id = editor.active_cursors().primary_id();
1919 editor.apply_event_to_active_buffer(&Event::Insert {
1920 position: 0,
1921 text: "hello world test".to_string(),
1922 cursor_id,
1923 });
1924
1925 editor.apply_event_to_active_buffer(&Event::AddCursor {
1927 cursor_id: CursorId(1),
1928 position: 5,
1929 anchor: None,
1930 });
1931 editor.apply_event_to_active_buffer(&Event::AddCursor {
1932 cursor_id: CursorId(2),
1933 position: 10,
1934 anchor: None,
1935 });
1936
1937 assert_eq!(editor.active_cursors().count(), 3);
1938
1939 let first_id = editor
1941 .active_cursors()
1942 .iter()
1943 .map(|(id, _)| id)
1944 .min_by_key(|id| id.0)
1945 .expect("Should have at least one cursor");
1946
1947 let events = editor
1949 .active_window_mut()
1950 .action_to_events(Action::RemoveSecondaryCursors);
1951 assert!(events.is_some());
1952
1953 let events = events.unwrap();
1954 let remove_cursor_events: Vec<_> = events
1957 .iter()
1958 .filter_map(|e| match e {
1959 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1960 _ => None,
1961 })
1962 .collect();
1963
1964 assert_eq!(remove_cursor_events.len(), 2);
1966
1967 for cursor_id in &remove_cursor_events {
1968 assert_ne!(*cursor_id, first_id);
1970 }
1971 }
1972
1973 #[test]
1974 fn test_action_to_events_scroll() {
1975 let config = Config::default();
1976 let (dir_context, _temp) = test_dir_context();
1977 let mut editor = Editor::new(
1978 config,
1979 80,
1980 24,
1981 dir_context,
1982 crate::view::color_support::ColorCapability::TrueColor,
1983 test_filesystem(),
1984 )
1985 .unwrap();
1986
1987 let events = editor
1989 .active_window_mut()
1990 .action_to_events(Action::ScrollUp);
1991 assert!(events.is_some());
1992 let events = events.unwrap();
1993 assert_eq!(events.len(), 1);
1994 match &events[0] {
1995 Event::Scroll { line_offset } => {
1996 assert_eq!(*line_offset, -1);
1997 }
1998 _ => panic!("Expected Scroll event"),
1999 }
2000
2001 let events = editor
2003 .active_window_mut()
2004 .action_to_events(Action::ScrollDown);
2005 assert!(events.is_some());
2006 let events = events.unwrap();
2007 assert_eq!(events.len(), 1);
2008 match &events[0] {
2009 Event::Scroll { line_offset } => {
2010 assert_eq!(*line_offset, 1);
2011 }
2012 _ => panic!("Expected Scroll event"),
2013 }
2014 }
2015
2016 #[test]
2017 fn test_action_to_events_none() {
2018 let config = Config::default();
2019 let (dir_context, _temp) = test_dir_context();
2020 let mut editor = Editor::new(
2021 config,
2022 80,
2023 24,
2024 dir_context,
2025 crate::view::color_support::ColorCapability::TrueColor,
2026 test_filesystem(),
2027 )
2028 .unwrap();
2029
2030 let events = editor.active_window_mut().action_to_events(Action::None);
2032 assert!(events.is_none());
2033 }
2034
2035 #[test]
2036 fn test_lsp_incremental_insert_generates_correct_range() {
2037 use crate::model::buffer::Buffer;
2040
2041 let buffer = Buffer::from_str_test("hello\nworld");
2042
2043 let position = 0;
2046 let (line, character) = buffer.position_to_lsp_position(position);
2047
2048 assert_eq!(line, 0, "Insertion at start should be line 0");
2049 assert_eq!(character, 0, "Insertion at start should be char 0");
2050
2051 let lsp_pos = Position::new(line as u32, character as u32);
2053 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2054
2055 assert_eq!(lsp_range.start.line, 0);
2056 assert_eq!(lsp_range.start.character, 0);
2057 assert_eq!(lsp_range.end.line, 0);
2058 assert_eq!(lsp_range.end.character, 0);
2059 assert_eq!(
2060 lsp_range.start, lsp_range.end,
2061 "Insert should have zero-width range"
2062 );
2063
2064 let position = 3;
2066 let (line, character) = buffer.position_to_lsp_position(position);
2067
2068 assert_eq!(line, 0);
2069 assert_eq!(character, 3);
2070
2071 let position = 6;
2073 let (line, character) = buffer.position_to_lsp_position(position);
2074
2075 assert_eq!(line, 1, "Position after newline should be line 1");
2076 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2077 }
2078
2079 #[test]
2080 fn test_lsp_incremental_delete_generates_correct_range() {
2081 use crate::model::buffer::Buffer;
2084
2085 let buffer = Buffer::from_str_test("hello\nworld");
2086
2087 let range_start = 1;
2089 let range_end = 5;
2090
2091 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2092 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2093
2094 assert_eq!(start_line, 0);
2095 assert_eq!(start_char, 1);
2096 assert_eq!(end_line, 0);
2097 assert_eq!(end_char, 5);
2098
2099 let lsp_range = LspRange::new(
2100 Position::new(start_line as u32, start_char as u32),
2101 Position::new(end_line as u32, end_char as u32),
2102 );
2103
2104 assert_eq!(lsp_range.start.line, 0);
2105 assert_eq!(lsp_range.start.character, 1);
2106 assert_eq!(lsp_range.end.line, 0);
2107 assert_eq!(lsp_range.end.character, 5);
2108 assert_ne!(
2109 lsp_range.start, lsp_range.end,
2110 "Delete should have non-zero range"
2111 );
2112
2113 let range_start = 4;
2115 let range_end = 8;
2116
2117 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2118 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2119
2120 assert_eq!(start_line, 0, "Delete start on line 0");
2121 assert_eq!(start_char, 4, "Delete start at char 4");
2122 assert_eq!(end_line, 1, "Delete end on line 1");
2123 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2124 }
2125
2126 #[test]
2127 fn test_lsp_incremental_utf16_encoding() {
2128 use crate::model::buffer::Buffer;
2131
2132 let buffer = Buffer::from_str_test("😀hello");
2134
2135 let (line, character) = buffer.position_to_lsp_position(4);
2137
2138 assert_eq!(line, 0);
2139 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2140
2141 let (line, character) = buffer.position_to_lsp_position(9);
2143
2144 assert_eq!(line, 0);
2145 assert_eq!(
2146 character, 7,
2147 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2148 );
2149
2150 let buffer = Buffer::from_str_test("café");
2152
2153 let (line, character) = buffer.position_to_lsp_position(3);
2155
2156 assert_eq!(line, 0);
2157 assert_eq!(character, 3);
2158
2159 let (line, character) = buffer.position_to_lsp_position(5);
2161
2162 assert_eq!(line, 0);
2163 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2164 }
2165
2166 #[test]
2167 fn test_lsp_content_change_event_structure() {
2168 let insert_change = TextDocumentContentChangeEvent {
2172 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2173 range_length: None,
2174 text: "NEW".to_string(),
2175 };
2176
2177 assert!(insert_change.range.is_some());
2178 assert_eq!(insert_change.text, "NEW");
2179 let range = insert_change.range.unwrap();
2180 assert_eq!(
2181 range.start, range.end,
2182 "Insert should have zero-width range"
2183 );
2184
2185 let delete_change = TextDocumentContentChangeEvent {
2187 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2188 range_length: None,
2189 text: String::new(),
2190 };
2191
2192 assert!(delete_change.range.is_some());
2193 assert_eq!(delete_change.text, "");
2194 let range = delete_change.range.unwrap();
2195 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2196 assert_eq!(range.start.line, 0);
2197 assert_eq!(range.start.character, 2);
2198 assert_eq!(range.end.line, 0);
2199 assert_eq!(range.end.character, 7);
2200 }
2201
2202 #[test]
2203 fn test_goto_matching_bracket_forward() {
2204 let config = Config::default();
2205 let (dir_context, _temp) = test_dir_context();
2206 let mut editor = Editor::new(
2207 config,
2208 80,
2209 24,
2210 dir_context,
2211 crate::view::color_support::ColorCapability::TrueColor,
2212 test_filesystem(),
2213 )
2214 .unwrap();
2215
2216 let cursor_id = editor.active_cursors().primary_id();
2218 editor.apply_event_to_active_buffer(&Event::Insert {
2219 position: 0,
2220 text: "fn main() { let x = (1 + 2); }".to_string(),
2221 cursor_id,
2222 });
2223
2224 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2226 cursor_id,
2227 old_position: 31,
2228 new_position: 10,
2229 old_anchor: None,
2230 new_anchor: None,
2231 old_sticky_column: 0,
2232 new_sticky_column: 0,
2233 });
2234
2235 assert_eq!(editor.active_cursors().primary().position, 10);
2236
2237 editor.goto_matching_bracket();
2239
2240 assert_eq!(editor.active_cursors().primary().position, 29);
2245 }
2246
2247 #[test]
2248 fn test_goto_matching_bracket_backward() {
2249 let config = Config::default();
2250 let (dir_context, _temp) = test_dir_context();
2251 let mut editor = Editor::new(
2252 config,
2253 80,
2254 24,
2255 dir_context,
2256 crate::view::color_support::ColorCapability::TrueColor,
2257 test_filesystem(),
2258 )
2259 .unwrap();
2260
2261 let cursor_id = editor.active_cursors().primary_id();
2263 editor.apply_event_to_active_buffer(&Event::Insert {
2264 position: 0,
2265 text: "fn main() { let x = (1 + 2); }".to_string(),
2266 cursor_id,
2267 });
2268
2269 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2271 cursor_id,
2272 old_position: 31,
2273 new_position: 26,
2274 old_anchor: None,
2275 new_anchor: None,
2276 old_sticky_column: 0,
2277 new_sticky_column: 0,
2278 });
2279
2280 editor.goto_matching_bracket();
2282
2283 assert_eq!(editor.active_cursors().primary().position, 20);
2285 }
2286
2287 #[test]
2288 fn test_goto_matching_bracket_nested() {
2289 let config = Config::default();
2290 let (dir_context, _temp) = test_dir_context();
2291 let mut editor = Editor::new(
2292 config,
2293 80,
2294 24,
2295 dir_context,
2296 crate::view::color_support::ColorCapability::TrueColor,
2297 test_filesystem(),
2298 )
2299 .unwrap();
2300
2301 let cursor_id = editor.active_cursors().primary_id();
2303 editor.apply_event_to_active_buffer(&Event::Insert {
2304 position: 0,
2305 text: "{a{b{c}d}e}".to_string(),
2306 cursor_id,
2307 });
2308
2309 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2311 cursor_id,
2312 old_position: 11,
2313 new_position: 0,
2314 old_anchor: None,
2315 new_anchor: None,
2316 old_sticky_column: 0,
2317 new_sticky_column: 0,
2318 });
2319
2320 editor.goto_matching_bracket();
2322
2323 assert_eq!(editor.active_cursors().primary().position, 10);
2325 }
2326
2327 #[test]
2328 fn test_search_case_sensitive() {
2329 let config = Config::default();
2330 let (dir_context, _temp) = test_dir_context();
2331 let mut editor = Editor::new(
2332 config,
2333 80,
2334 24,
2335 dir_context,
2336 crate::view::color_support::ColorCapability::TrueColor,
2337 test_filesystem(),
2338 )
2339 .unwrap();
2340
2341 let cursor_id = editor.active_cursors().primary_id();
2343 editor.apply_event_to_active_buffer(&Event::Insert {
2344 position: 0,
2345 text: "Hello hello HELLO".to_string(),
2346 cursor_id,
2347 });
2348
2349 editor.active_window_mut().search_case_sensitive = false;
2351 editor.perform_search("hello");
2352
2353 let search_state = editor.active_window().search_state.as_ref().unwrap();
2354 assert_eq!(
2355 search_state.matches.len(),
2356 3,
2357 "Should find all 3 matches case-insensitively"
2358 );
2359
2360 editor.active_window_mut().search_case_sensitive = true;
2362 editor.perform_search("hello");
2363
2364 let search_state = editor.active_window().search_state.as_ref().unwrap();
2365 assert_eq!(
2366 search_state.matches.len(),
2367 1,
2368 "Should find only 1 exact match"
2369 );
2370 assert_eq!(
2371 search_state.matches[0], 6,
2372 "Should find 'hello' at position 6"
2373 );
2374 }
2375
2376 #[test]
2377 fn test_search_whole_word() {
2378 let config = Config::default();
2379 let (dir_context, _temp) = test_dir_context();
2380 let mut editor = Editor::new(
2381 config,
2382 80,
2383 24,
2384 dir_context,
2385 crate::view::color_support::ColorCapability::TrueColor,
2386 test_filesystem(),
2387 )
2388 .unwrap();
2389
2390 let cursor_id = editor.active_cursors().primary_id();
2392 editor.apply_event_to_active_buffer(&Event::Insert {
2393 position: 0,
2394 text: "test testing tested attest test".to_string(),
2395 cursor_id,
2396 });
2397
2398 editor.active_window_mut().search_whole_word = false;
2400 editor.active_window_mut().search_case_sensitive = true;
2401 editor.perform_search("test");
2402
2403 let search_state = editor.active_window().search_state.as_ref().unwrap();
2404 assert_eq!(
2405 search_state.matches.len(),
2406 5,
2407 "Should find 'test' in all occurrences"
2408 );
2409
2410 editor.active_window_mut().search_whole_word = true;
2412 editor.perform_search("test");
2413
2414 let search_state = editor.active_window().search_state.as_ref().unwrap();
2415 assert_eq!(
2416 search_state.matches.len(),
2417 2,
2418 "Should find only whole word 'test'"
2419 );
2420 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2421 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2422 }
2423
2424 #[test]
2425 fn test_search_scan_completes_when_capped() {
2426 let config = Config::default();
2432 let (dir_context, _temp) = test_dir_context();
2433 let mut editor = Editor::new(
2434 config,
2435 80,
2436 24,
2437 dir_context,
2438 crate::view::color_support::ColorCapability::TrueColor,
2439 test_filesystem(),
2440 )
2441 .unwrap();
2442
2443 let buffer_id = editor.active_buffer();
2446 let regex = regex::bytes::Regex::new("test").unwrap();
2447 let fake_chunks = vec![
2448 crate::model::buffer::LineScanChunk {
2449 leaf_index: 0,
2450 byte_len: 100,
2451 already_known: true,
2452 },
2453 crate::model::buffer::LineScanChunk {
2454 leaf_index: 1,
2455 byte_len: 100,
2456 already_known: true,
2457 },
2458 ];
2459
2460 let chunked = crate::model::buffer::ChunkedSearchState {
2461 chunks: fake_chunks,
2462 next_chunk: 1, next_doc_offset: 100,
2464 total_bytes: 200,
2465 scanned_bytes: 100,
2466 regex,
2467 matches: vec![
2468 crate::model::buffer::SearchMatch {
2469 byte_offset: 10,
2470 length: 4,
2471 line: 1,
2472 column: 11,
2473 context: String::new(),
2474 },
2475 crate::model::buffer::SearchMatch {
2476 byte_offset: 50,
2477 length: 4,
2478 line: 1,
2479 column: 51,
2480 context: String::new(),
2481 },
2482 ],
2483 overlap_tail: Vec::new(),
2484 overlap_doc_offset: 0,
2485 max_matches: 10_000,
2486 capped: true, query_len: 4,
2488 running_line: 1,
2489 };
2490
2491 editor.active_window_mut().search_scan.start(
2492 buffer_id,
2493 Vec::new(),
2494 chunked,
2495 "test".to_string(),
2496 None,
2497 false,
2498 false,
2499 false,
2500 );
2501
2502 let result = editor.process_search_scan();
2504 assert!(
2505 result,
2506 "process_search_scan should return true (needs render)"
2507 );
2508
2509 assert_eq!(
2511 editor.active_window().search_scan.buffer_id(),
2512 None,
2513 "search_scan should be drained after capped scan completes"
2514 );
2515
2516 let search_state = editor
2518 .active_window()
2519 .search_state
2520 .as_ref()
2521 .expect("search_state should be set after scan finishes");
2522 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2523 assert_eq!(search_state.query, "test");
2524 assert!(
2525 search_state.capped,
2526 "search_state should be marked as capped"
2527 );
2528 }
2529
2530 #[test]
2531 fn test_bookmarks() {
2532 let config = Config::default();
2533 let (dir_context, _temp) = test_dir_context();
2534 let mut editor = Editor::new(
2535 config,
2536 80,
2537 24,
2538 dir_context,
2539 crate::view::color_support::ColorCapability::TrueColor,
2540 test_filesystem(),
2541 )
2542 .unwrap();
2543
2544 let cursor_id = editor.active_cursors().primary_id();
2546 editor.apply_event_to_active_buffer(&Event::Insert {
2547 position: 0,
2548 text: "Line 1\nLine 2\nLine 3".to_string(),
2549 cursor_id,
2550 });
2551
2552 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2554 cursor_id,
2555 old_position: 21,
2556 new_position: 7,
2557 old_anchor: None,
2558 new_anchor: None,
2559 old_sticky_column: 0,
2560 new_sticky_column: 0,
2561 });
2562
2563 editor.active_window_mut().set_bookmark('1');
2565 assert_eq!(
2566 editor
2567 .active_window()
2568 .bookmarks
2569 .get('1')
2570 .map(|b| b.position),
2571 Some(7)
2572 );
2573
2574 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2576 cursor_id,
2577 old_position: 7,
2578 new_position: 14,
2579 old_anchor: None,
2580 new_anchor: None,
2581 old_sticky_column: 0,
2582 new_sticky_column: 0,
2583 });
2584
2585 editor.jump_to_bookmark('1');
2587 assert_eq!(editor.active_cursors().primary().position, 7);
2588
2589 editor.active_window_mut().clear_bookmark('1');
2591 assert_eq!(editor.active_window().bookmarks.get('1'), None);
2592 }
2593
2594 #[test]
2595 fn test_action_enum_new_variants() {
2596 use serde_json::json;
2598
2599 let args = HashMap::new();
2600 assert_eq!(
2601 Action::from_str("smart_home", &args),
2602 Some(Action::SmartHome)
2603 );
2604 assert_eq!(
2605 Action::from_str("dedent_selection", &args),
2606 Some(Action::DedentSelection)
2607 );
2608 assert_eq!(
2609 Action::from_str("toggle_comment", &args),
2610 Some(Action::ToggleComment)
2611 );
2612 assert_eq!(
2613 Action::from_str("goto_matching_bracket", &args),
2614 Some(Action::GoToMatchingBracket)
2615 );
2616 assert_eq!(
2617 Action::from_str("list_bookmarks", &args),
2618 Some(Action::ListBookmarks)
2619 );
2620 assert_eq!(
2621 Action::from_str("toggle_search_case_sensitive", &args),
2622 Some(Action::ToggleSearchCaseSensitive)
2623 );
2624 assert_eq!(
2625 Action::from_str("toggle_search_whole_word", &args),
2626 Some(Action::ToggleSearchWholeWord)
2627 );
2628
2629 let mut args_with_char = HashMap::new();
2631 args_with_char.insert("char".to_string(), json!("5"));
2632 assert_eq!(
2633 Action::from_str("set_bookmark", &args_with_char),
2634 Some(Action::SetBookmark('5'))
2635 );
2636 assert_eq!(
2637 Action::from_str("jump_to_bookmark", &args_with_char),
2638 Some(Action::JumpToBookmark('5'))
2639 );
2640 assert_eq!(
2641 Action::from_str("clear_bookmark", &args_with_char),
2642 Some(Action::ClearBookmark('5'))
2643 );
2644 }
2645
2646 #[test]
2647 fn test_keybinding_new_defaults() {
2648 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2649
2650 let mut config = Config::default();
2654 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2655 let resolver = KeybindingResolver::new(&config);
2656
2657 let event = KeyEvent {
2659 code: KeyCode::Char('/'),
2660 modifiers: KeyModifiers::CONTROL,
2661 kind: KeyEventKind::Press,
2662 state: KeyEventState::NONE,
2663 };
2664 let action = resolver.resolve(&event, KeyContext::Normal);
2665 assert_eq!(action, Action::ToggleComment);
2666
2667 let event = KeyEvent {
2669 code: KeyCode::Char(']'),
2670 modifiers: KeyModifiers::CONTROL,
2671 kind: KeyEventKind::Press,
2672 state: KeyEventState::NONE,
2673 };
2674 let action = resolver.resolve(&event, KeyContext::Normal);
2675 assert_eq!(action, Action::GoToMatchingBracket);
2676
2677 let event = KeyEvent {
2679 code: KeyCode::Tab,
2680 modifiers: KeyModifiers::SHIFT,
2681 kind: KeyEventKind::Press,
2682 state: KeyEventState::NONE,
2683 };
2684 let action = resolver.resolve(&event, KeyContext::Normal);
2685 assert_eq!(action, Action::DedentSelection);
2686
2687 let event = KeyEvent {
2689 code: KeyCode::Char('g'),
2690 modifiers: KeyModifiers::CONTROL,
2691 kind: KeyEventKind::Press,
2692 state: KeyEventState::NONE,
2693 };
2694 let action = resolver.resolve(&event, KeyContext::Normal);
2695 assert_eq!(action, Action::GotoLine);
2696
2697 let event = KeyEvent {
2699 code: KeyCode::Char('5'),
2700 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2701 kind: KeyEventKind::Press,
2702 state: KeyEventState::NONE,
2703 };
2704 let action = resolver.resolve(&event, KeyContext::Normal);
2705 assert_eq!(action, Action::SetBookmark('5'));
2706
2707 let event = KeyEvent {
2708 code: KeyCode::Char('5'),
2709 modifiers: KeyModifiers::ALT,
2710 kind: KeyEventKind::Press,
2711 state: KeyEventState::NONE,
2712 };
2713 let action = resolver.resolve(&event, KeyContext::Normal);
2714 assert_eq!(action, Action::JumpToBookmark('5'));
2715 }
2716
2717 #[test]
2729 fn test_lsp_rename_didchange_positions_bug() {
2730 use crate::model::buffer::Buffer;
2731
2732 let config = Config::default();
2733 let (dir_context, _temp) = test_dir_context();
2734 let mut editor = Editor::new(
2735 config,
2736 80,
2737 24,
2738 dir_context,
2739 crate::view::color_support::ColorCapability::TrueColor,
2740 test_filesystem(),
2741 )
2742 .unwrap();
2743
2744 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2748 editor.active_state_mut().buffer =
2749 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2750
2751 let cursor_id = editor.active_cursors().primary_id();
2756
2757 let batch = Event::Batch {
2758 events: vec![
2759 Event::Delete {
2761 range: 23..26, deleted_text: "val".to_string(),
2763 cursor_id,
2764 },
2765 Event::Insert {
2766 position: 23,
2767 text: "value".to_string(),
2768 cursor_id,
2769 },
2770 Event::Delete {
2772 range: 7..10, deleted_text: "val".to_string(),
2774 cursor_id,
2775 },
2776 Event::Insert {
2777 position: 7,
2778 text: "value".to_string(),
2779 cursor_id,
2780 },
2781 ],
2782 description: "LSP Rename".to_string(),
2783 };
2784
2785 let lsp_changes_before = editor.active_window().collect_lsp_changes(&batch);
2787
2788 editor.apply_event_to_active_buffer(&batch);
2790
2791 let lsp_changes_after = editor.active_window().collect_lsp_changes(&batch);
2794
2795 let final_content = editor.active_state().buffer.to_string().unwrap();
2797 assert_eq!(
2798 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2799 "Buffer should have 'value' in both places"
2800 );
2801
2802 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2808
2809 let first_delete = &lsp_changes_before[0];
2810 let first_del_range = first_delete.range.unwrap();
2811 assert_eq!(
2812 first_del_range.start.line, 1,
2813 "First delete should be on line 1 (BEFORE)"
2814 );
2815 assert_eq!(
2816 first_del_range.start.character, 4,
2817 "First delete start should be at char 4 (BEFORE)"
2818 );
2819
2820 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2826
2827 let first_delete_after = &lsp_changes_after[0];
2828 let first_del_range_after = first_delete_after.range.unwrap();
2829
2830 eprintln!("BEFORE modification:");
2833 eprintln!(
2834 " Delete at line {}, char {}-{}",
2835 first_del_range.start.line,
2836 first_del_range.start.character,
2837 first_del_range.end.character
2838 );
2839 eprintln!("AFTER modification:");
2840 eprintln!(
2841 " Delete at line {}, char {}-{}",
2842 first_del_range_after.start.line,
2843 first_del_range_after.start.character,
2844 first_del_range_after.end.character
2845 );
2846
2847 assert_ne!(
2865 first_del_range_after.end.character, first_del_range.end.character,
2866 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2867 );
2868
2869 eprintln!("\n=== BUG DEMONSTRATED ===");
2870 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2871 eprintln!("the positions are WRONG because they're calculated from the");
2872 eprintln!("modified buffer, not the original buffer.");
2873 eprintln!("This causes the second rename to fail with 'content modified' error.");
2874 eprintln!("========================\n");
2875 }
2876
2877 #[test]
2878 fn test_lsp_rename_preserves_cursor_position() {
2879 use crate::model::buffer::Buffer;
2880
2881 let config = Config::default();
2882 let (dir_context, _temp) = test_dir_context();
2883 let mut editor = Editor::new(
2884 config,
2885 80,
2886 24,
2887 dir_context,
2888 crate::view::color_support::ColorCapability::TrueColor,
2889 test_filesystem(),
2890 )
2891 .unwrap();
2892
2893 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2897 editor.active_state_mut().buffer =
2898 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2899
2900 let original_cursor_pos = 23;
2902 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2903
2904 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2906 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2907 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2908
2909 let cursor_id = editor.active_cursors().primary_id();
2912 let buffer_id = editor.active_buffer();
2913
2914 let events = vec![
2915 Event::Delete {
2917 range: 23..26, deleted_text: "val".to_string(),
2919 cursor_id,
2920 },
2921 Event::Insert {
2922 position: 23,
2923 text: "value".to_string(),
2924 cursor_id,
2925 },
2926 Event::Delete {
2928 range: 7..10, deleted_text: "val".to_string(),
2930 cursor_id,
2931 },
2932 Event::Insert {
2933 position: 7,
2934 text: "value".to_string(),
2935 cursor_id,
2936 },
2937 ];
2938
2939 editor
2941 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2942 .unwrap();
2943
2944 let final_content = editor.active_state().buffer.to_string().unwrap();
2946 assert_eq!(
2947 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2948 "Buffer should have 'value' in both places"
2949 );
2950
2951 let final_cursor_pos = editor.active_cursors().primary().position;
2959 let expected_cursor_pos = 25; assert_eq!(
2962 final_cursor_pos, expected_cursor_pos,
2963 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2964 Original pos: {}, expected adjustment: +2 for first rename",
2965 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2966 );
2967
2968 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2970 assert_eq!(
2971 text_at_new_cursor, "value",
2972 "Cursor should be at the start of 'value' after rename"
2973 );
2974 }
2975
2976 #[test]
2977 fn test_lsp_rename_twice_consecutive() {
2978 use crate::model::buffer::Buffer;
2981
2982 let config = Config::default();
2983 let (dir_context, _temp) = test_dir_context();
2984 let mut editor = Editor::new(
2985 config,
2986 80,
2987 24,
2988 dir_context,
2989 crate::view::color_support::ColorCapability::TrueColor,
2990 test_filesystem(),
2991 )
2992 .unwrap();
2993
2994 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2996 editor.active_state_mut().buffer =
2997 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2998
2999 let cursor_id = editor.active_cursors().primary_id();
3000 let buffer_id = editor.active_buffer();
3001
3002 let events1 = vec![
3005 Event::Delete {
3007 range: 23..26,
3008 deleted_text: "val".to_string(),
3009 cursor_id,
3010 },
3011 Event::Insert {
3012 position: 23,
3013 text: "value".to_string(),
3014 cursor_id,
3015 },
3016 Event::Delete {
3018 range: 7..10,
3019 deleted_text: "val".to_string(),
3020 cursor_id,
3021 },
3022 Event::Insert {
3023 position: 7,
3024 text: "value".to_string(),
3025 cursor_id,
3026 },
3027 ];
3028
3029 let batch1 = Event::Batch {
3031 events: events1.clone(),
3032 description: "LSP Rename 1".to_string(),
3033 };
3034
3035 let lsp_changes1 = editor.active_window().collect_lsp_changes(&batch1);
3037
3038 assert_eq!(
3040 lsp_changes1.len(),
3041 4,
3042 "First rename should have 4 LSP changes"
3043 );
3044
3045 let first_del = &lsp_changes1[0];
3047 let first_del_range = first_del.range.unwrap();
3048 assert_eq!(first_del_range.start.line, 1, "First delete line");
3049 assert_eq!(
3050 first_del_range.start.character, 4,
3051 "First delete start char"
3052 );
3053 assert_eq!(first_del_range.end.character, 7, "First delete end char");
3054
3055 editor
3057 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
3058 .unwrap();
3059
3060 let after_first = editor.active_state().buffer.to_string().unwrap();
3062 assert_eq!(
3063 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3064 "After first rename"
3065 );
3066
3067 let events2 = vec![
3077 Event::Delete {
3079 range: 25..30,
3080 deleted_text: "value".to_string(),
3081 cursor_id,
3082 },
3083 Event::Insert {
3084 position: 25,
3085 text: "x".to_string(),
3086 cursor_id,
3087 },
3088 Event::Delete {
3090 range: 7..12,
3091 deleted_text: "value".to_string(),
3092 cursor_id,
3093 },
3094 Event::Insert {
3095 position: 7,
3096 text: "x".to_string(),
3097 cursor_id,
3098 },
3099 ];
3100
3101 let batch2 = Event::Batch {
3103 events: events2.clone(),
3104 description: "LSP Rename 2".to_string(),
3105 };
3106
3107 let lsp_changes2 = editor.active_window().collect_lsp_changes(&batch2);
3109
3110 assert_eq!(
3114 lsp_changes2.len(),
3115 4,
3116 "Second rename should have 4 LSP changes"
3117 );
3118
3119 let second_first_del = &lsp_changes2[0];
3121 let second_first_del_range = second_first_del.range.unwrap();
3122 assert_eq!(
3123 second_first_del_range.start.line, 1,
3124 "Second rename first delete should be on line 1"
3125 );
3126 assert_eq!(
3127 second_first_del_range.start.character, 4,
3128 "Second rename first delete start should be at char 4"
3129 );
3130 assert_eq!(
3131 second_first_del_range.end.character, 9,
3132 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3133 );
3134
3135 let second_third_del = &lsp_changes2[2];
3137 let second_third_del_range = second_third_del.range.unwrap();
3138 assert_eq!(
3139 second_third_del_range.start.line, 0,
3140 "Second rename third delete should be on line 0"
3141 );
3142 assert_eq!(
3143 second_third_del_range.start.character, 7,
3144 "Second rename third delete start should be at char 7"
3145 );
3146 assert_eq!(
3147 second_third_del_range.end.character, 12,
3148 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3149 );
3150
3151 editor
3153 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3154 .unwrap();
3155
3156 let after_second = editor.active_state().buffer.to_string().unwrap();
3158 assert_eq!(
3159 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3160 "After second rename"
3161 );
3162 }
3163
3164 #[test]
3165 fn test_ensure_active_tab_visible_static_offset() {
3166 let config = Config::default();
3167 let (dir_context, _temp) = test_dir_context();
3168 let mut editor = Editor::new(
3169 config,
3170 80,
3171 24,
3172 dir_context,
3173 crate::view::color_support::ColorCapability::TrueColor,
3174 test_filesystem(),
3175 )
3176 .unwrap();
3177 let split_id = editor.split_manager().active_split();
3178
3179 let buf1 = editor.new_buffer();
3181 editor
3182 .buffers_mut()
3183 .get_mut(&buf1)
3184 .unwrap()
3185 .buffer
3186 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3187 let buf2 = editor.new_buffer();
3188 editor
3189 .buffers_mut()
3190 .get_mut(&buf2)
3191 .unwrap()
3192 .buffer
3193 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3194 let buf3 = editor.new_buffer();
3195 editor
3196 .buffers_mut()
3197 .get_mut(&buf3)
3198 .unwrap()
3199 .buffer
3200 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3201
3202 {
3203 use crate::view::split::TabTarget;
3204 let view_state = editor.split_view_states_mut().get_mut(&split_id).unwrap();
3205 view_state.open_buffers = vec![
3206 TabTarget::Buffer(buf1),
3207 TabTarget::Buffer(buf2),
3208 TabTarget::Buffer(buf3),
3209 ];
3210 view_state.tab_scroll_offset = 50;
3211 }
3212
3213 editor
3217 .active_window_mut()
3218 .ensure_active_tab_visible(split_id, buf1, 25);
3219 assert_eq!(
3220 editor
3221 .split_view_states()
3222 .get(&split_id)
3223 .unwrap()
3224 .tab_scroll_offset,
3225 0
3226 );
3227
3228 editor
3230 .active_window_mut()
3231 .ensure_active_tab_visible(split_id, buf3, 25);
3232 let view_state = editor.split_view_states().get(&split_id).unwrap();
3233 assert!(view_state.tab_scroll_offset > 0);
3234 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3235 let total_width: usize = buffer_ids
3236 .iter()
3237 .enumerate()
3238 .map(|(idx, id)| {
3239 let state = editor.buffers().get(id).unwrap();
3240 let name_len = state
3241 .buffer
3242 .file_path()
3243 .and_then(|p| p.file_name())
3244 .and_then(|n| n.to_str())
3245 .map(|s| s.chars().count())
3246 .unwrap_or(0);
3247 let tab_width = 2 + name_len;
3248 if idx < buffer_ids.len() - 1 {
3249 tab_width + 1 } else {
3251 tab_width
3252 }
3253 })
3254 .sum();
3255 assert!(view_state.tab_scroll_offset <= total_width);
3256 }
3257}