ElyMark: find and replace, list continuation, indent, table menu, fence colors (C14.9)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dbd841c786
commit
d4a7d3ebd7
13 files changed
+477
-10
No files matched your search
@@ -53,6 +53,8 @@ pub struct Desk {
|
||||
asked: Option<u64>,
|
||||
/// The shown file's fingerprint when Git was last asked.
|
||||
asked_print: Option<ely_files::Fingerprint>,
|
||||
/// The find box, while open.
|
||||
pub(crate) finding: Option<crate::finding::Find>,
|
||||
}
|
||||
|
||||
impl Host for Desk {
|
||||
@@ -115,6 +117,9 @@ impl Host for Desk {
|
||||
self.look(id, cx);
|
||||
}
|
||||
self.restore_shown(id, cx);
|
||||
if self.finding.is_some() {
|
||||
self.count(cx);
|
||||
}
|
||||
// A save changes what Git sees.
|
||||
let print = self.active_tab().and_then(|tab| tab.session.fingerprint());
|
||||
if print != self.asked_print {
|
||||
@@ -196,6 +201,7 @@ pub(crate) fn open_in(
|
||||
tracked: None,
|
||||
asked: None,
|
||||
asked_print: None,
|
||||
finding: None,
|
||||
}
|
||||
});
|
||||
let menus = crate::keys::menus(&look, cx);
|
||||
@@ -413,6 +419,7 @@ impl Render for Desk {
|
||||
.on_action(cx.listener(Self::on_save_as))
|
||||
.on_action(cx.listener(Self::on_rename))
|
||||
.on_action(cx.listener(Self::on_close_tab))
|
||||
.on_action(cx.listener(Self::on_find))
|
||||
.on_action(cx.listener(|desk, _: &CheckDisk, _, cx| desk.look_all(cx)))
|
||||
.on_action(cx.listener(|desk, _: &VisualMode, window, cx| {
|
||||
desk.switch(Mode::Visual, window, cx)
|
||||
@@ -426,8 +433,9 @@ impl Render for Desk {
|
||||
.child(shell)
|
||||
.children(self.core_dialog(cx))
|
||||
.children(self.naming_element(cx))
|
||||
.children(self.compare_dialog(cx));
|
||||
Self::look_actions(root, cx)
|
||||
.children(self.compare_dialog(cx))
|
||||
.children(self.find_box(cx));
|
||||
Self::table_actions(Self::look_actions(root, cx), cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,10 @@ use super::{Desk, open_in};
|
||||
use crate::view::Mode;
|
||||
|
||||
/// ElyMark over a fresh data folder, as `main` opens it.
|
||||
pub(crate) fn start(cx: &mut TestAppContext, data: &Path) -> (Entity<Desk>, &'static mut VisualTestContext) {
|
||||
pub(crate) fn start(
|
||||
cx: &mut TestAppContext,
|
||||
data: &Path,
|
||||
) -> (Entity<Desk>, &'static mut VisualTestContext) {
|
||||
// The file watcher runs on a real thread.
|
||||
cx.executor().allow_parking();
|
||||
cx.update(ely_gpui_component::init_for_tests);
|
||||
@@ -28,6 +31,7 @@ pub(crate) fn start(cx: &mut TestAppContext, data: &Path) -> (Entity<Desk>, &'st
|
||||
.expect("a window")
|
||||
});
|
||||
let cx = VisualTestContext::from_window(window.into(), cx).into_mut();
|
||||
cx.update(|window, _| window.activate_window());
|
||||
cx.run_until_parked();
|
||||
let desk = held.take().expect("the desk");
|
||||
(desk, cx)
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
//! Find and replace in the shown file (MARK-06).
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use ely_gpui_component::{
|
||||
editor::{FindOptions, FindWidget, find_all},
|
||||
forms::{InputEvent, TextInput},
|
||||
primitives::{Severity, raise},
|
||||
theme::ActiveTheme as _,
|
||||
};
|
||||
use ely_office::prelude::*;
|
||||
use ely_ui::i18n::text;
|
||||
use gpui::{
|
||||
AnyElement, AppContext as _, Context, Entity, Focusable as _, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, SharedString, Styled as _, Subscription, Window, div,
|
||||
};
|
||||
|
||||
use crate::desk::Desk;
|
||||
use crate::view::MarkView;
|
||||
|
||||
/// The find box over one tab, and what it found.
|
||||
pub struct Find {
|
||||
tab: u64,
|
||||
query: Entity<TextInput>,
|
||||
replace: Option<Entity<TextInput>>,
|
||||
options: FindOptions,
|
||||
hits: Vec<Range<usize>>,
|
||||
current: Option<usize>,
|
||||
/// The pattern did not compile; why.
|
||||
error: Option<SharedString>,
|
||||
_query: Subscription,
|
||||
}
|
||||
|
||||
impl Desk {
|
||||
pub fn on_find(
|
||||
&mut self,
|
||||
_: &ely_office::FindReplace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(tab) = self.core.active else {
|
||||
let said = text(cx, "command.blocked.no_document", &[]);
|
||||
return self.toast(said, Severity::Warning, cx);
|
||||
};
|
||||
if let Some(find) = self.finding.as_ref().filter(|find| find.tab == tab) {
|
||||
return window.focus(&find.query.focus_handle(cx), cx);
|
||||
}
|
||||
self.close_find(window, cx);
|
||||
let query = cx.new(|cx| TextInput::new(window, cx));
|
||||
let subscription =
|
||||
cx.subscribe_in(&query, window, |desk, _, event, window, cx| match event {
|
||||
InputEvent::Changed => desk.count(cx),
|
||||
InputEvent::Submit => desk.step(true, window, cx),
|
||||
_ => {}
|
||||
});
|
||||
window.focus(&query.focus_handle(cx), cx);
|
||||
log::info!("mark: find opens on tab {tab}");
|
||||
self.finding = Some(Find {
|
||||
tab,
|
||||
query,
|
||||
replace: None,
|
||||
options: FindOptions::default(),
|
||||
hits: Vec::new(),
|
||||
current: None,
|
||||
error: None,
|
||||
_query: subscription,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn found_view(&self) -> Option<(Entity<MarkView>, &Find)> {
|
||||
let find = self.finding.as_ref()?;
|
||||
Some((self.tab(find.tab)?.view.clone(), find))
|
||||
}
|
||||
|
||||
/// Matches again in the tab's text, marked in source.
|
||||
pub fn count(&mut self, cx: &mut Context<Self>) {
|
||||
let Some((view, find)) = self.found_view() else {
|
||||
return;
|
||||
};
|
||||
let query = find.query.read(cx).text().to_string();
|
||||
let options = find.options;
|
||||
let document = view.read(cx).document().clone();
|
||||
let found = find_all(&document.read(cx).text(cx), &query, options);
|
||||
let Some(find) = self.finding.as_mut() else {
|
||||
return;
|
||||
};
|
||||
(find.hits, find.error) = match found {
|
||||
Ok(hits) => (hits, None),
|
||||
Err(why) => (Vec::new(), Some(why.into())),
|
||||
};
|
||||
find.current = find.current.filter(|current| *current < find.hits.len());
|
||||
let (hits, current) = (find.hits.clone(), find.current);
|
||||
mark(&document, &hits, current, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn step(&mut self, forward: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some((view, find)) = self.found_view() else {
|
||||
return;
|
||||
};
|
||||
let count = find.hits.len();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let current = match (find.current, forward) {
|
||||
(None, true) => 0,
|
||||
(None, false) => count - 1,
|
||||
(Some(at), true) => (at + 1) % count,
|
||||
(Some(at), false) => (at + count - 1) % count,
|
||||
};
|
||||
let range = find.hits[current].clone();
|
||||
let hits = find.hits.clone();
|
||||
if let Some(find) = self.finding.as_mut() {
|
||||
find.current = Some(current);
|
||||
}
|
||||
let document = view.read(cx).document().clone();
|
||||
mark(&document, &hits, Some(current), cx);
|
||||
document.update(cx, |document, cx| document.show_match(range, window, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace(&mut self, every: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some((view, find)) = self.found_view() else {
|
||||
return;
|
||||
};
|
||||
let with = find
|
||||
.replace
|
||||
.as_ref()
|
||||
.map_or(String::new(), |field| field.read(cx).text().to_string());
|
||||
let edits: Vec<(Range<usize>, String)> = match (every, find.current) {
|
||||
(true, _) => find
|
||||
.hits
|
||||
.iter()
|
||||
.map(|hit| (hit.clone(), with.clone()))
|
||||
.collect(),
|
||||
(false, Some(at)) => vec![(find.hits[at].clone(), with)],
|
||||
(false, None) => return self.step(true, window, cx),
|
||||
};
|
||||
if edits.is_empty() {
|
||||
return;
|
||||
}
|
||||
let document = view.read(cx).document().clone();
|
||||
document.update(cx, |document, cx| document.replace(edits, cx));
|
||||
self.count(cx);
|
||||
// The next match takes the replaced one's place.
|
||||
let next = self
|
||||
.finding
|
||||
.as_ref()
|
||||
.and_then(|find| find.current.and_then(|at| find.hits.get(at)).cloned());
|
||||
if let Some(range) = next.filter(|_| !every) {
|
||||
document.update(cx, |document, cx| document.show_match(range, window, cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn close_find(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(find) = self.finding.take() else {
|
||||
return;
|
||||
};
|
||||
if let Some(tab) = self.tab(find.tab) {
|
||||
let document = tab.view.read(cx).document().clone();
|
||||
mark(&document, &[], None, cx);
|
||||
window.focus(&tab.view.read(cx).focus.clone(), cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The find box over the file, while open.
|
||||
pub fn find_box(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let find = self
|
||||
.finding
|
||||
.as_ref()
|
||||
.filter(|find| Some(find.tab) == self.core.active)?;
|
||||
let this = cx.entity();
|
||||
let mut widget = FindWidget::new("mark-find", &find.query, find.current, find.hits.len())
|
||||
.options(find.options)
|
||||
.on_options({
|
||||
let this = this.clone();
|
||||
move |options, _, cx| {
|
||||
this.update(cx, |desk, cx| {
|
||||
if let Some(find) = desk.finding.as_mut() {
|
||||
find.options = options;
|
||||
}
|
||||
desk.count(cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.on_step({
|
||||
let this = this.clone();
|
||||
move |forward, window, cx| {
|
||||
this.update(cx, |desk, cx| desk.step(forward, window, cx))
|
||||
}
|
||||
})
|
||||
.on_replace({
|
||||
let this = this.clone();
|
||||
move |every, window, cx| this.update(cx, |desk, cx| desk.replace(every, window, cx))
|
||||
})
|
||||
.on_toggle_replace({
|
||||
let this = this.clone();
|
||||
move |show, window, cx| {
|
||||
this.update(cx, |desk, cx| {
|
||||
if let Some(find) = desk.finding.as_mut() {
|
||||
find.replace = show.then(|| cx.new(|cx| TextInput::new(window, cx)));
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
})
|
||||
.on_close(move |window, cx| this.update(cx, |desk, cx| desk.close_find(window, cx)));
|
||||
if let Some(field) = &find.replace {
|
||||
widget = widget.replace(field);
|
||||
}
|
||||
if let Some(error) = &find.error {
|
||||
widget = widget.error(error.clone());
|
||||
}
|
||||
let place = div()
|
||||
.absolute()
|
||||
.top_2()
|
||||
.right_4()
|
||||
.block_mouse_except_scroll()
|
||||
.child(widget);
|
||||
Some(raise("mark-find-layer", place).into_any_element())
|
||||
}
|
||||
}
|
||||
|
||||
/// The matches marked in the source; the current one stronger.
|
||||
fn mark(
|
||||
document: &Entity<ely_gpui_component::documents::MarkdownDocument>,
|
||||
hits: &[Range<usize>],
|
||||
current: Option<usize>,
|
||||
cx: &mut Context<Desk>,
|
||||
) {
|
||||
let tone = cx.theme().colors.warning;
|
||||
let marks = hits
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(at, hit)| {
|
||||
let strength = if Some(at) == current { 0.45 } else { 0.2 };
|
||||
(hit.clone(), tone.opacity(strength))
|
||||
})
|
||||
.collect();
|
||||
let editor = document.read(cx).editor().clone();
|
||||
editor.update(cx, |editor, cx| editor.set_backgrounds(marks, cx));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,120 @@
|
||||
use ely_gpui_component::forms::TextInput;
|
||||
use gpui::{AppContext as _, Entity, TestAppContext, VisualTestContext};
|
||||
|
||||
use crate::desk::{
|
||||
Desk,
|
||||
tests::{open, start},
|
||||
};
|
||||
|
||||
fn opened(
|
||||
text: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (
|
||||
Entity<Desk>,
|
||||
&'static mut VisualTestContext,
|
||||
[tempfile::TempDir; 2],
|
||||
) {
|
||||
let (data, files) = (
|
||||
tempfile::tempdir().expect("data"),
|
||||
tempfile::tempdir().expect("files"),
|
||||
);
|
||||
let path = files.path().join("a.md");
|
||||
std::fs::write(&path, text).expect("a file");
|
||||
let (desk, cx) = start(cx, data.path());
|
||||
open(&desk, &path, cx);
|
||||
(desk, cx, [data, files])
|
||||
}
|
||||
|
||||
fn query(desk: &Entity<Desk>, words: &str, cx: &mut VisualTestContext) {
|
||||
desk.update_in(cx, |desk, window, cx| {
|
||||
desk.on_find(&ely_office::FindReplace, window, cx)
|
||||
});
|
||||
cx.simulate_input(words);
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn current_text(desk: &Entity<Desk>, cx: &mut VisualTestContext) -> String {
|
||||
desk.read_with(cx, |desk, cx| {
|
||||
let view = desk.active_view().expect("a tab");
|
||||
view.read(cx).document().read(cx).text(cx)
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn every_match_is_counted_and_replaced_in_one_step(cx: &mut TestAppContext) {
|
||||
let (desk, cx, _files) = opened("a b a\n", cx);
|
||||
query(&desk, "a", cx);
|
||||
assert_eq!(
|
||||
desk.read_with(cx, |desk, _| desk
|
||||
.finding
|
||||
.as_ref()
|
||||
.map(|find| find.hits.len())),
|
||||
Some(2)
|
||||
);
|
||||
desk.update_in(cx, |desk, window, cx| {
|
||||
let field = cx.new(|cx| TextInput::new(window, cx));
|
||||
field.update(cx, |field, cx| field.set_text("x", cx));
|
||||
desk.finding.as_mut().expect("open").replace = Some(field);
|
||||
desk.replace(true, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(current_text(&desk, cx), "x b x\n");
|
||||
let focus = desk.read_with(cx, |desk, cx| {
|
||||
desk.active_view().expect("a tab").read(cx).focus.clone()
|
||||
});
|
||||
cx.update(|window, cx| window.focus(&focus, cx));
|
||||
cx.run_until_parked();
|
||||
cx.simulate_keystrokes("secondary-z");
|
||||
assert_eq!(
|
||||
current_text(&desk, cx),
|
||||
"a b a\n",
|
||||
"one undo takes all back"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_bad_pattern_says_why(cx: &mut TestAppContext) {
|
||||
let (desk, cx, _files) = opened("a\n", cx);
|
||||
desk.update_in(cx, |desk, window, cx| {
|
||||
desk.on_find(&ely_office::FindReplace, window, cx)
|
||||
});
|
||||
desk.update(cx, |desk, cx| {
|
||||
desk.finding.as_mut().expect("open").options.regex = true;
|
||||
desk.count(cx);
|
||||
});
|
||||
cx.simulate_input("(");
|
||||
cx.run_until_parked();
|
||||
let error = desk.read_with(cx, |desk, _| {
|
||||
desk.finding.as_ref().and_then(|find| find.error.clone())
|
||||
});
|
||||
assert!(error.is_some());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_step_shows_the_next_match(cx: &mut TestAppContext) {
|
||||
let (desk, cx, _files) = opened("one\n\ntwo one\n", cx);
|
||||
query(&desk, "one", cx);
|
||||
desk.update_in(cx, |desk, window, cx| {
|
||||
desk.step(true, window, cx);
|
||||
desk.step(true, window, cx);
|
||||
});
|
||||
let current = desk.read_with(cx, |desk, _| {
|
||||
desk.finding.as_ref().and_then(|find| find.current)
|
||||
});
|
||||
assert_eq!(current, Some(1));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn the_table_menu_inserts_and_edits_a_table(cx: &mut TestAppContext) {
|
||||
let (desk, cx, _files) = opened("Intro\n", cx);
|
||||
let focus = desk.read_with(cx, |desk, _| desk.core.focus.clone());
|
||||
cx.update(|window, cx| window.focus(&focus, cx));
|
||||
cx.dispatch_action(crate::tools::InsertTable);
|
||||
assert!(current_text(&desk, cx).contains("| --- | --- | --- |"));
|
||||
cx.dispatch_action(crate::tools::DeleteColumn);
|
||||
assert!(
|
||||
current_text(&desk, cx).contains("| --- | --- |\n"),
|
||||
"{}",
|
||||
current_text(&desk, cx)
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ pub fn bind(cx: &mut App) {
|
||||
KeyBinding::new("secondary-=", crate::look::Larger, at),
|
||||
KeyBinding::new("secondary--", crate::look::Smaller, at),
|
||||
KeyBinding::new("secondary-0", crate::look::ActualSize, at),
|
||||
KeyBinding::new("secondary-f", ely_office::FindReplace, at),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -35,7 +36,23 @@ pub fn menus(look: &crate::look::Look, cx: &App) -> Vec<Menu> {
|
||||
disabled: false,
|
||||
};
|
||||
let item = |key: &str, action: Box<dyn gpui::Action>| checked(key, action, false);
|
||||
use crate::tools::{
|
||||
ColumnLeft, ColumnRight, DeleteColumn, DeleteRow, InsertTable, RowAbove, RowBelow,
|
||||
};
|
||||
vec![
|
||||
Menu::new(text(cx, "menu.edit", &[]))
|
||||
.items([item("office.find.open", Box::new(ely_office::FindReplace))]),
|
||||
Menu::new(text(cx, "mark.menu.table", &[])).items([
|
||||
item("mark.table.insert", Box::new(InsertTable)),
|
||||
MenuItem::separator(),
|
||||
item("mark.table.row_above", Box::new(RowAbove)),
|
||||
item("mark.table.row_below", Box::new(RowBelow)),
|
||||
item("mark.table.column_left", Box::new(ColumnLeft)),
|
||||
item("mark.table.column_right", Box::new(ColumnRight)),
|
||||
MenuItem::separator(),
|
||||
item("mark.table.delete_row", Box::new(DeleteRow)),
|
||||
item("mark.table.delete_column", Box::new(DeleteColumn)),
|
||||
]),
|
||||
Menu::new(text(cx, "menu.view", &[])).items([
|
||||
MenuItem::action(label(Mode::Visual, cx), VisualMode),
|
||||
MenuItem::action(label(Mode::Source, cx), SourceMode),
|
||||
|
||||
@@ -5,6 +5,7 @@ mod autosave;
|
||||
mod compare;
|
||||
mod desk;
|
||||
mod engine;
|
||||
mod finding;
|
||||
mod folder;
|
||||
mod keys;
|
||||
mod look;
|
||||
@@ -14,6 +15,7 @@ mod rename;
|
||||
mod restore;
|
||||
mod side;
|
||||
mod text;
|
||||
mod tools;
|
||||
mod vcs;
|
||||
mod view;
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//! The table menu's work on the shown file (MARK-06).
|
||||
|
||||
use ely_gpui_component::{documents::TableEdit, primitives::Severity};
|
||||
use ely_office::prelude::*;
|
||||
use ely_ui::i18n::text;
|
||||
use gpui::{Context, InteractiveElement as _, actions};
|
||||
|
||||
use crate::desk::Desk;
|
||||
|
||||
actions!(
|
||||
mark,
|
||||
[
|
||||
InsertTable,
|
||||
RowAbove,
|
||||
RowBelow,
|
||||
ColumnLeft,
|
||||
ColumnRight,
|
||||
DeleteRow,
|
||||
DeleteColumn
|
||||
]
|
||||
);
|
||||
|
||||
impl Desk {
|
||||
fn table(&mut self, edit: Option<TableEdit>, cx: &mut Context<Self>) {
|
||||
let Some(view) = self.active_view() else {
|
||||
let said = text(cx, "command.blocked.no_document", &[]);
|
||||
return self.toast(said, Severity::Warning, cx);
|
||||
};
|
||||
let document = view.read(cx).document().clone();
|
||||
let done = document.update(cx, |document, cx| match edit {
|
||||
Some(edit) => document.edit_table(edit, cx),
|
||||
None => {
|
||||
document.insert_table(cx);
|
||||
true
|
||||
}
|
||||
});
|
||||
if !done {
|
||||
let said = text(cx, "mark.table.none", &[]);
|
||||
self.toast(said, Severity::Info, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn table_actions(element: gpui::Div, cx: &mut Context<Self>) -> gpui::Div {
|
||||
element
|
||||
.on_action(cx.listener(|desk, _: &InsertTable, _, cx| desk.table(None, cx)))
|
||||
.on_action(
|
||||
cx.listener(|desk, _: &RowAbove, _, cx| desk.table(Some(TableEdit::RowAbove), cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|desk, _: &RowBelow, _, cx| desk.table(Some(TableEdit::RowBelow), cx)),
|
||||
)
|
||||
.on_action(cx.listener(|desk, _: &ColumnLeft, _, cx| {
|
||||
desk.table(Some(TableEdit::ColumnLeft), cx)
|
||||
}))
|
||||
.on_action(cx.listener(|desk, _: &ColumnRight, _, cx| {
|
||||
desk.table(Some(TableEdit::ColumnRight), cx)
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|desk, _: &DeleteRow, _, cx| {
|
||||
desk.table(Some(TableEdit::DeleteRow), cx)
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(|desk, _: &DeleteColumn, _, cx| {
|
||||
desk.table(Some(TableEdit::DeleteColumn), cx)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,10 @@ impl MarkView {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn document(&self) -> &Entity<MarkdownDocument> {
|
||||
&self.document
|
||||
}
|
||||
|
||||
/// Every change is in the text at once.
|
||||
pub fn settled(&self) -> bool {
|
||||
true
|
||||
|
||||
Reference in new issue
Block a user