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 workspace_trust_prompt_cancellable: bool,
457
458 workspace_trust_markers: Vec<String>,
462
463 workspace_trust_scroll: u16,
466
467 should_detach: bool,
469
470 session_mode: bool,
472
473 software_cursor_only: bool,
475
476 session_name: Option<String>,
478
479 pending_escape_sequences: Vec<u8>,
482
483 restart_with_dir: Option<PathBuf>,
486
487 last_window_title: Option<String>,
494
495 terminal_width: u16,
498 terminal_height: u16,
499
500 mode_registry: ModeRegistry,
509
510 tokio_runtime: Option<Arc<tokio::runtime::Runtime>>,
512
513 async_bridge: Option<AsyncBridge>,
515
516 fs_manager: Arc<FsManager>,
534
535 authority: crate::services::authority::Authority,
545
546 pending_authority: Option<crate::services::authority::Authority>,
552
553 pub remote_indicator_override: Option<crate::view::ui::status_bar::RemoteIndicatorOverride>,
559
560 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
565
566 menu_state: crate::view::ui::MenuState,
588
589 menus: crate::config::MenuConfig,
591
592 working_dir: PathBuf,
600
601 pub(crate) windows: HashMap<fresh_core::WindowId, crate::app::window::Window>,
606
607 pub(crate) active_window: fresh_core::WindowId,
610
611 #[allow(dead_code)]
616 pub(crate) next_window_id: u64,
617
618 command_registry: Arc<RwLock<CommandRegistry>>,
636
637 quick_open_registry: QuickOpenRegistry,
639
640 plugin_manager: Arc<RwLock<PluginManager>>,
648
649 status_bar_token_registry: Mutex<HashMap<String, String>>,
655
656 pub(crate) plugin_schemas:
663 std::sync::Arc<std::sync::RwLock<HashMap<String, serde_json::Value>>>,
664
665 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
681
682 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
688 event_broadcaster: crate::model::control_event::EventBroadcaster,
707
708 #[cfg(feature = "plugins")]
715 pending_plugin_actions: Vec<(
716 String,
717 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
718 )>,
719
720 #[cfg(feature = "plugins")]
722 plugin_render_requested: bool,
723
724 recovery_service: RecoveryService,
751
752 full_redraw_requested: bool,
754
755 suspend_requested: bool,
758
759 time_source: SharedTimeSource,
761
762 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
772 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
774
775 status_log_path: Option<PathBuf>,
777
778 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
784
785 file_watcher_manager: crate::services::file_watcher::FileWatcherManager,
795
796 pub(crate) last_path_change_for_test: Option<(u64, std::path::PathBuf, &'static str)>,
802
803 pub(crate) last_watch_response_for_test: Option<(u64, Result<u64, String>)>,
807
808 pub(crate) preview_window_id: Option<fresh_core::WindowId>,
815
816 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
836
837 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
839
840 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
845
846 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
848
849 color_capability: crate::view::color_support::ColorCapability,
851
852 pub(crate) global_popups: crate::view::popup::PopupManager,
863
864 stdin_stream: stdin_stream::StdinStream,
882
883 pub(crate) previous_cursor_screen_pos: Option<((u16, u16), LeafId)>,
906 pub(crate) cursor_jump_animation: Option<crate::view::animation::AnimationId>,
909
910 pub(crate) pending_vb_animations: Vec<(u64, BufferId, fresh_core::api::PluginAnimationKind)>,
916
917 pub(crate) widget_registry: crate::widgets::WidgetRegistry,
925
926 pub(crate) floating_widget_panel: Option<FloatingWidgetState>,
933}
934
935pub(crate) const FLOATING_PANEL_BUFFER_ID: BufferId = BufferId(usize::MAX);
941
942#[derive(Debug, Clone)]
949pub(crate) struct FloatingWidgetState {
950 pub panel_id: crate::widgets::PanelId,
951 pub width_pct: u8,
952 pub height_pct: u8,
953 pub entries: Vec<fresh_core::text_property::TextPropertyEntry>,
957 pub focus_cursor: Option<crate::widgets::FocusCursor>,
959 pub embeds: Vec<crate::widgets::EmbedRect>,
966 pub overlays: Vec<crate::widgets::OverlayRow>,
973 pub last_inner_rect: Option<ratatui::layout::Rect>,
976}
977
978#[derive(Debug, Clone)]
980pub struct PendingFileOpen {
981 pub path: PathBuf,
983 pub line: Option<usize>,
985 pub column: Option<usize>,
987 pub end_line: Option<usize>,
989 pub end_column: Option<usize>,
991 pub message: Option<String>,
993 pub wait_id: Option<u64>,
995}
996
997impl Editor {
998 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1000 let trimmed = input.trim();
1001
1002 if trimmed.is_empty() {
1003 self.ansi_background = None;
1004 self.ansi_background_path = None;
1005 self.set_status_message(t!("status.background_cleared").to_string());
1006 return Ok(());
1007 }
1008
1009 let input_path = Path::new(trimmed);
1010 let resolved = if input_path.is_absolute() {
1011 input_path.to_path_buf()
1012 } else {
1013 self.working_dir.join(input_path)
1014 };
1015
1016 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1017
1018 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1019
1020 self.ansi_background = Some(parsed);
1021 self.ansi_background_path = Some(canonical.clone());
1022 self.set_status_message(
1023 t!(
1024 "view.background_set",
1025 path = canonical.display().to_string()
1026 )
1027 .to_string(),
1028 );
1029
1030 Ok(())
1031 }
1032
1033 #[doc(hidden)]
1037 pub fn buffer_count_for_tests(&self) -> usize {
1038 self.windows
1039 .get(&self.active_window)
1040 .map(|w| &w.buffers)
1041 .expect("active window present")
1042 .len()
1043 }
1044
1045 #[doc(hidden)]
1049 pub fn all_buffer_ids_for_tests(&self) -> Vec<BufferId> {
1050 let mut ids: Vec<BufferId> = self
1051 .windows
1052 .get(&self.active_window)
1053 .map(|w| &w.buffers)
1054 .expect("active window present")
1055 .ids();
1056 ids.sort_by_key(|id| id.0);
1057 ids
1058 }
1059
1060 pub fn active_state(&self) -> &EditorState {
1062 self.windows
1063 .get(&self.active_window)
1064 .map(|w| &w.buffers)
1065 .expect("active window present")
1066 .get(&self.active_buffer())
1067 .unwrap()
1068 }
1069
1070 pub fn active_state_mut(&mut self) -> &mut EditorState {
1072 let __buffer_id = self.active_buffer();
1073 self.windows
1074 .get_mut(&self.active_window)
1075 .map(|w| &mut w.buffers)
1076 .expect("active window present")
1077 .get_mut(&__buffer_id)
1078 .unwrap()
1079 }
1080
1081 pub fn active_cursors(&self) -> &Cursors {
1085 let split_id = self.effective_active_split();
1086 &self
1087 .windows
1088 .get(&self.active_window)
1089 .and_then(|w| w.buffers.splits())
1090 .map(|(_, vs)| vs)
1091 .expect("active window must have a populated split layout")
1092 .get(&split_id)
1093 .unwrap()
1094 .cursors
1095 }
1096
1097 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1099 let split_id = self.effective_active_split();
1100 &mut self
1101 .windows
1102 .get_mut(&self.active_window)
1103 .and_then(|w| w.split_view_states_mut())
1104 .expect("active window must have a populated split layout")
1105 .get_mut(&split_id)
1106 .unwrap()
1107 .cursors
1108 }
1109
1110 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1112 self.active_window_mut().completion_items = Some(items);
1113 }
1114
1115 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1117 let active_split = self
1118 .windows
1119 .get(&self.active_window)
1120 .and_then(|w| w.buffers.splits())
1121 .map(|(mgr, _)| mgr)
1122 .expect("active window must have a populated split layout")
1123 .active_split();
1124 &self
1125 .windows
1126 .get(&self.active_window)
1127 .and_then(|w| w.buffers.splits())
1128 .map(|(_, vs)| vs)
1129 .expect("active window must have a populated split layout")
1130 .get(&active_split)
1131 .unwrap()
1132 .viewport
1133 }
1134
1135 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1137 let active_split = self
1138 .windows
1139 .get(&self.active_window)
1140 .and_then(|w| w.buffers.splits())
1141 .map(|(mgr, _)| mgr)
1142 .expect("active window must have a populated split layout")
1143 .active_split();
1144 &mut self
1145 .windows
1146 .get_mut(&self.active_window)
1147 .and_then(|w| w.split_view_states_mut())
1148 .expect("active window must have a populated split layout")
1149 .get_mut(&active_split)
1150 .unwrap()
1151 .viewport
1152 }
1153
1154 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1156 if let Some(composite) = self.active_window().composite_buffers.get(&buffer_id) {
1158 return composite.name.clone();
1159 }
1160
1161 self.active_window()
1162 .buffer_metadata
1163 .get(&buffer_id)
1164 .map(|m| m.display_name.clone())
1165 .or_else(|| {
1166 self.windows
1167 .get(&self.active_window)
1168 .map(|w| &w.buffers)
1169 .expect("active window present")
1170 .get(&buffer_id)
1171 .and_then(|state| {
1172 state
1173 .buffer
1174 .file_path()
1175 .and_then(|p| p.file_name())
1176 .and_then(|n| n.to_str())
1177 .map(|s| s.to_string())
1178 })
1179 })
1180 .unwrap_or_else(|| "[No Name]".to_string())
1181 }
1182
1183 pub fn active_event_log(&self) -> &EventLog {
1193 self.active_window()
1194 .event_logs
1195 .get(&self.active_buffer())
1196 .unwrap()
1197 }
1198
1199 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1201 let buffer_id = self.active_buffer();
1202 self.active_window_mut()
1203 .event_logs
1204 .get_mut(&buffer_id)
1205 .unwrap()
1206 }
1207
1208 pub fn register_status_bar_element(
1212 &self,
1213 plugin_name: &str,
1214 token_name: &str,
1215 title: &str,
1216 ) -> Result<(), String> {
1217 if plugin_name.is_empty() {
1218 return Err("Plugin name cannot be empty".to_string());
1219 }
1220 if token_name.is_empty() {
1221 return Err("Token name cannot be empty".to_string());
1222 }
1223
1224 let key = format!("{}:{}", plugin_name, token_name);
1225 let mut registry = self.status_bar_token_registry.lock().unwrap();
1226
1227 if registry.contains_key(&key) {
1228 return Err(format!("Token '{}' already registered", key));
1229 }
1230
1231 registry.insert(key, title.to_string());
1232 Ok(())
1233 }
1234
1235 pub fn set_status_bar_value(
1240 &mut self,
1241 buffer_id: BufferId,
1242 key: &str,
1243 value: String,
1244 ) -> Result<(), String> {
1245 for window in self.windows.values_mut() {
1246 if window.buffers.contains_key(&buffer_id) {
1247 window
1248 .status_bar_values
1249 .entry(buffer_id)
1250 .or_default()
1251 .insert(key.to_string(), value);
1252 return Ok(());
1253 }
1254 }
1255 Err(format!("Buffer {:?} not found", buffer_id))
1256 }
1257
1258 pub fn get_status_bar_elements(&self) -> Vec<(String, String)> {
1261 self.status_bar_token_registry
1262 .lock()
1263 .unwrap()
1264 .iter()
1265 .map(|(k, title)| (format!("{{{}}}", k), title.clone()))
1266 .collect()
1267 }
1268
1269 pub fn get_status_bar_element_values(&self, buffer_id: BufferId) -> HashMap<String, String> {
1271 for window in self.windows.values() {
1272 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1273 return values.clone();
1274 }
1275 }
1276 HashMap::new()
1277 }
1278
1279 pub fn current_status_bar_value(&self, buffer_id: BufferId, key: &str) -> Option<&str> {
1283 for window in self.windows.values() {
1284 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1285 if let Some(v) = values.get(key) {
1286 return Some(v.as_str());
1287 }
1288 return None;
1289 }
1290 }
1291 None
1292 }
1293
1294 fn remove_plugin_status_bar_elements(&mut self, plugin_name: &str) {
1297 let prefix = format!("{}:", plugin_name);
1298 self.status_bar_token_registry
1299 .lock()
1300 .unwrap()
1301 .retain(|k, _| !k.starts_with(&prefix));
1302 for window in self.windows.values_mut() {
1303 for values in window.status_bar_values.values_mut() {
1304 values.retain(|k, _| !k.starts_with(&prefix));
1305 }
1306 }
1307 }
1308}
1309
1310fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1319 use crossterm::event::{KeyCode, KeyModifiers};
1320
1321 let mut modifiers = KeyModifiers::NONE;
1322 let mut remaining = key_str;
1323
1324 loop {
1326 if remaining.starts_with("C-") {
1327 modifiers |= KeyModifiers::CONTROL;
1328 remaining = &remaining[2..];
1329 } else if remaining.starts_with("M-") {
1330 modifiers |= KeyModifiers::ALT;
1331 remaining = &remaining[2..];
1332 } else if remaining.starts_with("S-") {
1333 modifiers |= KeyModifiers::SHIFT;
1334 remaining = &remaining[2..];
1335 } else {
1336 break;
1337 }
1338 }
1339
1340 let upper = remaining.to_uppercase();
1343 let code = match upper.as_str() {
1344 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1345 "TAB" => KeyCode::Tab,
1346 "BACKTAB" => KeyCode::BackTab,
1347 "ESC" | "ESCAPE" => KeyCode::Esc,
1348 "SPC" | "SPACE" => KeyCode::Char(' '),
1349 "DEL" | "DELETE" => KeyCode::Delete,
1350 "BS" | "BACKSPACE" => KeyCode::Backspace,
1351 "UP" => KeyCode::Up,
1352 "DOWN" => KeyCode::Down,
1353 "LEFT" => KeyCode::Left,
1354 "RIGHT" => KeyCode::Right,
1355 "HOME" => KeyCode::Home,
1356 "END" => KeyCode::End,
1357 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1358 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1359 s if s.starts_with('F') && s.len() > 1 => {
1360 if let Ok(n) = s[1..].parse::<u8>() {
1362 KeyCode::F(n)
1363 } else {
1364 return None;
1365 }
1366 }
1367 _ if remaining.len() == 1 => {
1368 let c = remaining.chars().next()?;
1371 if c.is_ascii_uppercase() {
1372 modifiers |= KeyModifiers::SHIFT;
1373 }
1374 KeyCode::Char(c.to_ascii_lowercase())
1375 }
1376 _ => return None,
1377 };
1378
1379 if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
1384 return Some((KeyCode::BackTab, modifiers.difference(KeyModifiers::SHIFT)));
1385 }
1386
1387 Some((code, modifiers))
1388}
1389
1390#[cfg(test)]
1391mod tests {
1392 use super::*;
1393 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1394 use tempfile::TempDir;
1395
1396 fn test_dir_context() -> (DirectoryContext, TempDir) {
1398 let temp_dir = TempDir::new().unwrap();
1399 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1400 (dir_context, temp_dir)
1401 }
1402
1403 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1405 Arc::new(crate::model::filesystem::StdFileSystem)
1406 }
1407
1408 #[test]
1409 fn parse_key_string_shift_tab_normalizes_to_backtab() {
1410 use crossterm::event::{KeyCode, KeyModifiers};
1411 assert_eq!(
1416 parse_key_string("S-Tab"),
1417 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1418 );
1419 assert_eq!(
1420 parse_key_string("BackTab"),
1421 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1422 );
1423 assert_eq!(
1425 parse_key_string("Tab"),
1426 Some((KeyCode::Tab, KeyModifiers::NONE)),
1427 );
1428 }
1429
1430 #[test]
1431 fn test_editor_new() {
1432 let config = Config::default();
1433 let (dir_context, _temp) = test_dir_context();
1434 let editor = Editor::new(
1435 config,
1436 80,
1437 24,
1438 dir_context,
1439 crate::view::color_support::ColorCapability::TrueColor,
1440 test_filesystem(),
1441 )
1442 .unwrap();
1443
1444 assert_eq!(editor.buffers().len(), 1);
1445 assert!(!editor.should_quit());
1446 }
1447
1448 #[test]
1449 fn test_new_buffer() {
1450 let config = Config::default();
1451 let (dir_context, _temp) = test_dir_context();
1452 let mut editor = Editor::new(
1453 config,
1454 80,
1455 24,
1456 dir_context,
1457 crate::view::color_support::ColorCapability::TrueColor,
1458 test_filesystem(),
1459 )
1460 .unwrap();
1461
1462 let id = editor.new_buffer();
1463 assert_eq!(editor.buffers().len(), 2);
1464 assert_eq!(editor.active_buffer(), id);
1465 }
1466
1467 #[test]
1468 #[ignore]
1469 fn test_clipboard() {
1470 let config = Config::default();
1471 let (dir_context, _temp) = test_dir_context();
1472 let mut editor = Editor::new(
1473 config,
1474 80,
1475 24,
1476 dir_context,
1477 crate::view::color_support::ColorCapability::TrueColor,
1478 test_filesystem(),
1479 )
1480 .unwrap();
1481
1482 editor.clipboard.set_internal("test".to_string());
1484
1485 editor.paste();
1487
1488 let content = editor.active_state().buffer.to_string().unwrap();
1489 assert_eq!(content, "test");
1490 }
1491
1492 #[test]
1493 fn test_action_to_events_insert_char() {
1494 let config = Config::default();
1495 let (dir_context, _temp) = test_dir_context();
1496 let mut editor = Editor::new(
1497 config,
1498 80,
1499 24,
1500 dir_context,
1501 crate::view::color_support::ColorCapability::TrueColor,
1502 test_filesystem(),
1503 )
1504 .unwrap();
1505
1506 let events = editor
1507 .active_window_mut()
1508 .action_to_events(Action::InsertChar('a'));
1509 assert!(events.is_some());
1510
1511 let events = events.unwrap();
1512 assert_eq!(events.len(), 1);
1513
1514 match &events[0] {
1515 Event::Insert { position, text, .. } => {
1516 assert_eq!(*position, 0);
1517 assert_eq!(text, "a");
1518 }
1519 _ => panic!("Expected Insert event"),
1520 }
1521 }
1522
1523 #[test]
1524 fn test_action_to_events_move_right() {
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
1546 .active_window_mut()
1547 .action_to_events(Action::MoveRight);
1548 assert!(events.is_some());
1549
1550 let events = events.unwrap();
1551 assert_eq!(events.len(), 1);
1552
1553 match &events[0] {
1554 Event::MoveCursor {
1555 new_position,
1556 new_anchor,
1557 ..
1558 } => {
1559 assert_eq!(*new_position, 5);
1561 assert_eq!(*new_anchor, None); }
1563 _ => panic!("Expected MoveCursor event"),
1564 }
1565 }
1566
1567 #[test]
1568 fn test_action_to_events_move_up_down() {
1569 let config = Config::default();
1570 let (dir_context, _temp) = test_dir_context();
1571 let mut editor = Editor::new(
1572 config,
1573 80,
1574 24,
1575 dir_context,
1576 crate::view::color_support::ColorCapability::TrueColor,
1577 test_filesystem(),
1578 )
1579 .unwrap();
1580
1581 let cursor_id = editor.active_cursors().primary_id();
1583 editor.apply_event_to_active_buffer(&Event::Insert {
1584 position: 0,
1585 text: "line1\nline2\nline3".to_string(),
1586 cursor_id,
1587 });
1588
1589 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1591 cursor_id,
1592 old_position: 0, new_position: 6,
1594 old_anchor: None, new_anchor: None,
1596 old_sticky_column: 0,
1597 new_sticky_column: 0,
1598 });
1599
1600 let events = editor.active_window_mut().action_to_events(Action::MoveUp);
1602 assert!(events.is_some());
1603 let events = events.unwrap();
1604 assert_eq!(events.len(), 1);
1605
1606 match &events[0] {
1607 Event::MoveCursor { new_position, .. } => {
1608 assert_eq!(*new_position, 0); }
1610 _ => panic!("Expected MoveCursor event"),
1611 }
1612 }
1613
1614 #[test]
1615 fn test_action_to_events_insert_newline() {
1616 let config = Config::default();
1617 let (dir_context, _temp) = test_dir_context();
1618 let mut editor = Editor::new(
1619 config,
1620 80,
1621 24,
1622 dir_context,
1623 crate::view::color_support::ColorCapability::TrueColor,
1624 test_filesystem(),
1625 )
1626 .unwrap();
1627
1628 let events = editor
1629 .active_window_mut()
1630 .action_to_events(Action::InsertNewline);
1631 assert!(events.is_some());
1632
1633 let events = events.unwrap();
1634 assert_eq!(events.len(), 1);
1635
1636 match &events[0] {
1637 Event::Insert { text, .. } => {
1638 assert_eq!(text, "\n");
1639 }
1640 _ => panic!("Expected Insert event"),
1641 }
1642 }
1643
1644 #[test]
1645 fn test_action_to_events_unimplemented() {
1646 let config = Config::default();
1647 let (dir_context, _temp) = test_dir_context();
1648 let mut editor = Editor::new(
1649 config,
1650 80,
1651 24,
1652 dir_context,
1653 crate::view::color_support::ColorCapability::TrueColor,
1654 test_filesystem(),
1655 )
1656 .unwrap();
1657
1658 assert!(editor
1660 .active_window_mut()
1661 .action_to_events(Action::Save)
1662 .is_none());
1663 assert!(editor
1664 .active_window_mut()
1665 .action_to_events(Action::Quit)
1666 .is_none());
1667 assert!(editor
1668 .active_window_mut()
1669 .action_to_events(Action::Undo)
1670 .is_none());
1671 }
1672
1673 #[test]
1674 fn test_action_to_events_delete_backward() {
1675 let config = Config::default();
1676 let (dir_context, _temp) = test_dir_context();
1677 let mut editor = Editor::new(
1678 config,
1679 80,
1680 24,
1681 dir_context,
1682 crate::view::color_support::ColorCapability::TrueColor,
1683 test_filesystem(),
1684 )
1685 .unwrap();
1686
1687 let cursor_id = editor.active_cursors().primary_id();
1689 editor.apply_event_to_active_buffer(&Event::Insert {
1690 position: 0,
1691 text: "hello".to_string(),
1692 cursor_id,
1693 });
1694
1695 let events = editor
1696 .active_window_mut()
1697 .action_to_events(Action::DeleteBackward);
1698 assert!(events.is_some());
1699
1700 let events = events.unwrap();
1701 assert_eq!(events.len(), 1);
1702
1703 match &events[0] {
1704 Event::Delete {
1705 range,
1706 deleted_text,
1707 ..
1708 } => {
1709 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1711 }
1712 _ => panic!("Expected Delete event"),
1713 }
1714 }
1715
1716 #[test]
1717 fn test_action_to_events_delete_forward() {
1718 let config = Config::default();
1719 let (dir_context, _temp) = test_dir_context();
1720 let mut editor = Editor::new(
1721 config,
1722 80,
1723 24,
1724 dir_context,
1725 crate::view::color_support::ColorCapability::TrueColor,
1726 test_filesystem(),
1727 )
1728 .unwrap();
1729
1730 let cursor_id = editor.active_cursors().primary_id();
1732 editor.apply_event_to_active_buffer(&Event::Insert {
1733 position: 0,
1734 text: "hello".to_string(),
1735 cursor_id,
1736 });
1737
1738 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1740 cursor_id,
1741 old_position: 0, new_position: 0,
1743 old_anchor: None, new_anchor: None,
1745 old_sticky_column: 0,
1746 new_sticky_column: 0,
1747 });
1748
1749 let events = editor
1750 .active_window_mut()
1751 .action_to_events(Action::DeleteForward);
1752 assert!(events.is_some());
1753
1754 let events = events.unwrap();
1755 assert_eq!(events.len(), 1);
1756
1757 match &events[0] {
1758 Event::Delete {
1759 range,
1760 deleted_text,
1761 ..
1762 } => {
1763 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1765 }
1766 _ => panic!("Expected Delete event"),
1767 }
1768 }
1769
1770 #[test]
1771 fn test_action_to_events_select_right() {
1772 let config = Config::default();
1773 let (dir_context, _temp) = test_dir_context();
1774 let mut editor = Editor::new(
1775 config,
1776 80,
1777 24,
1778 dir_context,
1779 crate::view::color_support::ColorCapability::TrueColor,
1780 test_filesystem(),
1781 )
1782 .unwrap();
1783
1784 let cursor_id = editor.active_cursors().primary_id();
1786 editor.apply_event_to_active_buffer(&Event::Insert {
1787 position: 0,
1788 text: "hello".to_string(),
1789 cursor_id,
1790 });
1791
1792 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1794 cursor_id,
1795 old_position: 0, new_position: 0,
1797 old_anchor: None, new_anchor: None,
1799 old_sticky_column: 0,
1800 new_sticky_column: 0,
1801 });
1802
1803 let events = editor
1804 .active_window_mut()
1805 .action_to_events(Action::SelectRight);
1806 assert!(events.is_some());
1807
1808 let events = events.unwrap();
1809 assert_eq!(events.len(), 1);
1810
1811 match &events[0] {
1812 Event::MoveCursor {
1813 new_position,
1814 new_anchor,
1815 ..
1816 } => {
1817 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1820 _ => panic!("Expected MoveCursor event"),
1821 }
1822 }
1823
1824 #[test]
1825 fn test_action_to_events_select_all() {
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 cursor_id = editor.active_cursors().primary_id();
1840 editor.apply_event_to_active_buffer(&Event::Insert {
1841 position: 0,
1842 text: "hello world".to_string(),
1843 cursor_id,
1844 });
1845
1846 let events = editor
1847 .active_window_mut()
1848 .action_to_events(Action::SelectAll);
1849 assert!(events.is_some());
1850
1851 let events = events.unwrap();
1852 assert_eq!(events.len(), 1);
1853
1854 match &events[0] {
1855 Event::MoveCursor {
1856 new_position,
1857 new_anchor,
1858 ..
1859 } => {
1860 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1863 _ => panic!("Expected MoveCursor event"),
1864 }
1865 }
1866
1867 #[test]
1868 fn test_action_to_events_document_nav() {
1869 let config = Config::default();
1870 let (dir_context, _temp) = test_dir_context();
1871 let mut editor = Editor::new(
1872 config,
1873 80,
1874 24,
1875 dir_context,
1876 crate::view::color_support::ColorCapability::TrueColor,
1877 test_filesystem(),
1878 )
1879 .unwrap();
1880
1881 let cursor_id = editor.active_cursors().primary_id();
1883 editor.apply_event_to_active_buffer(&Event::Insert {
1884 position: 0,
1885 text: "line1\nline2\nline3".to_string(),
1886 cursor_id,
1887 });
1888
1889 let events = editor
1891 .active_window_mut()
1892 .action_to_events(Action::MoveDocumentStart);
1893 assert!(events.is_some());
1894 let events = events.unwrap();
1895 match &events[0] {
1896 Event::MoveCursor { new_position, .. } => {
1897 assert_eq!(*new_position, 0);
1898 }
1899 _ => panic!("Expected MoveCursor event"),
1900 }
1901
1902 let events = editor
1904 .active_window_mut()
1905 .action_to_events(Action::MoveDocumentEnd);
1906 assert!(events.is_some());
1907 let events = events.unwrap();
1908 match &events[0] {
1909 Event::MoveCursor { new_position, .. } => {
1910 assert_eq!(*new_position, 17); }
1912 _ => panic!("Expected MoveCursor event"),
1913 }
1914 }
1915
1916 #[test]
1917 fn test_action_to_events_remove_secondary_cursors() {
1918 use crate::model::event::CursorId;
1919
1920 let config = Config::default();
1921 let (dir_context, _temp) = test_dir_context();
1922 let mut editor = Editor::new(
1923 config,
1924 80,
1925 24,
1926 dir_context,
1927 crate::view::color_support::ColorCapability::TrueColor,
1928 test_filesystem(),
1929 )
1930 .unwrap();
1931
1932 let cursor_id = editor.active_cursors().primary_id();
1934 editor.apply_event_to_active_buffer(&Event::Insert {
1935 position: 0,
1936 text: "hello world test".to_string(),
1937 cursor_id,
1938 });
1939
1940 editor.apply_event_to_active_buffer(&Event::AddCursor {
1942 cursor_id: CursorId(1),
1943 position: 5,
1944 anchor: None,
1945 });
1946 editor.apply_event_to_active_buffer(&Event::AddCursor {
1947 cursor_id: CursorId(2),
1948 position: 10,
1949 anchor: None,
1950 });
1951
1952 assert_eq!(editor.active_cursors().count(), 3);
1953
1954 let first_id = editor
1956 .active_cursors()
1957 .iter()
1958 .map(|(id, _)| id)
1959 .min_by_key(|id| id.0)
1960 .expect("Should have at least one cursor");
1961
1962 let events = editor
1964 .active_window_mut()
1965 .action_to_events(Action::RemoveSecondaryCursors);
1966 assert!(events.is_some());
1967
1968 let events = events.unwrap();
1969 let remove_cursor_events: Vec<_> = events
1972 .iter()
1973 .filter_map(|e| match e {
1974 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1975 _ => None,
1976 })
1977 .collect();
1978
1979 assert_eq!(remove_cursor_events.len(), 2);
1981
1982 for cursor_id in &remove_cursor_events {
1983 assert_ne!(*cursor_id, first_id);
1985 }
1986 }
1987
1988 #[test]
1989 fn test_action_to_events_scroll() {
1990 let config = Config::default();
1991 let (dir_context, _temp) = test_dir_context();
1992 let mut editor = Editor::new(
1993 config,
1994 80,
1995 24,
1996 dir_context,
1997 crate::view::color_support::ColorCapability::TrueColor,
1998 test_filesystem(),
1999 )
2000 .unwrap();
2001
2002 let events = editor
2004 .active_window_mut()
2005 .action_to_events(Action::ScrollUp);
2006 assert!(events.is_some());
2007 let events = events.unwrap();
2008 assert_eq!(events.len(), 1);
2009 match &events[0] {
2010 Event::Scroll { line_offset } => {
2011 assert_eq!(*line_offset, -1);
2012 }
2013 _ => panic!("Expected Scroll event"),
2014 }
2015
2016 let events = editor
2018 .active_window_mut()
2019 .action_to_events(Action::ScrollDown);
2020 assert!(events.is_some());
2021 let events = events.unwrap();
2022 assert_eq!(events.len(), 1);
2023 match &events[0] {
2024 Event::Scroll { line_offset } => {
2025 assert_eq!(*line_offset, 1);
2026 }
2027 _ => panic!("Expected Scroll event"),
2028 }
2029 }
2030
2031 #[test]
2032 fn test_action_to_events_none() {
2033 let config = Config::default();
2034 let (dir_context, _temp) = test_dir_context();
2035 let mut editor = Editor::new(
2036 config,
2037 80,
2038 24,
2039 dir_context,
2040 crate::view::color_support::ColorCapability::TrueColor,
2041 test_filesystem(),
2042 )
2043 .unwrap();
2044
2045 let events = editor.active_window_mut().action_to_events(Action::None);
2047 assert!(events.is_none());
2048 }
2049
2050 #[test]
2051 fn test_lsp_incremental_insert_generates_correct_range() {
2052 use crate::model::buffer::Buffer;
2055
2056 let buffer = Buffer::from_str_test("hello\nworld");
2057
2058 let position = 0;
2061 let (line, character) = buffer.position_to_lsp_position(position);
2062
2063 assert_eq!(line, 0, "Insertion at start should be line 0");
2064 assert_eq!(character, 0, "Insertion at start should be char 0");
2065
2066 let lsp_pos = Position::new(line as u32, character as u32);
2068 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2069
2070 assert_eq!(lsp_range.start.line, 0);
2071 assert_eq!(lsp_range.start.character, 0);
2072 assert_eq!(lsp_range.end.line, 0);
2073 assert_eq!(lsp_range.end.character, 0);
2074 assert_eq!(
2075 lsp_range.start, lsp_range.end,
2076 "Insert should have zero-width range"
2077 );
2078
2079 let position = 3;
2081 let (line, character) = buffer.position_to_lsp_position(position);
2082
2083 assert_eq!(line, 0);
2084 assert_eq!(character, 3);
2085
2086 let position = 6;
2088 let (line, character) = buffer.position_to_lsp_position(position);
2089
2090 assert_eq!(line, 1, "Position after newline should be line 1");
2091 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2092 }
2093
2094 #[test]
2095 fn test_lsp_incremental_delete_generates_correct_range() {
2096 use crate::model::buffer::Buffer;
2099
2100 let buffer = Buffer::from_str_test("hello\nworld");
2101
2102 let range_start = 1;
2104 let range_end = 5;
2105
2106 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2107 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2108
2109 assert_eq!(start_line, 0);
2110 assert_eq!(start_char, 1);
2111 assert_eq!(end_line, 0);
2112 assert_eq!(end_char, 5);
2113
2114 let lsp_range = LspRange::new(
2115 Position::new(start_line as u32, start_char as u32),
2116 Position::new(end_line as u32, end_char as u32),
2117 );
2118
2119 assert_eq!(lsp_range.start.line, 0);
2120 assert_eq!(lsp_range.start.character, 1);
2121 assert_eq!(lsp_range.end.line, 0);
2122 assert_eq!(lsp_range.end.character, 5);
2123 assert_ne!(
2124 lsp_range.start, lsp_range.end,
2125 "Delete should have non-zero range"
2126 );
2127
2128 let range_start = 4;
2130 let range_end = 8;
2131
2132 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2133 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2134
2135 assert_eq!(start_line, 0, "Delete start on line 0");
2136 assert_eq!(start_char, 4, "Delete start at char 4");
2137 assert_eq!(end_line, 1, "Delete end on line 1");
2138 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2139 }
2140
2141 #[test]
2142 fn test_lsp_incremental_utf16_encoding() {
2143 use crate::model::buffer::Buffer;
2146
2147 let buffer = Buffer::from_str_test("😀hello");
2149
2150 let (line, character) = buffer.position_to_lsp_position(4);
2152
2153 assert_eq!(line, 0);
2154 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2155
2156 let (line, character) = buffer.position_to_lsp_position(9);
2158
2159 assert_eq!(line, 0);
2160 assert_eq!(
2161 character, 7,
2162 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2163 );
2164
2165 let buffer = Buffer::from_str_test("café");
2167
2168 let (line, character) = buffer.position_to_lsp_position(3);
2170
2171 assert_eq!(line, 0);
2172 assert_eq!(character, 3);
2173
2174 let (line, character) = buffer.position_to_lsp_position(5);
2176
2177 assert_eq!(line, 0);
2178 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2179 }
2180
2181 #[test]
2182 fn test_lsp_content_change_event_structure() {
2183 let insert_change = TextDocumentContentChangeEvent {
2187 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2188 range_length: None,
2189 text: "NEW".to_string(),
2190 };
2191
2192 assert!(insert_change.range.is_some());
2193 assert_eq!(insert_change.text, "NEW");
2194 let range = insert_change.range.unwrap();
2195 assert_eq!(
2196 range.start, range.end,
2197 "Insert should have zero-width range"
2198 );
2199
2200 let delete_change = TextDocumentContentChangeEvent {
2202 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2203 range_length: None,
2204 text: String::new(),
2205 };
2206
2207 assert!(delete_change.range.is_some());
2208 assert_eq!(delete_change.text, "");
2209 let range = delete_change.range.unwrap();
2210 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2211 assert_eq!(range.start.line, 0);
2212 assert_eq!(range.start.character, 2);
2213 assert_eq!(range.end.line, 0);
2214 assert_eq!(range.end.character, 7);
2215 }
2216
2217 #[test]
2218 fn test_goto_matching_bracket_forward() {
2219 let config = Config::default();
2220 let (dir_context, _temp) = test_dir_context();
2221 let mut editor = Editor::new(
2222 config,
2223 80,
2224 24,
2225 dir_context,
2226 crate::view::color_support::ColorCapability::TrueColor,
2227 test_filesystem(),
2228 )
2229 .unwrap();
2230
2231 let cursor_id = editor.active_cursors().primary_id();
2233 editor.apply_event_to_active_buffer(&Event::Insert {
2234 position: 0,
2235 text: "fn main() { let x = (1 + 2); }".to_string(),
2236 cursor_id,
2237 });
2238
2239 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2241 cursor_id,
2242 old_position: 31,
2243 new_position: 10,
2244 old_anchor: None,
2245 new_anchor: None,
2246 old_sticky_column: 0,
2247 new_sticky_column: 0,
2248 });
2249
2250 assert_eq!(editor.active_cursors().primary().position, 10);
2251
2252 editor.goto_matching_bracket();
2254
2255 assert_eq!(editor.active_cursors().primary().position, 29);
2260 }
2261
2262 #[test]
2263 fn test_goto_matching_bracket_backward() {
2264 let config = Config::default();
2265 let (dir_context, _temp) = test_dir_context();
2266 let mut editor = Editor::new(
2267 config,
2268 80,
2269 24,
2270 dir_context,
2271 crate::view::color_support::ColorCapability::TrueColor,
2272 test_filesystem(),
2273 )
2274 .unwrap();
2275
2276 let cursor_id = editor.active_cursors().primary_id();
2278 editor.apply_event_to_active_buffer(&Event::Insert {
2279 position: 0,
2280 text: "fn main() { let x = (1 + 2); }".to_string(),
2281 cursor_id,
2282 });
2283
2284 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2286 cursor_id,
2287 old_position: 31,
2288 new_position: 26,
2289 old_anchor: None,
2290 new_anchor: None,
2291 old_sticky_column: 0,
2292 new_sticky_column: 0,
2293 });
2294
2295 editor.goto_matching_bracket();
2297
2298 assert_eq!(editor.active_cursors().primary().position, 20);
2300 }
2301
2302 #[test]
2303 fn test_goto_matching_bracket_nested() {
2304 let config = Config::default();
2305 let (dir_context, _temp) = test_dir_context();
2306 let mut editor = Editor::new(
2307 config,
2308 80,
2309 24,
2310 dir_context,
2311 crate::view::color_support::ColorCapability::TrueColor,
2312 test_filesystem(),
2313 )
2314 .unwrap();
2315
2316 let cursor_id = editor.active_cursors().primary_id();
2318 editor.apply_event_to_active_buffer(&Event::Insert {
2319 position: 0,
2320 text: "{a{b{c}d}e}".to_string(),
2321 cursor_id,
2322 });
2323
2324 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2326 cursor_id,
2327 old_position: 11,
2328 new_position: 0,
2329 old_anchor: None,
2330 new_anchor: None,
2331 old_sticky_column: 0,
2332 new_sticky_column: 0,
2333 });
2334
2335 editor.goto_matching_bracket();
2337
2338 assert_eq!(editor.active_cursors().primary().position, 10);
2340 }
2341
2342 #[test]
2343 fn test_search_case_sensitive() {
2344 let config = Config::default();
2345 let (dir_context, _temp) = test_dir_context();
2346 let mut editor = Editor::new(
2347 config,
2348 80,
2349 24,
2350 dir_context,
2351 crate::view::color_support::ColorCapability::TrueColor,
2352 test_filesystem(),
2353 )
2354 .unwrap();
2355
2356 let cursor_id = editor.active_cursors().primary_id();
2358 editor.apply_event_to_active_buffer(&Event::Insert {
2359 position: 0,
2360 text: "Hello hello HELLO".to_string(),
2361 cursor_id,
2362 });
2363
2364 editor.active_window_mut().search_case_sensitive = false;
2366 editor.perform_search("hello");
2367
2368 let search_state = editor.active_window().search_state.as_ref().unwrap();
2369 assert_eq!(
2370 search_state.matches.len(),
2371 3,
2372 "Should find all 3 matches case-insensitively"
2373 );
2374
2375 editor.active_window_mut().search_case_sensitive = true;
2377 editor.perform_search("hello");
2378
2379 let search_state = editor.active_window().search_state.as_ref().unwrap();
2380 assert_eq!(
2381 search_state.matches.len(),
2382 1,
2383 "Should find only 1 exact match"
2384 );
2385 assert_eq!(
2386 search_state.matches[0], 6,
2387 "Should find 'hello' at position 6"
2388 );
2389 }
2390
2391 #[test]
2392 fn test_search_whole_word() {
2393 let config = Config::default();
2394 let (dir_context, _temp) = test_dir_context();
2395 let mut editor = Editor::new(
2396 config,
2397 80,
2398 24,
2399 dir_context,
2400 crate::view::color_support::ColorCapability::TrueColor,
2401 test_filesystem(),
2402 )
2403 .unwrap();
2404
2405 let cursor_id = editor.active_cursors().primary_id();
2407 editor.apply_event_to_active_buffer(&Event::Insert {
2408 position: 0,
2409 text: "test testing tested attest test".to_string(),
2410 cursor_id,
2411 });
2412
2413 editor.active_window_mut().search_whole_word = false;
2415 editor.active_window_mut().search_case_sensitive = true;
2416 editor.perform_search("test");
2417
2418 let search_state = editor.active_window().search_state.as_ref().unwrap();
2419 assert_eq!(
2420 search_state.matches.len(),
2421 5,
2422 "Should find 'test' in all occurrences"
2423 );
2424
2425 editor.active_window_mut().search_whole_word = true;
2427 editor.perform_search("test");
2428
2429 let search_state = editor.active_window().search_state.as_ref().unwrap();
2430 assert_eq!(
2431 search_state.matches.len(),
2432 2,
2433 "Should find only whole word 'test'"
2434 );
2435 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2436 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2437 }
2438
2439 #[test]
2440 fn test_search_scan_completes_when_capped() {
2441 let config = Config::default();
2447 let (dir_context, _temp) = test_dir_context();
2448 let mut editor = Editor::new(
2449 config,
2450 80,
2451 24,
2452 dir_context,
2453 crate::view::color_support::ColorCapability::TrueColor,
2454 test_filesystem(),
2455 )
2456 .unwrap();
2457
2458 let buffer_id = editor.active_buffer();
2461 let regex = regex::bytes::Regex::new("test").unwrap();
2462 let fake_chunks = vec![
2463 crate::model::buffer::LineScanChunk {
2464 leaf_index: 0,
2465 byte_len: 100,
2466 already_known: true,
2467 },
2468 crate::model::buffer::LineScanChunk {
2469 leaf_index: 1,
2470 byte_len: 100,
2471 already_known: true,
2472 },
2473 ];
2474
2475 let chunked = crate::model::buffer::ChunkedSearchState {
2476 chunks: fake_chunks,
2477 next_chunk: 1, next_doc_offset: 100,
2479 total_bytes: 200,
2480 scanned_bytes: 100,
2481 regex,
2482 matches: vec![
2483 crate::model::buffer::SearchMatch {
2484 byte_offset: 10,
2485 length: 4,
2486 line: 1,
2487 column: 11,
2488 context: String::new(),
2489 },
2490 crate::model::buffer::SearchMatch {
2491 byte_offset: 50,
2492 length: 4,
2493 line: 1,
2494 column: 51,
2495 context: String::new(),
2496 },
2497 ],
2498 overlap_tail: Vec::new(),
2499 overlap_doc_offset: 0,
2500 max_matches: 10_000,
2501 capped: true, query_len: 4,
2503 running_line: 1,
2504 };
2505
2506 editor.active_window_mut().search_scan.start(
2507 buffer_id,
2508 Vec::new(),
2509 chunked,
2510 "test".to_string(),
2511 None,
2512 false,
2513 false,
2514 false,
2515 );
2516
2517 let result = editor.process_search_scan();
2519 assert!(
2520 result,
2521 "process_search_scan should return true (needs render)"
2522 );
2523
2524 assert_eq!(
2526 editor.active_window().search_scan.buffer_id(),
2527 None,
2528 "search_scan should be drained after capped scan completes"
2529 );
2530
2531 let search_state = editor
2533 .active_window()
2534 .search_state
2535 .as_ref()
2536 .expect("search_state should be set after scan finishes");
2537 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2538 assert_eq!(search_state.query, "test");
2539 assert!(
2540 search_state.capped,
2541 "search_state should be marked as capped"
2542 );
2543 }
2544
2545 #[test]
2546 fn test_bookmarks() {
2547 let config = Config::default();
2548 let (dir_context, _temp) = test_dir_context();
2549 let mut editor = Editor::new(
2550 config,
2551 80,
2552 24,
2553 dir_context,
2554 crate::view::color_support::ColorCapability::TrueColor,
2555 test_filesystem(),
2556 )
2557 .unwrap();
2558
2559 let cursor_id = editor.active_cursors().primary_id();
2561 editor.apply_event_to_active_buffer(&Event::Insert {
2562 position: 0,
2563 text: "Line 1\nLine 2\nLine 3".to_string(),
2564 cursor_id,
2565 });
2566
2567 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2569 cursor_id,
2570 old_position: 21,
2571 new_position: 7,
2572 old_anchor: None,
2573 new_anchor: None,
2574 old_sticky_column: 0,
2575 new_sticky_column: 0,
2576 });
2577
2578 editor.active_window_mut().set_bookmark('1');
2580 assert_eq!(
2581 editor
2582 .active_window()
2583 .bookmarks
2584 .get('1')
2585 .map(|b| b.position),
2586 Some(7)
2587 );
2588
2589 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2591 cursor_id,
2592 old_position: 7,
2593 new_position: 14,
2594 old_anchor: None,
2595 new_anchor: None,
2596 old_sticky_column: 0,
2597 new_sticky_column: 0,
2598 });
2599
2600 editor.jump_to_bookmark('1');
2602 assert_eq!(editor.active_cursors().primary().position, 7);
2603
2604 editor.active_window_mut().clear_bookmark('1');
2606 assert_eq!(editor.active_window().bookmarks.get('1'), None);
2607 }
2608
2609 #[test]
2610 fn test_action_enum_new_variants() {
2611 use serde_json::json;
2613
2614 let args = HashMap::new();
2615 assert_eq!(
2616 Action::from_str("smart_home", &args),
2617 Some(Action::SmartHome)
2618 );
2619 assert_eq!(
2620 Action::from_str("dedent_selection", &args),
2621 Some(Action::DedentSelection)
2622 );
2623 assert_eq!(
2624 Action::from_str("toggle_comment", &args),
2625 Some(Action::ToggleComment)
2626 );
2627 assert_eq!(
2628 Action::from_str("goto_matching_bracket", &args),
2629 Some(Action::GoToMatchingBracket)
2630 );
2631 assert_eq!(
2632 Action::from_str("list_bookmarks", &args),
2633 Some(Action::ListBookmarks)
2634 );
2635 assert_eq!(
2636 Action::from_str("toggle_search_case_sensitive", &args),
2637 Some(Action::ToggleSearchCaseSensitive)
2638 );
2639 assert_eq!(
2640 Action::from_str("toggle_search_whole_word", &args),
2641 Some(Action::ToggleSearchWholeWord)
2642 );
2643
2644 let mut args_with_char = HashMap::new();
2646 args_with_char.insert("char".to_string(), json!("5"));
2647 assert_eq!(
2648 Action::from_str("set_bookmark", &args_with_char),
2649 Some(Action::SetBookmark('5'))
2650 );
2651 assert_eq!(
2652 Action::from_str("jump_to_bookmark", &args_with_char),
2653 Some(Action::JumpToBookmark('5'))
2654 );
2655 assert_eq!(
2656 Action::from_str("clear_bookmark", &args_with_char),
2657 Some(Action::ClearBookmark('5'))
2658 );
2659 }
2660
2661 #[test]
2662 fn test_keybinding_new_defaults() {
2663 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2664
2665 let mut config = Config::default();
2669 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2670 let resolver = KeybindingResolver::new(&config);
2671
2672 let event = KeyEvent {
2674 code: KeyCode::Char('/'),
2675 modifiers: KeyModifiers::CONTROL,
2676 kind: KeyEventKind::Press,
2677 state: KeyEventState::NONE,
2678 };
2679 let action = resolver.resolve(&event, KeyContext::Normal);
2680 assert_eq!(action, Action::ToggleComment);
2681
2682 let event = KeyEvent {
2684 code: KeyCode::Char(']'),
2685 modifiers: KeyModifiers::CONTROL,
2686 kind: KeyEventKind::Press,
2687 state: KeyEventState::NONE,
2688 };
2689 let action = resolver.resolve(&event, KeyContext::Normal);
2690 assert_eq!(action, Action::GoToMatchingBracket);
2691
2692 let event = KeyEvent {
2694 code: KeyCode::Tab,
2695 modifiers: KeyModifiers::SHIFT,
2696 kind: KeyEventKind::Press,
2697 state: KeyEventState::NONE,
2698 };
2699 let action = resolver.resolve(&event, KeyContext::Normal);
2700 assert_eq!(action, Action::DedentSelection);
2701
2702 let event = KeyEvent {
2704 code: KeyCode::Char('g'),
2705 modifiers: KeyModifiers::CONTROL,
2706 kind: KeyEventKind::Press,
2707 state: KeyEventState::NONE,
2708 };
2709 let action = resolver.resolve(&event, KeyContext::Normal);
2710 assert_eq!(action, Action::GotoLine);
2711
2712 let event = KeyEvent {
2714 code: KeyCode::Char('5'),
2715 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2716 kind: KeyEventKind::Press,
2717 state: KeyEventState::NONE,
2718 };
2719 let action = resolver.resolve(&event, KeyContext::Normal);
2720 assert_eq!(action, Action::SetBookmark('5'));
2721
2722 let event = KeyEvent {
2723 code: KeyCode::Char('5'),
2724 modifiers: KeyModifiers::ALT,
2725 kind: KeyEventKind::Press,
2726 state: KeyEventState::NONE,
2727 };
2728 let action = resolver.resolve(&event, KeyContext::Normal);
2729 assert_eq!(action, Action::JumpToBookmark('5'));
2730 }
2731
2732 #[test]
2744 fn test_lsp_rename_didchange_positions_bug() {
2745 use crate::model::buffer::Buffer;
2746
2747 let config = Config::default();
2748 let (dir_context, _temp) = test_dir_context();
2749 let mut editor = Editor::new(
2750 config,
2751 80,
2752 24,
2753 dir_context,
2754 crate::view::color_support::ColorCapability::TrueColor,
2755 test_filesystem(),
2756 )
2757 .unwrap();
2758
2759 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2763 editor.active_state_mut().buffer =
2764 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2765
2766 let cursor_id = editor.active_cursors().primary_id();
2771
2772 let batch = Event::Batch {
2773 events: vec![
2774 Event::Delete {
2776 range: 23..26, deleted_text: "val".to_string(),
2778 cursor_id,
2779 },
2780 Event::Insert {
2781 position: 23,
2782 text: "value".to_string(),
2783 cursor_id,
2784 },
2785 Event::Delete {
2787 range: 7..10, deleted_text: "val".to_string(),
2789 cursor_id,
2790 },
2791 Event::Insert {
2792 position: 7,
2793 text: "value".to_string(),
2794 cursor_id,
2795 },
2796 ],
2797 description: "LSP Rename".to_string(),
2798 };
2799
2800 let lsp_changes_before = editor.active_window().collect_lsp_changes(&batch);
2802
2803 editor.apply_event_to_active_buffer(&batch);
2805
2806 let lsp_changes_after = editor.active_window().collect_lsp_changes(&batch);
2809
2810 let final_content = editor.active_state().buffer.to_string().unwrap();
2812 assert_eq!(
2813 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2814 "Buffer should have 'value' in both places"
2815 );
2816
2817 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2823
2824 let first_delete = &lsp_changes_before[0];
2825 let first_del_range = first_delete.range.unwrap();
2826 assert_eq!(
2827 first_del_range.start.line, 1,
2828 "First delete should be on line 1 (BEFORE)"
2829 );
2830 assert_eq!(
2831 first_del_range.start.character, 4,
2832 "First delete start should be at char 4 (BEFORE)"
2833 );
2834
2835 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2841
2842 let first_delete_after = &lsp_changes_after[0];
2843 let first_del_range_after = first_delete_after.range.unwrap();
2844
2845 eprintln!("BEFORE modification:");
2848 eprintln!(
2849 " Delete at line {}, char {}-{}",
2850 first_del_range.start.line,
2851 first_del_range.start.character,
2852 first_del_range.end.character
2853 );
2854 eprintln!("AFTER modification:");
2855 eprintln!(
2856 " Delete at line {}, char {}-{}",
2857 first_del_range_after.start.line,
2858 first_del_range_after.start.character,
2859 first_del_range_after.end.character
2860 );
2861
2862 assert_ne!(
2880 first_del_range_after.end.character, first_del_range.end.character,
2881 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2882 );
2883
2884 eprintln!("\n=== BUG DEMONSTRATED ===");
2885 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2886 eprintln!("the positions are WRONG because they're calculated from the");
2887 eprintln!("modified buffer, not the original buffer.");
2888 eprintln!("This causes the second rename to fail with 'content modified' error.");
2889 eprintln!("========================\n");
2890 }
2891
2892 #[test]
2893 fn test_lsp_rename_preserves_cursor_position() {
2894 use crate::model::buffer::Buffer;
2895
2896 let config = Config::default();
2897 let (dir_context, _temp) = test_dir_context();
2898 let mut editor = Editor::new(
2899 config,
2900 80,
2901 24,
2902 dir_context,
2903 crate::view::color_support::ColorCapability::TrueColor,
2904 test_filesystem(),
2905 )
2906 .unwrap();
2907
2908 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2912 editor.active_state_mut().buffer =
2913 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2914
2915 let original_cursor_pos = 23;
2917 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2918
2919 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2921 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2922 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2923
2924 let cursor_id = editor.active_cursors().primary_id();
2927 let buffer_id = editor.active_buffer();
2928
2929 let events = vec![
2930 Event::Delete {
2932 range: 23..26, deleted_text: "val".to_string(),
2934 cursor_id,
2935 },
2936 Event::Insert {
2937 position: 23,
2938 text: "value".to_string(),
2939 cursor_id,
2940 },
2941 Event::Delete {
2943 range: 7..10, deleted_text: "val".to_string(),
2945 cursor_id,
2946 },
2947 Event::Insert {
2948 position: 7,
2949 text: "value".to_string(),
2950 cursor_id,
2951 },
2952 ];
2953
2954 editor
2956 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2957 .unwrap();
2958
2959 let final_content = editor.active_state().buffer.to_string().unwrap();
2961 assert_eq!(
2962 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2963 "Buffer should have 'value' in both places"
2964 );
2965
2966 let final_cursor_pos = editor.active_cursors().primary().position;
2974 let expected_cursor_pos = 25; assert_eq!(
2977 final_cursor_pos, expected_cursor_pos,
2978 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2979 Original pos: {}, expected adjustment: +2 for first rename",
2980 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2981 );
2982
2983 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2985 assert_eq!(
2986 text_at_new_cursor, "value",
2987 "Cursor should be at the start of 'value' after rename"
2988 );
2989 }
2990
2991 #[test]
2992 fn test_lsp_rename_twice_consecutive() {
2993 use crate::model::buffer::Buffer;
2996
2997 let config = Config::default();
2998 let (dir_context, _temp) = test_dir_context();
2999 let mut editor = Editor::new(
3000 config,
3001 80,
3002 24,
3003 dir_context,
3004 crate::view::color_support::ColorCapability::TrueColor,
3005 test_filesystem(),
3006 )
3007 .unwrap();
3008
3009 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
3011 editor.active_state_mut().buffer =
3012 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
3013
3014 let cursor_id = editor.active_cursors().primary_id();
3015 let buffer_id = editor.active_buffer();
3016
3017 let events1 = vec![
3020 Event::Delete {
3022 range: 23..26,
3023 deleted_text: "val".to_string(),
3024 cursor_id,
3025 },
3026 Event::Insert {
3027 position: 23,
3028 text: "value".to_string(),
3029 cursor_id,
3030 },
3031 Event::Delete {
3033 range: 7..10,
3034 deleted_text: "val".to_string(),
3035 cursor_id,
3036 },
3037 Event::Insert {
3038 position: 7,
3039 text: "value".to_string(),
3040 cursor_id,
3041 },
3042 ];
3043
3044 let batch1 = Event::Batch {
3046 events: events1.clone(),
3047 description: "LSP Rename 1".to_string(),
3048 };
3049
3050 let lsp_changes1 = editor.active_window().collect_lsp_changes(&batch1);
3052
3053 assert_eq!(
3055 lsp_changes1.len(),
3056 4,
3057 "First rename should have 4 LSP changes"
3058 );
3059
3060 let first_del = &lsp_changes1[0];
3062 let first_del_range = first_del.range.unwrap();
3063 assert_eq!(first_del_range.start.line, 1, "First delete line");
3064 assert_eq!(
3065 first_del_range.start.character, 4,
3066 "First delete start char"
3067 );
3068 assert_eq!(first_del_range.end.character, 7, "First delete end char");
3069
3070 editor
3072 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
3073 .unwrap();
3074
3075 let after_first = editor.active_state().buffer.to_string().unwrap();
3077 assert_eq!(
3078 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3079 "After first rename"
3080 );
3081
3082 let events2 = vec![
3092 Event::Delete {
3094 range: 25..30,
3095 deleted_text: "value".to_string(),
3096 cursor_id,
3097 },
3098 Event::Insert {
3099 position: 25,
3100 text: "x".to_string(),
3101 cursor_id,
3102 },
3103 Event::Delete {
3105 range: 7..12,
3106 deleted_text: "value".to_string(),
3107 cursor_id,
3108 },
3109 Event::Insert {
3110 position: 7,
3111 text: "x".to_string(),
3112 cursor_id,
3113 },
3114 ];
3115
3116 let batch2 = Event::Batch {
3118 events: events2.clone(),
3119 description: "LSP Rename 2".to_string(),
3120 };
3121
3122 let lsp_changes2 = editor.active_window().collect_lsp_changes(&batch2);
3124
3125 assert_eq!(
3129 lsp_changes2.len(),
3130 4,
3131 "Second rename should have 4 LSP changes"
3132 );
3133
3134 let second_first_del = &lsp_changes2[0];
3136 let second_first_del_range = second_first_del.range.unwrap();
3137 assert_eq!(
3138 second_first_del_range.start.line, 1,
3139 "Second rename first delete should be on line 1"
3140 );
3141 assert_eq!(
3142 second_first_del_range.start.character, 4,
3143 "Second rename first delete start should be at char 4"
3144 );
3145 assert_eq!(
3146 second_first_del_range.end.character, 9,
3147 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3148 );
3149
3150 let second_third_del = &lsp_changes2[2];
3152 let second_third_del_range = second_third_del.range.unwrap();
3153 assert_eq!(
3154 second_third_del_range.start.line, 0,
3155 "Second rename third delete should be on line 0"
3156 );
3157 assert_eq!(
3158 second_third_del_range.start.character, 7,
3159 "Second rename third delete start should be at char 7"
3160 );
3161 assert_eq!(
3162 second_third_del_range.end.character, 12,
3163 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3164 );
3165
3166 editor
3168 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3169 .unwrap();
3170
3171 let after_second = editor.active_state().buffer.to_string().unwrap();
3173 assert_eq!(
3174 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3175 "After second rename"
3176 );
3177 }
3178
3179 #[test]
3180 fn test_ensure_active_tab_visible_static_offset() {
3181 let config = Config::default();
3182 let (dir_context, _temp) = test_dir_context();
3183 let mut editor = Editor::new(
3184 config,
3185 80,
3186 24,
3187 dir_context,
3188 crate::view::color_support::ColorCapability::TrueColor,
3189 test_filesystem(),
3190 )
3191 .unwrap();
3192 let split_id = editor.split_manager().active_split();
3193
3194 let buf1 = editor.new_buffer();
3196 editor
3197 .buffers_mut()
3198 .get_mut(&buf1)
3199 .unwrap()
3200 .buffer
3201 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3202 let buf2 = editor.new_buffer();
3203 editor
3204 .buffers_mut()
3205 .get_mut(&buf2)
3206 .unwrap()
3207 .buffer
3208 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3209 let buf3 = editor.new_buffer();
3210 editor
3211 .buffers_mut()
3212 .get_mut(&buf3)
3213 .unwrap()
3214 .buffer
3215 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3216
3217 {
3218 use crate::view::split::TabTarget;
3219 let view_state = editor.split_view_states_mut().get_mut(&split_id).unwrap();
3220 view_state.open_buffers = vec![
3221 TabTarget::Buffer(buf1),
3222 TabTarget::Buffer(buf2),
3223 TabTarget::Buffer(buf3),
3224 ];
3225 view_state.tab_scroll_offset = 50;
3226 }
3227
3228 editor
3232 .active_window_mut()
3233 .ensure_active_tab_visible(split_id, buf1, 25);
3234 assert_eq!(
3235 editor
3236 .split_view_states()
3237 .get(&split_id)
3238 .unwrap()
3239 .tab_scroll_offset,
3240 0
3241 );
3242
3243 editor
3245 .active_window_mut()
3246 .ensure_active_tab_visible(split_id, buf3, 25);
3247 let view_state = editor.split_view_states().get(&split_id).unwrap();
3248 assert!(view_state.tab_scroll_offset > 0);
3249 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3250 let total_width: usize = buffer_ids
3251 .iter()
3252 .enumerate()
3253 .map(|(idx, id)| {
3254 let state = editor.buffers().get(id).unwrap();
3255 let name_len = state
3256 .buffer
3257 .file_path()
3258 .and_then(|p| p.file_name())
3259 .and_then(|n| n.to_str())
3260 .map(|s| s.chars().count())
3261 .unwrap_or(0);
3262 let tab_width = 2 + name_len;
3263 if idx < buffer_ids.len() - 1 {
3264 tab_width + 1 } else {
3266 tab_width
3267 }
3268 })
3269 .sum();
3270 assert!(view_state.tab_scroll_offset <= total_width);
3271 }
3272}