use std::ops::Range;
use crate::{App, AppContext as _, Context, Entity, Pixels, SharedString, Window, px};
use super::super::InputState;
use super::codelens::CodelensState;
use super::extensions::ExtensionsState;
use super::inlay_hints::InlayState;
use super::lsp_attach::LspAttach;
use super::minimap::MinimapState;
use super::snippets::SnippetSession;
use crate::highlight::{DocumentSymbol, Highlighter};
pub struct EditorState {
pub(super) input: Entity<InputState>,
pub(super) outline: Vec<DocumentSymbol>,
pub(super) lsp: LspAttach,
pub(super) snippet: Option<SnippetSession>,
pub(super) inlay: InlayState,
pub(super) minimap: MinimapState,
pub(super) codelens: CodelensState,
pub(super) extensions: ExtensionsState,
pub(super) sticky_scroll: bool,
pub(super) sticky_position: super::sticky_scroll::StickyPosition,
language: Option<SharedString>,
font_size: Option<Pixels>,
}
impl EditorState {
pub fn new(window: &mut Window, cx: &mut Context<Self>, initial: &str) -> Self {
let input = cx.new(|cx| {
let mut state = InputState::new(window, cx)
.multi_line(true)
.rows(2)
.line_number(true)
.folding(true)
.indent_guides(true)
.auto_close_pairs(true)
.bracket_match(true)
.current_line_highlight(true);
let end = initial.len();
state.set_value(initial, window, cx);
state.set_selected_range(end..end, cx);
state
});
let mut this = Self {
input,
outline: Vec::new(),
lsp: LspAttach::new(cx.new(|_| crate::lsp::CompletionPopupState::default())),
snippet: None,
inlay: InlayState::new(),
minimap: MinimapState::new(),
codelens: CodelensState::new(),
extensions: ExtensionsState::new(),
sticky_scroll: true,
sticky_position: super::sticky_scroll::StickyPosition::Top,
language: None,
font_size: None,
};
this.refresh_outline(cx);
cx.subscribe_in(&this.input, window, move |this, _, event, window, cx| {
if !matches!(event, crate::input_ui::InputEvent::Change) {
return;
}
this.refresh_outline(cx);
this.maybe_auto_complete(window, cx);
this.request_codelenses(window, cx);
let edit = this.edit_event(cx);
this.fire_edit_event(&edit, window, cx);
})
.detach();
this
}
pub fn input(&self) -> &Entity<InputState> {
&self.input
}
pub fn text(&self, cx: &App) -> String {
self.input
.read_with(cx, |state, _| state.text().to_string())
}
pub fn set_value(
&mut self,
value: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.input.update(cx, |state, cx| {
state.set_value(value, window, cx);
});
self.refresh_outline(cx);
}
pub fn set_selected_range(&self, range: Range<usize>, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.set_selected_range(range, cx);
});
}
pub fn reveal_offset(&self, offset: usize, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.reveal_offset(offset, cx);
});
}
pub fn reveal_range(&self, range: Range<usize>, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.reveal_range(range, cx);
});
}
pub fn set_folding(&mut self, folding: bool, window: &mut Window, cx: &mut Context<Self>) {
self.input.update(cx, |state, cx| {
state.set_folding(folding, window, cx);
});
}
pub fn set_read_only(&self, read_only: bool, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.set_read_only(read_only, cx);
});
}
pub fn set_rulers(&self, columns: Vec<usize>, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.set_rulers(columns, cx);
});
}
pub fn set_ruler_color(&self, color: Option<crate::Hsla>, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.set_ruler_color(color, cx);
});
}
pub fn set_bracket_rainbow_enabled(&self, enabled: bool, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.set_bracket_rainbow_enabled(enabled, cx);
});
}
pub fn set_vim_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
self.input.update(cx, |state, cx| {
state.vim.enabled = enabled;
state.vim.mode = super::vim::VimMode::Normal;
state.vim.pending = None;
state.vim.anchor = None;
if enabled {
let cursor = state.cursor();
state.move_to(cursor, None, cx);
}
cx.notify();
});
cx.notify();
}
pub fn vim_enabled(&self, cx: &App) -> bool {
self.input.read_with(cx, |state, _| state.vim.enabled)
}
pub fn vim_mode(&self, cx: &App) -> Option<super::vim::VimMode> {
self.input
.read_with(cx, |state, _| state.vim.enabled.then_some(state.vim.mode))
}
}
#[derive(Debug, Clone, Copy)]
pub struct ChromeGeometry {
pub gutter_end: Pixels,
pub numbers_end: Pixels,
pub fold_end: Pixels,
pub text_start: Pixels,
}
impl EditorState {
pub(crate) fn vim_key(&mut self, key: &str, window: &mut Window, cx: &mut Context<Self>) {
self.input.update(cx, |state, cx| {
super::vim::handle_key(state, key, window, cx);
});
}
pub fn chrome_geometry(&self, cx: &App) -> Option<ChromeGeometry> {
self.input.read_with(cx, |state, _| {
let bounds = state.last_bounds?;
let layout = state.last_layout.as_ref()?;
let gutter = super::gutter::gutter_column_width(state);
let folding = state.mode.is_folding();
let fold_part = if folding {
super::super::input::FOLD_ICON_HITBOX_WIDTH
} else {
px(0.)
};
let full = layout.line_number_width;
let margin = super::super::input::LINE_NUMBER_RIGHT_MARGIN;
Some(ChromeGeometry {
gutter_end: bounds.origin.x + gutter,
numbers_end: bounds.origin.x + full - margin - fold_part,
fold_end: bounds.origin.x + full - margin,
text_start: bounds.origin.x + full,
})
})
}
pub(crate) fn vim_escape(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.input.update(cx, |state, cx| {
super::vim::escape_pressed(state, window, cx);
});
}
pub fn set_line_comment_prefix(&self, prefix: impl Into<SharedString>, cx: &mut App) {
let _ = self.input.update(cx, |state, cx| {
state.set_line_comment_prefix(prefix, cx);
});
}
pub fn cursor(&self, cx: &App) -> usize {
self.input.read_with(cx, |state, _| state.cursor())
}
pub fn set_highlighter(
&mut self,
highlighter: Option<Box<dyn Highlighter>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.input.update(cx, |state, cx| {
state.set_highlighter(highlighter, window, cx);
});
self.refresh_outline(cx);
}
pub fn set_language(&mut self, language: &str, window: &mut Window, cx: &mut Context<Self>) {
let highlighter = crate::highlight::highlighter_for(language);
self.language = if highlighter.is_some() {
Some(language.into())
} else {
None
};
self.set_highlighter(highlighter, window, cx);
}
pub fn language(&self) -> Option<SharedString> {
self.language.clone()
}
pub fn font_size(&self) -> Option<Pixels> {
self.font_size
}
pub fn set_font_size(&mut self, font_size: Option<Pixels>, cx: &mut Context<Self>) {
self.font_size = font_size;
self.input.update(cx, |state, cx| {
state.set_font_size_override(font_size, cx);
});
}
pub fn outline(&self) -> &[DocumentSymbol] {
&self.outline
}
pub fn goto_symbol(&mut self, symbol: &DocumentSymbol, cx: &mut Context<Self>) {
let symbol = symbol.clone();
self.input.update(cx, |state, cx| {
state.goto_symbol(&symbol, cx);
});
}
fn refresh_outline(&mut self, cx: &mut Context<Self>) {
let symbols = self
.input
.read_with(cx, |state, _| state.document_symbols());
self.outline = symbols;
cx.notify();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Entity;
struct Probe {
state: Entity<EditorState>,
}
impl crate::Render for Probe {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl crate::IntoElement {
crate::div()
}
}
#[rgpui::test]
fn editor_state_text_roundtrip(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
let text = editor.read_with(cx, |state, cx| state.text(cx));
assert_eq!(text, "fn main() {}\n");
let cursor = editor.read_with(cx, |state, cx| state.cursor(cx));
assert_eq!(cursor, "fn main() {}\n".len());
}
#[rgpui::test]
fn set_language_unknown_downgrades(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_language("cobol-xyz-not-registered", window, cx);
});
});
assert!(editor.read_with(cx, |state, _| state.language().is_none()));
assert!(editor.read_with(cx, |state, _| state.outline().is_empty()));
}
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))]
#[rgpui::test]
fn set_language_rust_available(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_language("rust", window, cx);
});
});
assert_eq!(
editor
.read_with(cx, |state, _| state.language())
.map(|s| s.to_string()),
Some("rust".to_string())
);
assert!(!editor.read_with(cx, |state, _| state.outline().is_empty()));
}
#[rgpui::test]
fn document_symbols_empty_without_highlighter(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
let symbols = editor.read_with(cx, |state, _| state.outline().to_vec());
assert!(symbols.is_empty());
}
#[rgpui::test]
fn goto_symbol_moves_cursor(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
let symbol = crate::highlight::DocumentSymbol {
kind: crate::highlight::SymbolKind::Function,
name: "main".into(),
range: 0..2,
start_row: 0,
};
editor.update(cx, |state, cx| {
state.goto_symbol(&symbol, cx);
});
let cursor = editor.read_with(cx, |state, cx| state.cursor(cx));
assert_eq!(cursor, 0);
}
#[rgpui::test]
fn initial_content_not_undoable(cx: &mut crate::TestAppContext) {
const INITIAL: &str = "fn main() {}\n";
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, INITIAL));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
let input = editor.read_with(cx, |state, _| state.input().clone());
cx.update(|window, cx| {
input.update(cx, |state, cx| {
crate::EntityInputHandler::replace_text_in_range(state, None, "a", window, cx);
crate::EntityInputHandler::replace_text_in_range(state, None, "b", window, cx);
});
});
assert_eq!(
input.read_with(cx, |state, _| state.value().to_string()),
"fn main() {}\nab".to_string()
);
cx.update(|window, cx| {
input.update(cx, |state, cx| {
state.undo(&crate::input_ui::Undo, window, cx)
});
});
assert_eq!(
input.read_with(cx, |state, _| state.value().to_string()),
INITIAL.to_string()
);
cx.update(|window, cx| {
input.update(cx, |state, cx| {
state.undo(&crate::input_ui::Undo, window, cx)
});
});
assert_eq!(
input.read_with(cx, |state, _| state.value().to_string()),
INITIAL.to_string()
);
}
struct StubHighlighter;
impl crate::highlight::Highlighter for StubHighlighter {
fn language(&self) -> SharedString {
"stub".into()
}
fn update(
&mut self,
_edit: Option<crate::highlight::TextEdit>,
_text: &ropey::Rope,
_folding: bool,
_window: &mut Window,
_cx: &mut App,
) {
}
fn styles(
&self,
range: &Range<usize>,
_resolver: &dyn crate::highlight::HighlightStyleResolver,
) -> Vec<(Range<usize>, crate::HighlightStyle)> {
vec![(range.clone(), crate::HighlightStyle::default())]
}
fn fold_ranges(&self, _text: &ropey::Rope) -> Vec<crate::highlight::FoldRange> {
Vec::new()
}
fn document_symbols(&self, _text: &ropey::Rope) -> Vec<crate::highlight::DocumentSymbol> {
vec![crate::highlight::DocumentSymbol {
kind: crate::highlight::SymbolKind::Function,
name: "main".into(),
range: 0..2,
start_row: 0,
}]
}
}
#[rgpui::test]
fn set_highlighter_refreshes_outline(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
assert!(editor.read_with(cx, |state, _| state.outline().is_empty()));
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_highlighter(Some(Box::new(StubHighlighter)), window, cx);
});
});
let outline = editor.read_with(cx, |state, _| state.outline().to_vec());
assert_eq!(outline.len(), 1);
assert_eq!(outline[0].name.to_string(), "main");
}
#[rgpui::test]
fn set_value_replaces_without_history(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "old\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_highlighter(Some(Box::new(StubHighlighter)), window, cx);
state.set_value("new\n", window, cx);
});
});
assert_eq!(editor.read_with(cx, |state, cx| state.text(cx)), "new\n");
assert_eq!(editor.read_with(cx, |state, cx| state.cursor(cx)), 0);
assert_eq!(editor.read_with(cx, |state, _| state.outline().len()), 1);
let input = editor.read_with(cx, |state, _| state.input().clone());
cx.update(|window, cx| {
input.update(cx, |state, cx| {
state.undo(&crate::input_ui::Undo, window, cx)
});
});
assert_eq!(
input.read_with(cx, |state, _| state.value().to_string()),
"new\n"
);
}
}