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 pub(crate) windows: HashMap<fresh_core::WindowId, crate::app::window::Window>,
599
600 pub(crate) active_window: fresh_core::WindowId,
603
604 #[allow(dead_code)]
609 pub(crate) next_window_id: u64,
610
611 command_registry: Arc<RwLock<CommandRegistry>>,
629
630 quick_open_registry: QuickOpenRegistry,
632
633 plugin_manager: Arc<RwLock<PluginManager>>,
641
642 status_bar_token_registry: Mutex<HashMap<String, String>>,
648
649 pub(crate) plugin_schemas:
656 std::sync::Arc<std::sync::RwLock<HashMap<String, serde_json::Value>>>,
657
658 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
674
675 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
681 event_broadcaster: crate::model::control_event::EventBroadcaster,
700
701 #[cfg(feature = "plugins")]
708 pending_plugin_actions: Vec<(
709 String,
710 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
711 )>,
712
713 #[cfg(feature = "plugins")]
715 plugin_render_requested: bool,
716
717 recovery_service: std::sync::Arc<std::sync::Mutex<RecoveryService>>,
747
748 full_redraw_requested: bool,
750
751 suspend_requested: bool,
754
755 time_source: SharedTimeSource,
757
758 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
768 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
770
771 status_log_path: Option<PathBuf>,
773
774 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
780
781 file_watcher_manager: crate::services::file_watcher::FileWatcherManager,
791
792 pub(crate) last_path_change_for_test: Option<(u64, std::path::PathBuf, &'static str)>,
798
799 pub(crate) last_watch_response_for_test: Option<(u64, Result<u64, String>)>,
803
804 pub(crate) preview_window_id: Option<fresh_core::WindowId>,
811
812 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
832
833 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
835
836 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
841
842 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
844
845 color_capability: crate::view::color_support::ColorCapability,
847
848 pub(crate) global_popups: crate::view::popup::PopupManager,
859
860 stdin_stream: stdin_stream::StdinStream,
878
879 pub(crate) previous_cursor_screen_pos: Option<((u16, u16), LeafId)>,
902 pub(crate) cursor_jump_animation: Option<crate::view::animation::AnimationId>,
905
906 pub(crate) pending_vb_animations: Vec<(u64, BufferId, fresh_core::api::PluginAnimationKind)>,
912
913 pub(crate) widget_registry: crate::widgets::WidgetRegistry,
921
922 pub(crate) floating_widget_panel: Option<FloatingWidgetState>,
929}
930
931pub(crate) const FLOATING_PANEL_BUFFER_ID: BufferId = BufferId(usize::MAX);
937
938#[derive(Debug, Clone)]
945pub(crate) struct FloatingWidgetState {
946 pub panel_id: crate::widgets::PanelId,
947 pub width_pct: u8,
948 pub height_pct: u8,
949 pub entries: Vec<fresh_core::text_property::TextPropertyEntry>,
953 pub focus_cursor: Option<crate::widgets::FocusCursor>,
955 pub embeds: Vec<crate::widgets::EmbedRect>,
962 pub overlays: Vec<crate::widgets::OverlayRow>,
969 pub scroll_regions: Vec<crate::widgets::ScrollRegion>,
973 pub scrollbar_tracks: Vec<WidgetScrollbarTrack>,
977 pub scrollbar_mouse: crate::view::ui::scrollbar::ScrollbarMouse,
980 pub scrollbar_drag_key: Option<String>,
982 pub last_inner_rect: Option<ratatui::layout::Rect>,
985}
986
987#[derive(Debug, Clone)]
990pub(crate) struct WidgetScrollbarTrack {
991 pub list_key: String,
992 pub rect: ratatui::layout::Rect,
993 pub total: usize,
994 pub visible: usize,
995 pub scroll: usize,
996}
997
998#[derive(Debug, Clone)]
1000pub struct PendingFileOpen {
1001 pub path: PathBuf,
1003 pub line: Option<usize>,
1005 pub column: Option<usize>,
1007 pub end_line: Option<usize>,
1009 pub end_column: Option<usize>,
1011 pub message: Option<String>,
1013 pub wait_id: Option<u64>,
1015}
1016
1017impl Editor {
1018 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1020 let trimmed = input.trim();
1021
1022 if trimmed.is_empty() {
1023 self.ansi_background = None;
1024 self.ansi_background_path = None;
1025 self.set_status_message(t!("status.background_cleared").to_string());
1026 return Ok(());
1027 }
1028
1029 let input_path = Path::new(trimmed);
1030 let resolved = if input_path.is_absolute() {
1031 input_path.to_path_buf()
1032 } else {
1033 self.working_dir().join(input_path)
1034 };
1035
1036 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1037
1038 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1039
1040 self.ansi_background = Some(parsed);
1041 self.ansi_background_path = Some(canonical.clone());
1042 self.set_status_message(
1043 t!(
1044 "view.background_set",
1045 path = canonical.display().to_string()
1046 )
1047 .to_string(),
1048 );
1049
1050 Ok(())
1051 }
1052
1053 #[doc(hidden)]
1057 pub fn buffer_count_for_tests(&self) -> usize {
1058 self.windows
1059 .get(&self.active_window)
1060 .map(|w| &w.buffers)
1061 .expect("active window present")
1062 .len()
1063 }
1064
1065 #[doc(hidden)]
1069 pub fn all_buffer_ids_for_tests(&self) -> Vec<BufferId> {
1070 let mut ids: Vec<BufferId> = self
1071 .windows
1072 .get(&self.active_window)
1073 .map(|w| &w.buffers)
1074 .expect("active window present")
1075 .ids();
1076 ids.sort_by_key(|id| id.0);
1077 ids
1078 }
1079
1080 pub fn active_state(&self) -> &EditorState {
1082 self.windows
1083 .get(&self.active_window)
1084 .map(|w| &w.buffers)
1085 .expect("active window present")
1086 .get(&self.active_buffer())
1087 .unwrap()
1088 }
1089
1090 pub fn active_state_mut(&mut self) -> &mut EditorState {
1092 let __buffer_id = self.active_buffer();
1093 self.windows
1094 .get_mut(&self.active_window)
1095 .map(|w| &mut w.buffers)
1096 .expect("active window present")
1097 .get_mut(&__buffer_id)
1098 .unwrap()
1099 }
1100
1101 pub fn active_cursors(&self) -> &Cursors {
1105 let split_id = self.effective_active_split();
1106 &self
1107 .windows
1108 .get(&self.active_window)
1109 .and_then(|w| w.buffers.splits())
1110 .map(|(_, vs)| vs)
1111 .expect("active window must have a populated split layout")
1112 .get(&split_id)
1113 .unwrap()
1114 .cursors
1115 }
1116
1117 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1119 let split_id = self.effective_active_split();
1120 &mut self
1121 .windows
1122 .get_mut(&self.active_window)
1123 .and_then(|w| w.split_view_states_mut())
1124 .expect("active window must have a populated split layout")
1125 .get_mut(&split_id)
1126 .unwrap()
1127 .cursors
1128 }
1129
1130 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1132 self.active_window_mut().completion_items = Some(items);
1133 }
1134
1135 pub fn active_viewport(&self) -> &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 &self
1145 .windows
1146 .get(&self.active_window)
1147 .and_then(|w| w.buffers.splits())
1148 .map(|(_, vs)| vs)
1149 .expect("active window must have a populated split layout")
1150 .get(&active_split)
1151 .unwrap()
1152 .viewport
1153 }
1154
1155 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1157 let active_split = self
1158 .windows
1159 .get(&self.active_window)
1160 .and_then(|w| w.buffers.splits())
1161 .map(|(mgr, _)| mgr)
1162 .expect("active window must have a populated split layout")
1163 .active_split();
1164 &mut self
1165 .windows
1166 .get_mut(&self.active_window)
1167 .and_then(|w| w.split_view_states_mut())
1168 .expect("active window must have a populated split layout")
1169 .get_mut(&active_split)
1170 .unwrap()
1171 .viewport
1172 }
1173
1174 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1176 if let Some(composite) = self.active_window().composite_buffers.get(&buffer_id) {
1178 return composite.name.clone();
1179 }
1180
1181 self.active_window()
1182 .buffer_metadata
1183 .get(&buffer_id)
1184 .map(|m| m.display_name.clone())
1185 .or_else(|| {
1186 self.windows
1187 .get(&self.active_window)
1188 .map(|w| &w.buffers)
1189 .expect("active window present")
1190 .get(&buffer_id)
1191 .and_then(|state| {
1192 state
1193 .buffer
1194 .file_path()
1195 .and_then(|p| p.file_name())
1196 .and_then(|n| n.to_str())
1197 .map(|s| s.to_string())
1198 })
1199 })
1200 .unwrap_or_else(|| "[No Name]".to_string())
1201 }
1202
1203 pub fn active_event_log(&self) -> &EventLog {
1213 self.active_window()
1214 .event_logs
1215 .get(&self.active_buffer())
1216 .unwrap()
1217 }
1218
1219 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1221 let buffer_id = self.active_buffer();
1222 self.active_window_mut()
1223 .event_logs
1224 .get_mut(&buffer_id)
1225 .unwrap()
1226 }
1227
1228 pub fn register_status_bar_element(
1232 &self,
1233 plugin_name: &str,
1234 token_name: &str,
1235 title: &str,
1236 ) -> Result<(), String> {
1237 if plugin_name.is_empty() {
1238 return Err("Plugin name cannot be empty".to_string());
1239 }
1240 if token_name.is_empty() {
1241 return Err("Token name cannot be empty".to_string());
1242 }
1243
1244 let key = format!("{}:{}", plugin_name, token_name);
1245 let mut registry = self.status_bar_token_registry.lock().unwrap();
1246
1247 if registry.contains_key(&key) {
1248 return Err(format!("Token '{}' already registered", key));
1249 }
1250
1251 registry.insert(key, title.to_string());
1252 Ok(())
1253 }
1254
1255 pub fn set_status_bar_value(
1260 &mut self,
1261 buffer_id: BufferId,
1262 key: &str,
1263 value: String,
1264 ) -> Result<(), String> {
1265 for window in self.windows.values_mut() {
1266 if window.buffers.contains_key(&buffer_id) {
1267 window
1268 .status_bar_values
1269 .entry(buffer_id)
1270 .or_default()
1271 .insert(key.to_string(), value);
1272 return Ok(());
1273 }
1274 }
1275 Err(format!("Buffer {:?} not found", buffer_id))
1276 }
1277
1278 pub fn get_status_bar_elements(&self) -> Vec<(String, String)> {
1281 self.status_bar_token_registry
1282 .lock()
1283 .unwrap()
1284 .iter()
1285 .map(|(k, title)| (format!("{{{}}}", k), title.clone()))
1286 .collect()
1287 }
1288
1289 pub fn get_status_bar_element_values(&self, buffer_id: BufferId) -> HashMap<String, String> {
1291 for window in self.windows.values() {
1292 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1293 return values.clone();
1294 }
1295 }
1296 HashMap::new()
1297 }
1298
1299 pub fn current_status_bar_value(&self, buffer_id: BufferId, key: &str) -> Option<&str> {
1303 for window in self.windows.values() {
1304 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1305 if let Some(v) = values.get(key) {
1306 return Some(v.as_str());
1307 }
1308 return None;
1309 }
1310 }
1311 None
1312 }
1313
1314 fn remove_plugin_status_bar_elements(&mut self, plugin_name: &str) {
1317 let prefix = format!("{}:", plugin_name);
1318 self.status_bar_token_registry
1319 .lock()
1320 .unwrap()
1321 .retain(|k, _| !k.starts_with(&prefix));
1322 for window in self.windows.values_mut() {
1323 for values in window.status_bar_values.values_mut() {
1324 values.retain(|k, _| !k.starts_with(&prefix));
1325 }
1326 }
1327 }
1328}
1329
1330fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1339 use crossterm::event::{KeyCode, KeyModifiers};
1340
1341 let mut modifiers = KeyModifiers::NONE;
1342 let mut remaining = key_str;
1343
1344 loop {
1346 if remaining.starts_with("C-") {
1347 modifiers |= KeyModifiers::CONTROL;
1348 remaining = &remaining[2..];
1349 } else if remaining.starts_with("M-") {
1350 modifiers |= KeyModifiers::ALT;
1351 remaining = &remaining[2..];
1352 } else if remaining.starts_with("S-") {
1353 modifiers |= KeyModifiers::SHIFT;
1354 remaining = &remaining[2..];
1355 } else {
1356 break;
1357 }
1358 }
1359
1360 let upper = remaining.to_uppercase();
1363 let code = match upper.as_str() {
1364 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1365 "TAB" => KeyCode::Tab,
1366 "BACKTAB" => KeyCode::BackTab,
1367 "ESC" | "ESCAPE" => KeyCode::Esc,
1368 "SPC" | "SPACE" => KeyCode::Char(' '),
1369 "DEL" | "DELETE" => KeyCode::Delete,
1370 "BS" | "BACKSPACE" => KeyCode::Backspace,
1371 "UP" => KeyCode::Up,
1372 "DOWN" => KeyCode::Down,
1373 "LEFT" => KeyCode::Left,
1374 "RIGHT" => KeyCode::Right,
1375 "HOME" => KeyCode::Home,
1376 "END" => KeyCode::End,
1377 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1378 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1379 s if s.starts_with('F') && s.len() > 1 => {
1380 if let Ok(n) = s[1..].parse::<u8>() {
1382 KeyCode::F(n)
1383 } else {
1384 return None;
1385 }
1386 }
1387 _ if remaining.len() == 1 => {
1388 let c = remaining.chars().next()?;
1391 if c.is_ascii_uppercase() {
1392 modifiers |= KeyModifiers::SHIFT;
1393 }
1394 KeyCode::Char(c.to_ascii_lowercase())
1395 }
1396 _ => return None,
1397 };
1398
1399 if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
1404 return Some((KeyCode::BackTab, modifiers.difference(KeyModifiers::SHIFT)));
1405 }
1406
1407 Some((code, modifiers))
1408}
1409
1410#[cfg(test)]
1411mod tests {
1412 use super::*;
1413 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1414 use tempfile::TempDir;
1415
1416 fn test_dir_context() -> (DirectoryContext, TempDir) {
1418 let temp_dir = TempDir::new().unwrap();
1419 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1420 (dir_context, temp_dir)
1421 }
1422
1423 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1425 Arc::new(crate::model::filesystem::StdFileSystem)
1426 }
1427
1428 #[test]
1429 fn parse_key_string_shift_tab_normalizes_to_backtab() {
1430 use crossterm::event::{KeyCode, KeyModifiers};
1431 assert_eq!(
1436 parse_key_string("S-Tab"),
1437 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1438 );
1439 assert_eq!(
1440 parse_key_string("BackTab"),
1441 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1442 );
1443 assert_eq!(
1445 parse_key_string("Tab"),
1446 Some((KeyCode::Tab, KeyModifiers::NONE)),
1447 );
1448 }
1449
1450 #[test]
1451 fn test_editor_new() {
1452 let config = Config::default();
1453 let (dir_context, _temp) = test_dir_context();
1454 let editor = Editor::new(
1455 config,
1456 80,
1457 24,
1458 dir_context,
1459 crate::view::color_support::ColorCapability::TrueColor,
1460 test_filesystem(),
1461 )
1462 .unwrap();
1463
1464 assert_eq!(editor.buffers().len(), 1);
1465 assert!(!editor.should_quit());
1466 }
1467
1468 #[test]
1469 fn test_new_buffer() {
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 let id = editor.new_buffer();
1483 assert_eq!(editor.buffers().len(), 2);
1484 assert_eq!(editor.active_buffer(), id);
1485 }
1486
1487 #[test]
1488 #[ignore]
1489 fn test_clipboard() {
1490 let config = Config::default();
1491 let (dir_context, _temp) = test_dir_context();
1492 let mut editor = Editor::new(
1493 config,
1494 80,
1495 24,
1496 dir_context,
1497 crate::view::color_support::ColorCapability::TrueColor,
1498 test_filesystem(),
1499 )
1500 .unwrap();
1501
1502 editor.clipboard.set_internal("test".to_string());
1504
1505 editor.paste();
1507
1508 let content = editor.active_state().buffer.to_string().unwrap();
1509 assert_eq!(content, "test");
1510 }
1511
1512 #[test]
1513 fn test_action_to_events_insert_char() {
1514 let config = Config::default();
1515 let (dir_context, _temp) = test_dir_context();
1516 let mut editor = Editor::new(
1517 config,
1518 80,
1519 24,
1520 dir_context,
1521 crate::view::color_support::ColorCapability::TrueColor,
1522 test_filesystem(),
1523 )
1524 .unwrap();
1525
1526 let events = editor
1527 .active_window_mut()
1528 .action_to_events(Action::InsertChar('a'));
1529 assert!(events.is_some());
1530
1531 let events = events.unwrap();
1532 assert_eq!(events.len(), 1);
1533
1534 match &events[0] {
1535 Event::Insert { position, text, .. } => {
1536 assert_eq!(*position, 0);
1537 assert_eq!(text, "a");
1538 }
1539 _ => panic!("Expected Insert event"),
1540 }
1541 }
1542
1543 #[test]
1544 fn test_action_to_events_move_right() {
1545 let config = Config::default();
1546 let (dir_context, _temp) = test_dir_context();
1547 let mut editor = Editor::new(
1548 config,
1549 80,
1550 24,
1551 dir_context,
1552 crate::view::color_support::ColorCapability::TrueColor,
1553 test_filesystem(),
1554 )
1555 .unwrap();
1556
1557 let cursor_id = editor.active_cursors().primary_id();
1559 editor.apply_event_to_active_buffer(&Event::Insert {
1560 position: 0,
1561 text: "hello".to_string(),
1562 cursor_id,
1563 });
1564
1565 let events = editor
1566 .active_window_mut()
1567 .action_to_events(Action::MoveRight);
1568 assert!(events.is_some());
1569
1570 let events = events.unwrap();
1571 assert_eq!(events.len(), 1);
1572
1573 match &events[0] {
1574 Event::MoveCursor {
1575 new_position,
1576 new_anchor,
1577 ..
1578 } => {
1579 assert_eq!(*new_position, 5);
1581 assert_eq!(*new_anchor, None); }
1583 _ => panic!("Expected MoveCursor event"),
1584 }
1585 }
1586
1587 #[test]
1588 fn test_action_to_events_move_up_down() {
1589 let config = Config::default();
1590 let (dir_context, _temp) = test_dir_context();
1591 let mut editor = Editor::new(
1592 config,
1593 80,
1594 24,
1595 dir_context,
1596 crate::view::color_support::ColorCapability::TrueColor,
1597 test_filesystem(),
1598 )
1599 .unwrap();
1600
1601 let cursor_id = editor.active_cursors().primary_id();
1603 editor.apply_event_to_active_buffer(&Event::Insert {
1604 position: 0,
1605 text: "line1\nline2\nline3".to_string(),
1606 cursor_id,
1607 });
1608
1609 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1611 cursor_id,
1612 old_position: 0, new_position: 6,
1614 old_anchor: None, new_anchor: None,
1616 old_sticky_column: 0,
1617 new_sticky_column: 0,
1618 });
1619
1620 let events = editor.active_window_mut().action_to_events(Action::MoveUp);
1622 assert!(events.is_some());
1623 let events = events.unwrap();
1624 assert_eq!(events.len(), 1);
1625
1626 match &events[0] {
1627 Event::MoveCursor { new_position, .. } => {
1628 assert_eq!(*new_position, 0); }
1630 _ => panic!("Expected MoveCursor event"),
1631 }
1632 }
1633
1634 #[test]
1635 fn test_action_to_events_insert_newline() {
1636 let config = Config::default();
1637 let (dir_context, _temp) = test_dir_context();
1638 let mut editor = Editor::new(
1639 config,
1640 80,
1641 24,
1642 dir_context,
1643 crate::view::color_support::ColorCapability::TrueColor,
1644 test_filesystem(),
1645 )
1646 .unwrap();
1647
1648 let events = editor
1649 .active_window_mut()
1650 .action_to_events(Action::InsertNewline);
1651 assert!(events.is_some());
1652
1653 let events = events.unwrap();
1654 assert_eq!(events.len(), 1);
1655
1656 match &events[0] {
1657 Event::Insert { text, .. } => {
1658 assert_eq!(text, "\n");
1659 }
1660 _ => panic!("Expected Insert event"),
1661 }
1662 }
1663
1664 #[test]
1665 fn test_action_to_events_unimplemented() {
1666 let config = Config::default();
1667 let (dir_context, _temp) = test_dir_context();
1668 let mut editor = Editor::new(
1669 config,
1670 80,
1671 24,
1672 dir_context,
1673 crate::view::color_support::ColorCapability::TrueColor,
1674 test_filesystem(),
1675 )
1676 .unwrap();
1677
1678 assert!(editor
1680 .active_window_mut()
1681 .action_to_events(Action::Save)
1682 .is_none());
1683 assert!(editor
1684 .active_window_mut()
1685 .action_to_events(Action::Quit)
1686 .is_none());
1687 assert!(editor
1688 .active_window_mut()
1689 .action_to_events(Action::Undo)
1690 .is_none());
1691 }
1692
1693 #[test]
1694 fn test_action_to_events_delete_backward() {
1695 let config = Config::default();
1696 let (dir_context, _temp) = test_dir_context();
1697 let mut editor = Editor::new(
1698 config,
1699 80,
1700 24,
1701 dir_context,
1702 crate::view::color_support::ColorCapability::TrueColor,
1703 test_filesystem(),
1704 )
1705 .unwrap();
1706
1707 let cursor_id = editor.active_cursors().primary_id();
1709 editor.apply_event_to_active_buffer(&Event::Insert {
1710 position: 0,
1711 text: "hello".to_string(),
1712 cursor_id,
1713 });
1714
1715 let events = editor
1716 .active_window_mut()
1717 .action_to_events(Action::DeleteBackward);
1718 assert!(events.is_some());
1719
1720 let events = events.unwrap();
1721 assert_eq!(events.len(), 1);
1722
1723 match &events[0] {
1724 Event::Delete {
1725 range,
1726 deleted_text,
1727 ..
1728 } => {
1729 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1731 }
1732 _ => panic!("Expected Delete event"),
1733 }
1734 }
1735
1736 #[test]
1737 fn test_action_to_events_delete_forward() {
1738 let config = Config::default();
1739 let (dir_context, _temp) = test_dir_context();
1740 let mut editor = Editor::new(
1741 config,
1742 80,
1743 24,
1744 dir_context,
1745 crate::view::color_support::ColorCapability::TrueColor,
1746 test_filesystem(),
1747 )
1748 .unwrap();
1749
1750 let cursor_id = editor.active_cursors().primary_id();
1752 editor.apply_event_to_active_buffer(&Event::Insert {
1753 position: 0,
1754 text: "hello".to_string(),
1755 cursor_id,
1756 });
1757
1758 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1760 cursor_id,
1761 old_position: 0, new_position: 0,
1763 old_anchor: None, new_anchor: None,
1765 old_sticky_column: 0,
1766 new_sticky_column: 0,
1767 });
1768
1769 let events = editor
1770 .active_window_mut()
1771 .action_to_events(Action::DeleteForward);
1772 assert!(events.is_some());
1773
1774 let events = events.unwrap();
1775 assert_eq!(events.len(), 1);
1776
1777 match &events[0] {
1778 Event::Delete {
1779 range,
1780 deleted_text,
1781 ..
1782 } => {
1783 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1785 }
1786 _ => panic!("Expected Delete event"),
1787 }
1788 }
1789
1790 #[test]
1791 fn test_action_to_events_select_right() {
1792 let config = Config::default();
1793 let (dir_context, _temp) = test_dir_context();
1794 let mut editor = Editor::new(
1795 config,
1796 80,
1797 24,
1798 dir_context,
1799 crate::view::color_support::ColorCapability::TrueColor,
1800 test_filesystem(),
1801 )
1802 .unwrap();
1803
1804 let cursor_id = editor.active_cursors().primary_id();
1806 editor.apply_event_to_active_buffer(&Event::Insert {
1807 position: 0,
1808 text: "hello".to_string(),
1809 cursor_id,
1810 });
1811
1812 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1814 cursor_id,
1815 old_position: 0, new_position: 0,
1817 old_anchor: None, new_anchor: None,
1819 old_sticky_column: 0,
1820 new_sticky_column: 0,
1821 });
1822
1823 let events = editor
1824 .active_window_mut()
1825 .action_to_events(Action::SelectRight);
1826 assert!(events.is_some());
1827
1828 let events = events.unwrap();
1829 assert_eq!(events.len(), 1);
1830
1831 match &events[0] {
1832 Event::MoveCursor {
1833 new_position,
1834 new_anchor,
1835 ..
1836 } => {
1837 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1840 _ => panic!("Expected MoveCursor event"),
1841 }
1842 }
1843
1844 #[test]
1845 fn test_action_to_events_select_all() {
1846 let config = Config::default();
1847 let (dir_context, _temp) = test_dir_context();
1848 let mut editor = Editor::new(
1849 config,
1850 80,
1851 24,
1852 dir_context,
1853 crate::view::color_support::ColorCapability::TrueColor,
1854 test_filesystem(),
1855 )
1856 .unwrap();
1857
1858 let cursor_id = editor.active_cursors().primary_id();
1860 editor.apply_event_to_active_buffer(&Event::Insert {
1861 position: 0,
1862 text: "hello world".to_string(),
1863 cursor_id,
1864 });
1865
1866 let events = editor
1867 .active_window_mut()
1868 .action_to_events(Action::SelectAll);
1869 assert!(events.is_some());
1870
1871 let events = events.unwrap();
1872 assert_eq!(events.len(), 1);
1873
1874 match &events[0] {
1875 Event::MoveCursor {
1876 new_position,
1877 new_anchor,
1878 ..
1879 } => {
1880 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1883 _ => panic!("Expected MoveCursor event"),
1884 }
1885 }
1886
1887 #[test]
1888 fn test_action_to_events_document_nav() {
1889 let config = Config::default();
1890 let (dir_context, _temp) = test_dir_context();
1891 let mut editor = Editor::new(
1892 config,
1893 80,
1894 24,
1895 dir_context,
1896 crate::view::color_support::ColorCapability::TrueColor,
1897 test_filesystem(),
1898 )
1899 .unwrap();
1900
1901 let cursor_id = editor.active_cursors().primary_id();
1903 editor.apply_event_to_active_buffer(&Event::Insert {
1904 position: 0,
1905 text: "line1\nline2\nline3".to_string(),
1906 cursor_id,
1907 });
1908
1909 let events = editor
1911 .active_window_mut()
1912 .action_to_events(Action::MoveDocumentStart);
1913 assert!(events.is_some());
1914 let events = events.unwrap();
1915 match &events[0] {
1916 Event::MoveCursor { new_position, .. } => {
1917 assert_eq!(*new_position, 0);
1918 }
1919 _ => panic!("Expected MoveCursor event"),
1920 }
1921
1922 let events = editor
1924 .active_window_mut()
1925 .action_to_events(Action::MoveDocumentEnd);
1926 assert!(events.is_some());
1927 let events = events.unwrap();
1928 match &events[0] {
1929 Event::MoveCursor { new_position, .. } => {
1930 assert_eq!(*new_position, 17); }
1932 _ => panic!("Expected MoveCursor event"),
1933 }
1934 }
1935
1936 #[test]
1937 fn test_action_to_events_remove_secondary_cursors() {
1938 use crate::model::event::CursorId;
1939
1940 let config = Config::default();
1941 let (dir_context, _temp) = test_dir_context();
1942 let mut editor = Editor::new(
1943 config,
1944 80,
1945 24,
1946 dir_context,
1947 crate::view::color_support::ColorCapability::TrueColor,
1948 test_filesystem(),
1949 )
1950 .unwrap();
1951
1952 let cursor_id = editor.active_cursors().primary_id();
1954 editor.apply_event_to_active_buffer(&Event::Insert {
1955 position: 0,
1956 text: "hello world test".to_string(),
1957 cursor_id,
1958 });
1959
1960 editor.apply_event_to_active_buffer(&Event::AddCursor {
1962 cursor_id: CursorId(1),
1963 position: 5,
1964 anchor: None,
1965 });
1966 editor.apply_event_to_active_buffer(&Event::AddCursor {
1967 cursor_id: CursorId(2),
1968 position: 10,
1969 anchor: None,
1970 });
1971
1972 assert_eq!(editor.active_cursors().count(), 3);
1973
1974 let first_id = editor
1976 .active_cursors()
1977 .iter()
1978 .map(|(id, _)| id)
1979 .min_by_key(|id| id.0)
1980 .expect("Should have at least one cursor");
1981
1982 let events = editor
1984 .active_window_mut()
1985 .action_to_events(Action::RemoveSecondaryCursors);
1986 assert!(events.is_some());
1987
1988 let events = events.unwrap();
1989 let remove_cursor_events: Vec<_> = events
1992 .iter()
1993 .filter_map(|e| match e {
1994 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1995 _ => None,
1996 })
1997 .collect();
1998
1999 assert_eq!(remove_cursor_events.len(), 2);
2001
2002 for cursor_id in &remove_cursor_events {
2003 assert_ne!(*cursor_id, first_id);
2005 }
2006 }
2007
2008 #[test]
2009 fn test_action_to_events_scroll() {
2010 let config = Config::default();
2011 let (dir_context, _temp) = test_dir_context();
2012 let mut editor = Editor::new(
2013 config,
2014 80,
2015 24,
2016 dir_context,
2017 crate::view::color_support::ColorCapability::TrueColor,
2018 test_filesystem(),
2019 )
2020 .unwrap();
2021
2022 let events = editor
2024 .active_window_mut()
2025 .action_to_events(Action::ScrollUp);
2026 assert!(events.is_some());
2027 let events = events.unwrap();
2028 assert_eq!(events.len(), 1);
2029 match &events[0] {
2030 Event::Scroll { line_offset } => {
2031 assert_eq!(*line_offset, -1);
2032 }
2033 _ => panic!("Expected Scroll event"),
2034 }
2035
2036 let events = editor
2038 .active_window_mut()
2039 .action_to_events(Action::ScrollDown);
2040 assert!(events.is_some());
2041 let events = events.unwrap();
2042 assert_eq!(events.len(), 1);
2043 match &events[0] {
2044 Event::Scroll { line_offset } => {
2045 assert_eq!(*line_offset, 1);
2046 }
2047 _ => panic!("Expected Scroll event"),
2048 }
2049 }
2050
2051 #[test]
2052 fn test_action_to_events_none() {
2053 let config = Config::default();
2054 let (dir_context, _temp) = test_dir_context();
2055 let mut editor = Editor::new(
2056 config,
2057 80,
2058 24,
2059 dir_context,
2060 crate::view::color_support::ColorCapability::TrueColor,
2061 test_filesystem(),
2062 )
2063 .unwrap();
2064
2065 let events = editor.active_window_mut().action_to_events(Action::None);
2067 assert!(events.is_none());
2068 }
2069
2070 #[test]
2071 fn test_lsp_incremental_insert_generates_correct_range() {
2072 use crate::model::buffer::Buffer;
2075
2076 let buffer = Buffer::from_str_test("hello\nworld");
2077
2078 let position = 0;
2081 let (line, character) = buffer.position_to_lsp_position(position);
2082
2083 assert_eq!(line, 0, "Insertion at start should be line 0");
2084 assert_eq!(character, 0, "Insertion at start should be char 0");
2085
2086 let lsp_pos = Position::new(line as u32, character as u32);
2088 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2089
2090 assert_eq!(lsp_range.start.line, 0);
2091 assert_eq!(lsp_range.start.character, 0);
2092 assert_eq!(lsp_range.end.line, 0);
2093 assert_eq!(lsp_range.end.character, 0);
2094 assert_eq!(
2095 lsp_range.start, lsp_range.end,
2096 "Insert should have zero-width range"
2097 );
2098
2099 let position = 3;
2101 let (line, character) = buffer.position_to_lsp_position(position);
2102
2103 assert_eq!(line, 0);
2104 assert_eq!(character, 3);
2105
2106 let position = 6;
2108 let (line, character) = buffer.position_to_lsp_position(position);
2109
2110 assert_eq!(line, 1, "Position after newline should be line 1");
2111 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2112 }
2113
2114 #[test]
2115 fn test_lsp_incremental_delete_generates_correct_range() {
2116 use crate::model::buffer::Buffer;
2119
2120 let buffer = Buffer::from_str_test("hello\nworld");
2121
2122 let range_start = 1;
2124 let range_end = 5;
2125
2126 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2127 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2128
2129 assert_eq!(start_line, 0);
2130 assert_eq!(start_char, 1);
2131 assert_eq!(end_line, 0);
2132 assert_eq!(end_char, 5);
2133
2134 let lsp_range = LspRange::new(
2135 Position::new(start_line as u32, start_char as u32),
2136 Position::new(end_line as u32, end_char as u32),
2137 );
2138
2139 assert_eq!(lsp_range.start.line, 0);
2140 assert_eq!(lsp_range.start.character, 1);
2141 assert_eq!(lsp_range.end.line, 0);
2142 assert_eq!(lsp_range.end.character, 5);
2143 assert_ne!(
2144 lsp_range.start, lsp_range.end,
2145 "Delete should have non-zero range"
2146 );
2147
2148 let range_start = 4;
2150 let range_end = 8;
2151
2152 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2153 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2154
2155 assert_eq!(start_line, 0, "Delete start on line 0");
2156 assert_eq!(start_char, 4, "Delete start at char 4");
2157 assert_eq!(end_line, 1, "Delete end on line 1");
2158 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2159 }
2160
2161 #[test]
2162 fn test_lsp_incremental_utf16_encoding() {
2163 use crate::model::buffer::Buffer;
2166
2167 let buffer = Buffer::from_str_test("😀hello");
2169
2170 let (line, character) = buffer.position_to_lsp_position(4);
2172
2173 assert_eq!(line, 0);
2174 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2175
2176 let (line, character) = buffer.position_to_lsp_position(9);
2178
2179 assert_eq!(line, 0);
2180 assert_eq!(
2181 character, 7,
2182 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2183 );
2184
2185 let buffer = Buffer::from_str_test("café");
2187
2188 let (line, character) = buffer.position_to_lsp_position(3);
2190
2191 assert_eq!(line, 0);
2192 assert_eq!(character, 3);
2193
2194 let (line, character) = buffer.position_to_lsp_position(5);
2196
2197 assert_eq!(line, 0);
2198 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2199 }
2200
2201 #[test]
2202 fn test_lsp_content_change_event_structure() {
2203 let insert_change = TextDocumentContentChangeEvent {
2207 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2208 range_length: None,
2209 text: "NEW".to_string(),
2210 };
2211
2212 assert!(insert_change.range.is_some());
2213 assert_eq!(insert_change.text, "NEW");
2214 let range = insert_change.range.unwrap();
2215 assert_eq!(
2216 range.start, range.end,
2217 "Insert should have zero-width range"
2218 );
2219
2220 let delete_change = TextDocumentContentChangeEvent {
2222 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2223 range_length: None,
2224 text: String::new(),
2225 };
2226
2227 assert!(delete_change.range.is_some());
2228 assert_eq!(delete_change.text, "");
2229 let range = delete_change.range.unwrap();
2230 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2231 assert_eq!(range.start.line, 0);
2232 assert_eq!(range.start.character, 2);
2233 assert_eq!(range.end.line, 0);
2234 assert_eq!(range.end.character, 7);
2235 }
2236
2237 #[test]
2238 fn test_goto_matching_bracket_forward() {
2239 let config = Config::default();
2240 let (dir_context, _temp) = test_dir_context();
2241 let mut editor = Editor::new(
2242 config,
2243 80,
2244 24,
2245 dir_context,
2246 crate::view::color_support::ColorCapability::TrueColor,
2247 test_filesystem(),
2248 )
2249 .unwrap();
2250
2251 let cursor_id = editor.active_cursors().primary_id();
2253 editor.apply_event_to_active_buffer(&Event::Insert {
2254 position: 0,
2255 text: "fn main() { let x = (1 + 2); }".to_string(),
2256 cursor_id,
2257 });
2258
2259 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2261 cursor_id,
2262 old_position: 31,
2263 new_position: 10,
2264 old_anchor: None,
2265 new_anchor: None,
2266 old_sticky_column: 0,
2267 new_sticky_column: 0,
2268 });
2269
2270 assert_eq!(editor.active_cursors().primary().position, 10);
2271
2272 editor.goto_matching_bracket();
2274
2275 assert_eq!(editor.active_cursors().primary().position, 29);
2280 }
2281
2282 #[test]
2283 fn test_goto_matching_bracket_backward() {
2284 let config = Config::default();
2285 let (dir_context, _temp) = test_dir_context();
2286 let mut editor = Editor::new(
2287 config,
2288 80,
2289 24,
2290 dir_context,
2291 crate::view::color_support::ColorCapability::TrueColor,
2292 test_filesystem(),
2293 )
2294 .unwrap();
2295
2296 let cursor_id = editor.active_cursors().primary_id();
2298 editor.apply_event_to_active_buffer(&Event::Insert {
2299 position: 0,
2300 text: "fn main() { let x = (1 + 2); }".to_string(),
2301 cursor_id,
2302 });
2303
2304 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2306 cursor_id,
2307 old_position: 31,
2308 new_position: 26,
2309 old_anchor: None,
2310 new_anchor: None,
2311 old_sticky_column: 0,
2312 new_sticky_column: 0,
2313 });
2314
2315 editor.goto_matching_bracket();
2317
2318 assert_eq!(editor.active_cursors().primary().position, 20);
2320 }
2321
2322 #[test]
2323 fn test_goto_matching_bracket_nested() {
2324 let config = Config::default();
2325 let (dir_context, _temp) = test_dir_context();
2326 let mut editor = Editor::new(
2327 config,
2328 80,
2329 24,
2330 dir_context,
2331 crate::view::color_support::ColorCapability::TrueColor,
2332 test_filesystem(),
2333 )
2334 .unwrap();
2335
2336 let cursor_id = editor.active_cursors().primary_id();
2338 editor.apply_event_to_active_buffer(&Event::Insert {
2339 position: 0,
2340 text: "{a{b{c}d}e}".to_string(),
2341 cursor_id,
2342 });
2343
2344 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2346 cursor_id,
2347 old_position: 11,
2348 new_position: 0,
2349 old_anchor: None,
2350 new_anchor: None,
2351 old_sticky_column: 0,
2352 new_sticky_column: 0,
2353 });
2354
2355 editor.goto_matching_bracket();
2357
2358 assert_eq!(editor.active_cursors().primary().position, 10);
2360 }
2361
2362 #[test]
2363 fn test_search_case_sensitive() {
2364 let config = Config::default();
2365 let (dir_context, _temp) = test_dir_context();
2366 let mut editor = Editor::new(
2367 config,
2368 80,
2369 24,
2370 dir_context,
2371 crate::view::color_support::ColorCapability::TrueColor,
2372 test_filesystem(),
2373 )
2374 .unwrap();
2375
2376 let cursor_id = editor.active_cursors().primary_id();
2378 editor.apply_event_to_active_buffer(&Event::Insert {
2379 position: 0,
2380 text: "Hello hello HELLO".to_string(),
2381 cursor_id,
2382 });
2383
2384 editor.active_window_mut().search_case_sensitive = false;
2386 editor.perform_search("hello");
2387
2388 let search_state = editor.active_window().search_state.as_ref().unwrap();
2389 assert_eq!(
2390 search_state.matches.len(),
2391 3,
2392 "Should find all 3 matches case-insensitively"
2393 );
2394
2395 editor.active_window_mut().search_case_sensitive = true;
2397 editor.perform_search("hello");
2398
2399 let search_state = editor.active_window().search_state.as_ref().unwrap();
2400 assert_eq!(
2401 search_state.matches.len(),
2402 1,
2403 "Should find only 1 exact match"
2404 );
2405 assert_eq!(
2406 search_state.matches[0], 6,
2407 "Should find 'hello' at position 6"
2408 );
2409 }
2410
2411 #[test]
2412 fn test_search_whole_word() {
2413 let config = Config::default();
2414 let (dir_context, _temp) = test_dir_context();
2415 let mut editor = Editor::new(
2416 config,
2417 80,
2418 24,
2419 dir_context,
2420 crate::view::color_support::ColorCapability::TrueColor,
2421 test_filesystem(),
2422 )
2423 .unwrap();
2424
2425 let cursor_id = editor.active_cursors().primary_id();
2427 editor.apply_event_to_active_buffer(&Event::Insert {
2428 position: 0,
2429 text: "test testing tested attest test".to_string(),
2430 cursor_id,
2431 });
2432
2433 editor.active_window_mut().search_whole_word = false;
2435 editor.active_window_mut().search_case_sensitive = true;
2436 editor.perform_search("test");
2437
2438 let search_state = editor.active_window().search_state.as_ref().unwrap();
2439 assert_eq!(
2440 search_state.matches.len(),
2441 5,
2442 "Should find 'test' in all occurrences"
2443 );
2444
2445 editor.active_window_mut().search_whole_word = true;
2447 editor.perform_search("test");
2448
2449 let search_state = editor.active_window().search_state.as_ref().unwrap();
2450 assert_eq!(
2451 search_state.matches.len(),
2452 2,
2453 "Should find only whole word 'test'"
2454 );
2455 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2456 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2457 }
2458
2459 #[test]
2460 fn test_search_scan_completes_when_capped() {
2461 let config = Config::default();
2467 let (dir_context, _temp) = test_dir_context();
2468 let mut editor = Editor::new(
2469 config,
2470 80,
2471 24,
2472 dir_context,
2473 crate::view::color_support::ColorCapability::TrueColor,
2474 test_filesystem(),
2475 )
2476 .unwrap();
2477
2478 let buffer_id = editor.active_buffer();
2481 let regex = regex::bytes::Regex::new("test").unwrap();
2482 let fake_chunks = vec![
2483 crate::model::buffer::LineScanChunk {
2484 leaf_index: 0,
2485 byte_len: 100,
2486 already_known: true,
2487 },
2488 crate::model::buffer::LineScanChunk {
2489 leaf_index: 1,
2490 byte_len: 100,
2491 already_known: true,
2492 },
2493 ];
2494
2495 let chunked = crate::model::buffer::ChunkedSearchState {
2496 chunks: fake_chunks,
2497 next_chunk: 1, next_doc_offset: 100,
2499 total_bytes: 200,
2500 scanned_bytes: 100,
2501 regex,
2502 matches: vec![
2503 crate::model::buffer::SearchMatch {
2504 byte_offset: 10,
2505 length: 4,
2506 line: 1,
2507 column: 11,
2508 context: String::new(),
2509 },
2510 crate::model::buffer::SearchMatch {
2511 byte_offset: 50,
2512 length: 4,
2513 line: 1,
2514 column: 51,
2515 context: String::new(),
2516 },
2517 ],
2518 overlap_tail: Vec::new(),
2519 overlap_doc_offset: 0,
2520 max_matches: 10_000,
2521 capped: true, query_len: 4,
2523 running_line: 1,
2524 };
2525
2526 editor.active_window_mut().search_scan.start(
2527 buffer_id,
2528 Vec::new(),
2529 chunked,
2530 "test".to_string(),
2531 None,
2532 false,
2533 false,
2534 false,
2535 );
2536
2537 let result = editor.process_search_scan();
2539 assert!(
2540 result,
2541 "process_search_scan should return true (needs render)"
2542 );
2543
2544 assert_eq!(
2546 editor.active_window().search_scan.buffer_id(),
2547 None,
2548 "search_scan should be drained after capped scan completes"
2549 );
2550
2551 let search_state = editor
2553 .active_window()
2554 .search_state
2555 .as_ref()
2556 .expect("search_state should be set after scan finishes");
2557 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2558 assert_eq!(search_state.query, "test");
2559 assert!(
2560 search_state.capped,
2561 "search_state should be marked as capped"
2562 );
2563 }
2564
2565 #[test]
2566 fn test_bookmarks() {
2567 let config = Config::default();
2568 let (dir_context, _temp) = test_dir_context();
2569 let mut editor = Editor::new(
2570 config,
2571 80,
2572 24,
2573 dir_context,
2574 crate::view::color_support::ColorCapability::TrueColor,
2575 test_filesystem(),
2576 )
2577 .unwrap();
2578
2579 let cursor_id = editor.active_cursors().primary_id();
2581 editor.apply_event_to_active_buffer(&Event::Insert {
2582 position: 0,
2583 text: "Line 1\nLine 2\nLine 3".to_string(),
2584 cursor_id,
2585 });
2586
2587 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2589 cursor_id,
2590 old_position: 21,
2591 new_position: 7,
2592 old_anchor: None,
2593 new_anchor: None,
2594 old_sticky_column: 0,
2595 new_sticky_column: 0,
2596 });
2597
2598 editor.active_window_mut().set_bookmark('1');
2600 assert_eq!(
2601 editor
2602 .active_window()
2603 .bookmarks
2604 .get('1')
2605 .map(|b| b.position),
2606 Some(7)
2607 );
2608
2609 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2611 cursor_id,
2612 old_position: 7,
2613 new_position: 14,
2614 old_anchor: None,
2615 new_anchor: None,
2616 old_sticky_column: 0,
2617 new_sticky_column: 0,
2618 });
2619
2620 editor.jump_to_bookmark('1');
2622 assert_eq!(editor.active_cursors().primary().position, 7);
2623
2624 editor.active_window_mut().clear_bookmark('1');
2626 assert_eq!(editor.active_window().bookmarks.get('1'), None);
2627 }
2628
2629 #[test]
2630 fn test_action_enum_new_variants() {
2631 use serde_json::json;
2633
2634 let args = HashMap::new();
2635 assert_eq!(
2636 Action::from_str("smart_home", &args),
2637 Some(Action::SmartHome)
2638 );
2639 assert_eq!(
2640 Action::from_str("dedent_selection", &args),
2641 Some(Action::DedentSelection)
2642 );
2643 assert_eq!(
2644 Action::from_str("toggle_comment", &args),
2645 Some(Action::ToggleComment)
2646 );
2647 assert_eq!(
2648 Action::from_str("goto_matching_bracket", &args),
2649 Some(Action::GoToMatchingBracket)
2650 );
2651 assert_eq!(
2652 Action::from_str("list_bookmarks", &args),
2653 Some(Action::ListBookmarks)
2654 );
2655 assert_eq!(
2656 Action::from_str("toggle_search_case_sensitive", &args),
2657 Some(Action::ToggleSearchCaseSensitive)
2658 );
2659 assert_eq!(
2660 Action::from_str("toggle_search_whole_word", &args),
2661 Some(Action::ToggleSearchWholeWord)
2662 );
2663
2664 let mut args_with_char = HashMap::new();
2666 args_with_char.insert("char".to_string(), json!("5"));
2667 assert_eq!(
2668 Action::from_str("set_bookmark", &args_with_char),
2669 Some(Action::SetBookmark('5'))
2670 );
2671 assert_eq!(
2672 Action::from_str("jump_to_bookmark", &args_with_char),
2673 Some(Action::JumpToBookmark('5'))
2674 );
2675 assert_eq!(
2676 Action::from_str("clear_bookmark", &args_with_char),
2677 Some(Action::ClearBookmark('5'))
2678 );
2679 }
2680
2681 #[test]
2682 fn test_keybinding_new_defaults() {
2683 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2684
2685 let mut config = Config::default();
2689 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2690 let resolver = KeybindingResolver::new(&config);
2691
2692 let event = KeyEvent {
2694 code: KeyCode::Char('/'),
2695 modifiers: KeyModifiers::CONTROL,
2696 kind: KeyEventKind::Press,
2697 state: KeyEventState::NONE,
2698 };
2699 let action = resolver.resolve(&event, KeyContext::Normal);
2700 assert_eq!(action, Action::ToggleComment);
2701
2702 let event = KeyEvent {
2704 code: KeyCode::Char(']'),
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::GoToMatchingBracket);
2711
2712 let event = KeyEvent {
2714 code: KeyCode::Tab,
2715 modifiers: KeyModifiers::SHIFT,
2716 kind: KeyEventKind::Press,
2717 state: KeyEventState::NONE,
2718 };
2719 let action = resolver.resolve(&event, KeyContext::Normal);
2720 assert_eq!(action, Action::DedentSelection);
2721
2722 let event = KeyEvent {
2724 code: KeyCode::Char('g'),
2725 modifiers: KeyModifiers::CONTROL,
2726 kind: KeyEventKind::Press,
2727 state: KeyEventState::NONE,
2728 };
2729 let action = resolver.resolve(&event, KeyContext::Normal);
2730 assert_eq!(action, Action::GotoLine);
2731
2732 let event = KeyEvent {
2734 code: KeyCode::Char('5'),
2735 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2736 kind: KeyEventKind::Press,
2737 state: KeyEventState::NONE,
2738 };
2739 let action = resolver.resolve(&event, KeyContext::Normal);
2740 assert_eq!(action, Action::SetBookmark('5'));
2741
2742 let event = KeyEvent {
2743 code: KeyCode::Char('5'),
2744 modifiers: KeyModifiers::ALT,
2745 kind: KeyEventKind::Press,
2746 state: KeyEventState::NONE,
2747 };
2748 let action = resolver.resolve(&event, KeyContext::Normal);
2749 assert_eq!(action, Action::JumpToBookmark('5'));
2750 }
2751
2752 #[test]
2764 fn test_lsp_rename_didchange_positions_bug() {
2765 use crate::model::buffer::Buffer;
2766
2767 let config = Config::default();
2768 let (dir_context, _temp) = test_dir_context();
2769 let mut editor = Editor::new(
2770 config,
2771 80,
2772 24,
2773 dir_context,
2774 crate::view::color_support::ColorCapability::TrueColor,
2775 test_filesystem(),
2776 )
2777 .unwrap();
2778
2779 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2783 editor.active_state_mut().buffer =
2784 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2785
2786 let cursor_id = editor.active_cursors().primary_id();
2791
2792 let batch = Event::Batch {
2793 events: vec![
2794 Event::Delete {
2796 range: 23..26, deleted_text: "val".to_string(),
2798 cursor_id,
2799 },
2800 Event::Insert {
2801 position: 23,
2802 text: "value".to_string(),
2803 cursor_id,
2804 },
2805 Event::Delete {
2807 range: 7..10, deleted_text: "val".to_string(),
2809 cursor_id,
2810 },
2811 Event::Insert {
2812 position: 7,
2813 text: "value".to_string(),
2814 cursor_id,
2815 },
2816 ],
2817 description: "LSP Rename".to_string(),
2818 };
2819
2820 let lsp_changes_before = editor.active_window().collect_lsp_changes(&batch);
2822
2823 editor.apply_event_to_active_buffer(&batch);
2825
2826 let lsp_changes_after = editor.active_window().collect_lsp_changes(&batch);
2829
2830 let final_content = editor.active_state().buffer.to_string().unwrap();
2832 assert_eq!(
2833 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2834 "Buffer should have 'value' in both places"
2835 );
2836
2837 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2843
2844 let first_delete = &lsp_changes_before[0];
2845 let first_del_range = first_delete.range.unwrap();
2846 assert_eq!(
2847 first_del_range.start.line, 1,
2848 "First delete should be on line 1 (BEFORE)"
2849 );
2850 assert_eq!(
2851 first_del_range.start.character, 4,
2852 "First delete start should be at char 4 (BEFORE)"
2853 );
2854
2855 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2861
2862 let first_delete_after = &lsp_changes_after[0];
2863 let first_del_range_after = first_delete_after.range.unwrap();
2864
2865 eprintln!("BEFORE modification:");
2868 eprintln!(
2869 " Delete at line {}, char {}-{}",
2870 first_del_range.start.line,
2871 first_del_range.start.character,
2872 first_del_range.end.character
2873 );
2874 eprintln!("AFTER modification:");
2875 eprintln!(
2876 " Delete at line {}, char {}-{}",
2877 first_del_range_after.start.line,
2878 first_del_range_after.start.character,
2879 first_del_range_after.end.character
2880 );
2881
2882 assert_ne!(
2900 first_del_range_after.end.character, first_del_range.end.character,
2901 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2902 );
2903
2904 eprintln!("\n=== BUG DEMONSTRATED ===");
2905 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2906 eprintln!("the positions are WRONG because they're calculated from the");
2907 eprintln!("modified buffer, not the original buffer.");
2908 eprintln!("This causes the second rename to fail with 'content modified' error.");
2909 eprintln!("========================\n");
2910 }
2911
2912 #[test]
2913 fn test_lsp_rename_preserves_cursor_position() {
2914 use crate::model::buffer::Buffer;
2915
2916 let config = Config::default();
2917 let (dir_context, _temp) = test_dir_context();
2918 let mut editor = Editor::new(
2919 config,
2920 80,
2921 24,
2922 dir_context,
2923 crate::view::color_support::ColorCapability::TrueColor,
2924 test_filesystem(),
2925 )
2926 .unwrap();
2927
2928 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2932 editor.active_state_mut().buffer =
2933 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2934
2935 let original_cursor_pos = 23;
2937 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2938
2939 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2941 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2942 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2943
2944 let cursor_id = editor.active_cursors().primary_id();
2947 let buffer_id = editor.active_buffer();
2948
2949 let events = vec![
2950 Event::Delete {
2952 range: 23..26, deleted_text: "val".to_string(),
2954 cursor_id,
2955 },
2956 Event::Insert {
2957 position: 23,
2958 text: "value".to_string(),
2959 cursor_id,
2960 },
2961 Event::Delete {
2963 range: 7..10, deleted_text: "val".to_string(),
2965 cursor_id,
2966 },
2967 Event::Insert {
2968 position: 7,
2969 text: "value".to_string(),
2970 cursor_id,
2971 },
2972 ];
2973
2974 editor
2976 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2977 .unwrap();
2978
2979 let final_content = editor.active_state().buffer.to_string().unwrap();
2981 assert_eq!(
2982 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2983 "Buffer should have 'value' in both places"
2984 );
2985
2986 let final_cursor_pos = editor.active_cursors().primary().position;
2994 let expected_cursor_pos = 25; assert_eq!(
2997 final_cursor_pos, expected_cursor_pos,
2998 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2999 Original pos: {}, expected adjustment: +2 for first rename",
3000 expected_cursor_pos, final_cursor_pos, original_cursor_pos
3001 );
3002
3003 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
3005 assert_eq!(
3006 text_at_new_cursor, "value",
3007 "Cursor should be at the start of 'value' after rename"
3008 );
3009 }
3010
3011 #[test]
3012 fn test_lsp_rename_twice_consecutive() {
3013 use crate::model::buffer::Buffer;
3016
3017 let config = Config::default();
3018 let (dir_context, _temp) = test_dir_context();
3019 let mut editor = Editor::new(
3020 config,
3021 80,
3022 24,
3023 dir_context,
3024 crate::view::color_support::ColorCapability::TrueColor,
3025 test_filesystem(),
3026 )
3027 .unwrap();
3028
3029 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
3031 editor.active_state_mut().buffer =
3032 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
3033
3034 let cursor_id = editor.active_cursors().primary_id();
3035 let buffer_id = editor.active_buffer();
3036
3037 let events1 = vec![
3040 Event::Delete {
3042 range: 23..26,
3043 deleted_text: "val".to_string(),
3044 cursor_id,
3045 },
3046 Event::Insert {
3047 position: 23,
3048 text: "value".to_string(),
3049 cursor_id,
3050 },
3051 Event::Delete {
3053 range: 7..10,
3054 deleted_text: "val".to_string(),
3055 cursor_id,
3056 },
3057 Event::Insert {
3058 position: 7,
3059 text: "value".to_string(),
3060 cursor_id,
3061 },
3062 ];
3063
3064 let batch1 = Event::Batch {
3066 events: events1.clone(),
3067 description: "LSP Rename 1".to_string(),
3068 };
3069
3070 let lsp_changes1 = editor.active_window().collect_lsp_changes(&batch1);
3072
3073 assert_eq!(
3075 lsp_changes1.len(),
3076 4,
3077 "First rename should have 4 LSP changes"
3078 );
3079
3080 let first_del = &lsp_changes1[0];
3082 let first_del_range = first_del.range.unwrap();
3083 assert_eq!(first_del_range.start.line, 1, "First delete line");
3084 assert_eq!(
3085 first_del_range.start.character, 4,
3086 "First delete start char"
3087 );
3088 assert_eq!(first_del_range.end.character, 7, "First delete end char");
3089
3090 editor
3092 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
3093 .unwrap();
3094
3095 let after_first = editor.active_state().buffer.to_string().unwrap();
3097 assert_eq!(
3098 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3099 "After first rename"
3100 );
3101
3102 let events2 = vec![
3112 Event::Delete {
3114 range: 25..30,
3115 deleted_text: "value".to_string(),
3116 cursor_id,
3117 },
3118 Event::Insert {
3119 position: 25,
3120 text: "x".to_string(),
3121 cursor_id,
3122 },
3123 Event::Delete {
3125 range: 7..12,
3126 deleted_text: "value".to_string(),
3127 cursor_id,
3128 },
3129 Event::Insert {
3130 position: 7,
3131 text: "x".to_string(),
3132 cursor_id,
3133 },
3134 ];
3135
3136 let batch2 = Event::Batch {
3138 events: events2.clone(),
3139 description: "LSP Rename 2".to_string(),
3140 };
3141
3142 let lsp_changes2 = editor.active_window().collect_lsp_changes(&batch2);
3144
3145 assert_eq!(
3149 lsp_changes2.len(),
3150 4,
3151 "Second rename should have 4 LSP changes"
3152 );
3153
3154 let second_first_del = &lsp_changes2[0];
3156 let second_first_del_range = second_first_del.range.unwrap();
3157 assert_eq!(
3158 second_first_del_range.start.line, 1,
3159 "Second rename first delete should be on line 1"
3160 );
3161 assert_eq!(
3162 second_first_del_range.start.character, 4,
3163 "Second rename first delete start should be at char 4"
3164 );
3165 assert_eq!(
3166 second_first_del_range.end.character, 9,
3167 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3168 );
3169
3170 let second_third_del = &lsp_changes2[2];
3172 let second_third_del_range = second_third_del.range.unwrap();
3173 assert_eq!(
3174 second_third_del_range.start.line, 0,
3175 "Second rename third delete should be on line 0"
3176 );
3177 assert_eq!(
3178 second_third_del_range.start.character, 7,
3179 "Second rename third delete start should be at char 7"
3180 );
3181 assert_eq!(
3182 second_third_del_range.end.character, 12,
3183 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3184 );
3185
3186 editor
3188 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3189 .unwrap();
3190
3191 let after_second = editor.active_state().buffer.to_string().unwrap();
3193 assert_eq!(
3194 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3195 "After second rename"
3196 );
3197 }
3198
3199 #[test]
3200 fn test_ensure_active_tab_visible_static_offset() {
3201 let config = Config::default();
3202 let (dir_context, _temp) = test_dir_context();
3203 let mut editor = Editor::new(
3204 config,
3205 80,
3206 24,
3207 dir_context,
3208 crate::view::color_support::ColorCapability::TrueColor,
3209 test_filesystem(),
3210 )
3211 .unwrap();
3212 let split_id = editor.split_manager().active_split();
3213
3214 let buf1 = editor.new_buffer();
3216 editor
3217 .buffers_mut()
3218 .get_mut(&buf1)
3219 .unwrap()
3220 .buffer
3221 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3222 let buf2 = editor.new_buffer();
3223 editor
3224 .buffers_mut()
3225 .get_mut(&buf2)
3226 .unwrap()
3227 .buffer
3228 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3229 let buf3 = editor.new_buffer();
3230 editor
3231 .buffers_mut()
3232 .get_mut(&buf3)
3233 .unwrap()
3234 .buffer
3235 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3236
3237 {
3238 use crate::view::split::TabTarget;
3239 let view_state = editor.split_view_states_mut().get_mut(&split_id).unwrap();
3240 view_state.open_buffers = vec![
3241 TabTarget::Buffer(buf1),
3242 TabTarget::Buffer(buf2),
3243 TabTarget::Buffer(buf3),
3244 ];
3245 view_state.tab_scroll_offset = 50;
3246 }
3247
3248 editor
3252 .active_window_mut()
3253 .ensure_active_tab_visible(split_id, buf1, 25);
3254 assert_eq!(
3255 editor
3256 .split_view_states()
3257 .get(&split_id)
3258 .unwrap()
3259 .tab_scroll_offset,
3260 0
3261 );
3262
3263 editor
3265 .active_window_mut()
3266 .ensure_active_tab_visible(split_id, buf3, 25);
3267 let view_state = editor.split_view_states().get(&split_id).unwrap();
3268 assert!(view_state.tab_scroll_offset > 0);
3269 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3270 let total_width: usize = buffer_ids
3271 .iter()
3272 .enumerate()
3273 .map(|(idx, id)| {
3274 let state = editor.buffers().get(id).unwrap();
3275 let name_len = state
3276 .buffer
3277 .file_path()
3278 .and_then(|p| p.file_name())
3279 .and_then(|n| n.to_str())
3280 .map(|s| s.chars().count())
3281 .unwrap_or(0);
3282 let tab_width = 2 + name_len;
3283 if idx < buffer_ids.len() - 1 {
3284 tab_width + 1 } else {
3286 tab_width
3287 }
3288 })
3289 .sum();
3290 assert!(view_state.tab_scroll_offset <= total_width);
3291 }
3292}