This commit is contained in:
2026-10-29 22:40:40 +03:00
commit dce595fbc0
18 changed files with 7983 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
//! Документы редактора и состояние вкладок.
use std::sync::atomic::{AtomicU64, Ordering};
use crate::syntax::{Language, SyntaxState};
/// Счётчик идентификаторов документов.
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
/// Открытый в редакторе документ.
pub struct Document {
/// Стабильный идентификатор, пока документ открыт.
///
/// По нему `TextEdit` и `ScrollArea` хранят курсор, выделение и прокрутку,
/// поэтому переключение вкладок не сбрасывает состояние редактора.
pub id: u64,
pub name: String,
pub content: String,
/// Язык подсветки синтаксиса.
///
/// `None` — подсветка выключена (поведение по умолчанию). Включается
/// только явно через `AppApi::set_language`, например из Lua-сценария.
pub language: Option<Language>,
/// Разбор tree-sitter и кэш подсветки для этого документа.
pub syntax: SyntaxState,
/// Содержимое на момент последнего «сохранения».
saved_content: String,
}
impl Document {
pub fn new(name: impl Into<String>, content: impl Into<String>) -> Self {
let content = content.into();
Self {
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
name: name.into(),
saved_content: content.clone(),
content,
language: None,
syntax: SyntaxState::default(),
}
}
/// Есть ли изменения, которые ещё не сохранены.
pub fn is_dirty(&self) -> bool {
self.content != self.saved_content
}
/// Помечает текущее содержимое сохранённым.
pub fn mark_saved(&mut self) {
self.saved_content = self.content.clone();
}
}
/// Открытые документы и активная вкладка.
///
/// Инвариант: `documents` никогда не пуст — редактор всегда показывает хотя бы
/// один документ. Это позволяет `active`/`active_mut` не возвращать `Option`.
pub struct EditorState {
documents: Vec<Document>,
active: usize,
next_untitled: u32,
}
impl Default for EditorState {
fn default() -> Self {
Self {
documents: vec![
Document::new("main.rs", SAMPLE_RUST),
Document::new("notes.md", SAMPLE_NOTES),
],
active: 0,
next_untitled: 0,
}
}
}
impl EditorState {
pub fn documents(&self) -> &[Document] {
&self.documents
}
pub fn active_index(&self) -> usize {
self.active
}
pub fn active(&self) -> &Document {
&self.documents[self.active]
}
pub fn active_mut(&mut self) -> &mut Document {
&mut self.documents[self.active]
}
/// Документ по номеру вкладки (для API).
pub fn document(&self, index: usize) -> Option<&Document> {
self.documents.get(index)
}
/// Изменяемый документ по номеру вкладки (для API).
pub fn document_mut(&mut self, index: usize) -> Option<&mut Document> {
self.documents.get_mut(index)
}
/// Делает вкладку активной, если индекс существует.
pub fn set_active(&mut self, index: usize) {
if index < self.documents.len() {
self.active = index;
}
}
/// Открывает пустую вкладку и делает её активной.
pub fn open_new(&mut self) {
self.next_untitled += 1;
self.documents.push(Document::new(
format!("untitled-{}.txt", self.next_untitled),
"",
));
self.active = self.documents.len() - 1;
}
/// Закрывает вкладку и выбирает соседнюю.
///
/// Если закрывается последняя вкладка, вместо неё открывается пустой документ.
pub fn close(&mut self, index: usize) {
if index >= self.documents.len() {
return;
}
self.documents.remove(index);
if self.documents.is_empty() {
self.open_new();
} else if index < self.active {
self.active -= 1;
} else if index == self.active {
self.active = self.active.min(self.documents.len() - 1);
}
}
}
const SAMPLE_RUST: &str = r#"//! Демонстрационный файл.
// Правьте код прямо в редакторе: вкладки и нумерация строк уже здесь.
fn main() {
let mut numbers = vec![3, 1, 2];
numbers.sort();
for (i, n) in numbers.iter().enumerate() {
println!("{i}: {n}");
}
}
"#;
const SAMPLE_NOTES: &str = r#"# Заметки
- «+» на панели вкладок открывает новый файл
- крестик на вкладке закрывает её
- Ctrl+S сохраняет вкладку в файл с её названием
"#;