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
@@ -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;
|
||||
Reference in new issue
Block a user