This commit is contained in:
2026-10-30 00:31:49 +03:00
parent dce595fbc0
commit e00f63e5da
17 changed files with 6479 additions and 3200 deletions
+150 -14
View File
@@ -1,18 +1,88 @@
//! Документы редактора и состояние вкладок.
//!
//! Это доменная модель приложения: текст, имена и языки вкладок живут здесь,
//! а виджет `Editor` из gpui-component показывает активный документ. Такое
//! разделение позволяет Lua-сценариям менять документы без доступа к `Window`
//! и `App`: вид обновляет редактор, когда меняется [`Workspace::revision`].
use std::sync::atomic::{AtomicU64, Ordering};
use crate::syntax::{Language, SyntaxState};
/// Счётчик идентификаторов документов.
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
/// Языки, для которых в редакторе включена подсветка tree-sitter.
///
/// Названия намеренно короткие (`js`, а не `javascript`): именно их возвращает
/// [`Language::name`] и показывает Lua-API. gpui-component принимает такие
/// псевдонимы и сам приводит их к имени грамматики.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Language {
Rust,
Go,
JavaScript,
Json,
Python,
}
impl Language {
/// Все поддерживаемые языки.
#[allow(dead_code)]
// Часть публичного интерфейса модуля: удобно для перебора языков снаружи.
pub const ALL: [Self; 5] = [
Self::Rust,
Self::Go,
Self::JavaScript,
Self::Json,
Self::Python,
];
/// Разбирает название языка из API/сценария.
///
/// Основные имена — `rust`, `go`, `js`, `json`, `python`; короткие
/// (`rs`, `golang`, `javascript`, `py`) и регистр тоже принимаются.
pub fn from_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"rust" | "rs" => Some(Self::Rust),
"go" | "golang" => Some(Self::Go),
"js" | "javascript" | "node" => Some(Self::JavaScript),
"json" => Some(Self::Json),
"python" | "py" => Some(Self::Python),
_ => None,
}
}
/// Каноническое имя языка (то, что возвращает `AppApi::language`).
pub fn name(self) -> &'static str {
match self {
Self::Rust => "rust",
Self::Go => "go",
Self::JavaScript => "js",
Self::Json => "json",
Self::Python => "python",
}
}
/// Имя языка для `EditorState::set_highlighter`.
///
/// Здесь `js` разворачивается в `javascript`: так грамматика находится
/// без дополнительных псевдонимов на нашей стороне.
pub fn highlight_name(self) -> &'static str {
match self {
Self::Rust => "rust",
Self::Go => "go",
Self::JavaScript => "javascript",
Self::Json => "json",
Self::Python => "python",
}
}
}
/// Открытый в редакторе документ.
pub struct Document {
/// Стабильный идентификатор, пока документ открыт.
///
/// По нему `TextEdit` и `ScrollArea` хранят курсор, выделение и прокрутку,
/// поэтому переключение вкладок не сбрасывает состояние редактора.
/// По нему вид отличает «ту же вкладку» от «другой вкладки с тем же
/// индексом» после закрытия соседей.
pub id: u64,
pub name: String,
pub content: String,
@@ -21,8 +91,8 @@ pub struct Document {
/// `None` — подсветка выключена (поведение по умолчанию). Включается
/// только явно через `AppApi::set_language`, например из Lua-сценария.
pub language: Option<Language>,
/// Разбор tree-sitter и кэш подсветки для этого документа.
pub syntax: SyntaxState,
/// Позиция курсора в байтах на момент ухода с вкладки.
pub cursor: usize,
/// Содержимое на момент последнего «сохранения».
saved_content: String,
}
@@ -36,7 +106,7 @@ impl Document {
saved_content: content.clone(),
content,
language: None,
syntax: SyntaxState::default(),
cursor: 0,
}
}
@@ -55,13 +125,19 @@ impl Document {
///
/// Инвариант: `documents` никогда не пуст — редактор всегда показывает хотя бы
/// один документ. Это позволяет `active`/`active_mut` не возвращать `Option`.
pub struct EditorState {
pub struct Workspace {
documents: Vec<Document>,
active: usize,
next_untitled: u32,
/// Номер «поколения» состояния.
///
/// Вид перечитывает активный документ в редактор только когда ревизия
/// изменилась. Набор текста сам обновляет документ и ревизию не трогает,
/// иначе редактор переставлял бы курсор после каждого символа.
revision: u64,
}
impl Default for EditorState {
impl Default for Workspace {
fn default() -> Self {
Self {
documents: vec![
@@ -70,11 +146,12 @@ impl Default for EditorState {
],
active: 0,
next_untitled: 0,
revision: 0,
}
}
}
impl EditorState {
impl Workspace {
pub fn documents(&self) -> &[Document] {
&self.documents
}
@@ -96,15 +173,26 @@ impl EditorState {
self.documents.get(index)
}
/// Изменяемый документ по номеру вкладки (для API).
pub fn document_mut(&mut self, index: usize) -> Option<&mut Document> {
self.documents.get_mut(index)
/// Изменяемый документ по стабильному идентификатору.
pub fn document_mut_by_id(&mut self, id: u64) -> Option<&mut Document> {
self.documents.iter_mut().find(|doc| doc.id == id)
}
/// Текущая ревизия состояния.
pub fn revision(&self) -> u64 {
self.revision
}
/// Сообщает виду, что документы нужно перечитать.
fn bump(&mut self) {
self.revision = self.revision.wrapping_add(1);
}
/// Делает вкладку активной, если индекс существует.
pub fn set_active(&mut self, index: usize) {
if index < self.documents.len() {
if index < self.documents.len() && index != self.active {
self.active = index;
self.bump();
}
}
@@ -116,6 +204,7 @@ impl EditorState {
"",
));
self.active = self.documents.len() - 1;
self.bump();
}
/// Закрывает вкладку и выбирает соседнюю.
@@ -135,6 +224,53 @@ impl EditorState {
} else if index == self.active {
self.active = self.active.min(self.documents.len() - 1);
}
self.bump();
}
/// Обновляет текст активного документа без изменения ревизии.
///
/// Используется видом после ввода в редакторе: сам виджет уже показывает
/// этот текст, поэтому повторная синхронизация только сбросила бы курсор.
pub fn replace_active_content(&mut self, content: String) {
self.active_mut().content = content;
}
/// Заменяет текст активной вкладки извне (Lua, меню, тесты).
pub fn set_active_text(&mut self, text: impl Into<String>) {
self.active_mut().content = text.into();
self.bump();
}
/// Включает подсветку для вкладки; возвращает `false`, если вкладки нет.
pub fn set_language(&mut self, index: usize, language: Language) -> bool {
let Some(doc) = self.documents.get_mut(index) else {
return false;
};
if doc.language != Some(language) {
doc.language = Some(language);
self.bump();
}
true
}
/// Выключает подсветку для вкладки; возвращает `false`, если вкладки нет.
pub fn clear_language(&mut self, index: usize) -> bool {
let Some(doc) = self.documents.get_mut(index) else {
return false;
};
if doc.language.take().is_some() {
self.bump();
}
true
}
/// Помечает активный документ сохранённым.
///
/// Ревизия меняется, чтобы индикатор несохранённых изменений на вкладке
/// перерисовался даже без отдельного уведомления вида.
pub fn mark_active_saved(&mut self) {
self.active_mut().mark_saved();
self.bump();
}
}