Skip to main content

editor_core/
workspace.rs

1//! Workspace and multi-document model.
2//!
3//! `editor-core` is intentionally UI-agnostic, but a full-featured editor typically needs a
4//! kernel-level model for managing multiple open documents/buffers.
5//!
6//! This module provides a small `Workspace` type that owns multiple [`EditorStateManager`]
7//! instances and provides:
8//! - stable, opaque document ids
9//! - optional URI->document lookup for integrations (e.g. LSP)
10//! - an "active document" convenience slot (host-driven)
11
12use crate::EditorStateManager;
13use std::collections::{BTreeMap, HashMap};
14
15/// Opaque identifier for an open document in a [`Workspace`].
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct DocumentId(u64);
18
19impl DocumentId {
20    /// Get the underlying numeric id.
21    pub fn get(self) -> u64 {
22        self.0
23    }
24}
25
26/// Metadata attached to a workspace document.
27#[derive(Debug, Clone)]
28pub struct DocumentMetadata {
29    /// Optional document URI/path (host-provided).
30    pub uri: Option<String>,
31}
32
33struct DocumentEntry {
34    meta: DocumentMetadata,
35    state: EditorStateManager,
36}
37
38/// Workspace-level errors.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum WorkspaceError {
41    /// A document with this uri already exists.
42    UriAlreadyOpen(String),
43    /// A document id was not found.
44    DocumentNotFound(DocumentId),
45}
46
47/// A collection of open documents/buffers and their state.
48#[derive(Default)]
49pub struct Workspace {
50    next_id: u64,
51    documents: BTreeMap<DocumentId, DocumentEntry>,
52    uri_to_id: HashMap<String, DocumentId>,
53    active: Option<DocumentId>,
54}
55
56impl std::fmt::Debug for Workspace {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("Workspace")
59            .field("next_id", &self.next_id)
60            .field("document_count", &self.documents.len())
61            .field("uri_count", &self.uri_to_id.len())
62            .field("active", &self.active)
63            .finish()
64    }
65}
66
67impl Workspace {
68    /// Create an empty workspace.
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Returns the number of open documents.
74    pub fn len(&self) -> usize {
75        self.documents.len()
76    }
77
78    /// Returns `true` if there are no open documents.
79    pub fn is_empty(&self) -> bool {
80        self.documents.is_empty()
81    }
82
83    /// Return the active document id (if any).
84    pub fn active_document_id(&self) -> Option<DocumentId> {
85        self.active
86    }
87
88    /// Set the active document.
89    pub fn set_active_document(&mut self, id: DocumentId) -> Result<(), WorkspaceError> {
90        if !self.documents.contains_key(&id) {
91            return Err(WorkspaceError::DocumentNotFound(id));
92        }
93        self.active = Some(id);
94        Ok(())
95    }
96
97    /// Open a new document in the workspace.
98    ///
99    /// - `uri` is optional and host-provided (e.g. `file:///...`).
100    /// - `text` is the initial contents.
101    pub fn open_document(
102        &mut self,
103        uri: Option<String>,
104        text: &str,
105        viewport_width: usize,
106    ) -> Result<DocumentId, WorkspaceError> {
107        if let Some(uri) = uri.as_ref()
108            && self.uri_to_id.contains_key(uri)
109        {
110            return Err(WorkspaceError::UriAlreadyOpen(uri.clone()));
111        }
112
113        let id = DocumentId(self.next_id);
114        self.next_id = self.next_id.saturating_add(1);
115
116        let state = EditorStateManager::new(text, viewport_width);
117        let meta = DocumentMetadata { uri: uri.clone() };
118
119        if let Some(uri) = uri {
120            self.uri_to_id.insert(uri, id);
121        }
122        self.documents.insert(id, DocumentEntry { meta, state });
123
124        if self.active.is_none() {
125            self.active = Some(id);
126        }
127
128        Ok(id)
129    }
130
131    /// Close a document.
132    pub fn close_document(&mut self, id: DocumentId) -> Result<(), WorkspaceError> {
133        let Some(entry) = self.documents.remove(&id) else {
134            return Err(WorkspaceError::DocumentNotFound(id));
135        };
136
137        if let Some(uri) = entry.meta.uri.as_ref() {
138            self.uri_to_id.remove(uri);
139        }
140
141        if self.active == Some(id) {
142            self.active = self.documents.keys().next().copied();
143        }
144
145        Ok(())
146    }
147
148    /// Look up a document by uri.
149    pub fn document_id_for_uri(&self, uri: &str) -> Option<DocumentId> {
150        self.uri_to_id.get(uri).copied()
151    }
152
153    /// Get a document's metadata.
154    pub fn document_metadata(&self, id: DocumentId) -> Option<&DocumentMetadata> {
155        self.documents.get(&id).map(|e| &e.meta)
156    }
157
158    /// Update a document's uri/path.
159    pub fn set_document_uri(
160        &mut self,
161        id: DocumentId,
162        uri: Option<String>,
163    ) -> Result<(), WorkspaceError> {
164        let Some(entry) = self.documents.get_mut(&id) else {
165            return Err(WorkspaceError::DocumentNotFound(id));
166        };
167
168        if let Some(next) = uri.as_ref()
169            && self.uri_to_id.contains_key(next)
170            && entry.meta.uri.as_deref() != Some(next.as_str())
171        {
172            return Err(WorkspaceError::UriAlreadyOpen(next.clone()));
173        }
174
175        if let Some(prev) = entry.meta.uri.take() {
176            self.uri_to_id.remove(&prev);
177        }
178
179        if let Some(next) = uri.clone() {
180            self.uri_to_id.insert(next, id);
181        }
182
183        entry.meta.uri = uri;
184        Ok(())
185    }
186
187    /// Get an immutable reference to a document state manager.
188    pub fn document(&self, id: DocumentId) -> Option<&EditorStateManager> {
189        self.documents.get(&id).map(|e| &e.state)
190    }
191
192    /// Get a mutable reference to a document state manager.
193    pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut EditorStateManager> {
194        self.documents.get_mut(&id).map(|e| &mut e.state)
195    }
196
197    /// Get the active document (if any).
198    pub fn active_document(&self) -> Option<&EditorStateManager> {
199        let id = self.active?;
200        self.document(id)
201    }
202
203    /// Get the active document mutably (if any).
204    pub fn active_document_mut(&mut self) -> Option<&mut EditorStateManager> {
205        let id = self.active?;
206        self.document_mut(id)
207    }
208
209    /// Iterate over open documents in `DocumentId` order.
210    pub fn iter(&self) -> impl Iterator<Item = (DocumentId, &EditorStateManager)> {
211        self.documents.iter().map(|(id, entry)| (*id, &entry.state))
212    }
213}