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, RwLock};
208use std::time::Instant;
209
210pub use self::types::{BufferKind, BufferMetadata, HoverTarget};
212pub use self::warning_domains::{
213 GeneralWarningDomain, LspWarningDomain, WarningAction, WarningActionId, WarningDomain,
214 WarningDomainRegistry, WarningLevel, WarningPopupContent,
215};
216pub use crate::model::event::BufferId;
217
218fn lsp_uri_to_host_path(
227 uri: &crate::app::types::LspUri,
228 translation: Option<&crate::services::authority::PathTranslation>,
229) -> Result<PathBuf, String> {
230 uri.to_host_path(translation)
231 .ok_or_else(|| "URI is not a file path".to_string())
232}
233
234#[derive(Clone, Debug)]
236pub struct PendingGrammar {
237 pub language: String,
239 pub grammar_path: String,
241 pub extensions: Vec<String>,
243}
244
245#[derive(Clone, Debug)]
247pub(crate) struct SemanticTokenRangeRequest {
248 pub(crate) buffer_id: BufferId,
249 pub(crate) version: u64,
250 pub(crate) range: Range<usize>,
251 pub(crate) start_line: usize,
252 pub(crate) end_line: usize,
253}
254
255#[derive(Clone, Copy, Debug)]
256pub(crate) enum SemanticTokensFullRequestKind {
257 Full,
258 FullDelta,
259}
260
261#[derive(Clone, Debug)]
262pub(crate) struct SemanticTokenFullRequest {
263 pub(crate) buffer_id: BufferId,
264 pub(crate) version: u64,
265 pub(crate) kind: SemanticTokensFullRequestKind,
266}
267
268#[derive(Clone, Debug)]
269pub(crate) struct FoldingRangeRequest {
270 pub(crate) buffer_id: BufferId,
271 pub(crate) version: u64,
272}
273
274#[derive(Clone, Debug)]
275pub(crate) struct InlayHintsRequest {
276 pub(crate) buffer_id: BufferId,
277 pub(crate) version: u64,
278}
279
280#[derive(Debug, Clone)]
286pub struct DabbrevCycleState {
287 pub original_prefix: String,
289 pub word_start: usize,
291 pub candidates: Vec<String>,
293 pub index: usize,
295}
296
297#[derive(Debug, Clone)]
312pub(crate) struct GotoLinePreviewSnapshot {
313 pub buffer_id: BufferId,
314 pub split_id: LeafId,
315 pub cursor_id: crate::model::event::CursorId,
316 pub position: usize,
317 pub anchor: Option<usize>,
318 pub sticky_column: usize,
319 pub viewport_top_byte: usize,
320 pub viewport_top_view_line_offset: usize,
321 pub viewport_left_column: usize,
322 pub last_jump_position: usize,
323}
324
325pub struct Editor {
327 next_buffer_id: usize,
349
350 pub(crate) buffer_id_alloc: crate::app::window_resources::BufferIdAllocator,
357
358 config: Arc<Config>,
378
379 config_snapshot_anchor: Arc<Config>,
381
382 config_cached_json: Arc<serde_json::Value>,
385
386 user_config_raw: Arc<serde_json::Value>,
388
389 dir_context: DirectoryContext,
391
392 grammar_registry: std::sync::Arc<crate::primitives::grammar::GrammarRegistry>,
394
395 pending_grammars: Vec<PendingGrammar>,
397
398 grammar_reload_pending: bool,
402
403 grammar_build_in_progress: bool,
406
407 needs_full_grammar_build: bool,
411
412 pending_grammar_callbacks: Vec<fresh_core::api::JsCallbackId>,
416
417 pub(crate) theme: Arc<RwLock<crate::view::theme::Theme>>,
422
423 theme_registry: Arc<crate::view::theme::ThemeRegistry>,
426
427 expanded_menus_cache: crate::view::ui::ExpandedMenusCache,
430
431 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
433
434 ansi_background: Option<crate::primitives::ansi_background::AnsiBackground>,
436
437 ansi_background_path: Option<PathBuf>,
439
440 background_fade: f32,
442
443 keybindings: Arc<RwLock<KeybindingResolver>>,
445
446 clipboard: crate::services::clipboard::Clipboard,
448
449 should_quit: bool,
451
452 should_detach: bool,
454
455 session_mode: bool,
457
458 software_cursor_only: bool,
460
461 session_name: Option<String>,
463
464 pending_escape_sequences: Vec<u8>,
467
468 restart_with_dir: Option<PathBuf>,
471
472 last_window_title: Option<String>,
479
480 terminal_width: u16,
483 terminal_height: u16,
484
485 mode_registry: ModeRegistry,
494
495 tokio_runtime: Option<Arc<tokio::runtime::Runtime>>,
497
498 async_bridge: Option<AsyncBridge>,
500
501 fs_manager: Arc<FsManager>,
519
520 authority: crate::services::authority::Authority,
530
531 pending_authority: Option<crate::services::authority::Authority>,
537
538 pub remote_indicator_override: Option<crate::view::ui::status_bar::RemoteIndicatorOverride>,
544
545 local_filesystem: Arc<dyn FileSystem + Send + Sync>,
550
551 menu_state: crate::view::ui::MenuState,
573
574 menus: crate::config::MenuConfig,
576
577 working_dir: PathBuf,
585
586 pub(crate) windows: HashMap<fresh_core::WindowId, crate::app::window::Window>,
591
592 pub(crate) active_window: fresh_core::WindowId,
595
596 #[allow(dead_code)]
601 pub(crate) next_window_id: u64,
602
603 command_registry: Arc<RwLock<CommandRegistry>>,
621
622 quick_open_registry: QuickOpenRegistry,
624
625 plugin_manager: Arc<RwLock<PluginManager>>,
633
634 background_process_handles: HashMap<u64, tokio::task::AbortHandle>,
653
654 host_process_handles: HashMap<u64, tokio::sync::oneshot::Sender<()>>,
660 event_broadcaster: crate::model::control_event::EventBroadcaster,
679
680 #[cfg(feature = "plugins")]
687 pending_plugin_actions: Vec<(
688 String,
689 crate::services::plugins::thread::oneshot::Receiver<anyhow::Result<()>>,
690 )>,
691
692 #[cfg(feature = "plugins")]
694 plugin_render_requested: bool,
695
696 recovery_service: RecoveryService,
723
724 full_redraw_requested: bool,
726
727 suspend_requested: bool,
730
731 time_source: SharedTimeSource,
733
734 plugin_global_state: HashMap<String, HashMap<String, serde_json::Value>>,
744 warning_log: Option<(std::sync::mpsc::Receiver<()>, PathBuf)>,
746
747 status_log_path: Option<PathBuf>,
749
750 update_checker: Option<crate::services::release_checker::PeriodicUpdateChecker>,
756
757 file_watcher_manager: crate::services::file_watcher::FileWatcherManager,
767
768 pub(crate) last_path_change_for_test: Option<(u64, std::path::PathBuf, &'static str)>,
774
775 pub(crate) last_watch_response_for_test: Option<(u64, Result<u64, String>)>,
779
780 pub(crate) preview_window_id: Option<fresh_core::WindowId>,
787
788 pub(crate) settings_state: Option<crate::view::settings::SettingsState>,
808
809 pub(crate) calibration_wizard: Option<calibration_wizard::CalibrationWizard>,
811
812 pub(crate) keybinding_editor: Option<keybinding_editor::KeybindingEditor>,
817
818 pub(crate) key_translator: crate::input::key_translator::KeyTranslator,
820
821 color_capability: crate::view::color_support::ColorCapability,
823
824 pub(crate) global_popups: crate::view::popup::PopupManager,
835
836 stdin_stream: stdin_stream::StdinStream,
854
855 pub(crate) previous_cursor_screen_pos: Option<((u16, u16), LeafId)>,
878 pub(crate) cursor_jump_animation: Option<crate::view::animation::AnimationId>,
881
882 pub(crate) pending_vb_animations: Vec<(u64, BufferId, fresh_core::api::PluginAnimationKind)>,
888
889 pub(crate) widget_registry: crate::widgets::WidgetRegistry,
897
898 pub(crate) floating_widget_panel: Option<FloatingWidgetState>,
905}
906
907pub(crate) const FLOATING_PANEL_BUFFER_ID: BufferId = BufferId(usize::MAX);
913
914#[derive(Debug, Clone)]
921pub(crate) struct FloatingWidgetState {
922 pub panel_id: crate::widgets::PanelId,
923 pub width_pct: u8,
924 pub height_pct: u8,
925 pub entries: Vec<fresh_core::text_property::TextPropertyEntry>,
929 pub focus_cursor: Option<crate::widgets::FocusCursor>,
931 pub embeds: Vec<crate::widgets::EmbedRect>,
938 pub last_inner_rect: Option<ratatui::layout::Rect>,
941}
942
943#[derive(Debug, Clone)]
945pub struct PendingFileOpen {
946 pub path: PathBuf,
948 pub line: Option<usize>,
950 pub column: Option<usize>,
952 pub end_line: Option<usize>,
954 pub end_column: Option<usize>,
956 pub message: Option<String>,
958 pub wait_id: Option<u64>,
960}
961
962impl Editor {
963 fn load_ansi_background(&mut self, input: &str) -> AnyhowResult<()> {
965 let trimmed = input.trim();
966
967 if trimmed.is_empty() {
968 self.ansi_background = None;
969 self.ansi_background_path = None;
970 self.set_status_message(t!("status.background_cleared").to_string());
971 return Ok(());
972 }
973
974 let input_path = Path::new(trimmed);
975 let resolved = if input_path.is_absolute() {
976 input_path.to_path_buf()
977 } else {
978 self.working_dir.join(input_path)
979 };
980
981 let canonical = resolved.canonicalize().unwrap_or_else(|_| resolved.clone());
982
983 let parsed = crate::primitives::ansi_background::AnsiBackground::from_file(&canonical)?;
984
985 self.ansi_background = Some(parsed);
986 self.ansi_background_path = Some(canonical.clone());
987 self.set_status_message(
988 t!(
989 "view.background_set",
990 path = canonical.display().to_string()
991 )
992 .to_string(),
993 );
994
995 Ok(())
996 }
997
998 #[doc(hidden)]
1002 pub fn buffer_count_for_tests(&self) -> usize {
1003 self.windows
1004 .get(&self.active_window)
1005 .map(|w| &w.buffers)
1006 .expect("active window present")
1007 .len()
1008 }
1009
1010 #[doc(hidden)]
1014 pub fn all_buffer_ids_for_tests(&self) -> Vec<BufferId> {
1015 let mut ids: Vec<BufferId> = self
1016 .windows
1017 .get(&self.active_window)
1018 .map(|w| &w.buffers)
1019 .expect("active window present")
1020 .ids();
1021 ids.sort_by_key(|id| id.0);
1022 ids
1023 }
1024
1025 pub fn active_state(&self) -> &EditorState {
1027 self.windows
1028 .get(&self.active_window)
1029 .map(|w| &w.buffers)
1030 .expect("active window present")
1031 .get(&self.active_buffer())
1032 .unwrap()
1033 }
1034
1035 pub fn active_state_mut(&mut self) -> &mut EditorState {
1037 let __buffer_id = self.active_buffer();
1038 self.windows
1039 .get_mut(&self.active_window)
1040 .map(|w| &mut w.buffers)
1041 .expect("active window present")
1042 .get_mut(&__buffer_id)
1043 .unwrap()
1044 }
1045
1046 pub fn active_cursors(&self) -> &Cursors {
1050 let split_id = self.effective_active_split();
1051 &self
1052 .windows
1053 .get(&self.active_window)
1054 .and_then(|w| w.buffers.splits())
1055 .map(|(_, vs)| vs)
1056 .expect("active window must have a populated split layout")
1057 .get(&split_id)
1058 .unwrap()
1059 .cursors
1060 }
1061
1062 pub fn active_cursors_mut(&mut self) -> &mut Cursors {
1064 let split_id = self.effective_active_split();
1065 &mut self
1066 .windows
1067 .get_mut(&self.active_window)
1068 .and_then(|w| w.split_view_states_mut())
1069 .expect("active window must have a populated split layout")
1070 .get_mut(&split_id)
1071 .unwrap()
1072 .cursors
1073 }
1074
1075 pub fn set_completion_items(&mut self, items: Vec<lsp_types::CompletionItem>) {
1077 self.active_window_mut().completion_items = Some(items);
1078 }
1079
1080 pub fn active_viewport(&self) -> &crate::view::viewport::Viewport {
1082 let active_split = self
1083 .windows
1084 .get(&self.active_window)
1085 .and_then(|w| w.buffers.splits())
1086 .map(|(mgr, _)| mgr)
1087 .expect("active window must have a populated split layout")
1088 .active_split();
1089 &self
1090 .windows
1091 .get(&self.active_window)
1092 .and_then(|w| w.buffers.splits())
1093 .map(|(_, vs)| vs)
1094 .expect("active window must have a populated split layout")
1095 .get(&active_split)
1096 .unwrap()
1097 .viewport
1098 }
1099
1100 pub fn active_viewport_mut(&mut self) -> &mut crate::view::viewport::Viewport {
1102 let active_split = self
1103 .windows
1104 .get(&self.active_window)
1105 .and_then(|w| w.buffers.splits())
1106 .map(|(mgr, _)| mgr)
1107 .expect("active window must have a populated split layout")
1108 .active_split();
1109 &mut self
1110 .windows
1111 .get_mut(&self.active_window)
1112 .and_then(|w| w.split_view_states_mut())
1113 .expect("active window must have a populated split layout")
1114 .get_mut(&active_split)
1115 .unwrap()
1116 .viewport
1117 }
1118
1119 pub fn get_buffer_display_name(&self, buffer_id: BufferId) -> String {
1121 if let Some(composite) = self.active_window().composite_buffers.get(&buffer_id) {
1123 return composite.name.clone();
1124 }
1125
1126 self.active_window()
1127 .buffer_metadata
1128 .get(&buffer_id)
1129 .map(|m| m.display_name.clone())
1130 .or_else(|| {
1131 self.windows
1132 .get(&self.active_window)
1133 .map(|w| &w.buffers)
1134 .expect("active window present")
1135 .get(&buffer_id)
1136 .and_then(|state| {
1137 state
1138 .buffer
1139 .file_path()
1140 .and_then(|p| p.file_name())
1141 .and_then(|n| n.to_str())
1142 .map(|s| s.to_string())
1143 })
1144 })
1145 .unwrap_or_else(|| "[No Name]".to_string())
1146 }
1147
1148 pub fn active_event_log(&self) -> &EventLog {
1158 self.active_window()
1159 .event_logs
1160 .get(&self.active_buffer())
1161 .unwrap()
1162 }
1163
1164 pub fn active_event_log_mut(&mut self) -> &mut EventLog {
1166 let buffer_id = self.active_buffer();
1167 self.active_window_mut()
1168 .event_logs
1169 .get_mut(&buffer_id)
1170 .unwrap()
1171 }
1172}
1173
1174fn parse_key_string(key_str: &str) -> Option<(KeyCode, KeyModifiers)> {
1183 use crossterm::event::{KeyCode, KeyModifiers};
1184
1185 let mut modifiers = KeyModifiers::NONE;
1186 let mut remaining = key_str;
1187
1188 loop {
1190 if remaining.starts_with("C-") {
1191 modifiers |= KeyModifiers::CONTROL;
1192 remaining = &remaining[2..];
1193 } else if remaining.starts_with("M-") {
1194 modifiers |= KeyModifiers::ALT;
1195 remaining = &remaining[2..];
1196 } else if remaining.starts_with("S-") {
1197 modifiers |= KeyModifiers::SHIFT;
1198 remaining = &remaining[2..];
1199 } else {
1200 break;
1201 }
1202 }
1203
1204 let upper = remaining.to_uppercase();
1207 let code = match upper.as_str() {
1208 "RET" | "RETURN" | "ENTER" => KeyCode::Enter,
1209 "TAB" => KeyCode::Tab,
1210 "BACKTAB" => KeyCode::BackTab,
1211 "ESC" | "ESCAPE" => KeyCode::Esc,
1212 "SPC" | "SPACE" => KeyCode::Char(' '),
1213 "DEL" | "DELETE" => KeyCode::Delete,
1214 "BS" | "BACKSPACE" => KeyCode::Backspace,
1215 "UP" => KeyCode::Up,
1216 "DOWN" => KeyCode::Down,
1217 "LEFT" => KeyCode::Left,
1218 "RIGHT" => KeyCode::Right,
1219 "HOME" => KeyCode::Home,
1220 "END" => KeyCode::End,
1221 "PAGEUP" | "PGUP" => KeyCode::PageUp,
1222 "PAGEDOWN" | "PGDN" => KeyCode::PageDown,
1223 s if s.starts_with('F') && s.len() > 1 => {
1224 if let Ok(n) = s[1..].parse::<u8>() {
1226 KeyCode::F(n)
1227 } else {
1228 return None;
1229 }
1230 }
1231 _ if remaining.len() == 1 => {
1232 let c = remaining.chars().next()?;
1235 if c.is_ascii_uppercase() {
1236 modifiers |= KeyModifiers::SHIFT;
1237 }
1238 KeyCode::Char(c.to_ascii_lowercase())
1239 }
1240 _ => return None,
1241 };
1242
1243 if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
1248 return Some((KeyCode::BackTab, modifiers.difference(KeyModifiers::SHIFT)));
1249 }
1250
1251 Some((code, modifiers))
1252}
1253
1254#[cfg(test)]
1255mod tests {
1256 use super::*;
1257 use lsp_types::{Position, Range as LspRange, TextDocumentContentChangeEvent};
1258 use tempfile::TempDir;
1259
1260 fn test_dir_context() -> (DirectoryContext, TempDir) {
1262 let temp_dir = TempDir::new().unwrap();
1263 let dir_context = DirectoryContext::for_testing(temp_dir.path());
1264 (dir_context, temp_dir)
1265 }
1266
1267 fn test_filesystem() -> Arc<dyn FileSystem + Send + Sync> {
1269 Arc::new(crate::model::filesystem::StdFileSystem)
1270 }
1271
1272 #[test]
1273 fn parse_key_string_shift_tab_normalizes_to_backtab() {
1274 use crossterm::event::{KeyCode, KeyModifiers};
1275 assert_eq!(
1280 parse_key_string("S-Tab"),
1281 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1282 );
1283 assert_eq!(
1284 parse_key_string("BackTab"),
1285 Some((KeyCode::BackTab, KeyModifiers::NONE)),
1286 );
1287 assert_eq!(
1289 parse_key_string("Tab"),
1290 Some((KeyCode::Tab, KeyModifiers::NONE)),
1291 );
1292 }
1293
1294 #[test]
1295 fn test_editor_new() {
1296 let config = Config::default();
1297 let (dir_context, _temp) = test_dir_context();
1298 let editor = Editor::new(
1299 config,
1300 80,
1301 24,
1302 dir_context,
1303 crate::view::color_support::ColorCapability::TrueColor,
1304 test_filesystem(),
1305 )
1306 .unwrap();
1307
1308 assert_eq!(editor.buffers().len(), 1);
1309 assert!(!editor.should_quit());
1310 }
1311
1312 #[test]
1313 fn test_new_buffer() {
1314 let config = Config::default();
1315 let (dir_context, _temp) = test_dir_context();
1316 let mut editor = Editor::new(
1317 config,
1318 80,
1319 24,
1320 dir_context,
1321 crate::view::color_support::ColorCapability::TrueColor,
1322 test_filesystem(),
1323 )
1324 .unwrap();
1325
1326 let id = editor.new_buffer();
1327 assert_eq!(editor.buffers().len(), 2);
1328 assert_eq!(editor.active_buffer(), id);
1329 }
1330
1331 #[test]
1332 #[ignore]
1333 fn test_clipboard() {
1334 let config = Config::default();
1335 let (dir_context, _temp) = test_dir_context();
1336 let mut editor = Editor::new(
1337 config,
1338 80,
1339 24,
1340 dir_context,
1341 crate::view::color_support::ColorCapability::TrueColor,
1342 test_filesystem(),
1343 )
1344 .unwrap();
1345
1346 editor.clipboard.set_internal("test".to_string());
1348
1349 editor.paste();
1351
1352 let content = editor.active_state().buffer.to_string().unwrap();
1353 assert_eq!(content, "test");
1354 }
1355
1356 #[test]
1357 fn test_action_to_events_insert_char() {
1358 let config = Config::default();
1359 let (dir_context, _temp) = test_dir_context();
1360 let mut editor = Editor::new(
1361 config,
1362 80,
1363 24,
1364 dir_context,
1365 crate::view::color_support::ColorCapability::TrueColor,
1366 test_filesystem(),
1367 )
1368 .unwrap();
1369
1370 let events = editor
1371 .active_window_mut()
1372 .action_to_events(Action::InsertChar('a'));
1373 assert!(events.is_some());
1374
1375 let events = events.unwrap();
1376 assert_eq!(events.len(), 1);
1377
1378 match &events[0] {
1379 Event::Insert { position, text, .. } => {
1380 assert_eq!(*position, 0);
1381 assert_eq!(text, "a");
1382 }
1383 _ => panic!("Expected Insert event"),
1384 }
1385 }
1386
1387 #[test]
1388 fn test_action_to_events_move_right() {
1389 let config = Config::default();
1390 let (dir_context, _temp) = test_dir_context();
1391 let mut editor = Editor::new(
1392 config,
1393 80,
1394 24,
1395 dir_context,
1396 crate::view::color_support::ColorCapability::TrueColor,
1397 test_filesystem(),
1398 )
1399 .unwrap();
1400
1401 let cursor_id = editor.active_cursors().primary_id();
1403 editor.apply_event_to_active_buffer(&Event::Insert {
1404 position: 0,
1405 text: "hello".to_string(),
1406 cursor_id,
1407 });
1408
1409 let events = editor
1410 .active_window_mut()
1411 .action_to_events(Action::MoveRight);
1412 assert!(events.is_some());
1413
1414 let events = events.unwrap();
1415 assert_eq!(events.len(), 1);
1416
1417 match &events[0] {
1418 Event::MoveCursor {
1419 new_position,
1420 new_anchor,
1421 ..
1422 } => {
1423 assert_eq!(*new_position, 5);
1425 assert_eq!(*new_anchor, None); }
1427 _ => panic!("Expected MoveCursor event"),
1428 }
1429 }
1430
1431 #[test]
1432 fn test_action_to_events_move_up_down() {
1433 let config = Config::default();
1434 let (dir_context, _temp) = test_dir_context();
1435 let mut editor = Editor::new(
1436 config,
1437 80,
1438 24,
1439 dir_context,
1440 crate::view::color_support::ColorCapability::TrueColor,
1441 test_filesystem(),
1442 )
1443 .unwrap();
1444
1445 let cursor_id = editor.active_cursors().primary_id();
1447 editor.apply_event_to_active_buffer(&Event::Insert {
1448 position: 0,
1449 text: "line1\nline2\nline3".to_string(),
1450 cursor_id,
1451 });
1452
1453 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1455 cursor_id,
1456 old_position: 0, new_position: 6,
1458 old_anchor: None, new_anchor: None,
1460 old_sticky_column: 0,
1461 new_sticky_column: 0,
1462 });
1463
1464 let events = editor.active_window_mut().action_to_events(Action::MoveUp);
1466 assert!(events.is_some());
1467 let events = events.unwrap();
1468 assert_eq!(events.len(), 1);
1469
1470 match &events[0] {
1471 Event::MoveCursor { new_position, .. } => {
1472 assert_eq!(*new_position, 0); }
1474 _ => panic!("Expected MoveCursor event"),
1475 }
1476 }
1477
1478 #[test]
1479 fn test_action_to_events_insert_newline() {
1480 let config = Config::default();
1481 let (dir_context, _temp) = test_dir_context();
1482 let mut editor = Editor::new(
1483 config,
1484 80,
1485 24,
1486 dir_context,
1487 crate::view::color_support::ColorCapability::TrueColor,
1488 test_filesystem(),
1489 )
1490 .unwrap();
1491
1492 let events = editor
1493 .active_window_mut()
1494 .action_to_events(Action::InsertNewline);
1495 assert!(events.is_some());
1496
1497 let events = events.unwrap();
1498 assert_eq!(events.len(), 1);
1499
1500 match &events[0] {
1501 Event::Insert { text, .. } => {
1502 assert_eq!(text, "\n");
1503 }
1504 _ => panic!("Expected Insert event"),
1505 }
1506 }
1507
1508 #[test]
1509 fn test_action_to_events_unimplemented() {
1510 let config = Config::default();
1511 let (dir_context, _temp) = test_dir_context();
1512 let mut editor = Editor::new(
1513 config,
1514 80,
1515 24,
1516 dir_context,
1517 crate::view::color_support::ColorCapability::TrueColor,
1518 test_filesystem(),
1519 )
1520 .unwrap();
1521
1522 assert!(editor
1524 .active_window_mut()
1525 .action_to_events(Action::Save)
1526 .is_none());
1527 assert!(editor
1528 .active_window_mut()
1529 .action_to_events(Action::Quit)
1530 .is_none());
1531 assert!(editor
1532 .active_window_mut()
1533 .action_to_events(Action::Undo)
1534 .is_none());
1535 }
1536
1537 #[test]
1538 fn test_action_to_events_delete_backward() {
1539 let config = Config::default();
1540 let (dir_context, _temp) = test_dir_context();
1541 let mut editor = Editor::new(
1542 config,
1543 80,
1544 24,
1545 dir_context,
1546 crate::view::color_support::ColorCapability::TrueColor,
1547 test_filesystem(),
1548 )
1549 .unwrap();
1550
1551 let cursor_id = editor.active_cursors().primary_id();
1553 editor.apply_event_to_active_buffer(&Event::Insert {
1554 position: 0,
1555 text: "hello".to_string(),
1556 cursor_id,
1557 });
1558
1559 let events = editor
1560 .active_window_mut()
1561 .action_to_events(Action::DeleteBackward);
1562 assert!(events.is_some());
1563
1564 let events = events.unwrap();
1565 assert_eq!(events.len(), 1);
1566
1567 match &events[0] {
1568 Event::Delete {
1569 range,
1570 deleted_text,
1571 ..
1572 } => {
1573 assert_eq!(range.clone(), 4..5); assert_eq!(deleted_text, "o");
1575 }
1576 _ => panic!("Expected Delete event"),
1577 }
1578 }
1579
1580 #[test]
1581 fn test_action_to_events_delete_forward() {
1582 let config = Config::default();
1583 let (dir_context, _temp) = test_dir_context();
1584 let mut editor = Editor::new(
1585 config,
1586 80,
1587 24,
1588 dir_context,
1589 crate::view::color_support::ColorCapability::TrueColor,
1590 test_filesystem(),
1591 )
1592 .unwrap();
1593
1594 let cursor_id = editor.active_cursors().primary_id();
1596 editor.apply_event_to_active_buffer(&Event::Insert {
1597 position: 0,
1598 text: "hello".to_string(),
1599 cursor_id,
1600 });
1601
1602 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1604 cursor_id,
1605 old_position: 0, new_position: 0,
1607 old_anchor: None, new_anchor: None,
1609 old_sticky_column: 0,
1610 new_sticky_column: 0,
1611 });
1612
1613 let events = editor
1614 .active_window_mut()
1615 .action_to_events(Action::DeleteForward);
1616 assert!(events.is_some());
1617
1618 let events = events.unwrap();
1619 assert_eq!(events.len(), 1);
1620
1621 match &events[0] {
1622 Event::Delete {
1623 range,
1624 deleted_text,
1625 ..
1626 } => {
1627 assert_eq!(range.clone(), 0..1); assert_eq!(deleted_text, "h");
1629 }
1630 _ => panic!("Expected Delete event"),
1631 }
1632 }
1633
1634 #[test]
1635 fn test_action_to_events_select_right() {
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 cursor_id = editor.active_cursors().primary_id();
1650 editor.apply_event_to_active_buffer(&Event::Insert {
1651 position: 0,
1652 text: "hello".to_string(),
1653 cursor_id,
1654 });
1655
1656 editor.apply_event_to_active_buffer(&Event::MoveCursor {
1658 cursor_id,
1659 old_position: 0, new_position: 0,
1661 old_anchor: None, new_anchor: None,
1663 old_sticky_column: 0,
1664 new_sticky_column: 0,
1665 });
1666
1667 let events = editor
1668 .active_window_mut()
1669 .action_to_events(Action::SelectRight);
1670 assert!(events.is_some());
1671
1672 let events = events.unwrap();
1673 assert_eq!(events.len(), 1);
1674
1675 match &events[0] {
1676 Event::MoveCursor {
1677 new_position,
1678 new_anchor,
1679 ..
1680 } => {
1681 assert_eq!(*new_position, 1); assert_eq!(*new_anchor, Some(0)); }
1684 _ => panic!("Expected MoveCursor event"),
1685 }
1686 }
1687
1688 #[test]
1689 fn test_action_to_events_select_all() {
1690 let config = Config::default();
1691 let (dir_context, _temp) = test_dir_context();
1692 let mut editor = Editor::new(
1693 config,
1694 80,
1695 24,
1696 dir_context,
1697 crate::view::color_support::ColorCapability::TrueColor,
1698 test_filesystem(),
1699 )
1700 .unwrap();
1701
1702 let cursor_id = editor.active_cursors().primary_id();
1704 editor.apply_event_to_active_buffer(&Event::Insert {
1705 position: 0,
1706 text: "hello world".to_string(),
1707 cursor_id,
1708 });
1709
1710 let events = editor
1711 .active_window_mut()
1712 .action_to_events(Action::SelectAll);
1713 assert!(events.is_some());
1714
1715 let events = events.unwrap();
1716 assert_eq!(events.len(), 1);
1717
1718 match &events[0] {
1719 Event::MoveCursor {
1720 new_position,
1721 new_anchor,
1722 ..
1723 } => {
1724 assert_eq!(*new_position, 11); assert_eq!(*new_anchor, Some(0)); }
1727 _ => panic!("Expected MoveCursor event"),
1728 }
1729 }
1730
1731 #[test]
1732 fn test_action_to_events_document_nav() {
1733 let config = Config::default();
1734 let (dir_context, _temp) = test_dir_context();
1735 let mut editor = Editor::new(
1736 config,
1737 80,
1738 24,
1739 dir_context,
1740 crate::view::color_support::ColorCapability::TrueColor,
1741 test_filesystem(),
1742 )
1743 .unwrap();
1744
1745 let cursor_id = editor.active_cursors().primary_id();
1747 editor.apply_event_to_active_buffer(&Event::Insert {
1748 position: 0,
1749 text: "line1\nline2\nline3".to_string(),
1750 cursor_id,
1751 });
1752
1753 let events = editor
1755 .active_window_mut()
1756 .action_to_events(Action::MoveDocumentStart);
1757 assert!(events.is_some());
1758 let events = events.unwrap();
1759 match &events[0] {
1760 Event::MoveCursor { new_position, .. } => {
1761 assert_eq!(*new_position, 0);
1762 }
1763 _ => panic!("Expected MoveCursor event"),
1764 }
1765
1766 let events = editor
1768 .active_window_mut()
1769 .action_to_events(Action::MoveDocumentEnd);
1770 assert!(events.is_some());
1771 let events = events.unwrap();
1772 match &events[0] {
1773 Event::MoveCursor { new_position, .. } => {
1774 assert_eq!(*new_position, 17); }
1776 _ => panic!("Expected MoveCursor event"),
1777 }
1778 }
1779
1780 #[test]
1781 fn test_action_to_events_remove_secondary_cursors() {
1782 use crate::model::event::CursorId;
1783
1784 let config = Config::default();
1785 let (dir_context, _temp) = test_dir_context();
1786 let mut editor = Editor::new(
1787 config,
1788 80,
1789 24,
1790 dir_context,
1791 crate::view::color_support::ColorCapability::TrueColor,
1792 test_filesystem(),
1793 )
1794 .unwrap();
1795
1796 let cursor_id = editor.active_cursors().primary_id();
1798 editor.apply_event_to_active_buffer(&Event::Insert {
1799 position: 0,
1800 text: "hello world test".to_string(),
1801 cursor_id,
1802 });
1803
1804 editor.apply_event_to_active_buffer(&Event::AddCursor {
1806 cursor_id: CursorId(1),
1807 position: 5,
1808 anchor: None,
1809 });
1810 editor.apply_event_to_active_buffer(&Event::AddCursor {
1811 cursor_id: CursorId(2),
1812 position: 10,
1813 anchor: None,
1814 });
1815
1816 assert_eq!(editor.active_cursors().count(), 3);
1817
1818 let first_id = editor
1820 .active_cursors()
1821 .iter()
1822 .map(|(id, _)| id)
1823 .min_by_key(|id| id.0)
1824 .expect("Should have at least one cursor");
1825
1826 let events = editor
1828 .active_window_mut()
1829 .action_to_events(Action::RemoveSecondaryCursors);
1830 assert!(events.is_some());
1831
1832 let events = events.unwrap();
1833 let remove_cursor_events: Vec<_> = events
1836 .iter()
1837 .filter_map(|e| match e {
1838 Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
1839 _ => None,
1840 })
1841 .collect();
1842
1843 assert_eq!(remove_cursor_events.len(), 2);
1845
1846 for cursor_id in &remove_cursor_events {
1847 assert_ne!(*cursor_id, first_id);
1849 }
1850 }
1851
1852 #[test]
1853 fn test_action_to_events_scroll() {
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 events = editor
1868 .active_window_mut()
1869 .action_to_events(Action::ScrollUp);
1870 assert!(events.is_some());
1871 let events = events.unwrap();
1872 assert_eq!(events.len(), 1);
1873 match &events[0] {
1874 Event::Scroll { line_offset } => {
1875 assert_eq!(*line_offset, -1);
1876 }
1877 _ => panic!("Expected Scroll event"),
1878 }
1879
1880 let events = editor
1882 .active_window_mut()
1883 .action_to_events(Action::ScrollDown);
1884 assert!(events.is_some());
1885 let events = events.unwrap();
1886 assert_eq!(events.len(), 1);
1887 match &events[0] {
1888 Event::Scroll { line_offset } => {
1889 assert_eq!(*line_offset, 1);
1890 }
1891 _ => panic!("Expected Scroll event"),
1892 }
1893 }
1894
1895 #[test]
1896 fn test_action_to_events_none() {
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 events = editor.active_window_mut().action_to_events(Action::None);
1911 assert!(events.is_none());
1912 }
1913
1914 #[test]
1915 fn test_lsp_incremental_insert_generates_correct_range() {
1916 use crate::model::buffer::Buffer;
1919
1920 let buffer = Buffer::from_str_test("hello\nworld");
1921
1922 let position = 0;
1925 let (line, character) = buffer.position_to_lsp_position(position);
1926
1927 assert_eq!(line, 0, "Insertion at start should be line 0");
1928 assert_eq!(character, 0, "Insertion at start should be char 0");
1929
1930 let lsp_pos = Position::new(line as u32, character as u32);
1932 let lsp_range = LspRange::new(lsp_pos, lsp_pos);
1933
1934 assert_eq!(lsp_range.start.line, 0);
1935 assert_eq!(lsp_range.start.character, 0);
1936 assert_eq!(lsp_range.end.line, 0);
1937 assert_eq!(lsp_range.end.character, 0);
1938 assert_eq!(
1939 lsp_range.start, lsp_range.end,
1940 "Insert should have zero-width range"
1941 );
1942
1943 let position = 3;
1945 let (line, character) = buffer.position_to_lsp_position(position);
1946
1947 assert_eq!(line, 0);
1948 assert_eq!(character, 3);
1949
1950 let position = 6;
1952 let (line, character) = buffer.position_to_lsp_position(position);
1953
1954 assert_eq!(line, 1, "Position after newline should be line 1");
1955 assert_eq!(character, 0, "Position at start of line 2 should be char 0");
1956 }
1957
1958 #[test]
1959 fn test_lsp_incremental_delete_generates_correct_range() {
1960 use crate::model::buffer::Buffer;
1963
1964 let buffer = Buffer::from_str_test("hello\nworld");
1965
1966 let range_start = 1;
1968 let range_end = 5;
1969
1970 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
1971 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
1972
1973 assert_eq!(start_line, 0);
1974 assert_eq!(start_char, 1);
1975 assert_eq!(end_line, 0);
1976 assert_eq!(end_char, 5);
1977
1978 let lsp_range = LspRange::new(
1979 Position::new(start_line as u32, start_char as u32),
1980 Position::new(end_line as u32, end_char as u32),
1981 );
1982
1983 assert_eq!(lsp_range.start.line, 0);
1984 assert_eq!(lsp_range.start.character, 1);
1985 assert_eq!(lsp_range.end.line, 0);
1986 assert_eq!(lsp_range.end.character, 5);
1987 assert_ne!(
1988 lsp_range.start, lsp_range.end,
1989 "Delete should have non-zero range"
1990 );
1991
1992 let range_start = 4;
1994 let range_end = 8;
1995
1996 let (start_line, start_char) = buffer.position_to_lsp_position(range_start);
1997 let (end_line, end_char) = buffer.position_to_lsp_position(range_end);
1998
1999 assert_eq!(start_line, 0, "Delete start on line 0");
2000 assert_eq!(start_char, 4, "Delete start at char 4");
2001 assert_eq!(end_line, 1, "Delete end on line 1");
2002 assert_eq!(end_char, 2, "Delete end at char 2 of line 1");
2003 }
2004
2005 #[test]
2006 fn test_lsp_incremental_utf16_encoding() {
2007 use crate::model::buffer::Buffer;
2010
2011 let buffer = Buffer::from_str_test("😀hello");
2013
2014 let (line, character) = buffer.position_to_lsp_position(4);
2016
2017 assert_eq!(line, 0);
2018 assert_eq!(character, 2, "Emoji should count as 2 UTF-16 code units");
2019
2020 let (line, character) = buffer.position_to_lsp_position(9);
2022
2023 assert_eq!(line, 0);
2024 assert_eq!(
2025 character, 7,
2026 "Should be 2 (emoji) + 5 (text) = 7 UTF-16 code units"
2027 );
2028
2029 let buffer = Buffer::from_str_test("café");
2031
2032 let (line, character) = buffer.position_to_lsp_position(3);
2034
2035 assert_eq!(line, 0);
2036 assert_eq!(character, 3);
2037
2038 let (line, character) = buffer.position_to_lsp_position(5);
2040
2041 assert_eq!(line, 0);
2042 assert_eq!(character, 4, "é should count as 1 UTF-16 code unit");
2043 }
2044
2045 #[test]
2046 fn test_lsp_content_change_event_structure() {
2047 let insert_change = TextDocumentContentChangeEvent {
2051 range: Some(LspRange::new(Position::new(0, 5), Position::new(0, 5))),
2052 range_length: None,
2053 text: "NEW".to_string(),
2054 };
2055
2056 assert!(insert_change.range.is_some());
2057 assert_eq!(insert_change.text, "NEW");
2058 let range = insert_change.range.unwrap();
2059 assert_eq!(
2060 range.start, range.end,
2061 "Insert should have zero-width range"
2062 );
2063
2064 let delete_change = TextDocumentContentChangeEvent {
2066 range: Some(LspRange::new(Position::new(0, 2), Position::new(0, 7))),
2067 range_length: None,
2068 text: String::new(),
2069 };
2070
2071 assert!(delete_change.range.is_some());
2072 assert_eq!(delete_change.text, "");
2073 let range = delete_change.range.unwrap();
2074 assert_ne!(range.start, range.end, "Delete should have non-zero range");
2075 assert_eq!(range.start.line, 0);
2076 assert_eq!(range.start.character, 2);
2077 assert_eq!(range.end.line, 0);
2078 assert_eq!(range.end.character, 7);
2079 }
2080
2081 #[test]
2082 fn test_goto_matching_bracket_forward() {
2083 let config = Config::default();
2084 let (dir_context, _temp) = test_dir_context();
2085 let mut editor = Editor::new(
2086 config,
2087 80,
2088 24,
2089 dir_context,
2090 crate::view::color_support::ColorCapability::TrueColor,
2091 test_filesystem(),
2092 )
2093 .unwrap();
2094
2095 let cursor_id = editor.active_cursors().primary_id();
2097 editor.apply_event_to_active_buffer(&Event::Insert {
2098 position: 0,
2099 text: "fn main() { let x = (1 + 2); }".to_string(),
2100 cursor_id,
2101 });
2102
2103 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2105 cursor_id,
2106 old_position: 31,
2107 new_position: 10,
2108 old_anchor: None,
2109 new_anchor: None,
2110 old_sticky_column: 0,
2111 new_sticky_column: 0,
2112 });
2113
2114 assert_eq!(editor.active_cursors().primary().position, 10);
2115
2116 editor.goto_matching_bracket();
2118
2119 assert_eq!(editor.active_cursors().primary().position, 29);
2124 }
2125
2126 #[test]
2127 fn test_goto_matching_bracket_backward() {
2128 let config = Config::default();
2129 let (dir_context, _temp) = test_dir_context();
2130 let mut editor = Editor::new(
2131 config,
2132 80,
2133 24,
2134 dir_context,
2135 crate::view::color_support::ColorCapability::TrueColor,
2136 test_filesystem(),
2137 )
2138 .unwrap();
2139
2140 let cursor_id = editor.active_cursors().primary_id();
2142 editor.apply_event_to_active_buffer(&Event::Insert {
2143 position: 0,
2144 text: "fn main() { let x = (1 + 2); }".to_string(),
2145 cursor_id,
2146 });
2147
2148 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2150 cursor_id,
2151 old_position: 31,
2152 new_position: 26,
2153 old_anchor: None,
2154 new_anchor: None,
2155 old_sticky_column: 0,
2156 new_sticky_column: 0,
2157 });
2158
2159 editor.goto_matching_bracket();
2161
2162 assert_eq!(editor.active_cursors().primary().position, 20);
2164 }
2165
2166 #[test]
2167 fn test_goto_matching_bracket_nested() {
2168 let config = Config::default();
2169 let (dir_context, _temp) = test_dir_context();
2170 let mut editor = Editor::new(
2171 config,
2172 80,
2173 24,
2174 dir_context,
2175 crate::view::color_support::ColorCapability::TrueColor,
2176 test_filesystem(),
2177 )
2178 .unwrap();
2179
2180 let cursor_id = editor.active_cursors().primary_id();
2182 editor.apply_event_to_active_buffer(&Event::Insert {
2183 position: 0,
2184 text: "{a{b{c}d}e}".to_string(),
2185 cursor_id,
2186 });
2187
2188 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2190 cursor_id,
2191 old_position: 11,
2192 new_position: 0,
2193 old_anchor: None,
2194 new_anchor: None,
2195 old_sticky_column: 0,
2196 new_sticky_column: 0,
2197 });
2198
2199 editor.goto_matching_bracket();
2201
2202 assert_eq!(editor.active_cursors().primary().position, 10);
2204 }
2205
2206 #[test]
2207 fn test_search_case_sensitive() {
2208 let config = Config::default();
2209 let (dir_context, _temp) = test_dir_context();
2210 let mut editor = Editor::new(
2211 config,
2212 80,
2213 24,
2214 dir_context,
2215 crate::view::color_support::ColorCapability::TrueColor,
2216 test_filesystem(),
2217 )
2218 .unwrap();
2219
2220 let cursor_id = editor.active_cursors().primary_id();
2222 editor.apply_event_to_active_buffer(&Event::Insert {
2223 position: 0,
2224 text: "Hello hello HELLO".to_string(),
2225 cursor_id,
2226 });
2227
2228 editor.active_window_mut().search_case_sensitive = false;
2230 editor.perform_search("hello");
2231
2232 let search_state = editor.active_window().search_state.as_ref().unwrap();
2233 assert_eq!(
2234 search_state.matches.len(),
2235 3,
2236 "Should find all 3 matches case-insensitively"
2237 );
2238
2239 editor.active_window_mut().search_case_sensitive = true;
2241 editor.perform_search("hello");
2242
2243 let search_state = editor.active_window().search_state.as_ref().unwrap();
2244 assert_eq!(
2245 search_state.matches.len(),
2246 1,
2247 "Should find only 1 exact match"
2248 );
2249 assert_eq!(
2250 search_state.matches[0], 6,
2251 "Should find 'hello' at position 6"
2252 );
2253 }
2254
2255 #[test]
2256 fn test_search_whole_word() {
2257 let config = Config::default();
2258 let (dir_context, _temp) = test_dir_context();
2259 let mut editor = Editor::new(
2260 config,
2261 80,
2262 24,
2263 dir_context,
2264 crate::view::color_support::ColorCapability::TrueColor,
2265 test_filesystem(),
2266 )
2267 .unwrap();
2268
2269 let cursor_id = editor.active_cursors().primary_id();
2271 editor.apply_event_to_active_buffer(&Event::Insert {
2272 position: 0,
2273 text: "test testing tested attest test".to_string(),
2274 cursor_id,
2275 });
2276
2277 editor.active_window_mut().search_whole_word = false;
2279 editor.active_window_mut().search_case_sensitive = true;
2280 editor.perform_search("test");
2281
2282 let search_state = editor.active_window().search_state.as_ref().unwrap();
2283 assert_eq!(
2284 search_state.matches.len(),
2285 5,
2286 "Should find 'test' in all occurrences"
2287 );
2288
2289 editor.active_window_mut().search_whole_word = true;
2291 editor.perform_search("test");
2292
2293 let search_state = editor.active_window().search_state.as_ref().unwrap();
2294 assert_eq!(
2295 search_state.matches.len(),
2296 2,
2297 "Should find only whole word 'test'"
2298 );
2299 assert_eq!(search_state.matches[0], 0, "First match at position 0");
2300 assert_eq!(search_state.matches[1], 27, "Second match at position 27");
2301 }
2302
2303 #[test]
2304 fn test_search_scan_completes_when_capped() {
2305 let config = Config::default();
2311 let (dir_context, _temp) = test_dir_context();
2312 let mut editor = Editor::new(
2313 config,
2314 80,
2315 24,
2316 dir_context,
2317 crate::view::color_support::ColorCapability::TrueColor,
2318 test_filesystem(),
2319 )
2320 .unwrap();
2321
2322 let buffer_id = editor.active_buffer();
2325 let regex = regex::bytes::Regex::new("test").unwrap();
2326 let fake_chunks = vec![
2327 crate::model::buffer::LineScanChunk {
2328 leaf_index: 0,
2329 byte_len: 100,
2330 already_known: true,
2331 },
2332 crate::model::buffer::LineScanChunk {
2333 leaf_index: 1,
2334 byte_len: 100,
2335 already_known: true,
2336 },
2337 ];
2338
2339 let chunked = crate::model::buffer::ChunkedSearchState {
2340 chunks: fake_chunks,
2341 next_chunk: 1, next_doc_offset: 100,
2343 total_bytes: 200,
2344 scanned_bytes: 100,
2345 regex,
2346 matches: vec![
2347 crate::model::buffer::SearchMatch {
2348 byte_offset: 10,
2349 length: 4,
2350 line: 1,
2351 column: 11,
2352 context: String::new(),
2353 },
2354 crate::model::buffer::SearchMatch {
2355 byte_offset: 50,
2356 length: 4,
2357 line: 1,
2358 column: 51,
2359 context: String::new(),
2360 },
2361 ],
2362 overlap_tail: Vec::new(),
2363 overlap_doc_offset: 0,
2364 max_matches: 10_000,
2365 capped: true, query_len: 4,
2367 running_line: 1,
2368 };
2369
2370 editor.active_window_mut().search_scan.start(
2371 buffer_id,
2372 Vec::new(),
2373 chunked,
2374 "test".to_string(),
2375 None,
2376 false,
2377 false,
2378 false,
2379 );
2380
2381 let result = editor.process_search_scan();
2383 assert!(
2384 result,
2385 "process_search_scan should return true (needs render)"
2386 );
2387
2388 assert_eq!(
2390 editor.active_window().search_scan.buffer_id(),
2391 None,
2392 "search_scan should be drained after capped scan completes"
2393 );
2394
2395 let search_state = editor
2397 .active_window()
2398 .search_state
2399 .as_ref()
2400 .expect("search_state should be set after scan finishes");
2401 assert_eq!(search_state.matches.len(), 2, "Should have 2 matches");
2402 assert_eq!(search_state.query, "test");
2403 assert!(
2404 search_state.capped,
2405 "search_state should be marked as capped"
2406 );
2407 }
2408
2409 #[test]
2410 fn test_bookmarks() {
2411 let config = Config::default();
2412 let (dir_context, _temp) = test_dir_context();
2413 let mut editor = Editor::new(
2414 config,
2415 80,
2416 24,
2417 dir_context,
2418 crate::view::color_support::ColorCapability::TrueColor,
2419 test_filesystem(),
2420 )
2421 .unwrap();
2422
2423 let cursor_id = editor.active_cursors().primary_id();
2425 editor.apply_event_to_active_buffer(&Event::Insert {
2426 position: 0,
2427 text: "Line 1\nLine 2\nLine 3".to_string(),
2428 cursor_id,
2429 });
2430
2431 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2433 cursor_id,
2434 old_position: 21,
2435 new_position: 7,
2436 old_anchor: None,
2437 new_anchor: None,
2438 old_sticky_column: 0,
2439 new_sticky_column: 0,
2440 });
2441
2442 editor.active_window_mut().set_bookmark('1');
2444 assert_eq!(
2445 editor
2446 .active_window()
2447 .bookmarks
2448 .get('1')
2449 .map(|b| b.position),
2450 Some(7)
2451 );
2452
2453 editor.apply_event_to_active_buffer(&Event::MoveCursor {
2455 cursor_id,
2456 old_position: 7,
2457 new_position: 14,
2458 old_anchor: None,
2459 new_anchor: None,
2460 old_sticky_column: 0,
2461 new_sticky_column: 0,
2462 });
2463
2464 editor.jump_to_bookmark('1');
2466 assert_eq!(editor.active_cursors().primary().position, 7);
2467
2468 editor.active_window_mut().clear_bookmark('1');
2470 assert_eq!(editor.active_window().bookmarks.get('1'), None);
2471 }
2472
2473 #[test]
2474 fn test_action_enum_new_variants() {
2475 use serde_json::json;
2477
2478 let args = HashMap::new();
2479 assert_eq!(
2480 Action::from_str("smart_home", &args),
2481 Some(Action::SmartHome)
2482 );
2483 assert_eq!(
2484 Action::from_str("dedent_selection", &args),
2485 Some(Action::DedentSelection)
2486 );
2487 assert_eq!(
2488 Action::from_str("toggle_comment", &args),
2489 Some(Action::ToggleComment)
2490 );
2491 assert_eq!(
2492 Action::from_str("goto_matching_bracket", &args),
2493 Some(Action::GoToMatchingBracket)
2494 );
2495 assert_eq!(
2496 Action::from_str("list_bookmarks", &args),
2497 Some(Action::ListBookmarks)
2498 );
2499 assert_eq!(
2500 Action::from_str("toggle_search_case_sensitive", &args),
2501 Some(Action::ToggleSearchCaseSensitive)
2502 );
2503 assert_eq!(
2504 Action::from_str("toggle_search_whole_word", &args),
2505 Some(Action::ToggleSearchWholeWord)
2506 );
2507
2508 let mut args_with_char = HashMap::new();
2510 args_with_char.insert("char".to_string(), json!("5"));
2511 assert_eq!(
2512 Action::from_str("set_bookmark", &args_with_char),
2513 Some(Action::SetBookmark('5'))
2514 );
2515 assert_eq!(
2516 Action::from_str("jump_to_bookmark", &args_with_char),
2517 Some(Action::JumpToBookmark('5'))
2518 );
2519 assert_eq!(
2520 Action::from_str("clear_bookmark", &args_with_char),
2521 Some(Action::ClearBookmark('5'))
2522 );
2523 }
2524
2525 #[test]
2526 fn test_keybinding_new_defaults() {
2527 use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
2528
2529 let mut config = Config::default();
2533 config.active_keybinding_map = crate::config::KeybindingMapName("default".to_string());
2534 let resolver = KeybindingResolver::new(&config);
2535
2536 let event = KeyEvent {
2538 code: KeyCode::Char('/'),
2539 modifiers: KeyModifiers::CONTROL,
2540 kind: KeyEventKind::Press,
2541 state: KeyEventState::NONE,
2542 };
2543 let action = resolver.resolve(&event, KeyContext::Normal);
2544 assert_eq!(action, Action::ToggleComment);
2545
2546 let event = KeyEvent {
2548 code: KeyCode::Char(']'),
2549 modifiers: KeyModifiers::CONTROL,
2550 kind: KeyEventKind::Press,
2551 state: KeyEventState::NONE,
2552 };
2553 let action = resolver.resolve(&event, KeyContext::Normal);
2554 assert_eq!(action, Action::GoToMatchingBracket);
2555
2556 let event = KeyEvent {
2558 code: KeyCode::Tab,
2559 modifiers: KeyModifiers::SHIFT,
2560 kind: KeyEventKind::Press,
2561 state: KeyEventState::NONE,
2562 };
2563 let action = resolver.resolve(&event, KeyContext::Normal);
2564 assert_eq!(action, Action::DedentSelection);
2565
2566 let event = KeyEvent {
2568 code: KeyCode::Char('g'),
2569 modifiers: KeyModifiers::CONTROL,
2570 kind: KeyEventKind::Press,
2571 state: KeyEventState::NONE,
2572 };
2573 let action = resolver.resolve(&event, KeyContext::Normal);
2574 assert_eq!(action, Action::GotoLine);
2575
2576 let event = KeyEvent {
2578 code: KeyCode::Char('5'),
2579 modifiers: KeyModifiers::CONTROL | KeyModifiers::SHIFT,
2580 kind: KeyEventKind::Press,
2581 state: KeyEventState::NONE,
2582 };
2583 let action = resolver.resolve(&event, KeyContext::Normal);
2584 assert_eq!(action, Action::SetBookmark('5'));
2585
2586 let event = KeyEvent {
2587 code: KeyCode::Char('5'),
2588 modifiers: KeyModifiers::ALT,
2589 kind: KeyEventKind::Press,
2590 state: KeyEventState::NONE,
2591 };
2592 let action = resolver.resolve(&event, KeyContext::Normal);
2593 assert_eq!(action, Action::JumpToBookmark('5'));
2594 }
2595
2596 #[test]
2608 fn test_lsp_rename_didchange_positions_bug() {
2609 use crate::model::buffer::Buffer;
2610
2611 let config = Config::default();
2612 let (dir_context, _temp) = test_dir_context();
2613 let mut editor = Editor::new(
2614 config,
2615 80,
2616 24,
2617 dir_context,
2618 crate::view::color_support::ColorCapability::TrueColor,
2619 test_filesystem(),
2620 )
2621 .unwrap();
2622
2623 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2627 editor.active_state_mut().buffer =
2628 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2629
2630 let cursor_id = editor.active_cursors().primary_id();
2635
2636 let batch = Event::Batch {
2637 events: vec![
2638 Event::Delete {
2640 range: 23..26, deleted_text: "val".to_string(),
2642 cursor_id,
2643 },
2644 Event::Insert {
2645 position: 23,
2646 text: "value".to_string(),
2647 cursor_id,
2648 },
2649 Event::Delete {
2651 range: 7..10, deleted_text: "val".to_string(),
2653 cursor_id,
2654 },
2655 Event::Insert {
2656 position: 7,
2657 text: "value".to_string(),
2658 cursor_id,
2659 },
2660 ],
2661 description: "LSP Rename".to_string(),
2662 };
2663
2664 let lsp_changes_before = editor.active_window().collect_lsp_changes(&batch);
2666
2667 editor.apply_event_to_active_buffer(&batch);
2669
2670 let lsp_changes_after = editor.active_window().collect_lsp_changes(&batch);
2673
2674 let final_content = editor.active_state().buffer.to_string().unwrap();
2676 assert_eq!(
2677 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2678 "Buffer should have 'value' in both places"
2679 );
2680
2681 assert_eq!(lsp_changes_before.len(), 4, "Should have 4 changes");
2687
2688 let first_delete = &lsp_changes_before[0];
2689 let first_del_range = first_delete.range.unwrap();
2690 assert_eq!(
2691 first_del_range.start.line, 1,
2692 "First delete should be on line 1 (BEFORE)"
2693 );
2694 assert_eq!(
2695 first_del_range.start.character, 4,
2696 "First delete start should be at char 4 (BEFORE)"
2697 );
2698
2699 assert_eq!(lsp_changes_after.len(), 4, "Should have 4 changes");
2705
2706 let first_delete_after = &lsp_changes_after[0];
2707 let first_del_range_after = first_delete_after.range.unwrap();
2708
2709 eprintln!("BEFORE modification:");
2712 eprintln!(
2713 " Delete at line {}, char {}-{}",
2714 first_del_range.start.line,
2715 first_del_range.start.character,
2716 first_del_range.end.character
2717 );
2718 eprintln!("AFTER modification:");
2719 eprintln!(
2720 " Delete at line {}, char {}-{}",
2721 first_del_range_after.start.line,
2722 first_del_range_after.start.character,
2723 first_del_range_after.end.character
2724 );
2725
2726 assert_ne!(
2744 first_del_range_after.end.character, first_del_range.end.character,
2745 "BUG CONFIRMED: LSP positions are different when calculated after buffer modification!"
2746 );
2747
2748 eprintln!("\n=== BUG DEMONSTRATED ===");
2749 eprintln!("When collect_lsp_changes() is called AFTER buffer modification,");
2750 eprintln!("the positions are WRONG because they're calculated from the");
2751 eprintln!("modified buffer, not the original buffer.");
2752 eprintln!("This causes the second rename to fail with 'content modified' error.");
2753 eprintln!("========================\n");
2754 }
2755
2756 #[test]
2757 fn test_lsp_rename_preserves_cursor_position() {
2758 use crate::model::buffer::Buffer;
2759
2760 let config = Config::default();
2761 let (dir_context, _temp) = test_dir_context();
2762 let mut editor = Editor::new(
2763 config,
2764 80,
2765 24,
2766 dir_context,
2767 crate::view::color_support::ColorCapability::TrueColor,
2768 test_filesystem(),
2769 )
2770 .unwrap();
2771
2772 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2776 editor.active_state_mut().buffer =
2777 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2778
2779 let original_cursor_pos = 23;
2781 editor.active_cursors_mut().primary_mut().position = original_cursor_pos;
2782
2783 let buffer_text = editor.active_state().buffer.to_string().unwrap();
2785 let text_at_cursor = buffer_text[original_cursor_pos..original_cursor_pos + 3].to_string();
2786 assert_eq!(text_at_cursor, "val", "Cursor should be at 'val'");
2787
2788 let cursor_id = editor.active_cursors().primary_id();
2791 let buffer_id = editor.active_buffer();
2792
2793 let 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
2818 editor
2820 .apply_events_to_buffer_as_bulk_edit(buffer_id, events, "LSP Rename".to_string())
2821 .unwrap();
2822
2823 let final_content = editor.active_state().buffer.to_string().unwrap();
2825 assert_eq!(
2826 final_content, "fn foo(value: i32) {\n value + 1\n}\n",
2827 "Buffer should have 'value' in both places"
2828 );
2829
2830 let final_cursor_pos = editor.active_cursors().primary().position;
2838 let expected_cursor_pos = 25; assert_eq!(
2841 final_cursor_pos, expected_cursor_pos,
2842 "Cursor should be at position {} (start of 'value' on line 1), but was at {}. \
2843 Original pos: {}, expected adjustment: +2 for first rename",
2844 expected_cursor_pos, final_cursor_pos, original_cursor_pos
2845 );
2846
2847 let text_at_new_cursor = &final_content[final_cursor_pos..final_cursor_pos + 5];
2849 assert_eq!(
2850 text_at_new_cursor, "value",
2851 "Cursor should be at the start of 'value' after rename"
2852 );
2853 }
2854
2855 #[test]
2856 fn test_lsp_rename_twice_consecutive() {
2857 use crate::model::buffer::Buffer;
2860
2861 let config = Config::default();
2862 let (dir_context, _temp) = test_dir_context();
2863 let mut editor = Editor::new(
2864 config,
2865 80,
2866 24,
2867 dir_context,
2868 crate::view::color_support::ColorCapability::TrueColor,
2869 test_filesystem(),
2870 )
2871 .unwrap();
2872
2873 let initial = "fn foo(val: i32) {\n val + 1\n}\n";
2875 editor.active_state_mut().buffer =
2876 Buffer::from_str(initial, 1024 * 1024, test_filesystem());
2877
2878 let cursor_id = editor.active_cursors().primary_id();
2879 let buffer_id = editor.active_buffer();
2880
2881 let events1 = vec![
2884 Event::Delete {
2886 range: 23..26,
2887 deleted_text: "val".to_string(),
2888 cursor_id,
2889 },
2890 Event::Insert {
2891 position: 23,
2892 text: "value".to_string(),
2893 cursor_id,
2894 },
2895 Event::Delete {
2897 range: 7..10,
2898 deleted_text: "val".to_string(),
2899 cursor_id,
2900 },
2901 Event::Insert {
2902 position: 7,
2903 text: "value".to_string(),
2904 cursor_id,
2905 },
2906 ];
2907
2908 let batch1 = Event::Batch {
2910 events: events1.clone(),
2911 description: "LSP Rename 1".to_string(),
2912 };
2913
2914 let lsp_changes1 = editor.active_window().collect_lsp_changes(&batch1);
2916
2917 assert_eq!(
2919 lsp_changes1.len(),
2920 4,
2921 "First rename should have 4 LSP changes"
2922 );
2923
2924 let first_del = &lsp_changes1[0];
2926 let first_del_range = first_del.range.unwrap();
2927 assert_eq!(first_del_range.start.line, 1, "First delete line");
2928 assert_eq!(
2929 first_del_range.start.character, 4,
2930 "First delete start char"
2931 );
2932 assert_eq!(first_del_range.end.character, 7, "First delete end char");
2933
2934 editor
2936 .apply_events_to_buffer_as_bulk_edit(buffer_id, events1, "LSP Rename 1".to_string())
2937 .unwrap();
2938
2939 let after_first = editor.active_state().buffer.to_string().unwrap();
2941 assert_eq!(
2942 after_first, "fn foo(value: i32) {\n value + 1\n}\n",
2943 "After first rename"
2944 );
2945
2946 let events2 = vec![
2956 Event::Delete {
2958 range: 25..30,
2959 deleted_text: "value".to_string(),
2960 cursor_id,
2961 },
2962 Event::Insert {
2963 position: 25,
2964 text: "x".to_string(),
2965 cursor_id,
2966 },
2967 Event::Delete {
2969 range: 7..12,
2970 deleted_text: "value".to_string(),
2971 cursor_id,
2972 },
2973 Event::Insert {
2974 position: 7,
2975 text: "x".to_string(),
2976 cursor_id,
2977 },
2978 ];
2979
2980 let batch2 = Event::Batch {
2982 events: events2.clone(),
2983 description: "LSP Rename 2".to_string(),
2984 };
2985
2986 let lsp_changes2 = editor.active_window().collect_lsp_changes(&batch2);
2988
2989 assert_eq!(
2993 lsp_changes2.len(),
2994 4,
2995 "Second rename should have 4 LSP changes"
2996 );
2997
2998 let second_first_del = &lsp_changes2[0];
3000 let second_first_del_range = second_first_del.range.unwrap();
3001 assert_eq!(
3002 second_first_del_range.start.line, 1,
3003 "Second rename first delete should be on line 1"
3004 );
3005 assert_eq!(
3006 second_first_del_range.start.character, 4,
3007 "Second rename first delete start should be at char 4"
3008 );
3009 assert_eq!(
3010 second_first_del_range.end.character, 9,
3011 "Second rename first delete end should be at char 9 (4 + 5 for 'value')"
3012 );
3013
3014 let second_third_del = &lsp_changes2[2];
3016 let second_third_del_range = second_third_del.range.unwrap();
3017 assert_eq!(
3018 second_third_del_range.start.line, 0,
3019 "Second rename third delete should be on line 0"
3020 );
3021 assert_eq!(
3022 second_third_del_range.start.character, 7,
3023 "Second rename third delete start should be at char 7"
3024 );
3025 assert_eq!(
3026 second_third_del_range.end.character, 12,
3027 "Second rename third delete end should be at char 12 (7 + 5 for 'value')"
3028 );
3029
3030 editor
3032 .apply_events_to_buffer_as_bulk_edit(buffer_id, events2, "LSP Rename 2".to_string())
3033 .unwrap();
3034
3035 let after_second = editor.active_state().buffer.to_string().unwrap();
3037 assert_eq!(
3038 after_second, "fn foo(x: i32) {\n x + 1\n}\n",
3039 "After second rename"
3040 );
3041 }
3042
3043 #[test]
3044 fn test_ensure_active_tab_visible_static_offset() {
3045 let config = Config::default();
3046 let (dir_context, _temp) = test_dir_context();
3047 let mut editor = Editor::new(
3048 config,
3049 80,
3050 24,
3051 dir_context,
3052 crate::view::color_support::ColorCapability::TrueColor,
3053 test_filesystem(),
3054 )
3055 .unwrap();
3056 let split_id = editor.split_manager().active_split();
3057
3058 let buf1 = editor.new_buffer();
3060 editor
3061 .buffers_mut()
3062 .get_mut(&buf1)
3063 .unwrap()
3064 .buffer
3065 .rename_file_path(std::path::PathBuf::from("aaa_long_name_01.txt"));
3066 let buf2 = editor.new_buffer();
3067 editor
3068 .buffers_mut()
3069 .get_mut(&buf2)
3070 .unwrap()
3071 .buffer
3072 .rename_file_path(std::path::PathBuf::from("bbb_long_name_02.txt"));
3073 let buf3 = editor.new_buffer();
3074 editor
3075 .buffers_mut()
3076 .get_mut(&buf3)
3077 .unwrap()
3078 .buffer
3079 .rename_file_path(std::path::PathBuf::from("ccc_long_name_03.txt"));
3080
3081 {
3082 use crate::view::split::TabTarget;
3083 let view_state = editor.split_view_states_mut().get_mut(&split_id).unwrap();
3084 view_state.open_buffers = vec![
3085 TabTarget::Buffer(buf1),
3086 TabTarget::Buffer(buf2),
3087 TabTarget::Buffer(buf3),
3088 ];
3089 view_state.tab_scroll_offset = 50;
3090 }
3091
3092 editor
3096 .active_window_mut()
3097 .ensure_active_tab_visible(split_id, buf1, 25);
3098 assert_eq!(
3099 editor
3100 .split_view_states()
3101 .get(&split_id)
3102 .unwrap()
3103 .tab_scroll_offset,
3104 0
3105 );
3106
3107 editor
3109 .active_window_mut()
3110 .ensure_active_tab_visible(split_id, buf3, 25);
3111 let view_state = editor.split_view_states().get(&split_id).unwrap();
3112 assert!(view_state.tab_scroll_offset > 0);
3113 let buffer_ids: Vec<_> = view_state.buffer_tab_ids_vec();
3114 let total_width: usize = buffer_ids
3115 .iter()
3116 .enumerate()
3117 .map(|(idx, id)| {
3118 let state = editor.buffers().get(id).unwrap();
3119 let name_len = state
3120 .buffer
3121 .file_path()
3122 .and_then(|p| p.file_name())
3123 .and_then(|n| n.to_str())
3124 .map(|s| s.chars().count())
3125 .unwrap_or(0);
3126 let tab_width = 2 + name_len;
3127 if idx < buffer_ids.len() - 1 {
3128 tab_width + 1 } else {
3130 tab_width
3131 }
3132 })
3133 .sum();
3134 assert!(view_state.tab_scroll_offset <= total_width);
3135 }
3136}