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 pub(crate) materialize_pending: std::collections::HashSet<fresh_core::WindowId>,
611
612 #[allow(dead_code)]
617 pub(crate) next_window_id: u64,
618
619 command_registry: Arc<RwLock<CommandRegistry>>,
637
638 quick_open_registry: QuickOpenRegistry,
640
641 plugin_manager: Arc<RwLock<PluginManager>>,
649
650 status_bar_token_registry: Mutex<HashMap<String, String>>,
656
657 pub(crate) plugin_schemas:
664 std::sync::Arc<std::sync::RwLock<HashMap<String, serde_json::Value>>>,
665
666 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
682
683 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
689 event_broadcaster: crate::model::control_event::EventBroadcaster,
708
709 #[cfg(feature = "plugins")]
716 pending_plugin_actions: Vec<(
717 String,
718 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
719 )>,
720
721 #[cfg(feature = "plugins")]
723 plugin_render_requested: bool,
724
725 recovery_service: std::sync::Arc<std::sync::Mutex<RecoveryService>>,
755
756 full_redraw_requested: bool,
758
759 suspend_requested: bool,
762
763 time_source: SharedTimeSource,
765
766 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
776 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
778
779 status_log_path: Option<PathBuf>,
781
782 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
788
789 file_watcher_manager: crate::services::file_watcher::FileWatcherManager,
799
800 pub(crate) last_path_change_for_test: Option<(u64, std::path::PathBuf, &'static str)>,
806
807 pub(crate) last_watch_response_for_test: Option<(u64, Result<u64, String>)>,
811
812 pub(crate) preview_window_id: Option<fresh_core::WindowId>,
819
820 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
840
841 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
843
844 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
849
850 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
852
853 color_capability: crate::view::color_support::ColorCapability,
855
856 pub(crate) global_popups: crate::view::popup::PopupManager,
867
868 stdin_stream: stdin_stream::StdinStream,
886
887 pub(crate) previous_cursor_screen_pos: Option<((u16, u16), LeafId)>,
910 pub(crate) cursor_jump_animation: Option<crate::view::animation::AnimationId>,
913
914 pub(crate) pending_vb_animations: Vec<(u64, BufferId, fresh_core::api::PluginAnimationKind)>,
920
921 pub(crate) widget_registry: crate::widgets::WidgetRegistry,
929
930 pub(crate) floating_widget_panel: Option<FloatingWidgetState>,
937}
938
939pub(crate) const FLOATING_PANEL_BUFFER_ID: BufferId = BufferId(usize::MAX);
945
946#[derive(Debug, Clone)]
953pub(crate) struct FloatingWidgetState {
954 pub panel_id: crate::widgets::PanelId,
955 pub width_pct: u8,
956 pub height_pct: u8,
957 pub entries: Vec<fresh_core::text_property::TextPropertyEntry>,
961 pub focus_cursor: Option<crate::widgets::FocusCursor>,
963 pub embeds: Vec<crate::widgets::EmbedRect>,
970 pub overlays: Vec<crate::widgets::OverlayRow>,
977 pub scroll_regions: Vec<crate::widgets::ScrollRegion>,
981 pub scrollbar_tracks: Vec<WidgetScrollbarTrack>,
985 pub scrollbar_mouse: crate::view::ui::scrollbar::ScrollbarMouse,
988 pub scrollbar_drag_key: Option<String>,
990 pub last_inner_rect: Option<ratatui::layout::Rect>,
993}
994
995#[derive(Debug, Clone)]
998pub(crate) struct WidgetScrollbarTrack {
999 pub list_key: String,
1000 pub rect: ratatui::layout::Rect,
1001 pub total: usize,
1002 pub visible: usize,
1003 pub scroll: usize,
1004}
1005
1006#[derive(Debug, Clone)]
1008pub struct PendingFileOpen {
1009 pub path: PathBuf,
1011 pub line: Option<usize>,
1013 pub column: Option<usize>,
1015 pub end_line: Option<usize>,
1017 pub end_column: Option<usize>,
1019 pub message: Option<String>,
1021 pub wait_id: Option<u64>,
1023}
1024
1025impl Editor {
1026 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
1028 let trimmed = input.trim();
1029
1030 if trimmed.is_empty() {
1031 self.ansi_background = None;
1032 self.ansi_background_path = None;
1033 self.set_status_message(t!("status.background_cleared").to_string());
1034 return Ok(());
1035 }
1036
1037 let input_path = Path::new(trimmed);
1038 let resolved = if input_path.is_absolute() {
1039 input_path.to_path_buf()
1040 } else {
1041 self.working_dir().join(input_path)
1042 };
1043
1044 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
1045
1046 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
1047
1048 self.ansi_background = Some(parsed);
1049 self.ansi_background_path = Some(canonical.clone());
1050 self.set_status_message(
1051 t!(
1052 "view.background_set",
1053 path = canonical.display().to_string()
1054 )
1055 .to_string(),
1056 );
1057
1058 Ok(())
1059 }
1060
1061 #[doc(hidden)]
1065 pub fn buffer_count_for_tests(&self) -> usize {
1066 self.windows
1067 .get(&self.active_window)
1068 .map(|w| &w.buffers)
1069 .expect("active window present")
1070 .len()
1071 }
1072
1073 #[doc(hidden)]
1077 pub fn all_buffer_ids_for_tests(&self) -> Vec<BufferId> {
1078 let mut ids: Vec<BufferId> = self
1079 .windows
1080 .get(&self.active_window)
1081 .map(|w| &w.buffers)
1082 .expect("active window present")
1083 .ids();
1084 ids.sort_by_key(|id| id.0);
1085 ids
1086 }
1087
1088 pub fn active_state(&self) -> &EditorState {
1090 self.windows
1091 .get(&self.active_window)
1092 .map(|w| &w.buffers)
1093 .expect("active window present")
1094 .get(&self.active_buffer())
1095 .unwrap()
1096 }
1097
1098 pub fn active_state_mut(&mut self) -> &mut EditorState {
1100 let __buffer_id = self.active_buffer();
1101 self.windows
1102 .get_mut(&self.active_window)
1103 .map(|w| &mut w.buffers)
1104 .expect("active window present")
1105 .get_mut(&__buffer_id)
1106 .unwrap()
1107 }
1108
1109 pub fn active_cursors(&self) -> &Cursors {
1113 let split_id = self.effective_active_split();
1114 &self
1115 .windows
1116 .get(&self.active_window)
1117 .and_then(|w| w.buffers.splits())
1118 .map(|(_, vs)| vs)
1119 .expect("active window must have a populated split layout")
1120 .get(&split_id)
1121 .unwrap()
1122 .cursors
1123 }
1124
1125 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1127 let split_id = self.effective_active_split();
1128 &mut self
1129 .windows
1130 .get_mut(&self.active_window)
1131 .and_then(|w| w.split_view_states_mut())
1132 .expect("active window must have a populated split layout")
1133 .get_mut(&split_id)
1134 .unwrap()
1135 .cursors
1136 }
1137
1138 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1140 self.active_window_mut().completion_items = Some(items);
1141 }
1142
1143 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1145 let active_split = self
1146 .windows
1147 .get(&self.active_window)
1148 .and_then(|w| w.buffers.splits())
1149 .map(|(mgr, _)| mgr)
1150 .expect("active window must have a populated split layout")
1151 .active_split();
1152 &self
1153 .windows
1154 .get(&self.active_window)
1155 .and_then(|w| w.buffers.splits())
1156 .map(|(_, vs)| vs)
1157 .expect("active window must have a populated split layout")
1158 .get(&active_split)
1159 .unwrap()
1160 .viewport
1161 }
1162
1163 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1165 let active_split = self
1166 .windows
1167 .get(&self.active_window)
1168 .and_then(|w| w.buffers.splits())
1169 .map(|(mgr, _)| mgr)
1170 .expect("active window must have a populated split layout")
1171 .active_split();
1172 &mut self
1173 .windows
1174 .get_mut(&self.active_window)
1175 .and_then(|w| w.split_view_states_mut())
1176 .expect("active window must have a populated split layout")
1177 .get_mut(&active_split)
1178 .unwrap()
1179 .viewport
1180 }
1181
1182 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1184 if let Some(composite) = self.active_window().composite_buffers.get(&buffer_id) {
1186 return composite.name.clone();
1187 }
1188
1189 self.active_window()
1190 .buffer_metadata
1191 .get(&buffer_id)
1192 .map(|m| m.display_name.clone())
1193 .or_else(|| {
1194 self.windows
1195 .get(&self.active_window)
1196 .map(|w| &w.buffers)
1197 .expect("active window present")
1198 .get(&buffer_id)
1199 .and_then(|state| {
1200 state
1201 .buffer
1202 .file_path()
1203 .and_then(|p| p.file_name())
1204 .and_then(|n| n.to_str())
1205 .map(|s| s.to_string())
1206 })
1207 })
1208 .unwrap_or_else(|| "[No Name]".to_string())
1209 }
1210
1211 pub fn active_event_log(&self) -> &EventLog {
1221 self.active_window()
1222 .event_logs
1223 .get(&self.active_buffer())
1224 .unwrap()
1225 }
1226
1227 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1229 let buffer_id = self.active_buffer();
1230 self.active_window_mut()
1231 .event_logs
1232 .get_mut(&buffer_id)
1233 .unwrap()
1234 }
1235
1236 pub fn register_status_bar_element(
1240 &self,
1241 plugin_name: &str,
1242 token_name: &str,
1243 title: &str,
1244 ) -> Result<(), String> {
1245 if plugin_name.is_empty() {
1246 return Err("Plugin name cannot be empty".to_string());
1247 }
1248 if token_name.is_empty() {
1249 return Err("Token name cannot be empty".to_string());
1250 }
1251
1252 let key = format!("{}:{}", plugin_name, token_name);
1253 let mut registry = self.status_bar_token_registry.lock().unwrap();
1254
1255 if registry.contains_key(&key) {
1256 return Err(format!("Token '{}' already registered", key));
1257 }
1258
1259 registry.insert(key, title.to_string());
1260 Ok(())
1261 }
1262
1263 pub fn set_status_bar_value(
1268 &mut self,
1269 buffer_id: BufferId,
1270 key: &str,
1271 value: String,
1272 ) -> Result<(), String> {
1273 for window in self.windows.values_mut() {
1274 if window.buffers.contains_key(&buffer_id) {
1275 window
1276 .status_bar_values
1277 .entry(buffer_id)
1278 .or_default()
1279 .insert(key.to_string(), value);
1280 return Ok(());
1281 }
1282 }
1283 Err(format!("Buffer {:?} not found", buffer_id))
1284 }
1285
1286 pub fn get_status_bar_elements(&self) -> Vec<(String, String)> {
1289 self.status_bar_token_registry
1290 .lock()
1291 .unwrap()
1292 .iter()
1293 .map(|(k, title)| (format!("{{{}}}", k), title.clone()))
1294 .collect()
1295 }
1296
1297 pub fn get_status_bar_element_values(&self, buffer_id: BufferId) -> HashMap<String, String> {
1299 for window in self.windows.values() {
1300 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1301 return values.clone();
1302 }
1303 }
1304 HashMap::new()
1305 }
1306
1307 pub fn current_status_bar_value(&self, buffer_id: BufferId, key: &str) -> Option<&str> {
1311 for window in self.windows.values() {
1312 if let Some(values) = window.status_bar_values.get(&buffer_id) {
1313 if let Some(v) = values.get(key) {
1314 return Some(v.as_str());
1315 }
1316 return None;
1317 }
1318 }
1319 None
1320 }
1321
1322 fn remove_plugin_status_bar_elements(&mut self, plugin_name: &str) {
1325 let prefix = format!("{}:", plugin_name);
1326 self.status_bar_token_registry
1327 .lock()
1328 .unwrap()
1329 .retain(|k, _| !k.starts_with(&prefix));
1330 for window in self.windows.values_mut() {
1331 for values in window.status_bar_values.values_mut() {
1332 values.retain(|k, _| !k.starts_with(&prefix));
1333 }
1334 }
1335 }
1336}
1337
1338fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1347 use crossterm::event::{KeyCode, KeyModifiers};
1348
1349 let mut modifiers = KeyModifiers::NONE;
1350 let mut remaining = key_str;
1351
1352 loop {
1354 if remaining.starts_with("C-") {
1355 modifiers |= KeyModifiers::CONTROL;
1356 remaining = &remaining[2..];
1357 } else if remaining.starts_with("M-") {
1358 modifiers |= KeyModifiers::ALT;
1359 remaining = &remaining[2..];
1360 } else if remaining.starts_with("S-") {
1361 modifiers |= KeyModifiers::SHIFT;
1362 remaining = &remaining[2..];
1363 } else {
1364 break;
1365 }
1366 }
1367
1368 let upper = remaining.to_uppercase();
1371 let code = match upper.as_str() {
1372 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1373 "TAB" => KeyCode::Tab,
1374 "BACKTAB" => KeyCode::BackTab,
1375 "ESC" | "ESCAPE" => KeyCode::Esc,
1376 "SPC" | "SPACE" => KeyCode::Char(' '),
1377 "DEL" | "DELETE" => KeyCode::Delete,
1378 "BS" | "BACKSPACE" => KeyCode::Backspace,
1379 "UP" => KeyCode::Up,
1380 "DOWN" => KeyCode::Down,
1381 "LEFT" => KeyCode::Left,
1382 "RIGHT" => KeyCode::Right,
1383 "HOME" => KeyCode::Home,
1384 "END" => KeyCode::End,
1385 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1386 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1387 s if s.starts_with('F') && s.len() > 1 => {
1388 if let Ok(n) = s[1..].parse::<u8>() {
1390 KeyCode::F(n)
1391 } else {
1392 return None;
1393 }
1394 }
1395 _ if remaining.len() == 1 => {
1396 let c = remaining.chars().next()?;
1399 if c.is_ascii_uppercase() {
1400 modifiers |= KeyModifiers::SHIFT;
1401 }
1402 KeyCode::Char(c.to_ascii_lowercase())
1403 }
1404 _ => return None,
1405 };
1406
1407 if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
1412 return Some((KeyCode::BackTab, modifiers.difference(KeyModifiers::SHIFT)));
1413 }
1414
1415 Some((code, modifiers))
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420 use super::*;
1421 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1422 use tempfile::TempDir;
1423
1424 fn test_dir_context() -> (DirectoryContext, TempDir) {
1426 let temp_dir = TempDir::new().unwrap();
1427 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1428 (dir_context, temp_dir)
1429 }
1430
1431 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1433 Arc::new(crate::model::filesystem::StdFileSystem)
1434 }
1435
1436 #[test]
1437 fn parse_key_string_shift_tab_normalizes_to_backtab() {
1438 use crossterm::event::{KeyCode, KeyModifiers};
1439 assert_eq!(
1444 parse_key_string("S-Tab"),
1445 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1446 );
1447 assert_eq!(
1448 parse_key_string("BackTab"),
1449 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1450 );
1451 assert_eq!(
1453 parse_key_string("Tab"),
1454 Some((KeyCode::Tab, KeyModifiers::NONE)),
1455 );
1456 }
1457
1458 #[test]
1459 fn test_editor_new() {
1460 let config = Config::default();
1461 let (dir_context, _temp) = test_dir_context();
1462 let editor = Editor::new(
1463 config,
1464 80,
1465 24,
1466 dir_context,
1467 crate::view::color_support::ColorCapability::TrueColor,
1468 test_filesystem(),
1469 )
1470 .unwrap();
1471
1472 assert_eq!(editor.buffers().len(), 1);
1473 assert!(!editor.should_quit());
1474 }
1475
1476 #[test]
1477 fn test_new_buffer() {
1478 let config = Config::default();
1479 let (dir_context, _temp) = test_dir_context();
1480 let mut editor = Editor::new(
1481 config,
1482 80,
1483 24,
1484 dir_context,
1485 crate::view::color_support::ColorCapability::TrueColor,
1486 test_filesystem(),
1487 )
1488 .unwrap();
1489
1490 let id = editor.new_buffer();
1491 assert_eq!(editor.buffers().len(), 2);
1492 assert_eq!(editor.active_buffer(), id);
1493 }
1494
1495 #[test]
1496 #[ignore]
1497 fn test_clipboard() {
1498 let config = Config::default();
1499 let (dir_context, _temp) = test_dir_context();
1500 let mut editor = Editor::new(
1501 config,
1502 80,
1503 24,
1504 dir_context,
1505 crate::view::color_support::ColorCapability::TrueColor,
1506 test_filesystem(),
1507 )
1508 .unwrap();
1509
1510 editor.clipboard.set_internal("test".to_string());
1512
1513 editor.paste();
1515
1516 let content = editor.active_state().buffer.to_string().unwrap();
1517 assert_eq!(content, "test");
1518 }
1519
1520 #[test]
1521 fn test_action_to_events_insert_char() {
1522 let config = Config::default();
1523 let (dir_context, _temp) = test_dir_context();
1524 let mut editor = Editor::new(
1525 config,
1526 80,
1527 24,
1528 dir_context,
1529 crate::view::color_support::ColorCapability::TrueColor,
1530 test_filesystem(),
1531 )
1532 .unwrap();
1533
1534 let events = editor
1535 .active_window_mut()
1536 .action_to_events(Action::InsertChar('a'));
1537 assert!(events.is_some());
1538
1539 let events = events.unwrap();
1540 assert_eq!(events.len(), 1);
1541
1542 match &events[0] {
1543 Event::Insert { position, text, .. } => {
1544 assert_eq!(*position, 0);
1545 assert_eq!(text, "a");
1546 }
1547 _ => panic!("Expected Insert event"),
1548 }
1549 }
1550
1551 #[test]
1552 fn test_action_to_events_move_right() {
1553 let config = Config::default();
1554 let (dir_context, _temp) = test_dir_context();
1555 let mut editor = Editor::new(
1556 config,
1557 80,
1558 24,
1559 dir_context,
1560 crate::view::color_support::ColorCapability::TrueColor,
1561 test_filesystem(),
1562 )
1563 .unwrap();
1564
1565 let cursor_id = editor.active_cursors().primary_id();
1567 editor.apply_event_to_active_buffer(&Event::Insert {
1568 position: 0,
1569 text: "hello".to_string(),
1570 cursor_id,
1571 });
1572
1573 let events = editor
1574 .active_window_mut()
1575 .action_to_events(Action::MoveRight);
1576 assert!(events.is_some());
1577
1578 let events = events.unwrap();
1579 assert_eq!(events.len(), 1);
1580
1581 match &events[0] {
1582 Event::MoveCursor {
1583 new_position,
1584 new_anchor,
1585 ..
1586 } => {
1587 assert_eq!(*new_position, 5);
1589 assert_eq!(*new_anchor, None); }
1591 _ => panic!("Expected MoveCursor event"),
1592 }
1593 }
1594
1595 #[test]
1596 fn test_action_to_events_move_up_down() {
1597 let config = Config::default();
1598 let (dir_context, _temp) = test_dir_context();
1599 let mut editor = Editor::new(
1600 config,
1601 80,
1602 24,
1603 dir_context,
1604 crate::view::color_support::ColorCapability::TrueColor,
1605 test_filesystem(),
1606 )
1607 .unwrap();
1608
1609 let cursor_id = editor.active_cursors().primary_id();
1611 editor.apply_event_to_active_buffer(&Event::Insert {
1612 position: 0,
1613 text: "line1\nline2\nline3".to_string(),
1614 cursor_id,
1615 });
1616
1617 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1619 cursor_id,
1620 old_position: 0, new_position: 6,
1622 old_anchor: None, new_anchor: None,
1624 old_sticky_column: 0,
1625 new_sticky_column: 0,
1626 });
1627
1628 let events = editor.active_window_mut().action_to_events(Action::MoveUp);
1630 assert!(events.is_some());
1631 let events = events.unwrap();
1632 assert_eq!(events.len(), 1);
1633
1634 match &events[0] {
1635 Event::MoveCursor { new_position, .. } => {
1636 assert_eq!(*new_position, 0); }
1638 _ => panic!("Expected MoveCursor event"),
1639 }
1640 }
1641
1642 #[test]
1643 fn test_action_to_events_insert_newline() {
1644 let config = Config::default();
1645 let (dir_context, _temp) = test_dir_context();
1646 let mut editor = Editor::new(
1647 config,
1648 80,
1649 24,
1650 dir_context,
1651 crate::view::color_support::ColorCapability::TrueColor,
1652 test_filesystem(),
1653 )
1654 .unwrap();
1655
1656 let events = editor
1657 .active_window_mut()
1658 .action_to_events(Action::InsertNewline);
1659 assert!(events.is_some());
1660
1661 let events = events.unwrap();
1662 assert_eq!(events.len(), 1);
1663
1664 match &events[0] {
1665 Event::Insert { text, .. } => {
1666 assert_eq!(text, "\n");
1667 }
1668 _ => panic!("Expected Insert event"),
1669 }
1670 }
1671
1672 #[test]
1673 fn test_action_to_events_unimplemented() {
1674 let config = Config::default();
1675 let (dir_context, _temp) = test_dir_context();
1676 let mut editor = Editor::new(
1677 config,
1678 80,
1679 24,
1680 dir_context,
1681 crate::view::color_support::ColorCapability::TrueColor,
1682 test_filesystem(),
1683 )
1684 .unwrap();
1685
1686 assert!(editor
1688 .active_window_mut()
1689 .action_to_events(Action::Save)
1690 .is_none());
1691 assert!(editor
1692 .active_window_mut()
1693 .action_to_events(Action::Quit)
1694 .is_none());
1695 assert!(editor
1696 .active_window_mut()
1697 .action_to_events(Action::Undo)
1698 .is_none());
1699 }
1700
1701 #[test]
1702 fn test_action_to_events_delete_backward() {
1703 let config = Config::default();
1704 let (dir_context, _temp) = test_dir_context();
1705 let mut editor = Editor::new(
1706 config,
1707 80,
1708 24,
1709 dir_context,
1710 crate::view::color_support::ColorCapability::TrueColor,
1711 test_filesystem(),
1712 )
1713 .unwrap();
1714
1715 let cursor_id = editor.active_cursors().primary_id();
1717 editor.apply_event_to_active_buffer(&Event::Insert {
1718 position: 0,
1719 text: "hello".to_string(),
1720 cursor_id,
1721 });
1722
1723 let events = editor
1724 .active_window_mut()
1725 .action_to_events(Action::DeleteBackward);
1726 assert!(events.is_some());
1727
1728 let events = events.unwrap();
1729 assert_eq!(events.len(), 1);
1730
1731 match &events[0] {
1732 Event::Delete {
1733 range,
1734 deleted_text,
1735 ..
1736 } => {
1737 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1739 }
1740 _ => panic!("Expected Delete event"),
1741 }
1742 }
1743
1744 #[test]
1745 fn test_action_to_events_delete_forward() {
1746 let config = Config::default();
1747 let (dir_context, _temp) = test_dir_context();
1748 let mut editor = Editor::new(
1749 config,
1750 80,
1751 24,
1752 dir_context,
1753 crate::view::color_support::ColorCapability::TrueColor,
1754 test_filesystem(),
1755 )
1756 .unwrap();
1757
1758 let cursor_id = editor.active_cursors().primary_id();
1760 editor.apply_event_to_active_buffer(&Event::Insert {
1761 position: 0,
1762 text: "hello".to_string(),
1763 cursor_id,
1764 });
1765
1766 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1768 cursor_id,
1769 old_position: 0, new_position: 0,
1771 old_anchor: None, new_anchor: None,
1773 old_sticky_column: 0,
1774 new_sticky_column: 0,
1775 });
1776
1777 let events = editor
1778 .active_window_mut()
1779 .action_to_events(Action::DeleteForward);
1780 assert!(events.is_some());
1781
1782 let events = events.unwrap();
1783 assert_eq!(events.len(), 1);
1784
1785 match &events[0] {
1786 Event::Delete {
1787 range,
1788 deleted_text,
1789 ..
1790 } => {
1791 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1793 }
1794 _ => panic!("Expected Delete event"),
1795 }
1796 }
1797
1798 #[test]
1799 fn test_action_to_events_select_right() {
1800 let config = Config::default();
1801 let (dir_context, _temp) = test_dir_context();
1802 let mut editor = Editor::new(
1803 config,
1804 80,
1805 24,
1806 dir_context,
1807 crate::view::color_support::ColorCapability::TrueColor,
1808 test_filesystem(),
1809 )
1810 .unwrap();
1811
1812 let cursor_id = editor.active_cursors().primary_id();
1814 editor.apply_event_to_active_buffer(&Event::Insert {
1815 position: 0,
1816 text: "hello".to_string(),
1817 cursor_id,
1818 });
1819
1820 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1822 cursor_id,
1823 old_position: 0, new_position: 0,
1825 old_anchor: None, new_anchor: None,
1827 old_sticky_column: 0,
1828 new_sticky_column: 0,
1829 });
1830
1831 let events = editor
1832 .active_window_mut()
1833 .action_to_events(Action::SelectRight);
1834 assert!(events.is_some());
1835
1836 let events = events.unwrap();
1837 assert_eq!(events.len(), 1);
1838
1839 match &events[0] {
1840 Event::MoveCursor {
1841 new_position,
1842 new_anchor,
1843 ..
1844 } => {
1845 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1848 _ => panic!("Expected MoveCursor event"),
1849 }
1850 }
1851
1852 #[test]
1853 fn test_action_to_events_select_all() {
1854 let config = Config::default();
1855 let (dir_context, _temp) = test_dir_context();
1856 let mut editor = Editor::new(
1857 config,
1858 80,
1859 24,
1860 dir_context,
1861 crate::view::color_support::ColorCapability::TrueColor,
1862 test_filesystem(),
1863 )
1864 .unwrap();
1865
1866 let cursor_id = editor.active_cursors().primary_id();
1868 editor.apply_event_to_active_buffer(&Event::Insert {
1869 position: 0,
1870 text: "hello world".to_string(),
1871 cursor_id,
1872 });
1873
1874 let events = editor
1875 .active_window_mut()
1876 .action_to_events(Action::SelectAll);
1877 assert!(events.is_some());
1878
1879 let events = events.unwrap();
1880 assert_eq!(events.len(), 1);
1881
1882 match &events[0] {
1883 Event::MoveCursor {
1884 new_position,
1885 new_anchor,
1886 ..
1887 } => {
1888 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1891 _ => panic!("Expected MoveCursor event"),
1892 }
1893 }
1894
1895 #[test]
1896 fn test_action_to_events_document_nav() {
1897 let config = Config::default();
1898 let (dir_context, _temp) = test_dir_context();
1899 let mut editor = Editor::new(
1900 config,
1901 80,
1902 24,
1903 dir_context,
1904 crate::view::color_support::ColorCapability::TrueColor,
1905 test_filesystem(),
1906 )
1907 .unwrap();
1908
1909 let cursor_id = editor.active_cursors().primary_id();
1911 editor.apply_event_to_active_buffer(&Event::Insert {
1912 position: 0,
1913 text: "line1\nline2\nline3".to_string(),
1914 cursor_id,
1915 });
1916
1917 let events = editor
1919 .active_window_mut()
1920 .action_to_events(Action::MoveDocumentStart);
1921 assert!(events.is_some());
1922 let events = events.unwrap();
1923 match &events[0] {
1924 Event::MoveCursor { new_position, .. } => {
1925 assert_eq!(*new_position, 0);
1926 }
1927 _ => panic!("Expected MoveCursor event"),
1928 }
1929
1930 let events = editor
1932 .active_window_mut()
1933 .action_to_events(Action::MoveDocumentEnd);
1934 assert!(events.is_some());
1935 let events = events.unwrap();
1936 match &events[0] {
1937 Event::MoveCursor { new_position, .. } => {
1938 assert_eq!(*new_position, 17); }
1940 _ => panic!("Expected MoveCursor event"),
1941 }
1942 }
1943
1944 #[test]
1945 fn test_action_to_events_remove_secondary_cursors() {
1946 use crate::model::event::CursorId;
1947
1948 let config = Config::default();
1949 let (dir_context, _temp) = test_dir_context();
1950 let mut editor = Editor::new(
1951 config,
1952 80,
1953 24,
1954 dir_context,
1955 crate::view::color_support::ColorCapability::TrueColor,
1956 test_filesystem(),
1957 )
1958 .unwrap();
1959
1960 let cursor_id = editor.active_cursors().primary_id();
1962 editor.apply_event_to_active_buffer(&Event::Insert {
1963 position: 0,
1964 text: "hello world test".to_string(),
1965 cursor_id,
1966 });
1967
1968 editor.apply_event_to_active_buffer(&Event::AddCursor {
1970 cursor_id: CursorId(1),
1971 position: 5,
1972 anchor: None,
1973 });
1974 editor.apply_event_to_active_buffer(&Event::AddCursor {
1975 cursor_id: CursorId(2),
1976 position: 10,
1977 anchor: None,
1978 });
1979
1980 assert_eq!(editor.active_cursors().count(), 3);
1981
1982 let first_id = editor
1984 .active_cursors()
1985 .iter()
1986 .map(|(id, _)| id)
1987 .min_by_key(|id| id.0)
1988 .expect("Should have at least one cursor");
1989
1990 let events = editor
1992 .active_window_mut()
1993 .action_to_events(Action::RemoveSecondaryCursors);
1994 assert!(events.is_some());
1995
1996 let events = events.unwrap();
1997 let remove_cursor_events: Vec<_> = events
2000 .iter()
2001 .filter_map(|e| match e {
2002 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
2003 _ => None,
2004 })
2005 .collect();
2006
2007 assert_eq!(remove_cursor_events.len(), 2);
2009
2010 for cursor_id in &remove_cursor_events {
2011 assert_ne!(*cursor_id, first_id);
2013 }
2014 }
2015
2016 #[test]
2017 fn test_action_to_events_scroll() {
2018 let config = Config::default();
2019 let (dir_context, _temp) = test_dir_context();
2020 let mut editor = Editor::new(
2021 config,
2022 80,
2023 24,
2024 dir_context,
2025 crate::view::color_support::ColorCapability::TrueColor,
2026 test_filesystem(),
2027 )
2028 .unwrap();
2029
2030 let events = editor
2032 .active_window_mut()
2033 .action_to_events(Action::ScrollUp);
2034 assert!(events.is_some());
2035 let events = events.unwrap();
2036 assert_eq!(events.len(), 1);
2037 match &events[0] {
2038 Event::Scroll { line_offset } => {
2039 assert_eq!(*line_offset, -1);
2040 }
2041 _ => panic!("Expected Scroll event"),
2042 }
2043
2044 let events = editor
2046 .active_window_mut()
2047 .action_to_events(Action::ScrollDown);
2048 assert!(events.is_some());
2049 let events = events.unwrap();
2050 assert_eq!(events.len(), 1);
2051 match &events[0] {
2052 Event::Scroll { line_offset } => {
2053 assert_eq!(*line_offset, 1);
2054 }
2055 _ => panic!("Expected Scroll event"),
2056 }
2057 }
2058
2059 #[test]
2060 fn test_action_to_events_none() {
2061 let config = Config::default();
2062 let (dir_context, _temp) = test_dir_context();
2063 let mut editor = Editor::new(
2064 config,
2065 80,
2066 24,
2067 dir_context,
2068 crate::view::color_support::ColorCapability::TrueColor,
2069 test_filesystem(),
2070 )
2071 .unwrap();
2072
2073 let events = editor.active_window_mut().action_to_events(Action::None);
2075 assert!(events.is_none());
2076 }
2077
2078 #[test]
2079 fn test_lsp_incremental_insert_generates_correct_range() {
2080 use crate::model::buffer::Buffer;
2083
2084 let buffer = Buffer::from_str_test("hello\nworld");
2085
2086 let position = 0;
2089 let (line, character) = buffer.position_to_lsp_position(position);
2090
2091 assert_eq!(line, 0, "Insertion at start should be line 0");
2092 assert_eq!(character, 0, "Insertion at start should be char 0");
2093
2094 let lsp_pos = Position::new(line as u32, character as u32);
2096 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
2097
2098 assert_eq!(lsp_range.start.line, 0);
2099 assert_eq!(lsp_range.start.character, 0);
2100 assert_eq!(lsp_range.end.line, 0);
2101 assert_eq!(lsp_range.end.character, 0);
2102 assert_eq!(
2103 lsp_range.start, lsp_range.end,
2104 "Insert should have zero-width range"
2105 );
2106
2107 let position = 3;
2109 let (line, character) = buffer.position_to_lsp_position(position);
2110
2111 assert_eq!(line, 0);
2112 assert_eq!(character, 3);
2113
2114 let position = 6;
2116 let (line, character) = buffer.position_to_lsp_position(position);
2117
2118 assert_eq!(line, 1, "Position after newline should be line 1");
2119 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
2120 }
2121
2122 #[test]
2123 fn test_lsp_incremental_delete_generates_correct_range() {
2124 use crate::model::buffer::Buffer;
2127
2128 let buffer = Buffer::from_str_test("hello\nworld");
2129
2130 let range_start = 1;
2132 let range_end = 5;
2133
2134 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2135 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2136
2137 assert_eq!(start_line, 0);
2138 assert_eq!(start_char, 1);
2139 assert_eq!(end_line, 0);
2140 assert_eq!(end_char, 5);
2141
2142 let lsp_range = LspRange::new(
2143 Position::new(start_line as u32, start_char as u32),
2144 Position::new(end_line as u32, end_char as u32),
2145 );
2146
2147 assert_eq!(lsp_range.start.line, 0);
2148 assert_eq!(lsp_range.start.character, 1);
2149 assert_eq!(lsp_range.end.line, 0);
2150 assert_eq!(lsp_range.end.character, 5);
2151 assert_ne!(
2152 lsp_range.start, lsp_range.end,
2153 "Delete should have non-zero range"
2154 );
2155
2156 let range_start = 4;
2158 let range_end = 8;
2159
2160 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
2161 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
2162
2163 assert_eq!(start_line, 0, "Delete start on line 0");
2164 assert_eq!(start_char, 4, "Delete start at char 4");
2165 assert_eq!(end_line, 1, "Delete end on line 1");
2166 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2167 }
2168
2169 #[test]
2170 fn test_lsp_incremental_utf16_encoding() {
2171 use crate::model::buffer::Buffer;
2174
2175 let buffer = Buffer::from_str_test("😀hello");
2177
2178 let (line, character) = buffer.position_to_lsp_position(4);
2180
2181 assert_eq!(line, 0);
2182 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2183
2184 let (line, character) = buffer.position_to_lsp_position(9);
2186
2187 assert_eq!(line, 0);
2188 assert_eq!(
2189 character, 7,
2190 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2191 );
2192
2193 let buffer = Buffer::from_str_test("café");
2195
2196 let (line, character) = buffer.position_to_lsp_position(3);
2198
2199 assert_eq!(line, 0);
2200 assert_eq!(character, 3);
2201
2202 let (line, character) = buffer.position_to_lsp_position(5);
2204
2205 assert_eq!(line, 0);
2206 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2207 }
2208
2209 #[test]
2210 fn test_lsp_content_change_event_structure() {
2211 let insert_change = TextDocumentContentChangeEvent {
2215 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2216 range_length: None,
2217 text: "NEW".to_string(),
2218 };
2219
2220 assert!(insert_change.range.is_some());
2221 assert_eq!(insert_change.text, "NEW");
2222 let range = insert_change.range.unwrap();
2223 assert_eq!(
2224 range.start, range.end,
2225 "Insert should have zero-width range"
2226 );
2227
2228 let delete_change = TextDocumentContentChangeEvent {
2230 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2231 range_length: None,
2232 text: String::new(),
2233 };
2234
2235 assert!(delete_change.range.is_some());
2236 assert_eq!(delete_change.text, "");
2237 let range = delete_change.range.unwrap();
2238 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2239 assert_eq!(range.start.line, 0);
2240 assert_eq!(range.start.character, 2);
2241 assert_eq!(range.end.line, 0);
2242 assert_eq!(range.end.character, 7);
2243 }
2244
2245 #[test]
2246 fn test_goto_matching_bracket_forward() {
2247 let config = Config::default();
2248 let (dir_context, _temp) = test_dir_context();
2249 let mut editor = Editor::new(
2250 config,
2251 80,
2252 24,
2253 dir_context,
2254 crate::view::color_support::ColorCapability::TrueColor,
2255 test_filesystem(),
2256 )
2257 .unwrap();
2258
2259 let cursor_id = editor.active_cursors().primary_id();
2261 editor.apply_event_to_active_buffer(&Event::Insert {
2262 position: 0,
2263 text: "fn main() { let x = (1 + 2); }".to_string(),
2264 cursor_id,
2265 });
2266
2267 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2269 cursor_id,
2270 old_position: 31,
2271 new_position: 10,
2272 old_anchor: None,
2273 new_anchor: None,
2274 old_sticky_column: 0,
2275 new_sticky_column: 0,
2276 });
2277
2278 assert_eq!(editor.active_cursors().primary().position, 10);
2279
2280 editor.goto_matching_bracket();
2282
2283 assert_eq!(editor.active_cursors().primary().position, 29);
2288 }
2289
2290 #[test]
2291 fn test_goto_matching_bracket_backward() {
2292 let config = Config::default();
2293 let (dir_context, _temp) = test_dir_context();
2294 let mut editor = Editor::new(
2295 config,
2296 80,
2297 24,
2298 dir_context,
2299 crate::view::color_support::ColorCapability::TrueColor,
2300 test_filesystem(),
2301 )
2302 .unwrap();
2303
2304 let cursor_id = editor.active_cursors().primary_id();
2306 editor.apply_event_to_active_buffer(&Event::Insert {
2307 position: 0,
2308 text: "fn main() { let x = (1 + 2); }".to_string(),
2309 cursor_id,
2310 });
2311
2312 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2314 cursor_id,
2315 old_position: 31,
2316 new_position: 26,
2317 old_anchor: None,
2318 new_anchor: None,
2319 old_sticky_column: 0,
2320 new_sticky_column: 0,
2321 });
2322
2323 editor.goto_matching_bracket();
2325
2326 assert_eq!(editor.active_cursors().primary().position, 20);
2328 }
2329
2330 #[test]
2331 fn test_goto_matching_bracket_nested() {
2332 let config = Config::default();
2333 let (dir_context, _temp) = test_dir_context();
2334 let mut editor = Editor::new(
2335 config,
2336 80,
2337 24,
2338 dir_context,
2339 crate::view::color_support::ColorCapability::TrueColor,
2340 test_filesystem(),
2341 )
2342 .unwrap();
2343
2344 let cursor_id = editor.active_cursors().primary_id();
2346 editor.apply_event_to_active_buffer(&Event::Insert {
2347 position: 0,
2348 text: "{a{b{c}d}e}".to_string(),
2349 cursor_id,
2350 });
2351
2352 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2354 cursor_id,
2355 old_position: 11,
2356 new_position: 0,
2357 old_anchor: None,
2358 new_anchor: None,
2359 old_sticky_column: 0,
2360 new_sticky_column: 0,
2361 });
2362
2363 editor.goto_matching_bracket();
2365
2366 assert_eq!(editor.active_cursors().primary().position, 10);
2368 }
2369
2370 #[test]
2371 fn test_search_case_sensitive() {
2372 let config = Config::default();
2373 let (dir_context, _temp) = test_dir_context();
2374 let mut editor = Editor::new(
2375 config,
2376 80,
2377 24,
2378 dir_context,
2379 crate::view::color_support::ColorCapability::TrueColor,
2380 test_filesystem(),
2381 )
2382 .unwrap();
2383
2384 let cursor_id = editor.active_cursors().primary_id();
2386 editor.apply_event_to_active_buffer(&Event::Insert {
2387 position: 0,
2388 text: "Hello hello HELLO".to_string(),
2389 cursor_id,
2390 });
2391
2392 editor.active_window_mut().search_case_sensitive = false;
2394 editor.perform_search("hello");
2395
2396 let search_state = editor.active_window().search_state.as_ref().unwrap();
2397 assert_eq!(
2398 search_state.matches.len(),
2399 3,
2400 "Should find all 3 matches case-insensitively"
2401 );
2402
2403 editor.active_window_mut().search_case_sensitive = true;
2405 editor.perform_search("hello");
2406
2407 let search_state = editor.active_window().search_state.as_ref().unwrap();
2408 assert_eq!(
2409 search_state.matches.len(),
2410 1,
2411 "Should find only 1 exact match"
2412 );
2413 assert_eq!(
2414 search_state.matches[0], 6,
2415 "Should find 'hello' at position 6"
2416 );
2417 }
2418
2419 #[test]
2420 fn test_search_whole_word() {
2421 let config = Config::default();
2422 let (dir_context, _temp) = test_dir_context();
2423 let mut editor = Editor::new(
2424 config,
2425 80,
2426 24,
2427 dir_context,
2428 crate::view::color_support::ColorCapability::TrueColor,
2429 test_filesystem(),
2430 )
2431 .unwrap();
2432
2433 let cursor_id = editor.active_cursors().primary_id();
2435 editor.apply_event_to_active_buffer(&Event::Insert {
2436 position: 0,
2437 text: "test testing tested attest test".to_string(),
2438 cursor_id,
2439 });
2440
2441 editor.active_window_mut().search_whole_word = false;
2443 editor.active_window_mut().search_case_sensitive = true;
2444 editor.perform_search("test");
2445
2446 let search_state = editor.active_window().search_state.as_ref().unwrap();
2447 assert_eq!(
2448 search_state.matches.len(),
2449 5,
2450 "Should find 'test' in all occurrences"
2451 );
2452
2453 editor.active_window_mut().search_whole_word = true;
2455 editor.perform_search("test");
2456
2457 let search_state = editor.active_window().search_state.as_ref().unwrap();
2458 assert_eq!(
2459 search_state.matches.len(),
2460 2,
2461 "Should find only whole word 'test'"
2462 );
2463 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2464 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2465 }
2466
2467 #[test]
2468 fn test_search_scan_completes_when_capped() {
2469 let config = Config::default();
2475 let (dir_context, _temp) = test_dir_context();
2476 let mut editor = Editor::new(
2477 config,
2478 80,
2479 24,
2480 dir_context,
2481 crate::view::color_support::ColorCapability::TrueColor,
2482 test_filesystem(),
2483 )
2484 .unwrap();
2485
2486 let buffer_id = editor.active_buffer();
2489 let regex = regex::bytes::Regex::new("test").unwrap();
2490 let fake_chunks = vec![
2491 crate::model::buffer::LineScanChunk {
2492 leaf_index: 0,
2493 byte_len: 100,
2494 already_known: true,
2495 },
2496 crate::model::buffer::LineScanChunk {
2497 leaf_index: 1,
2498 byte_len: 100,
2499 already_known: true,
2500 },
2501 ];
2502
2503 let chunked = crate::model::buffer::ChunkedSearchState {
2504 chunks: fake_chunks,
2505 next_chunk: 1, next_doc_offset: 100,
2507 total_bytes: 200,
2508 scanned_bytes: 100,
2509 regex,
2510 matches: vec![
2511 crate::model::buffer::SearchMatch {
2512 byte_offset: 10,
2513 length: 4,
2514 line: 1,
2515 column: 11,
2516 context: String::new(),
2517 },
2518 crate::model::buffer::SearchMatch {
2519 byte_offset: 50,
2520 length: 4,
2521 line: 1,
2522 column: 51,
2523 context: String::new(),
2524 },
2525 ],
2526 overlap_tail: Vec::new(),
2527 overlap_doc_offset: 0,
2528 max_matches: 10_000,
2529 capped: true, query_len: 4,
2531 running_line: 1,
2532 };
2533
2534 editor.active_window_mut().search_scan.start(
2535 buffer_id,
2536 Vec::new(),
2537 chunked,
2538 "test".to_string(),
2539 None,
2540 false,
2541 false,
2542 false,
2543 );
2544
2545 let result = editor.process_search_scan();
2547 assert!(
2548 result,
2549 "process_search_scan should return true (needs render)"
2550 );
2551
2552 assert_eq!(
2554 editor.active_window().search_scan.buffer_id(),
2555 None,
2556 "search_scan should be drained after capped scan completes"
2557 );
2558
2559 let search_state = editor
2561 .active_window()
2562 .search_state
2563 .as_ref()
2564 .expect("search_state should be set after scan finishes");
2565 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2566 assert_eq!(search_state.query, "test");
2567 assert!(
2568 search_state.capped,
2569 "search_state should be marked as capped"
2570 );
2571 }
2572
2573 #[test]
2574 fn test_bookmarks() {
2575 let config = Config::default();
2576 let (dir_context, _temp) = test_dir_context();
2577 let mut editor = Editor::new(
2578 config,
2579 80,
2580 24,
2581 dir_context,
2582 crate::view::color_support::ColorCapability::TrueColor,
2583 test_filesystem(),
2584 )
2585 .unwrap();
2586
2587 let cursor_id = editor.active_cursors().primary_id();
2589 editor.apply_event_to_active_buffer(&Event::Insert {
2590 position: 0,
2591 text: "Line 1\nLine 2\nLine 3".to_string(),
2592 cursor_id,
2593 });
2594
2595 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2597 cursor_id,
2598 old_position: 21,
2599 new_position: 7,
2600 old_anchor: None,
2601 new_anchor: None,
2602 old_sticky_column: 0,
2603 new_sticky_column: 0,
2604 });
2605
2606 editor.active_window_mut().set_bookmark('1');
2608 assert_eq!(
2609 editor
2610 .active_window()
2611 .bookmarks
2612 .get('1')
2613 .map(|b| b.position),
2614 Some(7)
2615 );
2616
2617 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2619 cursor_id,
2620 old_position: 7,
2621 new_position: 14,
2622 old_anchor: None,
2623 new_anchor: None,
2624 old_sticky_column: 0,
2625 new_sticky_column: 0,
2626 });
2627
2628 editor.jump_to_bookmark('1');
2630 assert_eq!(editor.active_cursors().primary().position, 7);
2631
2632 editor.active_window_mut().clear_bookmark('1');
2634 assert_eq!(editor.active_window().bookmarks.get('1'), None);
2635 }
2636
2637 #[test]
2638 fn test_action_enum_new_variants() {
2639 use serde_json::json;
2641
2642 let args = HashMap::new();
2643 assert_eq!(
2644 Action::from_str("smart_home", &args),
2645 Some(Action::SmartHome)
2646 );
2647 assert_eq!(
2648 Action::from_str("dedent_selection", &args),
2649 Some(Action::DedentSelection)
2650 );
2651 assert_eq!(
2652 Action::from_str("toggle_comment", &args),
2653 Some(Action::ToggleComment)
2654 );
2655 assert_eq!(
2656 Action::from_str("goto_matching_bracket", &args),
2657 Some(Action::GoToMatchingBracket)
2658 );
2659 assert_eq!(
2660 Action::from_str("list_bookmarks", &args),
2661 Some(Action::ListBookmarks)
2662 );
2663 assert_eq!(
2664 Action::from_str("toggle_search_case_sensitive", &args),
2665 Some(Action::ToggleSearchCaseSensitive)
2666 );
2667 assert_eq!(
2668 Action::from_str("toggle_search_whole_word", &args),
2669 Some(Action::ToggleSearchWholeWord)
2670 );
2671
2672 let mut args_with_char = HashMap::new();
2674 args_with_char.insert("char".to_string(), json!("5"));
2675 assert_eq!(
2676 Action::from_str("set_bookmark", &args_with_char),
2677 Some(Action::SetBookmark('5'))
2678 );
2679 assert_eq!(
2680 Action::from_str("jump_to_bookmark", &args_with_char),
2681 Some(Action::JumpToBookmark('5'))
2682 );
2683 assert_eq!(
2684 Action::from_str("clear_bookmark", &args_with_char),
2685 Some(Action::ClearBookmark('5'))
2686 );
2687 }
2688
2689 #[test]
2690 fn test_keybinding_new_defaults() {
2691 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2692
2693 let mut config = Config::default();
2697 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2698 let resolver = KeybindingResolver::new(&config);
2699
2700 let event = KeyEvent {
2702 code: KeyCode::Char('/'),
2703 modifiers: KeyModifiers::CONTROL,
2704 kind: KeyEventKind::Press,
2705 state: KeyEventState::NONE,
2706 };
2707 let action = resolver.resolve(&event, KeyContext::Normal);
2708 assert_eq!(action, Action::ToggleComment);
2709
2710 let event = KeyEvent {
2712 code: KeyCode::Char(']'),
2713 modifiers: KeyModifiers::CONTROL,
2714 kind: KeyEventKind::Press,
2715 state: KeyEventState::NONE,
2716 };
2717 let action = resolver.resolve(&event, KeyContext::Normal);
2718 assert_eq!(action, Action::GoToMatchingBracket);
2719
2720 let event = KeyEvent {
2722 code: KeyCode::Tab,
2723 modifiers: KeyModifiers::SHIFT,
2724 kind: KeyEventKind::Press,
2725 state: KeyEventState::NONE,
2726 };
2727 let action = resolver.resolve(&event, KeyContext::Normal);
2728 assert_eq!(action, Action::DedentSelection);
2729
2730 let event = KeyEvent {
2732 code: KeyCode::Char('g'),
2733 modifiers: KeyModifiers::CONTROL,
2734 kind: KeyEventKind::Press,
2735 state: KeyEventState::NONE,
2736 };
2737 let action = resolver.resolve(&event, KeyContext::Normal);
2738 assert_eq!(action, Action::GotoLine);
2739
2740 let event = KeyEvent {
2742 code: KeyCode::Char('5'),
2743 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2744 kind: KeyEventKind::Press,
2745 state: KeyEventState::NONE,
2746 };
2747 let action = resolver.resolve(&event, KeyContext::Normal);
2748 assert_eq!(action, Action::SetBookmark('5'));
2749
2750 let event = KeyEvent {
2751 code: KeyCode::Char('5'),
2752 modifiers: KeyModifiers::ALT,
2753 kind: KeyEventKind::Press,
2754 state: KeyEventState::NONE,
2755 };
2756 let action = resolver.resolve(&event, KeyContext::Normal);
2757 assert_eq!(action, Action::JumpToBookmark('5'));
2758 }
2759
2760 #[test]
2772 fn test_lsp_rename_didchange_positions_bug() {
2773 use crate::model::buffer::Buffer;
2774
2775 let config = Config::default();
2776 let (dir_context, _temp) = test_dir_context();
2777 let mut editor = Editor::new(
2778 config,
2779 80,
2780 24,
2781 dir_context,
2782 crate::view::color_support::ColorCapability::TrueColor,
2783 test_filesystem(),
2784 )
2785 .unwrap();
2786
2787 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2791 editor.active_state_mut().buffer =
2792 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2793
2794 let cursor_id = editor.active_cursors().primary_id();
2799
2800 let batch = Event::Batch {
2801 events: vec![
2802 Event::Delete {
2804 range: 23..26, deleted_text: "val".to_string(),
2806 cursor_id,
2807 },
2808 Event::Insert {
2809 position: 23,
2810 text: "value".to_string(),
2811 cursor_id,
2812 },
2813 Event::Delete {
2815 range: 7..10, deleted_text: "val".to_string(),
2817 cursor_id,
2818 },
2819 Event::Insert {
2820 position: 7,
2821 text: "value".to_string(),
2822 cursor_id,
2823 },
2824 ],
2825 description: "LSP Rename".to_string(),
2826 };
2827
2828 let lsp_changes_before = editor.active_window().collect_lsp_changes(&batch);
2830
2831 editor.apply_event_to_active_buffer(&batch);
2833
2834 let lsp_changes_after = editor.active_window().collect_lsp_changes(&batch);
2837
2838 let final_content = editor.active_state().buffer.to_string().unwrap();
2840 assert_eq!(
2841 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2842 "Buffer should have 'value' in both places"
2843 );
2844
2845 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2851
2852 let first_delete = &lsp_changes_before[0];
2853 let first_del_range = first_delete.range.unwrap();
2854 assert_eq!(
2855 first_del_range.start.line, 1,
2856 "First delete should be on line 1 (BEFORE)"
2857 );
2858 assert_eq!(
2859 first_del_range.start.character, 4,
2860 "First delete start should be at char 4 (BEFORE)"
2861 );
2862
2863 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2869
2870 let first_delete_after = &lsp_changes_after[0];
2871 let first_del_range_after = first_delete_after.range.unwrap();
2872
2873 eprintln!("BEFORE modification:");
2876 eprintln!(
2877 " Delete at line {}, char {}-{}",
2878 first_del_range.start.line,
2879 first_del_range.start.character,
2880 first_del_range.end.character
2881 );
2882 eprintln!("AFTER modification:");
2883 eprintln!(
2884 " Delete at line {}, char {}-{}",
2885 first_del_range_after.start.line,
2886 first_del_range_after.start.character,
2887 first_del_range_after.end.character
2888 );
2889
2890 assert_ne!(
2908 first_del_range_after.end.character, first_del_range.end.character,
2909 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2910 );
2911
2912 eprintln!("\n=== BUG DEMONSTRATED ===");
2913 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2914 eprintln!("the positions are WRONG because they're calculated from the");
2915 eprintln!("modified buffer, not the original buffer.");
2916 eprintln!("This causes the second rename to fail with 'content modified' error.");
2917 eprintln!("========================\n");
2918 }
2919
2920 #[test]
2921 fn test_lsp_rename_preserves_cursor_position() {
2922 use crate::model::buffer::Buffer;
2923
2924 let config = Config::default();
2925 let (dir_context, _temp) = test_dir_context();
2926 let mut editor = Editor::new(
2927 config,
2928 80,
2929 24,
2930 dir_context,
2931 crate::view::color_support::ColorCapability::TrueColor,
2932 test_filesystem(),
2933 )
2934 .unwrap();
2935
2936 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2940 editor.active_state_mut().buffer =
2941 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2942
2943 let original_cursor_pos = 23;
2945 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2946
2947 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2949 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2950 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2951
2952 let cursor_id = editor.active_cursors().primary_id();
2955 let buffer_id = editor.active_buffer();
2956
2957 let events = vec![
2958 Event::Delete {
2960 range: 23..26, deleted_text: "val".to_string(),
2962 cursor_id,
2963 },
2964 Event::Insert {
2965 position: 23,
2966 text: "value".to_string(),
2967 cursor_id,
2968 },
2969 Event::Delete {
2971 range: 7..10, deleted_text: "val".to_string(),
2973 cursor_id,
2974 },
2975 Event::Insert {
2976 position: 7,
2977 text: "value".to_string(),
2978 cursor_id,
2979 },
2980 ];
2981
2982 editor
2984 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2985 .unwrap();
2986
2987 let final_content = editor.active_state().buffer.to_string().unwrap();
2989 assert_eq!(
2990 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2991 "Buffer should have 'value' in both places"
2992 );
2993
2994 let final_cursor_pos = editor.active_cursors().primary().position;
3002 let expected_cursor_pos = 25; assert_eq!(
3005 final_cursor_pos, expected_cursor_pos,
3006 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
3007 Original pos: {}, expected adjustment: +2 for first rename",
3008 expected_cursor_pos, final_cursor_pos, original_cursor_pos
3009 );
3010
3011 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
3013 assert_eq!(
3014 text_at_new_cursor, "value",
3015 "Cursor should be at the start of 'value' after rename"
3016 );
3017 }
3018
3019 #[test]
3020 fn test_lsp_rename_twice_consecutive() {
3021 use crate::model::buffer::Buffer;
3024
3025 let config = Config::default();
3026 let (dir_context, _temp) = test_dir_context();
3027 let mut editor = Editor::new(
3028 config,
3029 80,
3030 24,
3031 dir_context,
3032 crate::view::color_support::ColorCapability::TrueColor,
3033 test_filesystem(),
3034 )
3035 .unwrap();
3036
3037 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
3039 editor.active_state_mut().buffer =
3040 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
3041
3042 let cursor_id = editor.active_cursors().primary_id();
3043 let buffer_id = editor.active_buffer();
3044
3045 let events1 = vec![
3048 Event::Delete {
3050 range: 23..26,
3051 deleted_text: "val".to_string(),
3052 cursor_id,
3053 },
3054 Event::Insert {
3055 position: 23,
3056 text: "value".to_string(),
3057 cursor_id,
3058 },
3059 Event::Delete {
3061 range: 7..10,
3062 deleted_text: "val".to_string(),
3063 cursor_id,
3064 },
3065 Event::Insert {
3066 position: 7,
3067 text: "value".to_string(),
3068 cursor_id,
3069 },
3070 ];
3071
3072 let batch1 = Event::Batch {
3074 events: events1.clone(),
3075 description: "LSP Rename 1".to_string(),
3076 };
3077
3078 let lsp_changes1 = editor.active_window().collect_lsp_changes(&batch1);
3080
3081 assert_eq!(
3083 lsp_changes1.len(),
3084 4,
3085 "First rename should have 4 LSP changes"
3086 );
3087
3088 let first_del = &lsp_changes1[0];
3090 let first_del_range = first_del.range.unwrap();
3091 assert_eq!(first_del_range.start.line, 1, "First delete line");
3092 assert_eq!(
3093 first_del_range.start.character, 4,
3094 "First delete start char"
3095 );
3096 assert_eq!(first_del_range.end.character, 7, "First delete end char");
3097
3098 editor
3100 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
3101 .unwrap();
3102
3103 let after_first = editor.active_state().buffer.to_string().unwrap();
3105 assert_eq!(
3106 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
3107 "After first rename"
3108 );
3109
3110 let events2 = vec![
3120 Event::Delete {
3122 range: 25..30,
3123 deleted_text: "value".to_string(),
3124 cursor_id,
3125 },
3126 Event::Insert {
3127 position: 25,
3128 text: "x".to_string(),
3129 cursor_id,
3130 },
3131 Event::Delete {
3133 range: 7..12,
3134 deleted_text: "value".to_string(),
3135 cursor_id,
3136 },
3137 Event::Insert {
3138 position: 7,
3139 text: "x".to_string(),
3140 cursor_id,
3141 },
3142 ];
3143
3144 let batch2 = Event::Batch {
3146 events: events2.clone(),
3147 description: "LSP Rename 2".to_string(),
3148 };
3149
3150 let lsp_changes2 = editor.active_window().collect_lsp_changes(&batch2);
3152
3153 assert_eq!(
3157 lsp_changes2.len(),
3158 4,
3159 "Second rename should have 4 LSP changes"
3160 );
3161
3162 let second_first_del = &lsp_changes2[0];
3164 let second_first_del_range = second_first_del.range.unwrap();
3165 assert_eq!(
3166 second_first_del_range.start.line, 1,
3167 "Second rename first delete should be on line 1"
3168 );
3169 assert_eq!(
3170 second_first_del_range.start.character, 4,
3171 "Second rename first delete start should be at char 4"
3172 );
3173 assert_eq!(
3174 second_first_del_range.end.character, 9,
3175 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3176 );
3177
3178 let second_third_del = &lsp_changes2[2];
3180 let second_third_del_range = second_third_del.range.unwrap();
3181 assert_eq!(
3182 second_third_del_range.start.line, 0,
3183 "Second rename third delete should be on line 0"
3184 );
3185 assert_eq!(
3186 second_third_del_range.start.character, 7,
3187 "Second rename third delete start should be at char 7"
3188 );
3189 assert_eq!(
3190 second_third_del_range.end.character, 12,
3191 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3192 );
3193
3194 editor
3196 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3197 .unwrap();
3198
3199 let after_second = editor.active_state().buffer.to_string().unwrap();
3201 assert_eq!(
3202 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3203 "After second rename"
3204 );
3205 }
3206
3207 #[test]
3208 fn test_ensure_active_tab_visible_static_offset() {
3209 let config = Config::default();
3210 let (dir_context, _temp) = test_dir_context();
3211 let mut editor = Editor::new(
3212 config,
3213 80,
3214 24,
3215 dir_context,
3216 crate::view::color_support::ColorCapability::TrueColor,
3217 test_filesystem(),
3218 )
3219 .unwrap();
3220 let split_id = editor.split_manager().active_split();
3221
3222 let buf1 = editor.new_buffer();
3224 editor
3225 .buffers_mut()
3226 .get_mut(&buf1)
3227 .unwrap()
3228 .buffer
3229 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3230 let buf2 = editor.new_buffer();
3231 editor
3232 .buffers_mut()
3233 .get_mut(&buf2)
3234 .unwrap()
3235 .buffer
3236 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3237 let buf3 = editor.new_buffer();
3238 editor
3239 .buffers_mut()
3240 .get_mut(&buf3)
3241 .unwrap()
3242 .buffer
3243 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3244
3245 {
3246 use crate::view::split::TabTarget;
3247 let view_state = editor.split_view_states_mut().get_mut(&split_id).unwrap();
3248 view_state.open_buffers = vec![
3249 TabTarget::Buffer(buf1),
3250 TabTarget::Buffer(buf2),
3251 TabTarget::Buffer(buf3),
3252 ];
3253 view_state.tab_scroll_offset = 50;
3254 }
3255
3256 editor
3260 .active_window_mut()
3261 .ensure_active_tab_visible(split_id, buf1, 25);
3262 assert_eq!(
3263 editor
3264 .split_view_states()
3265 .get(&split_id)
3266 .unwrap()
3267 .tab_scroll_offset,
3268 0
3269 );
3270
3271 editor
3273 .active_window_mut()
3274 .ensure_active_tab_visible(split_id, buf3, 25);
3275 let view_state = editor.split_view_states().get(&split_id).unwrap();
3276 assert!(view_state.tab_scroll_offset > 0);
3277 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3278 let total_width: usize = buffer_ids
3279 .iter()
3280 .enumerate()
3281 .map(|(idx, id)| {
3282 let state = editor.buffers().get(id).unwrap();
3283 let name_len = state
3284 .buffer
3285 .file_path()
3286 .and_then(|p| p.file_name())
3287 .and_then(|n| n.to_str())
3288 .map(|s| s.chars().count())
3289 .unwrap_or(0);
3290 let tab_width = 2 + name_len;
3291 if idx < buffer_ids.len() - 1 {
3292 tab_width + 1 } else {
3294 tab_width
3295 }
3296 })
3297 .sum();
3298 assert!(view_state.tab_scroll_offset <= total_width);
3299 }
3300}