Skip to main content

editor_core/
workspace.rs

1//! Workspace and multi-buffer / multi-view model.
2//!
3//! `editor-core` is intentionally UI-agnostic, but a full-featured editor typically needs a
4//! kernel-level model for:
5//!
6//! - managing multiple open buffers (text + undo + derived metadata)
7//! - managing multiple views into the same buffer (split panes)
8//!
9//! This module provides a small [`Workspace`] that owns:
10//! - `BufferId` + `CommandExecutor` (buffer text + undo + derived state)
11//! - `ViewId` + per-view state (selections/cursors, wrap config, scroll)
12//!
13//! The workspace executes commands **against a specific view**. Text edits are applied to the
14//! underlying buffer, and any resulting [`crate::TextDelta`] is broadcast to all views of the
15//! same buffer.
16
17use crate::commands::{
18    AutoPairsConfig, Command, CommandExecutor, CommandResult, CursorCommand, EditCommand,
19    TextEditSpec, UndoHistoryRestoreError, UndoHistorySnapshot,
20};
21use crate::decorations::{Decoration, DecorationLayerId};
22use crate::delta::TextDelta;
23use crate::intervals::FoldRegion;
24use crate::processing::ProcessingEdit;
25use crate::search::{SearchError, SearchMatch, SearchOptions, find_all};
26use crate::selection_set::selection_direction;
27use crate::snippets::SnippetSession;
28use crate::state::CursorState;
29use crate::{AnchorBias, TextAnchor};
30use crate::{
31    IndentationConfig, LineEnding, LineIndex, Position, Selection, SelectionDirection,
32    TabKeyBehavior, ViewCommand,
33};
34use crate::{StateChange, StateChangeCallback, StateChangeType, WrapIndent, WrapMode};
35use std::collections::{BTreeMap, HashMap};
36use std::ops::Range;
37use std::sync::Arc;
38
39/// Opaque identifier for an open buffer in a [`Workspace`].
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct BufferId(u64);
42
43impl BufferId {
44    /// Create a `BufferId` from a raw numeric id.
45    ///
46    /// This is intended for interoperability boundaries (e.g. FFI) that persist ids externally.
47    pub const fn from_raw(id: u64) -> Self {
48        Self(id)
49    }
50
51    /// Get the underlying numeric id.
52    pub fn get(self) -> u64 {
53        self.0
54    }
55}
56
57/// Opaque identifier for a view into a buffer in a [`Workspace`].
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct ViewId(u64);
60
61impl ViewId {
62    /// Create a `ViewId` from a raw numeric id.
63    ///
64    /// This is intended for interoperability boundaries (e.g. FFI) that persist ids externally.
65    pub const fn from_raw(id: u64) -> Self {
66        Self(id)
67    }
68
69    /// Get the underlying numeric id.
70    pub fn get(self) -> u64 {
71        self.0
72    }
73}
74
75/// Metadata attached to a workspace buffer.
76#[derive(Debug, Clone)]
77pub struct BufferMetadata {
78    /// Optional buffer URI/path (host-provided).
79    pub uri: Option<String>,
80}
81
82/// Result of opening a buffer (a buffer always starts with a default view).
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct OpenBufferResult {
85    /// The created buffer id.
86    pub buffer_id: BufferId,
87    /// The initial view id into that buffer.
88    pub view_id: ViewId,
89}
90
91/// A navigation target produced by jump-list operations.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct JumpTarget {
94    /// Target buffer id.
95    pub buffer_id: BufferId,
96    /// Target position in logical coordinates.
97    pub position: Position,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101struct ViewCore {
102    cursor_position: Position,
103    selection: Option<Selection>,
104    secondary_selections: Vec<Selection>,
105    viewport_width: usize,
106    wrap_mode: WrapMode,
107    wrap_indent: WrapIndent,
108    tab_width: usize,
109    tab_key_behavior: TabKeyBehavior,
110    indentation_config: IndentationConfig,
111    auto_pairs: AutoPairsConfig,
112    snippet_session: Option<SnippetSession>,
113    preferred_x_cells: Option<usize>,
114}
115
116impl ViewCore {
117    fn from_executor(executor: &CommandExecutor) -> Self {
118        let editor = executor.editor();
119        Self {
120            cursor_position: editor.cursor_position(),
121            selection: editor.selection().cloned(),
122            secondary_selections: editor.secondary_selections().to_vec(),
123            viewport_width: editor.viewport_width(),
124            wrap_mode: editor.layout_engine().wrap_mode(),
125            wrap_indent: editor.layout_engine().wrap_indent(),
126            tab_width: editor.layout_engine().tab_width(),
127            tab_key_behavior: executor.tab_key_behavior(),
128            indentation_config: executor.indentation_config().clone(),
129            auto_pairs: executor.auto_pairs_config().clone(),
130            snippet_session: executor.snippet_session().cloned(),
131            preferred_x_cells: executor.preferred_x_cells(),
132        }
133    }
134
135    fn apply_to_executor(&self, executor: &mut CommandExecutor) {
136        let mut invalidate_visual_rows = false;
137        let editor = executor.editor_mut();
138        editor.set_cursor_state(
139            self.cursor_position,
140            self.selection.clone(),
141            self.secondary_selections.clone(),
142        );
143
144        if editor.viewport_width() != self.viewport_width {
145            invalidate_visual_rows = true;
146        }
147
148        let before_wrap_mode = editor.layout_engine().wrap_mode();
149        let before_wrap_indent = editor.layout_engine().wrap_indent();
150        let before_tab_width = editor.layout_engine().tab_width();
151        let before_viewport_width = editor.layout_engine().viewport_width();
152        if before_wrap_mode != self.wrap_mode
153            || before_wrap_indent != self.wrap_indent
154            || before_tab_width != self.tab_width
155            || before_viewport_width != self.viewport_width
156        {
157            invalidate_visual_rows = true;
158        }
159
160        if invalidate_visual_rows {
161            editor.set_view_options(
162                self.viewport_width,
163                self.wrap_mode,
164                self.wrap_indent,
165                self.tab_width,
166            );
167        }
168
169        executor.set_tab_key_behavior(self.tab_key_behavior);
170        executor.set_indentation_config(self.indentation_config.clone());
171        executor.set_auto_pairs_config(self.auto_pairs.clone());
172        executor.set_snippet_session(self.snippet_session.clone());
173        executor.set_preferred_x_cells(self.preferred_x_cells);
174    }
175}
176
177struct BufferEntry {
178    meta: BufferMetadata,
179    executor: CommandExecutor,
180    version: u64,
181    last_text_delta: Option<Arc<TextDelta>>,
182    bookmarks: BookmarkSet,
183    marks: MarkSet,
184}
185
186struct ViewEntry {
187    buffer: BufferId,
188    core: ViewCore,
189    version: u64,
190    callbacks: Vec<StateChangeCallback>,
191    scroll_top: usize,
192    scroll_sub_row_offset: u16,
193    overscan_rows: usize,
194    viewport_height: Option<usize>,
195    last_text_delta: Option<Arc<TextDelta>>,
196    jump_list: JumpList,
197}
198
199/// Workspace-level errors.
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum WorkspaceError {
202    /// A buffer with this uri already exists.
203    UriAlreadyOpen(String),
204    /// A buffer id was not found.
205    BufferNotFound(BufferId),
206    /// A view id was not found.
207    ViewNotFound(ViewId),
208    /// Executing a command failed.
209    CommandFailed {
210        /// Target view id.
211        view: ViewId,
212        /// Error message.
213        message: String,
214    },
215    /// Applying edits to a buffer failed.
216    ApplyEditsFailed {
217        /// Target buffer id.
218        buffer: BufferId,
219        /// Error message.
220        message: String,
221    },
222}
223
224/// Errors produced when restoring undo history for a workspace buffer.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum WorkspaceUndoHistoryRestoreError {
227    /// A buffer id was not found.
228    BufferNotFound(BufferId),
229    /// Restoring the undo history failed (corrupt snapshot or version mismatch).
230    RestoreFailed {
231        /// Target buffer id.
232        buffer: BufferId,
233        /// Underlying restore error.
234        error: UndoHistoryRestoreError,
235    },
236}
237
238impl std::fmt::Display for WorkspaceUndoHistoryRestoreError {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        match self {
241            Self::BufferNotFound(id) => write!(f, "Buffer not found (id={})", id.get()),
242            Self::RestoreFailed { buffer, error } => {
243                write!(
244                    f,
245                    "Restore undo history failed (buffer={}): {}",
246                    buffer.get(),
247                    error
248                )
249            }
250        }
251    }
252}
253
254impl std::error::Error for WorkspaceUndoHistoryRestoreError {}
255
256/// Search matches for a single open buffer in a [`Workspace`].
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct WorkspaceSearchResult {
259    /// Buffer id.
260    pub id: BufferId,
261    /// Optional URI/path metadata.
262    pub uri: Option<String>,
263    /// All matches in this buffer (character offsets, half-open).
264    pub matches: Vec<SearchMatch>,
265}
266
267/// Smooth-scrolling state for a view.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub struct ViewSmoothScrollState {
270    /// Top visual row anchor.
271    pub top_visual_row: usize,
272    /// Sub-row offset within `top_visual_row` (0..=65535, normalized).
273    pub sub_row_offset: u16,
274    /// Overscan rows for prefetching.
275    pub overscan_rows: usize,
276}
277
278/// Viewport state for a workspace view, including visual totals and smooth-scrolling metadata.
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct WorkspaceViewportState {
281    /// Viewport width (in cells).
282    pub width: usize,
283    /// Viewport height (line count, host-provided).
284    pub height: Option<usize>,
285    /// Current top visual row.
286    pub scroll_top: usize,
287    /// Visible visual range.
288    pub visible_lines: Range<usize>,
289    /// Total visual line count under current view config (wrap + folding aware).
290    pub total_visual_lines: usize,
291    /// Smooth-scroll metadata.
292    pub smooth_scroll: ViewSmoothScrollState,
293    /// Recommended prefetch range using overscan rows.
294    pub prefetch_lines: Range<usize>,
295}
296
297fn apply_char_offset_delta(mut offset: usize, delta: &TextDelta) -> usize {
298    for edit in &delta.edits {
299        let start = edit.start;
300        let end = edit.end();
301        let deleted_len = edit.deleted_len();
302        let inserted_len = edit.inserted_len();
303
304        if offset < start {
305            continue;
306        }
307
308        if offset < end {
309            // If the caret was inside the replaced range, anchor it at the end of the inserted text.
310            offset = start.saturating_add(inserted_len);
311            continue;
312        }
313
314        // After the replaced range: shift by the net length delta.
315        if inserted_len >= deleted_len {
316            offset = offset.saturating_add(inserted_len - deleted_len);
317        } else {
318            offset = offset.saturating_sub(deleted_len - inserted_len);
319        }
320    }
321
322    offset
323}
324
325fn apply_position_delta(
326    old_index: &LineIndex,
327    new_index: &LineIndex,
328    pos: Position,
329    delta: &TextDelta,
330) -> Position {
331    let before = old_index.position_to_char_offset(pos.line, pos.column);
332    let after = apply_char_offset_delta(before, delta);
333    let (line, column) = new_index.char_offset_to_position(after);
334    Position::new(line, column)
335}
336
337fn apply_selection_delta(
338    old_index: &LineIndex,
339    new_index: &LineIndex,
340    selection: &Selection,
341    delta: &TextDelta,
342) -> Selection {
343    let start = apply_position_delta(old_index, new_index, selection.start, delta);
344    let end = apply_position_delta(old_index, new_index, selection.end, delta);
345    Selection {
346        start,
347        end,
348        direction: selection_direction(start, end),
349    }
350}
351
352#[derive(Debug, Default, Clone, PartialEq, Eq)]
353struct BookmarkSet {
354    anchors: Vec<TextAnchor>,
355}
356
357impl BookmarkSet {
358    fn toggle_line_start(&mut self, line_start_offset: usize) -> bool {
359        let anchor = TextAnchor::new(line_start_offset, AnchorBias::Left);
360        match self
361            .anchors
362            .binary_search_by_key(&anchor.offset, |a| a.offset)
363        {
364            Ok(idx) => {
365                self.anchors.remove(idx);
366                false
367            }
368            Err(idx) => {
369                self.anchors.insert(idx, anchor);
370                true
371            }
372        }
373    }
374
375    fn clear(&mut self) {
376        self.anchors.clear();
377    }
378
379    fn apply_delta(&mut self, delta: &TextDelta) {
380        for a in &mut self.anchors {
381            a.apply_delta(delta);
382        }
383        self.anchors.sort_by_key(|a| a.offset);
384        self.anchors.dedup_by_key(|a| a.offset);
385    }
386
387    fn line_numbers(&self, line_index: &LineIndex) -> Vec<usize> {
388        let mut lines: Vec<usize> = self
389            .anchors
390            .iter()
391            .map(|a| line_index.char_offset_to_position(a.offset).0)
392            .collect();
393        lines.sort_unstable();
394        lines.dedup();
395        lines
396    }
397
398    fn next_after_line_start(&self, current_line_start: usize) -> Option<TextAnchor> {
399        self.anchors
400            .iter()
401            .copied()
402            .find(|a| a.offset > current_line_start)
403            .or_else(|| self.anchors.first().copied())
404    }
405
406    fn prev_before_line_start(&self, current_line_start: usize) -> Option<TextAnchor> {
407        self.anchors
408            .iter()
409            .copied()
410            .rfind(|a| a.offset < current_line_start)
411            .or_else(|| self.anchors.last().copied())
412    }
413}
414
415#[derive(Debug, Default, Clone, PartialEq, Eq)]
416struct MarkSet {
417    marks: BTreeMap<String, TextAnchor>,
418}
419
420impl MarkSet {
421    fn set(&mut self, name: String, offset: usize) {
422        self.marks
423            .insert(name, TextAnchor::new(offset, AnchorBias::Right));
424    }
425
426    fn get(&self, name: &str) -> Option<TextAnchor> {
427        self.marks.get(name).copied()
428    }
429
430    fn remove(&mut self, name: &str) -> bool {
431        self.marks.remove(name).is_some()
432    }
433
434    fn clear(&mut self) {
435        self.marks.clear();
436    }
437
438    fn names(&self) -> Vec<String> {
439        self.marks.keys().cloned().collect()
440    }
441
442    fn apply_delta(&mut self, delta: &TextDelta) {
443        for anchor in self.marks.values_mut() {
444            anchor.apply_delta(delta);
445        }
446    }
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
450struct JumpEntry {
451    buffer_id: BufferId,
452    anchor: TextAnchor,
453}
454
455#[derive(Debug, Default, Clone, PartialEq, Eq)]
456struct JumpList {
457    back: Vec<JumpEntry>,
458    forward: Vec<JumpEntry>,
459    max_len: usize,
460}
461
462impl JumpList {
463    fn new(max_len: usize) -> Self {
464        Self {
465            back: Vec::new(),
466            forward: Vec::new(),
467            max_len: max_len.max(1),
468        }
469    }
470
471    fn record(&mut self, entry: JumpEntry) {
472        if self.back.last().is_some_and(|last| *last == entry) {
473            return;
474        }
475
476        self.back.push(entry);
477        self.forward.clear();
478
479        if self.back.len() > self.max_len {
480            let overflow = self.back.len() - self.max_len;
481            self.back.drain(0..overflow);
482        }
483    }
484
485    fn back(&mut self, current: JumpEntry) -> Option<JumpEntry> {
486        let target = self.back.pop()?;
487        if !self.forward.last().is_some_and(|last| *last == current) {
488            self.forward.push(current);
489        }
490        Some(target)
491    }
492
493    fn forward(&mut self, current: JumpEntry) -> Option<JumpEntry> {
494        let target = self.forward.pop()?;
495        if !self.back.last().is_some_and(|last| *last == current) {
496            self.back.push(current);
497        }
498        Some(target)
499    }
500
501    fn clear(&mut self) {
502        self.back.clear();
503        self.forward.clear();
504    }
505
506    fn apply_delta(&mut self, buffer_id: BufferId, delta: &TextDelta) {
507        for entry in self
508            .back
509            .iter_mut()
510            .chain(self.forward.iter_mut())
511            .filter(|e| e.buffer_id == buffer_id)
512        {
513            entry.anchor.apply_delta(delta);
514        }
515    }
516}
517
518/// A collection of open buffers and their views.
519#[derive(Default)]
520pub struct Workspace {
521    next_buffer_id: u64,
522    buffers: BTreeMap<BufferId, BufferEntry>,
523    uri_to_buffer: HashMap<String, BufferId>,
524
525    next_view_id: u64,
526    views: BTreeMap<ViewId, ViewEntry>,
527    active_view: Option<ViewId>,
528
529    intelligence: crate::WorkspaceIntelligence,
530}
531
532impl std::fmt::Debug for Workspace {
533    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534        f.debug_struct("Workspace")
535            .field("buffer_count", &self.buffers.len())
536            .field("view_count", &self.views.len())
537            .field("uri_count", &self.uri_to_buffer.len())
538            .field("active_view", &self.active_view)
539            .field("intelligence_set_count", &self.intelligence.len())
540            .finish()
541    }
542}
543
544impl Workspace {
545    /// Create an empty workspace.
546    pub fn new() -> Self {
547        Self::default()
548    }
549
550    /// Returns the number of open buffers.
551    pub fn len(&self) -> usize {
552        self.buffers.len()
553    }
554
555    /// Returns `true` if there are no open buffers.
556    pub fn is_empty(&self) -> bool {
557        self.buffers.is_empty()
558    }
559
560    /// Returns the number of open views.
561    pub fn view_count(&self) -> usize {
562        self.views.len()
563    }
564
565    /// Return the active view id (if any).
566    pub fn active_view_id(&self) -> Option<ViewId> {
567        self.active_view
568    }
569
570    /// Return the active buffer id (if any).
571    pub fn active_buffer_id(&self) -> Option<BufferId> {
572        let view_id = self.active_view?;
573        self.views.get(&view_id).map(|v| v.buffer)
574    }
575
576    /// Read workspace-scoped language intelligence result sets (references/call hierarchy/etc.).
577    pub fn intelligence(&self) -> &crate::WorkspaceIntelligence {
578        &self.intelligence
579    }
580
581    /// Mutate workspace-scoped language intelligence result sets (references/call hierarchy/etc.).
582    pub fn intelligence_mut(&mut self) -> &mut crate::WorkspaceIntelligence {
583        &mut self.intelligence
584    }
585
586    /// Set the active view.
587    pub fn set_active_view(&mut self, id: ViewId) -> Result<(), WorkspaceError> {
588        if !self.views.contains_key(&id) {
589            return Err(WorkspaceError::ViewNotFound(id));
590        }
591        self.active_view = Some(id);
592        Ok(())
593    }
594
595    /// Open a new buffer in the workspace, creating an initial view.
596    ///
597    /// - `uri` is optional and host-provided (e.g. `file:///...`).
598    /// - `text` is the initial contents.
599    /// - `viewport_width` is the initial view's wrap width.
600    pub fn open_buffer(
601        &mut self,
602        uri: Option<String>,
603        text: &str,
604        viewport_width: usize,
605    ) -> Result<OpenBufferResult, WorkspaceError> {
606        if let Some(uri) = uri.as_ref()
607            && self.uri_to_buffer.contains_key(uri)
608        {
609            return Err(WorkspaceError::UriAlreadyOpen(uri.clone()));
610        }
611
612        let buffer_id = BufferId(self.next_buffer_id);
613        self.next_buffer_id = self.next_buffer_id.saturating_add(1);
614
615        let executor = CommandExecutor::new(text, viewport_width);
616        let meta = BufferMetadata { uri: uri.clone() };
617        self.buffers.insert(
618            buffer_id,
619            BufferEntry {
620                meta,
621                executor,
622                version: 0,
623                last_text_delta: None,
624                bookmarks: BookmarkSet::default(),
625                marks: MarkSet::default(),
626            },
627        );
628
629        if let Some(uri) = uri {
630            self.uri_to_buffer.insert(uri, buffer_id);
631        }
632
633        let view_id = self.create_view(buffer_id, viewport_width)?;
634
635        if self.active_view.is_none() {
636            self.active_view = Some(view_id);
637        }
638
639        Ok(OpenBufferResult { buffer_id, view_id })
640    }
641
642    /// Close a buffer (and all its views).
643    pub fn close_buffer(&mut self, id: BufferId) -> Result<(), WorkspaceError> {
644        let Some(entry) = self.buffers.remove(&id) else {
645            return Err(WorkspaceError::BufferNotFound(id));
646        };
647
648        if let Some(uri) = entry.meta.uri.as_ref() {
649            self.uri_to_buffer.remove(uri);
650        }
651
652        let views_to_remove: Vec<ViewId> = self
653            .views
654            .iter()
655            .filter_map(|(vid, v)| if v.buffer == id { Some(*vid) } else { None })
656            .collect();
657        for view_id in views_to_remove {
658            self.views.remove(&view_id);
659        }
660
661        if self
662            .active_view
663            .is_some_and(|active| !self.views.contains_key(&active))
664        {
665            self.active_view = self.views.keys().next().copied();
666        }
667
668        Ok(())
669    }
670
671    /// Close a view. If it was the last view of its buffer, the buffer is also closed.
672    pub fn close_view(&mut self, id: ViewId) -> Result<(), WorkspaceError> {
673        let Some(view) = self.views.remove(&id) else {
674            return Err(WorkspaceError::ViewNotFound(id));
675        };
676
677        if self.active_view == Some(id) {
678            self.active_view = self.views.keys().next().copied();
679        }
680
681        let still_has_views = self.views.values().any(|v| v.buffer == view.buffer);
682        if !still_has_views {
683            self.close_buffer(view.buffer)?;
684        }
685
686        Ok(())
687    }
688
689    /// Return all open buffer ids in deterministic order.
690    pub fn buffer_ids(&self) -> Vec<BufferId> {
691        let mut ids: Vec<BufferId> = self.buffers.keys().copied().collect();
692        ids.sort_by_key(|id| id.get());
693        ids
694    }
695
696    /// Return all open view ids in deterministic order.
697    pub fn view_ids(&self) -> Vec<ViewId> {
698        let mut ids: Vec<ViewId> = self.views.keys().copied().collect();
699        ids.sort_by_key(|id| id.get());
700        ids
701    }
702
703    /// Create a new view into an existing buffer.
704    pub fn create_view(
705        &mut self,
706        buffer: BufferId,
707        viewport_width: usize,
708    ) -> Result<ViewId, WorkspaceError> {
709        let Some(buffer_entry) = self.buffers.get_mut(&buffer) else {
710            return Err(WorkspaceError::BufferNotFound(buffer));
711        };
712
713        // Create a view state by starting from the executor defaults, but overriding width and
714        // clearing selection/cursors.
715        let mut core = ViewCore::from_executor(&buffer_entry.executor);
716        core.cursor_position = Position::new(0, 0);
717        core.selection = None;
718        core.secondary_selections.clear();
719        core.viewport_width = viewport_width.max(1);
720        core.preferred_x_cells = None;
721
722        let view_id = ViewId(self.next_view_id);
723        self.next_view_id = self.next_view_id.saturating_add(1);
724
725        self.views.insert(
726            view_id,
727            ViewEntry {
728                buffer,
729                core,
730                version: 0,
731                callbacks: Vec::new(),
732                scroll_top: 0,
733                scroll_sub_row_offset: 0,
734                overscan_rows: 0,
735                viewport_height: None,
736                last_text_delta: None,
737                jump_list: JumpList::new(200),
738            },
739        );
740
741        Ok(view_id)
742    }
743
744    /// Look up a buffer by uri.
745    pub fn buffer_id_for_uri(&self, uri: &str) -> Option<BufferId> {
746        self.uri_to_buffer.get(uri).copied()
747    }
748
749    /// Get a reference to a buffer's line index (logical line/column <-> char offsets).
750    pub fn buffer_line_index(&self, buffer_id: BufferId) -> Result<&LineIndex, WorkspaceError> {
751        let Some(buffer) = self.buffers.get(&buffer_id) else {
752            return Err(WorkspaceError::BufferNotFound(buffer_id));
753        };
754        Ok(buffer.executor.editor().line_index())
755    }
756
757    /// Get the document length for a buffer in Unicode scalar values (Rust `char`s).
758    pub fn buffer_char_count(&self, buffer_id: BufferId) -> Result<usize, WorkspaceError> {
759        let Some(buffer) = self.buffers.get(&buffer_id) else {
760            return Err(WorkspaceError::BufferNotFound(buffer_id));
761        };
762        Ok(buffer.executor.editor().char_count())
763    }
764
765    /// Get a slice of the buffer text as a `String` by character offset + length.
766    ///
767    /// Notes:
768    /// - `start` and `len` are in Unicode scalar indices (Rust `char`s), not bytes.
769    /// - Out-of-bounds ranges are clamped by the underlying text buffer.
770    pub fn buffer_text_range(
771        &self,
772        buffer_id: BufferId,
773        start: usize,
774        len: usize,
775    ) -> Result<String, WorkspaceError> {
776        let Some(buffer) = self.buffers.get(&buffer_id) else {
777            return Err(WorkspaceError::BufferNotFound(buffer_id));
778        };
779        Ok(buffer.executor.editor().text_range(start, len))
780    }
781
782    /// Get all decoration layers for a buffer.
783    pub fn buffer_decorations(
784        &self,
785        buffer_id: BufferId,
786    ) -> Result<&BTreeMap<DecorationLayerId, Vec<Decoration>>, WorkspaceError> {
787        let Some(buffer) = self.buffers.get(&buffer_id) else {
788            return Err(WorkspaceError::BufferNotFound(buffer_id));
789        };
790        Ok(buffer.executor.editor().decorations())
791    }
792
793    /// Get the current folding regions for a buffer (user folds + derived folds).
794    pub fn folding_regions_for_buffer(
795        &self,
796        buffer_id: BufferId,
797    ) -> Result<Vec<FoldRegion>, WorkspaceError> {
798        let Some(buffer) = self.buffers.get(&buffer_id) else {
799            return Err(WorkspaceError::BufferNotFound(buffer_id));
800        };
801        Ok(buffer
802            .executor
803            .editor()
804            .folding_manager()
805            .regions()
806            .to_vec())
807    }
808
809    /// Returns whether a buffer has unsaved text edits.
810    ///
811    /// Notes:
812    /// - This tracks the executor's "clean point" (usually the last `mark_saved_*` call),
813    ///   and is restored by undoing back to that clean point.
814    pub fn buffer_is_modified(&self, buffer_id: BufferId) -> Result<bool, WorkspaceError> {
815        let Some(buffer) = self.buffers.get(&buffer_id) else {
816            return Err(WorkspaceError::BufferNotFound(buffer_id));
817        };
818        Ok(!buffer.executor.is_clean())
819    }
820
821    /// Return the preferred line ending for saving this buffer.
822    pub fn line_ending_for_buffer(
823        &self,
824        buffer_id: BufferId,
825    ) -> Result<LineEnding, WorkspaceError> {
826        let Some(buffer) = self.buffers.get(&buffer_id) else {
827            return Err(WorkspaceError::BufferNotFound(buffer_id));
828        };
829        Ok(buffer.executor.line_ending())
830    }
831
832    /// Override the preferred line ending for saving this buffer.
833    pub fn set_line_ending_for_buffer(
834        &mut self,
835        buffer_id: BufferId,
836        line_ending: LineEnding,
837    ) -> Result<(), WorkspaceError> {
838        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
839            return Err(WorkspaceError::BufferNotFound(buffer_id));
840        };
841        buffer.executor.set_line_ending(line_ending);
842        Ok(())
843    }
844
845    /// Returns whether the view's underlying buffer has unsaved text edits.
846    pub fn is_modified_for_view(&self, view_id: ViewId) -> Result<bool, WorkspaceError> {
847        let buffer_id = self.buffer_id_for_view(view_id)?;
848        self.buffer_is_modified(buffer_id)
849    }
850
851    /// Return the preferred line ending for saving this view's underlying buffer.
852    pub fn line_ending_for_view(&self, view_id: ViewId) -> Result<LineEnding, WorkspaceError> {
853        let buffer_id = self.buffer_id_for_view(view_id)?;
854        self.line_ending_for_buffer(buffer_id)
855    }
856
857    /// Override the preferred line ending for saving this view's underlying buffer.
858    pub fn set_line_ending_for_view(
859        &mut self,
860        view_id: ViewId,
861        line_ending: LineEnding,
862    ) -> Result<(), WorkspaceError> {
863        let buffer_id = self.buffer_id_for_view(view_id)?;
864        self.set_line_ending_for_buffer(buffer_id, line_ending)
865    }
866
867    /// Mark the current state of a buffer as saved (clean point).
868    pub fn mark_saved_for_buffer(&mut self, buffer_id: BufferId) -> Result<(), WorkspaceError> {
869        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
870            return Err(WorkspaceError::BufferNotFound(buffer_id));
871        };
872        buffer.executor.mark_clean();
873        Ok(())
874    }
875
876    /// Mark the current state of a view's buffer as saved (clean point).
877    pub fn mark_saved_for_view(&mut self, view_id: ViewId) -> Result<(), WorkspaceError> {
878        let buffer_id = self.buffer_id_for_view(view_id)?;
879        self.mark_saved_for_buffer(buffer_id)
880    }
881
882    /// Capture a persistable snapshot of a buffer's undo/redo history.
883    pub fn undo_history_snapshot_for_buffer(
884        &self,
885        buffer_id: BufferId,
886    ) -> Result<UndoHistorySnapshot, WorkspaceError> {
887        let Some(buffer) = self.buffers.get(&buffer_id) else {
888            return Err(WorkspaceError::BufferNotFound(buffer_id));
889        };
890        Ok(buffer.executor.undo_history_snapshot())
891    }
892
893    /// Restore a buffer's undo/redo history from a previously captured snapshot.
894    ///
895    /// Notes:
896    /// - This does **not** modify the current buffer text.
897    /// - Callers should only restore a snapshot into the **same text** it was captured from.
898    pub fn restore_undo_history_for_buffer(
899        &mut self,
900        buffer_id: BufferId,
901        snapshot: UndoHistorySnapshot,
902    ) -> Result<(), WorkspaceUndoHistoryRestoreError> {
903        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
904            return Err(WorkspaceUndoHistoryRestoreError::BufferNotFound(buffer_id));
905        };
906
907        buffer.last_text_delta = None;
908        for view in self.views.values_mut() {
909            if view.buffer == buffer_id {
910                view.last_text_delta = None;
911            }
912        }
913
914        buffer
915            .executor
916            .restore_undo_history(snapshot)
917            .map_err(|err| WorkspaceUndoHistoryRestoreError::RestoreFailed {
918                buffer: buffer_id,
919                error: err,
920            })?;
921
922        Ok(())
923    }
924
925    /// Get a buffer's metadata.
926    pub fn buffer_metadata(&self, id: BufferId) -> Option<&BufferMetadata> {
927        self.buffers.get(&id).map(|e| &e.meta)
928    }
929
930    /// Get the buffer id that a view is pointing at.
931    pub fn buffer_id_for_view(&self, id: ViewId) -> Result<BufferId, WorkspaceError> {
932        self.views
933            .get(&id)
934            .map(|v| v.buffer)
935            .ok_or(WorkspaceError::ViewNotFound(id))
936    }
937
938    /// Get the primary cursor position for a view.
939    pub fn cursor_position_for_view(&self, id: ViewId) -> Result<Position, WorkspaceError> {
940        self.views
941            .get(&id)
942            .map(|v| v.core.cursor_position)
943            .ok_or(WorkspaceError::ViewNotFound(id))
944    }
945
946    /// Get the primary selection for a view (None means "empty selection / caret only").
947    pub fn selection_for_view(&self, id: ViewId) -> Result<Option<Selection>, WorkspaceError> {
948        self.views
949            .get(&id)
950            .map(|v| v.core.selection.clone())
951            .ok_or(WorkspaceError::ViewNotFound(id))
952    }
953
954    /// Get the current tab width setting for a view (in monospace cells).
955    pub fn tab_width_for_view(&self, id: ViewId) -> Result<usize, WorkspaceError> {
956        self.views
957            .get(&id)
958            .map(|v| v.core.tab_width)
959            .ok_or(WorkspaceError::ViewNotFound(id))
960    }
961
962    /// Get the current viewport width setting for a view (in monospace cells).
963    pub fn viewport_width_for_view(&self, id: ViewId) -> Result<usize, WorkspaceError> {
964        self.views
965            .get(&id)
966            .map(|v| v.core.viewport_width)
967            .ok_or(WorkspaceError::ViewNotFound(id))
968    }
969
970    /// Get the current soft wrap mode for a view.
971    pub fn wrap_mode_for_view(&self, id: ViewId) -> Result<WrapMode, WorkspaceError> {
972        self.views
973            .get(&id)
974            .map(|v| v.core.wrap_mode)
975            .ok_or(WorkspaceError::ViewNotFound(id))
976    }
977
978    /// Get the current wrapped-line indentation policy for a view.
979    pub fn wrap_indent_for_view(&self, id: ViewId) -> Result<WrapIndent, WorkspaceError> {
980        self.views
981            .get(&id)
982            .map(|v| v.core.wrap_indent)
983            .ok_or(WorkspaceError::ViewNotFound(id))
984    }
985
986    /// Get the current tab key behavior for a view.
987    pub fn tab_key_behavior_for_view(&self, id: ViewId) -> Result<TabKeyBehavior, WorkspaceError> {
988        self.views
989            .get(&id)
990            .map(|v| v.core.tab_key_behavior)
991            .ok_or(WorkspaceError::ViewNotFound(id))
992    }
993
994    /// Get the current indentation configuration for a view.
995    pub fn indentation_config_for_view(
996        &self,
997        id: ViewId,
998    ) -> Result<IndentationConfig, WorkspaceError> {
999        self.views
1000            .get(&id)
1001            .map(|v| v.core.indentation_config.clone())
1002            .ok_or(WorkspaceError::ViewNotFound(id))
1003    }
1004
1005    /// Get the current auto-pairs configuration for a view.
1006    pub fn auto_pairs_config_for_view(
1007        &self,
1008        id: ViewId,
1009    ) -> Result<AutoPairsConfig, WorkspaceError> {
1010        self.views
1011            .get(&id)
1012            .map(|v| v.core.auto_pairs.clone())
1013            .ok_or(WorkspaceError::ViewNotFound(id))
1014    }
1015
1016    /// Get a view's normalized cursor/selection snapshot.
1017    ///
1018    /// This matches the semantics of `EditorStateManager::get_cursor_state`, but for workspace views.
1019    pub fn cursor_state_for_view(&self, id: ViewId) -> Result<CursorState, WorkspaceError> {
1020        let Some(view) = self.views.get(&id) else {
1021            return Err(WorkspaceError::ViewNotFound(id));
1022        };
1023        let Some(buffer) = self.buffers.get(&view.buffer) else {
1024            return Err(WorkspaceError::BufferNotFound(view.buffer));
1025        };
1026
1027        let line_index = buffer.executor.editor().line_index();
1028
1029        let mut selections: Vec<Selection> =
1030            Vec::with_capacity(1 + view.core.secondary_selections.len());
1031        let primary = view.core.selection.clone().unwrap_or(Selection {
1032            start: view.core.cursor_position,
1033            end: view.core.cursor_position,
1034            direction: SelectionDirection::Forward,
1035        });
1036        selections.push(primary);
1037        selections.extend(view.core.secondary_selections.iter().cloned());
1038
1039        let (selections, primary_selection_index) =
1040            crate::selection_set::normalize_selections(selections, 0);
1041        let primary = selections
1042            .get(primary_selection_index)
1043            .cloned()
1044            .unwrap_or(Selection {
1045                start: view.core.cursor_position,
1046                end: view.core.cursor_position,
1047                direction: SelectionDirection::Forward,
1048            });
1049
1050        let position = primary.end;
1051        let offset = line_index.position_to_char_offset(position.line, position.column);
1052
1053        let selection = if primary.start == primary.end {
1054            None
1055        } else {
1056            Some(primary)
1057        };
1058
1059        let multi_cursors: Vec<Position> = selections
1060            .iter()
1061            .enumerate()
1062            .filter_map(|(idx, sel)| {
1063                if idx == primary_selection_index {
1064                    None
1065                } else {
1066                    Some(sel.end)
1067                }
1068            })
1069            .collect();
1070
1071        Ok(CursorState {
1072            position,
1073            offset,
1074            multi_cursors,
1075            selection,
1076            selections,
1077            primary_selection_index,
1078        })
1079    }
1080
1081    /// Get the scroll position (top visual row) for a view.
1082    pub fn scroll_top_for_view(&self, id: ViewId) -> Result<usize, WorkspaceError> {
1083        self.views
1084            .get(&id)
1085            .map(|v| v.scroll_top)
1086            .ok_or(WorkspaceError::ViewNotFound(id))
1087    }
1088
1089    /// Get the sub-row smooth-scroll offset for a view.
1090    pub fn scroll_sub_row_offset_for_view(&self, id: ViewId) -> Result<u16, WorkspaceError> {
1091        self.views
1092            .get(&id)
1093            .map(|v| v.scroll_sub_row_offset)
1094            .ok_or(WorkspaceError::ViewNotFound(id))
1095    }
1096
1097    /// Get overscan rows for a view.
1098    pub fn overscan_rows_for_view(&self, id: ViewId) -> Result<usize, WorkspaceError> {
1099        self.views
1100            .get(&id)
1101            .map(|v| v.overscan_rows)
1102            .ok_or(WorkspaceError::ViewNotFound(id))
1103    }
1104
1105    /// Get smooth-scroll state for a view.
1106    pub fn smooth_scroll_state_for_view(
1107        &self,
1108        id: ViewId,
1109    ) -> Result<ViewSmoothScrollState, WorkspaceError> {
1110        let Some(view) = self.views.get(&id) else {
1111            return Err(WorkspaceError::ViewNotFound(id));
1112        };
1113        Ok(ViewSmoothScrollState {
1114            top_visual_row: view.scroll_top,
1115            sub_row_offset: view.scroll_sub_row_offset,
1116            overscan_rows: view.overscan_rows,
1117        })
1118    }
1119
1120    /// Update a buffer's uri/path.
1121    pub fn set_buffer_uri(
1122        &mut self,
1123        id: BufferId,
1124        uri: Option<String>,
1125    ) -> Result<(), WorkspaceError> {
1126        let Some(entry) = self.buffers.get_mut(&id) else {
1127            return Err(WorkspaceError::BufferNotFound(id));
1128        };
1129
1130        if let Some(next) = uri.as_ref()
1131            && self.uri_to_buffer.contains_key(next)
1132            && entry.meta.uri.as_deref() != Some(next.as_str())
1133        {
1134            return Err(WorkspaceError::UriAlreadyOpen(next.clone()));
1135        }
1136
1137        if let Some(prev) = entry.meta.uri.take() {
1138            self.uri_to_buffer.remove(&prev);
1139        }
1140
1141        if let Some(next) = uri.clone() {
1142            self.uri_to_buffer.insert(next, id);
1143        }
1144
1145        entry.meta.uri = uri;
1146        Ok(())
1147    }
1148
1149    /// Get a view's current version (increments on view-local changes and buffer changes).
1150    pub fn view_version(&self, id: ViewId) -> Option<u64> {
1151        self.views.get(&id).map(|v| v.version)
1152    }
1153
1154    /// Get the last broadcast text delta for this view (if any).
1155    pub fn last_text_delta_for_view(&self, id: ViewId) -> Option<&Arc<TextDelta>> {
1156        self.views.get(&id)?.last_text_delta.as_ref()
1157    }
1158
1159    /// Take the last broadcast text delta for this view (if any).
1160    pub fn take_last_text_delta_for_view(&mut self, id: ViewId) -> Option<Arc<TextDelta>> {
1161        self.views.get_mut(&id)?.last_text_delta.take()
1162    }
1163
1164    /// Take the last text delta for a buffer (if any).
1165    ///
1166    /// This is useful for incremental consumers (e.g. LSP sync) that want to observe each buffer
1167    /// edit exactly once, regardless of how many views exist for that buffer.
1168    pub fn take_last_text_delta_for_buffer(
1169        &mut self,
1170        id: BufferId,
1171    ) -> Result<Option<Arc<TextDelta>>, WorkspaceError> {
1172        let Some(buffer) = self.buffers.get_mut(&id) else {
1173            return Err(WorkspaceError::BufferNotFound(id));
1174        };
1175        Ok(buffer.last_text_delta.take())
1176    }
1177
1178    /// Subscribe to changes for a view.
1179    pub fn subscribe_view<F>(&mut self, id: ViewId, callback: F) -> Result<(), WorkspaceError>
1180    where
1181        F: FnMut(&StateChange) + Send + 'static,
1182    {
1183        let Some(view) = self.views.get_mut(&id) else {
1184            return Err(WorkspaceError::ViewNotFound(id));
1185        };
1186
1187        view.callbacks.push(Box::new(callback));
1188        Ok(())
1189    }
1190
1191    fn notify_view(
1192        view: &mut ViewEntry,
1193        change_type: StateChangeType,
1194        delta: Option<Arc<TextDelta>>,
1195    ) {
1196        let old_version = view.version;
1197        view.version = view.version.saturating_add(1);
1198
1199        let mut change = StateChange::new(change_type, old_version, view.version);
1200        if let Some(delta) = delta {
1201            change = change.with_text_delta(delta);
1202        }
1203
1204        for cb in &mut view.callbacks {
1205            cb(&change);
1206        }
1207    }
1208
1209    fn command_change_type(command: &Command) -> Option<StateChangeType> {
1210        match command {
1211            Command::Edit(EditCommand::Delete { length: 0, .. }) => None,
1212            Command::Edit(EditCommand::Replace {
1213                length: 0, text, ..
1214            }) if text.is_empty() => None,
1215            Command::Edit(EditCommand::EndUndoGroup) => None,
1216            Command::Edit(_) => Some(StateChangeType::DocumentModified),
1217            Command::Cursor(
1218                CursorCommand::MoveTo { .. }
1219                | CursorCommand::MoveBy { .. }
1220                | CursorCommand::MoveVisualBy { .. }
1221                | CursorCommand::MoveToVisual { .. }
1222                | CursorCommand::MoveToLineStart
1223                | CursorCommand::MoveToLineEnd
1224                | CursorCommand::MoveToVisualLineStart
1225                | CursorCommand::MoveToVisualLineEnd
1226                | CursorCommand::MoveGraphemeLeft
1227                | CursorCommand::MoveGraphemeRight
1228                | CursorCommand::MoveWordLeft
1229                | CursorCommand::MoveWordRight
1230                | CursorCommand::MoveToMatchingBracket
1231                | CursorCommand::FindNext { .. }
1232                | CursorCommand::FindPrev { .. },
1233            ) => Some(StateChangeType::CursorMoved),
1234            Command::Cursor(_) => Some(StateChangeType::SelectionChanged),
1235            Command::View(ViewCommand::ScrollTo { .. } | ViewCommand::GetViewport { .. }) => None,
1236            Command::View(_) => Some(StateChangeType::ViewportChanged),
1237            Command::Style(
1238                crate::StyleCommand::AddStyle { .. }
1239                | crate::StyleCommand::RemoveStyle { .. }
1240                | crate::StyleCommand::UpdateBracketMatchHighlights
1241                | crate::StyleCommand::ClearBracketMatchHighlights,
1242            ) => Some(StateChangeType::StyleChanged),
1243            Command::Style(
1244                crate::StyleCommand::Fold { .. }
1245                | crate::StyleCommand::Unfold { .. }
1246                | crate::StyleCommand::UnfoldAll,
1247            ) => Some(StateChangeType::FoldingChanged),
1248        }
1249    }
1250
1251    /// Execute a command against a specific view.
1252    ///
1253    /// - Cursor/selection state is view-local.
1254    /// - Text edits and derived-state edits are applied to the underlying buffer.
1255    /// - Any text delta is broadcast to all views of that buffer.
1256    pub fn execute(
1257        &mut self,
1258        view_id: ViewId,
1259        command: Command,
1260    ) -> Result<CommandResult, WorkspaceError> {
1261        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1262            return Err(WorkspaceError::ViewNotFound(view_id));
1263        };
1264
1265        let change_type = Self::command_change_type(&command);
1266        if change_type.is_none() {
1267            // Still run command because it may validate (e.g. ScrollTo), but treat as no version bump.
1268        }
1269
1270        // Borrow maps separately so we can mutably access a view and its buffer.
1271        let views = &mut self.views;
1272        let buffers = &mut self.buffers;
1273
1274        let Some(view) = views.get_mut(&view_id) else {
1275            return Err(WorkspaceError::ViewNotFound(view_id));
1276        };
1277        let Some(buffer) = buffers.get_mut(&buffer_id) else {
1278            return Err(WorkspaceError::BufferNotFound(buffer_id));
1279        };
1280
1281        let before_view_core = view.core.clone();
1282        let before_line_index = buffer.executor.editor().line_index().clone();
1283        let before_char_count = buffer.executor.editor().char_count();
1284
1285        // Load view-local state into the executor, execute, then snapshot it back.
1286        view.core.apply_to_executor(&mut buffer.executor);
1287
1288        let result = buffer.executor.execute(command.clone()).map_err(|err| {
1289            WorkspaceError::CommandFailed {
1290                view: view_id,
1291                message: err.to_string(),
1292            }
1293        })?;
1294
1295        view.core = ViewCore::from_executor(&buffer.executor);
1296
1297        let delta = buffer.executor.take_last_text_delta().map(Arc::new);
1298        let after_char_count = buffer.executor.editor().char_count();
1299
1300        // Detect no-ops: successful execution but no meaningful state change.
1301        let view_changed = view.core != before_view_core;
1302        let buffer_text_changed = delta.is_some()
1303            // `Backspace`/`DeleteForward` can succeed as boundary no-ops; detect via char count.
1304            || after_char_count != before_char_count;
1305
1306        let buffer_derived_changed = matches!(command, Command::Style(_));
1307
1308        if !(view_changed || buffer_text_changed || buffer_derived_changed) {
1309            return Ok(result);
1310        }
1311
1312        let change_type = if buffer_text_changed {
1313            StateChangeType::DocumentModified
1314        } else {
1315            change_type.unwrap_or(StateChangeType::ViewportChanged)
1316        };
1317
1318        if buffer_text_changed || buffer_derived_changed {
1319            // Broadcast to all views of this buffer.
1320            let delta_arc = delta.clone();
1321            if let Some(delta_arc) = delta_arc {
1322                buffer.last_text_delta = Some(delta_arc.clone());
1323                for other in views.values_mut() {
1324                    if other.buffer != buffer_id {
1325                        continue;
1326                    }
1327                    other.last_text_delta = Some(delta_arc.clone());
1328                }
1329            } else {
1330                buffer.last_text_delta = None;
1331            }
1332
1333            // Shift other views' cursor/selections through the delta (if any).
1334            if let Some(ref delta_arc) = delta {
1335                let new_index = buffer.executor.editor().line_index();
1336                for (other_id, other) in views.iter_mut() {
1337                    if other.buffer != buffer_id || *other_id == view_id {
1338                        continue;
1339                    }
1340
1341                    other.core.cursor_position = apply_position_delta(
1342                        &before_line_index,
1343                        new_index,
1344                        other.core.cursor_position,
1345                        delta_arc,
1346                    );
1347
1348                    if let Some(ref sel) = other.core.selection {
1349                        other.core.selection = Some(apply_selection_delta(
1350                            &before_line_index,
1351                            new_index,
1352                            sel,
1353                            delta_arc,
1354                        ));
1355                    }
1356
1357                    for sel in &mut other.core.secondary_selections {
1358                        *sel = apply_selection_delta(&before_line_index, new_index, sel, delta_arc);
1359                    }
1360
1361                    if let Some(ref mut session) = other.core.snippet_session {
1362                        session.apply_delta(delta_arc);
1363                    }
1364                }
1365
1366                // Keep navigation state stable under edits.
1367                buffer.bookmarks.apply_delta(delta_arc);
1368                buffer.marks.apply_delta(delta_arc);
1369                for other in views.values_mut() {
1370                    if other.buffer != buffer_id {
1371                        continue;
1372                    }
1373                    other.jump_list.apply_delta(buffer_id, delta_arc);
1374                }
1375            }
1376
1377            for other in views.values_mut() {
1378                if other.buffer != buffer_id {
1379                    continue;
1380                }
1381                Self::notify_view(other, change_type, delta.clone());
1382            }
1383
1384            if buffer_text_changed && let Some(uri) = buffer.meta.uri.as_deref() {
1385                self.intelligence.mark_stale_for_uri(uri);
1386            }
1387
1388            buffer.version = buffer.version.saturating_add(1);
1389        } else {
1390            Self::notify_view(view, change_type, None);
1391        }
1392
1393        Ok(result)
1394    }
1395
1396    /// Return `true` if the given view currently has an active snippet session.
1397    ///
1398    /// Snippet sessions are created by snippet inserts (for example LSP completion items with
1399    /// `insertTextFormat == 2`) and allow tab/shift-tab navigation between placeholders.
1400    pub fn has_active_snippet_session(&self, view_id: ViewId) -> Result<bool, WorkspaceError> {
1401        let Some(view) = self.views.get(&view_id) else {
1402            return Err(WorkspaceError::ViewNotFound(view_id));
1403        };
1404        Ok(view
1405            .core
1406            .snippet_session
1407            .as_ref()
1408            .map(|s| s.is_active())
1409            .unwrap_or(false))
1410    }
1411
1412    /// Toggle a bookmark at the **current cursor line** for the given view.
1413    ///
1414    /// Returns `true` if a bookmark was added, or `false` if an existing bookmark on that line was
1415    /// removed.
1416    pub fn toggle_bookmark_at_cursor_line(
1417        &mut self,
1418        view_id: ViewId,
1419    ) -> Result<bool, WorkspaceError> {
1420        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1421            return Err(WorkspaceError::ViewNotFound(view_id));
1422        };
1423        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
1424            return Err(WorkspaceError::BufferNotFound(buffer_id));
1425        };
1426        let Some(view) = self.views.get(&view_id) else {
1427            return Err(WorkspaceError::ViewNotFound(view_id));
1428        };
1429
1430        let line_start = buffer
1431            .executor
1432            .editor()
1433            .line_index()
1434            .position_to_char_offset(view.core.cursor_position.line, 0);
1435
1436        let added = buffer.bookmarks.toggle_line_start(line_start);
1437
1438        for v in self.views.values_mut() {
1439            if v.buffer == buffer_id {
1440                Self::notify_view(v, StateChangeType::NavigationChanged, None);
1441            }
1442        }
1443
1444        Ok(added)
1445    }
1446
1447    /// Return all bookmark line numbers (0-based) for a buffer.
1448    pub fn bookmark_lines(&self, buffer_id: BufferId) -> Result<Vec<usize>, WorkspaceError> {
1449        let Some(buffer) = self.buffers.get(&buffer_id) else {
1450            return Err(WorkspaceError::BufferNotFound(buffer_id));
1451        };
1452        Ok(buffer
1453            .bookmarks
1454            .line_numbers(buffer.executor.editor().line_index()))
1455    }
1456
1457    /// Clear all bookmarks for a buffer.
1458    pub fn clear_bookmarks(&mut self, buffer_id: BufferId) -> Result<(), WorkspaceError> {
1459        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
1460            return Err(WorkspaceError::BufferNotFound(buffer_id));
1461        };
1462        buffer.bookmarks.clear();
1463
1464        for v in self.views.values_mut() {
1465            if v.buffer == buffer_id {
1466                Self::notify_view(v, StateChangeType::NavigationChanged, None);
1467            }
1468        }
1469
1470        Ok(())
1471    }
1472
1473    fn move_view_cursor_to_anchor(
1474        view: &mut ViewEntry,
1475        buffer: &BufferEntry,
1476        anchor: TextAnchor,
1477    ) -> Position {
1478        let (line, column) = buffer
1479            .executor
1480            .editor()
1481            .line_index()
1482            .char_offset_to_position(anchor.offset);
1483        view.core.cursor_position = Position::new(line, column);
1484        view.core.preferred_x_cells = buffer
1485            .executor
1486            .editor()
1487            .logical_position_to_visual(line, column)
1488            .map(|(_, x)| x);
1489        view.core.selection = None;
1490        view.core.secondary_selections.clear();
1491        view.core.cursor_position
1492    }
1493
1494    /// Move the cursor to the next bookmark (wrapping to the first bookmark).
1495    ///
1496    /// Returns the new cursor position, or `None` if there are no bookmarks.
1497    pub fn goto_next_bookmark(
1498        &mut self,
1499        view_id: ViewId,
1500    ) -> Result<Option<Position>, WorkspaceError> {
1501        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1502            return Err(WorkspaceError::ViewNotFound(view_id));
1503        };
1504        let Some(buffer) = self.buffers.get(&buffer_id) else {
1505            return Err(WorkspaceError::BufferNotFound(buffer_id));
1506        };
1507
1508        let current_line_start = buffer
1509            .executor
1510            .editor()
1511            .line_index()
1512            .position_to_char_offset(
1513                self.views
1514                    .get(&view_id)
1515                    .ok_or(WorkspaceError::ViewNotFound(view_id))?
1516                    .core
1517                    .cursor_position
1518                    .line,
1519                0,
1520            );
1521
1522        let Some(target) = buffer.bookmarks.next_after_line_start(current_line_start) else {
1523            return Ok(None);
1524        };
1525
1526        let Some(view) = self.views.get_mut(&view_id) else {
1527            return Err(WorkspaceError::ViewNotFound(view_id));
1528        };
1529        let pos = Self::move_view_cursor_to_anchor(view, buffer, target);
1530        Self::notify_view(view, StateChangeType::SelectionChanged, None);
1531        Ok(Some(pos))
1532    }
1533
1534    /// Move the cursor to the previous bookmark (wrapping to the last bookmark).
1535    ///
1536    /// Returns the new cursor position, or `None` if there are no bookmarks.
1537    pub fn goto_prev_bookmark(
1538        &mut self,
1539        view_id: ViewId,
1540    ) -> Result<Option<Position>, WorkspaceError> {
1541        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1542            return Err(WorkspaceError::ViewNotFound(view_id));
1543        };
1544        let Some(buffer) = self.buffers.get(&buffer_id) else {
1545            return Err(WorkspaceError::BufferNotFound(buffer_id));
1546        };
1547
1548        let current_line_start = buffer
1549            .executor
1550            .editor()
1551            .line_index()
1552            .position_to_char_offset(
1553                self.views
1554                    .get(&view_id)
1555                    .ok_or(WorkspaceError::ViewNotFound(view_id))?
1556                    .core
1557                    .cursor_position
1558                    .line,
1559                0,
1560            );
1561
1562        let Some(target) = buffer.bookmarks.prev_before_line_start(current_line_start) else {
1563            return Ok(None);
1564        };
1565
1566        let Some(view) = self.views.get_mut(&view_id) else {
1567            return Err(WorkspaceError::ViewNotFound(view_id));
1568        };
1569        let pos = Self::move_view_cursor_to_anchor(view, buffer, target);
1570        Self::notify_view(view, StateChangeType::SelectionChanged, None);
1571        Ok(Some(pos))
1572    }
1573
1574    /// Set (or replace) a named mark at the current cursor position of the given view.
1575    pub fn set_mark_at_cursor(
1576        &mut self,
1577        view_id: ViewId,
1578        name: String,
1579    ) -> Result<(), WorkspaceError> {
1580        if name.trim().is_empty() {
1581            return Err(WorkspaceError::CommandFailed {
1582                view: view_id,
1583                message: "Mark name cannot be empty".to_string(),
1584            });
1585        }
1586
1587        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1588            return Err(WorkspaceError::ViewNotFound(view_id));
1589        };
1590        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
1591            return Err(WorkspaceError::BufferNotFound(buffer_id));
1592        };
1593        let Some(view) = self.views.get(&view_id) else {
1594            return Err(WorkspaceError::ViewNotFound(view_id));
1595        };
1596
1597        let pos = view.core.cursor_position;
1598        let offset = buffer
1599            .executor
1600            .editor()
1601            .line_index()
1602            .position_to_char_offset(pos.line, pos.column);
1603        buffer.marks.set(name, offset);
1604
1605        for v in self.views.values_mut() {
1606            if v.buffer == buffer_id {
1607                Self::notify_view(v, StateChangeType::NavigationChanged, None);
1608            }
1609        }
1610
1611        Ok(())
1612    }
1613
1614    /// Move the cursor to a named mark (if present).
1615    ///
1616    /// Returns the new cursor position, or `None` if the mark does not exist.
1617    pub fn goto_mark(
1618        &mut self,
1619        view_id: ViewId,
1620        name: &str,
1621    ) -> Result<Option<Position>, WorkspaceError> {
1622        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1623            return Err(WorkspaceError::ViewNotFound(view_id));
1624        };
1625        let Some(buffer) = self.buffers.get(&buffer_id) else {
1626            return Err(WorkspaceError::BufferNotFound(buffer_id));
1627        };
1628
1629        let Some(anchor) = buffer.marks.get(name) else {
1630            return Ok(None);
1631        };
1632
1633        let Some(view) = self.views.get_mut(&view_id) else {
1634            return Err(WorkspaceError::ViewNotFound(view_id));
1635        };
1636        let pos = Self::move_view_cursor_to_anchor(view, buffer, anchor);
1637        Self::notify_view(view, StateChangeType::SelectionChanged, None);
1638        Ok(Some(pos))
1639    }
1640
1641    /// Remove a named mark from a buffer.
1642    ///
1643    /// Returns `true` if the mark existed.
1644    pub fn clear_mark(&mut self, buffer_id: BufferId, name: &str) -> Result<bool, WorkspaceError> {
1645        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
1646            return Err(WorkspaceError::BufferNotFound(buffer_id));
1647        };
1648        let existed = buffer.marks.remove(name);
1649        if existed {
1650            for v in self.views.values_mut() {
1651                if v.buffer == buffer_id {
1652                    Self::notify_view(v, StateChangeType::NavigationChanged, None);
1653                }
1654            }
1655        }
1656        Ok(existed)
1657    }
1658
1659    /// Return all mark names for a buffer (deterministic order).
1660    pub fn mark_names(&self, buffer_id: BufferId) -> Result<Vec<String>, WorkspaceError> {
1661        let Some(buffer) = self.buffers.get(&buffer_id) else {
1662            return Err(WorkspaceError::BufferNotFound(buffer_id));
1663        };
1664        Ok(buffer.marks.names())
1665    }
1666
1667    /// Clear all marks for a buffer.
1668    pub fn clear_all_marks(&mut self, buffer_id: BufferId) -> Result<(), WorkspaceError> {
1669        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
1670            return Err(WorkspaceError::BufferNotFound(buffer_id));
1671        };
1672        buffer.marks.clear();
1673        for v in self.views.values_mut() {
1674            if v.buffer == buffer_id {
1675                Self::notify_view(v, StateChangeType::NavigationChanged, None);
1676            }
1677        }
1678        Ok(())
1679    }
1680
1681    /// Record the current cursor position as a jump-list location for a view.
1682    ///
1683    /// Typical usage: call this *before* performing a “jump” (go-to-definition, search result,
1684    /// symbol navigation, ...).
1685    pub fn push_jump_location(&mut self, view_id: ViewId) -> Result<(), WorkspaceError> {
1686        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1687            return Err(WorkspaceError::ViewNotFound(view_id));
1688        };
1689        let Some(buffer) = self.buffers.get(&buffer_id) else {
1690            return Err(WorkspaceError::BufferNotFound(buffer_id));
1691        };
1692        let Some(view) = self.views.get_mut(&view_id) else {
1693            return Err(WorkspaceError::ViewNotFound(view_id));
1694        };
1695
1696        let pos = view.core.cursor_position;
1697        let offset = buffer
1698            .executor
1699            .editor()
1700            .line_index()
1701            .position_to_char_offset(pos.line, pos.column);
1702
1703        view.jump_list.record(JumpEntry {
1704            buffer_id,
1705            anchor: TextAnchor::new(offset, AnchorBias::Right),
1706        });
1707
1708        Self::notify_view(view, StateChangeType::NavigationChanged, None);
1709        Ok(())
1710    }
1711
1712    /// Jump back in the view's jump list.
1713    ///
1714    /// Returns the navigation target (including the buffer id). If the target belongs to the
1715    /// current view's buffer, this method also moves the cursor and clears selection.
1716    pub fn jump_back(&mut self, view_id: ViewId) -> Result<Option<JumpTarget>, WorkspaceError> {
1717        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1718            return Err(WorkspaceError::ViewNotFound(view_id));
1719        };
1720        let Some(buffer) = self.buffers.get(&buffer_id) else {
1721            return Err(WorkspaceError::BufferNotFound(buffer_id));
1722        };
1723
1724        let current_pos = self
1725            .views
1726            .get(&view_id)
1727            .ok_or(WorkspaceError::ViewNotFound(view_id))?
1728            .core
1729            .cursor_position;
1730        let current_offset = buffer
1731            .executor
1732            .editor()
1733            .line_index()
1734            .position_to_char_offset(current_pos.line, current_pos.column);
1735        let current = JumpEntry {
1736            buffer_id,
1737            anchor: TextAnchor::new(current_offset, AnchorBias::Right),
1738        };
1739
1740        let Some(view) = self.views.get_mut(&view_id) else {
1741            return Err(WorkspaceError::ViewNotFound(view_id));
1742        };
1743        let Some(target) = view.jump_list.back(current) else {
1744            return Ok(None);
1745        };
1746
1747        let Some(target_buffer) = self.buffers.get(&target.buffer_id) else {
1748            Self::notify_view(view, StateChangeType::NavigationChanged, None);
1749            return Ok(None);
1750        };
1751
1752        let (line, column) = target_buffer
1753            .executor
1754            .editor()
1755            .line_index()
1756            .char_offset_to_position(target.anchor.offset);
1757        let target_pos = Position::new(line, column);
1758
1759        let out = JumpTarget {
1760            buffer_id: target.buffer_id,
1761            position: target_pos,
1762        };
1763
1764        if target.buffer_id == buffer_id {
1765            Self::move_view_cursor_to_anchor(view, buffer, target.anchor);
1766            Self::notify_view(view, StateChangeType::SelectionChanged, None);
1767        } else {
1768            Self::notify_view(view, StateChangeType::NavigationChanged, None);
1769        }
1770
1771        Ok(Some(out))
1772    }
1773
1774    /// Jump forward in the view's jump list.
1775    ///
1776    /// Returns the navigation target (including the buffer id). If the target belongs to the
1777    /// current view's buffer, this method also moves the cursor and clears selection.
1778    pub fn jump_forward(&mut self, view_id: ViewId) -> Result<Option<JumpTarget>, WorkspaceError> {
1779        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
1780            return Err(WorkspaceError::ViewNotFound(view_id));
1781        };
1782        let Some(buffer) = self.buffers.get(&buffer_id) else {
1783            return Err(WorkspaceError::BufferNotFound(buffer_id));
1784        };
1785
1786        let current_pos = self
1787            .views
1788            .get(&view_id)
1789            .ok_or(WorkspaceError::ViewNotFound(view_id))?
1790            .core
1791            .cursor_position;
1792        let current_offset = buffer
1793            .executor
1794            .editor()
1795            .line_index()
1796            .position_to_char_offset(current_pos.line, current_pos.column);
1797        let current = JumpEntry {
1798            buffer_id,
1799            anchor: TextAnchor::new(current_offset, AnchorBias::Right),
1800        };
1801
1802        let Some(view) = self.views.get_mut(&view_id) else {
1803            return Err(WorkspaceError::ViewNotFound(view_id));
1804        };
1805        let Some(target) = view.jump_list.forward(current) else {
1806            return Ok(None);
1807        };
1808
1809        let Some(target_buffer) = self.buffers.get(&target.buffer_id) else {
1810            Self::notify_view(view, StateChangeType::NavigationChanged, None);
1811            return Ok(None);
1812        };
1813
1814        let (line, column) = target_buffer
1815            .executor
1816            .editor()
1817            .line_index()
1818            .char_offset_to_position(target.anchor.offset);
1819        let target_pos = Position::new(line, column);
1820
1821        let out = JumpTarget {
1822            buffer_id: target.buffer_id,
1823            position: target_pos,
1824        };
1825
1826        if target.buffer_id == buffer_id {
1827            Self::move_view_cursor_to_anchor(view, buffer, target.anchor);
1828            Self::notify_view(view, StateChangeType::SelectionChanged, None);
1829        } else {
1830            Self::notify_view(view, StateChangeType::NavigationChanged, None);
1831        }
1832
1833        Ok(Some(out))
1834    }
1835
1836    /// Clear the jump list (both back/forward stacks) for a view.
1837    pub fn clear_jump_list(&mut self, view_id: ViewId) -> Result<(), WorkspaceError> {
1838        let Some(view) = self.views.get_mut(&view_id) else {
1839            return Err(WorkspaceError::ViewNotFound(view_id));
1840        };
1841        view.jump_list.clear();
1842        Self::notify_view(view, StateChangeType::NavigationChanged, None);
1843        Ok(())
1844    }
1845
1846    /// Apply a previously produced [`JumpTarget`] to a view (moves the cursor and clears
1847    /// selection).
1848    pub fn apply_jump_target(
1849        &mut self,
1850        view_id: ViewId,
1851        target: JumpTarget,
1852    ) -> Result<(), WorkspaceError> {
1853        let Some(view) = self.views.get_mut(&view_id) else {
1854            return Err(WorkspaceError::ViewNotFound(view_id));
1855        };
1856        if view.buffer != target.buffer_id {
1857            return Err(WorkspaceError::CommandFailed {
1858                view: view_id,
1859                message: "JumpTarget buffer does not match view buffer".to_string(),
1860            });
1861        }
1862
1863        let Some(buffer) = self.buffers.get(&view.buffer) else {
1864            return Err(WorkspaceError::BufferNotFound(view.buffer));
1865        };
1866
1867        view.core.cursor_position = target.position;
1868        view.core.preferred_x_cells = buffer
1869            .executor
1870            .editor()
1871            .logical_position_to_visual(target.position.line, target.position.column)
1872            .map(|(_, x)| x);
1873        view.core.selection = None;
1874        view.core.secondary_selections.clear();
1875
1876        Self::notify_view(view, StateChangeType::SelectionChanged, None);
1877        Ok(())
1878    }
1879
1880    /// Set the viewport height for a view (used for `ViewportState` calculations).
1881    pub fn set_viewport_height(
1882        &mut self,
1883        view_id: ViewId,
1884        height: usize,
1885    ) -> Result<(), WorkspaceError> {
1886        let Some(view) = self.views.get_mut(&view_id) else {
1887            return Err(WorkspaceError::ViewNotFound(view_id));
1888        };
1889        view.viewport_height = Some(height);
1890        Ok(())
1891    }
1892
1893    /// Set the scroll position (top visual row) for a view.
1894    pub fn set_scroll_top(
1895        &mut self,
1896        view_id: ViewId,
1897        scroll_top: usize,
1898    ) -> Result<(), WorkspaceError> {
1899        let Some(view) = self.views.get_mut(&view_id) else {
1900            return Err(WorkspaceError::ViewNotFound(view_id));
1901        };
1902        view.scroll_top = scroll_top;
1903        Ok(())
1904    }
1905
1906    /// Set sub-row smooth-scroll offset for a view.
1907    pub fn set_scroll_sub_row_offset(
1908        &mut self,
1909        view_id: ViewId,
1910        sub_row_offset: u16,
1911    ) -> Result<(), WorkspaceError> {
1912        let Some(view) = self.views.get_mut(&view_id) else {
1913            return Err(WorkspaceError::ViewNotFound(view_id));
1914        };
1915        view.scroll_sub_row_offset = sub_row_offset;
1916        Ok(())
1917    }
1918
1919    /// Set overscan rows for a view.
1920    pub fn set_overscan_rows(
1921        &mut self,
1922        view_id: ViewId,
1923        overscan_rows: usize,
1924    ) -> Result<(), WorkspaceError> {
1925        let Some(view) = self.views.get_mut(&view_id) else {
1926            return Err(WorkspaceError::ViewNotFound(view_id));
1927        };
1928        view.overscan_rows = overscan_rows;
1929        Ok(())
1930    }
1931
1932    /// Set smooth-scroll state for a view.
1933    pub fn set_smooth_scroll_state(
1934        &mut self,
1935        view_id: ViewId,
1936        state: ViewSmoothScrollState,
1937    ) -> Result<(), WorkspaceError> {
1938        let Some(view) = self.views.get_mut(&view_id) else {
1939            return Err(WorkspaceError::ViewNotFound(view_id));
1940        };
1941        view.scroll_top = state.top_visual_row;
1942        view.scroll_sub_row_offset = state.sub_row_offset;
1943        view.overscan_rows = state.overscan_rows;
1944        Ok(())
1945    }
1946
1947    /// Get viewport state for a view, including total visual lines and overscan prefetch range.
1948    pub fn viewport_state_for_view(
1949        &mut self,
1950        view_id: ViewId,
1951    ) -> Result<WorkspaceViewportState, WorkspaceError> {
1952        let Some((
1953            buffer_id,
1954            view_core,
1955            scroll_top,
1956            viewport_height,
1957            sub_row_offset,
1958            overscan_rows,
1959        )) = self.views.get(&view_id).map(|v| {
1960            (
1961                v.buffer,
1962                v.core.clone(),
1963                v.scroll_top,
1964                v.viewport_height,
1965                v.scroll_sub_row_offset,
1966                v.overscan_rows,
1967            )
1968        })
1969        else {
1970            return Err(WorkspaceError::ViewNotFound(view_id));
1971        };
1972
1973        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
1974            return Err(WorkspaceError::BufferNotFound(buffer_id));
1975        };
1976        view_core.apply_to_executor(&mut buffer.executor);
1977        let editor = buffer.executor.editor();
1978
1979        let total_visual_lines = editor.visual_line_count();
1980        let visible_end = if let Some(height) = viewport_height {
1981            scroll_top.saturating_add(height).min(total_visual_lines)
1982        } else {
1983            total_visual_lines
1984        };
1985        let visible_lines = scroll_top.min(total_visual_lines)..visible_end;
1986        let prefetch_start = visible_lines.start.saturating_sub(overscan_rows);
1987        let prefetch_end = visible_lines
1988            .end
1989            .saturating_add(overscan_rows)
1990            .min(total_visual_lines);
1991
1992        Ok(WorkspaceViewportState {
1993            width: editor.viewport_width(),
1994            height: viewport_height,
1995            scroll_top,
1996            visible_lines,
1997            total_visual_lines,
1998            smooth_scroll: ViewSmoothScrollState {
1999                top_visual_row: scroll_top,
2000                sub_row_offset,
2001                overscan_rows,
2002            },
2003            prefetch_lines: prefetch_start..prefetch_end,
2004        })
2005    }
2006
2007    /// Get total visual lines for a view (wrap + folding aware).
2008    pub fn total_visual_lines_for_view(
2009        &mut self,
2010        view_id: ViewId,
2011    ) -> Result<usize, WorkspaceError> {
2012        Ok(self.viewport_state_for_view(view_id)?.total_visual_lines)
2013    }
2014
2015    /// Map global visual row to `(logical_line, visual_in_logical)` for a view.
2016    pub fn visual_to_logical_for_view(
2017        &mut self,
2018        view_id: ViewId,
2019        visual_row: usize,
2020    ) -> Result<(usize, usize), WorkspaceError> {
2021        let Some((buffer_id, view_core)) =
2022            self.views.get(&view_id).map(|v| (v.buffer, v.core.clone()))
2023        else {
2024            return Err(WorkspaceError::ViewNotFound(view_id));
2025        };
2026        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2027            return Err(WorkspaceError::BufferNotFound(buffer_id));
2028        };
2029        view_core.apply_to_executor(&mut buffer.executor);
2030        Ok(buffer.executor.editor().visual_to_logical_line(visual_row))
2031    }
2032
2033    /// Map logical position to global visual `(row, x_cells)` for a view.
2034    pub fn logical_to_visual_for_view(
2035        &mut self,
2036        view_id: ViewId,
2037        line: usize,
2038        column: usize,
2039    ) -> Result<Option<(usize, usize)>, WorkspaceError> {
2040        let Some((buffer_id, view_core)) =
2041            self.views.get(&view_id).map(|v| (v.buffer, v.core.clone()))
2042        else {
2043            return Err(WorkspaceError::ViewNotFound(view_id));
2044        };
2045        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2046            return Err(WorkspaceError::BufferNotFound(buffer_id));
2047        };
2048        view_core.apply_to_executor(&mut buffer.executor);
2049        Ok(buffer
2050            .executor
2051            .editor()
2052            .logical_position_to_visual(line, column))
2053    }
2054
2055    /// Map visual `(row, x_cells)` back to logical position for a view.
2056    pub fn visual_position_to_logical_for_view(
2057        &mut self,
2058        view_id: ViewId,
2059        visual_row: usize,
2060        x_cells: usize,
2061    ) -> Result<Option<Position>, WorkspaceError> {
2062        let Some((buffer_id, view_core)) =
2063            self.views.get(&view_id).map(|v| (v.buffer, v.core.clone()))
2064        else {
2065            return Err(WorkspaceError::ViewNotFound(view_id));
2066        };
2067        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2068            return Err(WorkspaceError::BufferNotFound(buffer_id));
2069        };
2070        view_core.apply_to_executor(&mut buffer.executor);
2071        Ok(buffer
2072            .executor
2073            .editor()
2074            .visual_position_to_logical(visual_row, x_cells))
2075    }
2076
2077    /// Get the full document text for a buffer.
2078    pub fn buffer_text(&self, buffer_id: BufferId) -> Result<String, WorkspaceError> {
2079        let Some(buffer) = self.buffers.get(&buffer_id) else {
2080            return Err(WorkspaceError::BufferNotFound(buffer_id));
2081        };
2082        Ok(buffer.executor.editor().get_text())
2083    }
2084
2085    /// Get the full document text converted to the buffer's preferred line ending for saving.
2086    pub fn buffer_text_for_saving(&self, buffer_id: BufferId) -> Result<String, WorkspaceError> {
2087        let Some(buffer) = self.buffers.get(&buffer_id) else {
2088            return Err(WorkspaceError::BufferNotFound(buffer_id));
2089        };
2090        let text = buffer.executor.editor().get_text();
2091        Ok(buffer.executor.line_ending().apply_to_text(&text))
2092    }
2093
2094    /// Get the full document text converted to the view's preferred line ending for saving.
2095    pub fn text_for_saving_for_view(&self, view_id: ViewId) -> Result<String, WorkspaceError> {
2096        let buffer_id = self.buffer_id_for_view(view_id)?;
2097        self.buffer_text_for_saving(buffer_id)
2098    }
2099
2100    /// Get styled viewport content for a view (by visual line).
2101    pub fn get_viewport_content_styled(
2102        &mut self,
2103        view_id: ViewId,
2104        start_visual_row: usize,
2105        count: usize,
2106    ) -> Result<crate::HeadlessGrid, WorkspaceError> {
2107        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
2108            return Err(WorkspaceError::ViewNotFound(view_id));
2109        };
2110
2111        let view_core = self
2112            .views
2113            .get(&view_id)
2114            .map(|v| v.core.clone())
2115            .ok_or(WorkspaceError::ViewNotFound(view_id))?;
2116
2117        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2118            return Err(WorkspaceError::BufferNotFound(buffer_id));
2119        };
2120
2121        view_core.apply_to_executor(&mut buffer.executor);
2122        Ok(buffer
2123            .executor
2124            .editor()
2125            .get_headless_grid_styled(start_visual_row, count))
2126    }
2127
2128    /// Get lightweight minimap content for a view (by visual line).
2129    pub fn get_minimap_content(
2130        &mut self,
2131        view_id: ViewId,
2132        start_visual_row: usize,
2133        count: usize,
2134    ) -> Result<crate::MinimapGrid, WorkspaceError> {
2135        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
2136            return Err(WorkspaceError::ViewNotFound(view_id));
2137        };
2138
2139        let view_core = self
2140            .views
2141            .get(&view_id)
2142            .map(|v| v.core.clone())
2143            .ok_or(WorkspaceError::ViewNotFound(view_id))?;
2144
2145        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2146            return Err(WorkspaceError::BufferNotFound(buffer_id));
2147        };
2148
2149        view_core.apply_to_executor(&mut buffer.executor);
2150        Ok(buffer
2151            .executor
2152            .editor()
2153            .get_minimap_grid(start_visual_row, count))
2154    }
2155
2156    /// Get a decoration-aware composed viewport snapshot for a view (by composed visual line).
2157    ///
2158    /// This snapshot can include virtual text (inlay hints, code lens) injected from the buffer's
2159    /// decoration layers. See [`crate::EditorCore::get_headless_grid_composed`] for details.
2160    pub fn get_viewport_content_composed(
2161        &mut self,
2162        view_id: ViewId,
2163        start_visual_row: usize,
2164        count: usize,
2165    ) -> Result<crate::ComposedGrid, WorkspaceError> {
2166        let Some(buffer_id) = self.views.get(&view_id).map(|v| v.buffer) else {
2167            return Err(WorkspaceError::ViewNotFound(view_id));
2168        };
2169
2170        let view_core = self
2171            .views
2172            .get(&view_id)
2173            .map(|v| v.core.clone())
2174            .ok_or(WorkspaceError::ViewNotFound(view_id))?;
2175
2176        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2177            return Err(WorkspaceError::BufferNotFound(buffer_id));
2178        };
2179
2180        view_core.apply_to_executor(&mut buffer.executor);
2181        Ok(buffer
2182            .executor
2183            .editor()
2184            .get_headless_grid_composed(start_visual_row, count))
2185    }
2186
2187    /// Apply derived-state edits to a buffer and broadcast them to all views of that buffer.
2188    pub fn apply_processing_edits<I>(
2189        &mut self,
2190        buffer_id: BufferId,
2191        edits: I,
2192    ) -> Result<(), WorkspaceError>
2193    where
2194        I: IntoIterator<Item = ProcessingEdit>,
2195    {
2196        let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2197            return Err(WorkspaceError::BufferNotFound(buffer_id));
2198        };
2199
2200        let mut style_changed = false;
2201        let mut folding_changed = false;
2202        let mut diagnostics_changed = false;
2203        let mut decorations_changed = false;
2204        let mut symbols_changed = false;
2205
2206        for edit in edits {
2207            match edit {
2208                ProcessingEdit::ReplaceStyleLayer { layer, intervals } => {
2209                    buffer
2210                        .executor
2211                        .editor_mut()
2212                        .replace_style_layer(layer, intervals);
2213                    style_changed = true;
2214                }
2215                ProcessingEdit::ClearStyleLayer { layer } => {
2216                    buffer.executor.editor_mut().clear_style_layer(layer);
2217                    style_changed = true;
2218                }
2219                ProcessingEdit::ReplaceFoldingRegions {
2220                    regions,
2221                    preserve_collapsed,
2222                } => {
2223                    buffer
2224                        .executor
2225                        .editor_mut()
2226                        .replace_folding_regions(regions, preserve_collapsed);
2227                    folding_changed = true;
2228                }
2229                ProcessingEdit::ClearFoldingRegions => {
2230                    buffer.executor.editor_mut().clear_derived_folding_regions();
2231                    folding_changed = true;
2232                }
2233                ProcessingEdit::ReplaceDiagnostics { diagnostics } => {
2234                    buffer
2235                        .executor
2236                        .editor_mut()
2237                        .replace_diagnostics(diagnostics);
2238                    diagnostics_changed = true;
2239                }
2240                ProcessingEdit::ClearDiagnostics => {
2241                    buffer.executor.editor_mut().clear_diagnostics();
2242                    diagnostics_changed = true;
2243                }
2244                ProcessingEdit::ReplaceDecorations { layer, decorations } => {
2245                    buffer
2246                        .executor
2247                        .editor_mut()
2248                        .replace_decorations(layer, decorations);
2249                    decorations_changed = true;
2250                }
2251                ProcessingEdit::ClearDecorations { layer } => {
2252                    buffer.executor.editor_mut().clear_decorations(layer);
2253                    decorations_changed = true;
2254                }
2255                ProcessingEdit::ReplaceDocumentSymbols { symbols } => {
2256                    buffer
2257                        .executor
2258                        .editor_mut()
2259                        .replace_document_symbols(symbols);
2260                    symbols_changed = true;
2261                }
2262                ProcessingEdit::ClearDocumentSymbols => {
2263                    buffer.executor.editor_mut().clear_document_symbols();
2264                    symbols_changed = true;
2265                }
2266            }
2267        }
2268
2269        let change_type = if folding_changed {
2270            Some(StateChangeType::FoldingChanged)
2271        } else if style_changed {
2272            Some(StateChangeType::StyleChanged)
2273        } else if decorations_changed {
2274            Some(StateChangeType::DecorationsChanged)
2275        } else if diagnostics_changed {
2276            Some(StateChangeType::DiagnosticsChanged)
2277        } else if symbols_changed {
2278            Some(StateChangeType::SymbolsChanged)
2279        } else {
2280            None
2281        };
2282
2283        if let Some(change_type) = change_type {
2284            for view in self.views.values_mut() {
2285                if view.buffer == buffer_id {
2286                    Self::notify_view(view, change_type, None);
2287                }
2288            }
2289            buffer.version = buffer.version.saturating_add(1);
2290        }
2291
2292        Ok(())
2293    }
2294
2295    /// Search across all open buffers in the workspace.
2296    ///
2297    /// - This is purely in-memory (no file I/O).
2298    /// - Match ranges are returned as **character offsets** (half-open).
2299    pub fn search_all_open_buffers(
2300        &self,
2301        query: &str,
2302        options: SearchOptions,
2303    ) -> Result<Vec<WorkspaceSearchResult>, SearchError> {
2304        let mut out: Vec<WorkspaceSearchResult> = Vec::new();
2305
2306        for (id, entry) in &self.buffers {
2307            let text = entry.executor.editor().get_text();
2308            let matches = find_all(&text, query, options)?;
2309            if matches.is_empty() {
2310                continue;
2311            }
2312
2313            out.push(WorkspaceSearchResult {
2314                id: *id,
2315                uri: entry.meta.uri.clone(),
2316                matches,
2317            });
2318        }
2319
2320        Ok(out)
2321    }
2322
2323    /// Apply a set of text edits to multiple open buffers.
2324    ///
2325    /// - This is purely in-memory (no file I/O).
2326    /// - Edits are applied as a single undoable step **per buffer**.
2327    /// - Buffers are applied in deterministic `BufferId` order.
2328    pub fn apply_text_edits<I>(
2329        &mut self,
2330        edits: I,
2331    ) -> Result<Vec<(BufferId, usize)>, WorkspaceError>
2332    where
2333        I: IntoIterator<Item = (BufferId, Vec<TextEditSpec>)>,
2334    {
2335        let mut by_id: BTreeMap<BufferId, Vec<TextEditSpec>> = BTreeMap::new();
2336        for (id, mut buffer_edits) in edits {
2337            by_id.entry(id).or_default().append(&mut buffer_edits);
2338        }
2339
2340        let mut applied: Vec<(BufferId, usize)> = Vec::new();
2341        for (buffer_id, buffer_edits) in by_id {
2342            let edit_count = buffer_edits.len();
2343            if edit_count == 0 {
2344                continue;
2345            }
2346
2347            let Some(buffer) = self.buffers.get_mut(&buffer_id) else {
2348                return Err(WorkspaceError::BufferNotFound(buffer_id));
2349            };
2350
2351            let before_line_index = buffer.executor.editor().line_index().clone();
2352            let before_char_count = buffer.executor.editor().char_count();
2353
2354            // Apply without relying on any specific view selection: load a neutral view state.
2355            let neutral = ViewCore {
2356                cursor_position: Position::new(0, 0),
2357                selection: None,
2358                secondary_selections: Vec::new(),
2359                viewport_width: buffer.executor.editor().viewport_width().max(1),
2360                wrap_mode: buffer.executor.editor().layout_engine().wrap_mode(),
2361                wrap_indent: buffer.executor.editor().layout_engine().wrap_indent(),
2362                tab_width: buffer.executor.editor().layout_engine().tab_width(),
2363                tab_key_behavior: buffer.executor.tab_key_behavior(),
2364                indentation_config: buffer.executor.indentation_config().clone(),
2365                auto_pairs: buffer.executor.auto_pairs_config().clone(),
2366                snippet_session: None,
2367                preferred_x_cells: None,
2368            };
2369            neutral.apply_to_executor(&mut buffer.executor);
2370
2371            buffer
2372                .executor
2373                .execute(Command::Edit(EditCommand::ApplyTextEdits {
2374                    edits: buffer_edits,
2375                }))
2376                .map_err(|err| WorkspaceError::ApplyEditsFailed {
2377                    buffer: buffer_id,
2378                    message: err.to_string(),
2379                })?;
2380
2381            let delta = buffer.executor.take_last_text_delta().map(Arc::new);
2382            let after_char_count = buffer.executor.editor().char_count();
2383            let changed = delta.is_some() || after_char_count != before_char_count;
2384
2385            if changed {
2386                if let Some(uri) = buffer.meta.uri.as_deref() {
2387                    self.intelligence.mark_stale_for_uri(uri);
2388                }
2389
2390                if let Some(ref delta_arc) = delta {
2391                    buffer.last_text_delta = Some(delta_arc.clone());
2392                    let new_index = buffer.executor.editor().line_index();
2393                    for view in self.views.values_mut() {
2394                        if view.buffer != buffer_id {
2395                            continue;
2396                        }
2397
2398                        view.last_text_delta = Some(delta_arc.clone());
2399
2400                        view.core.cursor_position = apply_position_delta(
2401                            &before_line_index,
2402                            new_index,
2403                            view.core.cursor_position,
2404                            delta_arc,
2405                        );
2406                        if let Some(ref sel) = view.core.selection {
2407                            view.core.selection = Some(apply_selection_delta(
2408                                &before_line_index,
2409                                new_index,
2410                                sel,
2411                                delta_arc,
2412                            ));
2413                        }
2414                        for sel in &mut view.core.secondary_selections {
2415                            *sel = apply_selection_delta(
2416                                &before_line_index,
2417                                new_index,
2418                                sel,
2419                                delta_arc,
2420                            );
2421                        }
2422
2423                        Self::notify_view(
2424                            view,
2425                            StateChangeType::DocumentModified,
2426                            Some(delta_arc.clone()),
2427                        );
2428                    }
2429                } else {
2430                    buffer.last_text_delta = None;
2431                    for view in self.views.values_mut() {
2432                        if view.buffer == buffer_id {
2433                            Self::notify_view(view, StateChangeType::DocumentModified, None);
2434                        }
2435                    }
2436                }
2437
2438                buffer.version = buffer.version.saturating_add(1);
2439            }
2440
2441            applied.push((buffer_id, edit_count));
2442        }
2443
2444        Ok(applied)
2445    }
2446}