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
Generated
+1
-1
@@ -1922,7 +1922,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ely-gpui-component"
|
||||
version = "0.1.1"
|
||||
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=2e196a10a7c3265eac6d5c52ebcec465ad0374c8#2e196a10a7c3265eac6d5c52ebcec465ad0374c8"
|
||||
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=ce4a1e9cb5fc712e9d03f5bb703d13685f1cb67a#ce4a1e9cb5fc712e9d03f5bb703d13685f1cb67a"
|
||||
dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ jiff = "0.2"
|
||||
anyhow = "1"
|
||||
directories = "6"
|
||||
dunce = "1"
|
||||
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "2e196a10a7c3265eac6d5c52ebcec465ad0374c8" }
|
||||
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "ce4a1e9cb5fc712e9d03f5bb703d13685f1cb67a" }
|
||||
env_logger = "0.11"
|
||||
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] }
|
||||
gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit", "wayland", "x11"] }
|
||||
|
||||
@@ -40,7 +40,7 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木
|
||||
|
||||
## 组件映射
|
||||
|
||||
依赖 `ely-gpui-component`(pin 2e196a1,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。
|
||||
依赖 `ely-gpui-component`(pin ce4a1e9,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。
|
||||
|
||||
| 需要 | 用 |
|
||||
| --- | --- |
|
||||
@@ -283,4 +283,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely
|
||||
- 2026-10-11:Linux 包的实测(L1.6)。本机(Omarchy,Hyprland 0.56)装上六个 Arch 包与六个 AppImage,`linux-installed.sh` 两种都过;deb、rpm、Arch 包在 Ubuntu 26.04、Fedora 44、Arch 容器里装上、全部 ELF 找得到库;deb 与 rpm 的六个应用在各自容器的 Xvfb 下画出窗口(`linux-distros.sh`)。截图按窗口(`grim -T` 加 Hyprland 的 `stableId`):按区域截时用户切换工作区或别的窗口盖住即截错。ElyMark 的文件监视略去读取后不会自激:重读前先比指纹(长度与修改时间),不打开文件。
|
||||
- 2026-10-11(用户定:替换 v0.1.3 的 Linux 包):v0.1.3 发布里原有的六个 AppImage(无头,打不开窗口)已删去,换成本机容器里建出的六个应用各四种包(共 24 个),出自 L1.7 的提交(晚于该标签);上传后逐个核对大小,ElyTree 的 AppImage 下载回来散列相同。`package.sh` 把每个 RUNPATH 按所在 ELF 解析(`$ORIGIN` 或 `${ORIGIN}` 开头,规范化后须在 AppDir 内),否则拒绝(如未修正的 cairo):CI 用的是别处建好的引擎,旧构建会在这里报错而不是带进包里。替换的 Linux 包对应的源码随之上传到同一发布(`source.sh`,文件名带构建提交号),发布说明写明 Linux 包出自哪个提交。
|
||||
- 2026-10-11:`ci.sh` 用 `ELY_SHARE` 时,CI 克隆里的 `engines`、`reference` 须是指向共享克隆的链接(Windows 为目录联接,不查);是本克隆自建的目录或指向别处即报错,不混用。`.gitignore` 的 `/engines`、`/reference` 不带斜杠,链接也被忽略(`source.sh` 要求工作区干净)。
|
||||
- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。组件库 f59c9f7:文字超过读取上限(2 MiB)即丢掉旧的读取,大纲与块为空。ElyMark(C14.4):视图即一个 `MarkdownDocument`,文本即文件原文(BOM 与 CRLF 照 `text.rs` 存回),新建、打开、保存、另存、重命名经外壳照旧;视图在建成后的下一轮更新向外壳报一次载入(文档在 `new` 里当场解析,外壳此时还未订阅);文字超过 2 MiB 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印Line truncated
|
||||
- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。组件库 f59c9f7:文字超过读取上限(2 MiB)即丢掉旧的读取,大纲与块为空。ElyMark(C14.4):视图即一个 `MarkdownDocument`,文本即文件原文(BOM 与 CRLF 照 `text.rs` 存回),新建、打开、保存、另存、重命名经外壳照旧;视图在建成后的下一轮更新向外壳报一次载入(文档在 `new` 里当场解析,外壳此时还未订阅);文字超过 2 MiB 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印Line truncated
|
||||
@@ -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
|
||||
|
||||
@@ -21,7 +21,6 @@ pub(super) const EN: &[(&str, &str)] = &[
|
||||
("mark.table.delete_row", "Delete Row"),
|
||||
("mark.table.delete_column", "Delete Column"),
|
||||
("mark.table.none", "Put the caret in a table first"),
|
||||
("mark.table.visual", "Tables change in the visual mode"),
|
||||
("mark.attach.untitled", "Save the document first: images go beside it"),
|
||||
("mark.attach.preview", "Switch to an editing mode to add images"),
|
||||
("mark.attach.images", "Only images can be added"),
|
||||
@@ -110,7 +109,6 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
|
||||
("mark.table.delete_row", "删除行"),
|
||||
("mark.table.delete_column", "删除列"),
|
||||
("mark.table.none", "请先把插入点放进表格"),
|
||||
("mark.table.visual", "表格在可视模式下修改"),
|
||||
("mark.attach.untitled", "请先保存文档:图片放在它旁边"),
|
||||
("mark.attach.preview", "请切到编辑模式再添加图片"),
|
||||
("mark.attach.images", "只能添加图片"),
|
||||
|
||||
@@ -36,7 +36,7 @@ fetch() { # name url rev [sparse paths...]
|
||||
echo "fetch $name $rev"
|
||||
}
|
||||
|
||||
fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git 2e196a10a7c3265eac6d5c52ebcec465ad0374c8
|
||||
fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git ce4a1e9cb5fc712e9d03f5bb703d13685f1cb67a
|
||||
fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b
|
||||
fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \
|
||||
include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \
|
||||
|
||||
Reference in new issue
Block a user