ElyMark: dropped and pasted pictures copy into assets and link at the caret (C14.10)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
72e9441399
commit
b74f4e4e72
12 files changed
+357
-63
No files matched your search
Generated
+1
-1
@@ -1922,7 +1922,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ely-gpui-component"
|
name = "ely-gpui-component"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=cd16b204f0ba56473b9ffb831bf2483bc232a6bb#cd16b204f0ba56473b9ffb831bf2483bc232a6bb"
|
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=d126a7e7f7977dddae598486dd99aba79ce1f5f0#d126a7e7f7977dddae598486dd99aba79ce1f5f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alacritty_terminal",
|
"alacritty_terminal",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ jiff = "0.2"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
directories = "6"
|
directories = "6"
|
||||||
dunce = "1"
|
dunce = "1"
|
||||||
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "cd16b204f0ba56473b9ffb831bf2483bc232a6bb" }
|
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "d126a7e7f7977dddae598486dd99aba79ce1f5f0" }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] }
|
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"] }
|
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 cd16b20,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 d126a7e,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: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(用户定:替换 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:`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
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
//! Pictures dropped or pasted, copied beside the file (MARK-07).
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ely_gpui_component::documents::Attachment;
|
||||||
|
use ely_ui::i18n::text;
|
||||||
|
use gpui::{App, Context, ImageFormat};
|
||||||
|
|
||||||
|
use crate::view::{Failed, MarkView, Mode};
|
||||||
|
|
||||||
|
const PICTURES: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"];
|
||||||
|
|
||||||
|
/// What goes into `assets/`: a file, or pasted bytes.
|
||||||
|
enum Source {
|
||||||
|
File(PathBuf),
|
||||||
|
Bytes(String, Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn said(key: &str, cx: &App) -> Failed {
|
||||||
|
Failed(text(cx, key, &[]).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkView {
|
||||||
|
/// Copies pictures off the window, then links them.
|
||||||
|
pub(crate) fn attach(&mut self, attached: &Attachment, cx: &mut Context<Self>) {
|
||||||
|
if self.mode == Mode::Preview {
|
||||||
|
return cx.emit(said("mark.attach.preview", cx));
|
||||||
|
}
|
||||||
|
let Some(folder) = self.folder.clone() else {
|
||||||
|
return cx.emit(said("mark.attach.untitled", cx));
|
||||||
|
};
|
||||||
|
let sources: Vec<Source> = match attached {
|
||||||
|
Attachment::Files(paths) => {
|
||||||
|
let (pictures, others): (Vec<&PathBuf>, Vec<&PathBuf>) =
|
||||||
|
paths.iter().partition(|path| picture(path));
|
||||||
|
if !others.is_empty() {
|
||||||
|
cx.emit(said("mark.attach.images", cx));
|
||||||
|
}
|
||||||
|
pictures.into_iter().cloned().map(Source::File).collect()
|
||||||
|
}
|
||||||
|
Attachment::Picture(image) => {
|
||||||
|
let millis = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("the clock is past 1970")
|
||||||
|
.as_millis();
|
||||||
|
let name = format!("pasted-{millis}.{}", extension(image.format()));
|
||||||
|
vec![Source::Bytes(name, image.bytes().to_vec())]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if sources.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log::info!("mark: {} pictures copy in", sources.len());
|
||||||
|
let assets = folder.join("assets");
|
||||||
|
cx.spawn(async move |view, cx| {
|
||||||
|
let copied = cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
sources
|
||||||
|
.into_iter()
|
||||||
|
.map(|source| match source {
|
||||||
|
Source::File(path) => ely_files::import_attachment(&path, &assets),
|
||||||
|
Source::Bytes(name, bytes) => {
|
||||||
|
ely_files::write_attachment(&name, &bytes, &assets)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<io::Result<Vec<PathBuf>>>()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
view.update(cx, |view, cx| view.attached(folder, copied, cx))
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Links the copies, unless the file moved meanwhile.
|
||||||
|
fn attached(
|
||||||
|
&mut self,
|
||||||
|
folder: PathBuf,
|
||||||
|
copied: io::Result<Vec<PathBuf>>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let copied = match copied {
|
||||||
|
Ok(copied) => copied,
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("mark: a picture did not copy: {error}");
|
||||||
|
let said = text(cx, "office.error.engine", &[("error", &error.to_string())]);
|
||||||
|
return cx.emit(Failed(said.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if self.folder.as_ref() != Some(&folder) {
|
||||||
|
log::info!("mark: the file moved while pictures copied");
|
||||||
|
let assets = folder.join("assets").display().to_string();
|
||||||
|
let said = text(cx, "mark.attach.moved", &[("folder", &assets)]);
|
||||||
|
return cx.emit(Failed(said.to_string()));
|
||||||
|
}
|
||||||
|
let links: Vec<String> = copied.iter().map(|copy| link(&folder, copy)).collect();
|
||||||
|
let written = self
|
||||||
|
.document()
|
||||||
|
.clone()
|
||||||
|
.update(cx, |document, cx| document.insert(&links.join("\n"), cx));
|
||||||
|
if !written {
|
||||||
|
cx.emit(said("mark.attach.preview", cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn picture(path: &Path) -> bool {
|
||||||
|
path.extension()
|
||||||
|
.is_some_and(|ext| PICTURES.contains(&ext.to_string_lossy().to_ascii_lowercase().as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extension(format: ImageFormat) -> &'static str {
|
||||||
|
match format {
|
||||||
|
ImageFormat::Png => "png",
|
||||||
|
ImageFormat::Jpeg => "jpg",
|
||||||
|
ImageFormat::Gif => "gif",
|
||||||
|
ImageFormat::Webp => "webp",
|
||||||
|
ImageFormat::Svg => "svg",
|
||||||
|
ImageFormat::Bmp => "bmp",
|
||||||
|
ImageFormat::Tiff => "tiff",
|
||||||
|
ImageFormat::Ico => "ico",
|
||||||
|
ImageFormat::Pnm => "pnm",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Markdown link to `copy`, relative to `folder`.
|
||||||
|
fn link(folder: &Path, copy: &Path) -> String {
|
||||||
|
let relative = copy.strip_prefix(folder).expect("copied under the folder");
|
||||||
|
let address: Vec<String> = relative
|
||||||
|
.components()
|
||||||
|
.map(|part| escaped(&part.as_os_str().to_string_lossy()))
|
||||||
|
.collect();
|
||||||
|
let stem = copy.file_stem().unwrap_or_default().to_string_lossy();
|
||||||
|
let alt = stem
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('[', "\\[")
|
||||||
|
.replace(']', "\\]");
|
||||||
|
format!("", address.join("/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A path part, every byte but plain ones escaped.
|
||||||
|
fn escaped(part: &str) -> String {
|
||||||
|
part.bytes()
|
||||||
|
.map(|byte| match byte {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
|
||||||
|
(byte as char).to_string()
|
||||||
|
}
|
||||||
|
other => format!("%{other:02X}"),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{cell::RefCell, path::Path, rc::Rc};
|
||||||
|
|
||||||
|
use ely_gpui_component::documents::Attachment;
|
||||||
|
use gpui::{Entity, Image, ImageFormat, TestAppContext, VisualTestContext};
|
||||||
|
|
||||||
|
use super::link;
|
||||||
|
use crate::desk::{
|
||||||
|
Desk,
|
||||||
|
tests::{open, start},
|
||||||
|
};
|
||||||
|
use crate::view::{Failed, MarkView};
|
||||||
|
|
||||||
|
/// What the view says, in order.
|
||||||
|
type Said = Rc<RefCell<Vec<String>>>;
|
||||||
|
|
||||||
|
/// A saved file open in ElyMark, and what it says.
|
||||||
|
fn saved(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> (
|
||||||
|
Entity<MarkView>,
|
||||||
|
&'static mut VisualTestContext,
|
||||||
|
[tempfile::TempDir; 2],
|
||||||
|
Said,
|
||||||
|
) {
|
||||||
|
let (data, files) = (
|
||||||
|
tempfile::tempdir().expect("data"),
|
||||||
|
tempfile::tempdir().expect("files"),
|
||||||
|
);
|
||||||
|
let path = files.path().join("a.md");
|
||||||
|
std::fs::write(
|
||||||
|
&path, "a
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.expect("a file");
|
||||||
|
let (desk, cx) = start(cx, data.path());
|
||||||
|
open(&desk, &path, cx);
|
||||||
|
let view = desk.read_with(cx, |desk: &Desk, _| desk.active_view().expect("a tab"));
|
||||||
|
let said = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let heard = said.clone();
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
cx.subscribe(&view, move |_, failed: &Failed, _| {
|
||||||
|
heard.borrow_mut().push(failed.0.clone())
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
});
|
||||||
|
(view, cx, [data, files], said)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attach(view: &Entity<MarkView>, attached: Attachment, cx: &mut VisualTestContext) {
|
||||||
|
let document = view.read_with(cx, |view, _| view.document().clone());
|
||||||
|
document.update(cx, |_, cx| cx.emit(attached));
|
||||||
|
cx.run_until_parked();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current(view: &Entity<MarkView>, cx: &mut VisualTestContext) -> String {
|
||||||
|
view.read_with(cx, |view, cx| view.document().read(cx).text(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_dropped_picture_is_copied_beside_and_linked(cx: &mut TestAppContext) {
|
||||||
|
let (view, cx, [_, files], said) = saved(cx);
|
||||||
|
let shot = files.path().join("my shot.png");
|
||||||
|
std::fs::write(&shot, "png").expect("a picture");
|
||||||
|
attach(&view, Attachment::Files(vec![shot]), cx);
|
||||||
|
assert!(files.path().join("assets/my shot.png").exists());
|
||||||
|
assert!(
|
||||||
|
current(&view, cx).contains(""),
|
||||||
|
"{}",
|
||||||
|
current(&view, cx)
|
||||||
|
);
|
||||||
|
assert!(said.borrow().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_pasted_picture_is_written_and_linked(cx: &mut TestAppContext) {
|
||||||
|
let (view, cx, [_, files], _) = saved(cx);
|
||||||
|
attach(
|
||||||
|
&view,
|
||||||
|
Attachment::Picture(Image::from_bytes(ImageFormat::Png, vec![1])),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
let assets: Vec<_> = std::fs::read_dir(files.path().join("assets"))
|
||||||
|
.expect("assets")
|
||||||
|
.collect();
|
||||||
|
assert_eq!(assets.len(), 1);
|
||||||
|
assert!(current(&view, cx).contains("](assets/pasted-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn other_files_and_untitled_text_take_no_picture(cx: &mut TestAppContext) {
|
||||||
|
let (view, cx, [_, files], said) = saved(cx);
|
||||||
|
let note = files.path().join("note.txt");
|
||||||
|
std::fs::write(¬e, "text").expect("a file");
|
||||||
|
attach(&view, Attachment::Files(vec![note]), cx);
|
||||||
|
assert!(!files.path().join("assets").exists());
|
||||||
|
assert_eq!(said.borrow().len(), 1, "other files are refused");
|
||||||
|
view.update(cx, |view, _| view.folder = None);
|
||||||
|
attach(
|
||||||
|
&view,
|
||||||
|
Attachment::Picture(Image::from_bytes(ImageFormat::Png, vec![1])),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert_eq!(said.borrow().len(), 2, "untitled text is refused");
|
||||||
|
assert_eq!(
|
||||||
|
current(&view, cx),
|
||||||
|
"a
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_file_moved_while_copying_gets_no_link(cx: &mut TestAppContext) {
|
||||||
|
let (view, cx, [_, files], said) = saved(cx);
|
||||||
|
let document = view.read_with(cx, |view, _| view.document().clone());
|
||||||
|
document.update(cx, |_, cx| {
|
||||||
|
cx.emit(Attachment::Picture(Image::from_bytes(
|
||||||
|
ImageFormat::Png,
|
||||||
|
vec![1],
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let elsewhere = files.path().join("elsewhere");
|
||||||
|
view.update(cx, |view, cx| view.moved(elsewhere, cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
current(&view, cx),
|
||||||
|
"a
|
||||||
|
"
|
||||||
|
);
|
||||||
|
assert_eq!(said.borrow().len(), 1, "the move is said");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn links_escape_their_address_and_alt() {
|
||||||
|
let folder = Path::new("/notes");
|
||||||
|
let copy = folder.join("assets").join("a b[1].png");
|
||||||
|
assert_eq!(
|
||||||
|
link(folder, ©),
|
||||||
|
"![a b\\[1\\]](assets/a%20b%5B1%5D.png)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use std::{
|
|||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
hash::{DefaultHasher, Hash as _, Hasher as _},
|
hash::{DefaultHasher, Hash as _, Hasher as _},
|
||||||
io,
|
io,
|
||||||
path::{Path, PathBuf},
|
path::Path,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,12 +36,6 @@ impl Text {
|
|||||||
|
|
||||||
pub type Doc = Arc<Mutex<Text>>;
|
pub type Doc = Arc<Mutex<Text>>;
|
||||||
|
|
||||||
/// A load: the file as read, and where it is.
|
|
||||||
pub struct Opened {
|
|
||||||
pub source: Source,
|
|
||||||
pub path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The history mark: this text's length and hash.
|
/// The history mark: this text's length and hash.
|
||||||
pub fn mark(text: &str) -> String {
|
pub fn mark(text: &str) -> String {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
@@ -56,10 +50,10 @@ impl Engine for Mark {
|
|||||||
type Worker = ();
|
type Worker = ();
|
||||||
type Doc = Doc;
|
type Doc = Doc;
|
||||||
type View = MarkView;
|
type View = MarkView;
|
||||||
type Loaded = Opened;
|
type Loaded = Source;
|
||||||
type Import = Infallible;
|
type Import = Infallible;
|
||||||
|
|
||||||
fn load(_: &mut (), from: Load<Infallible>) -> io::Result<Opened> {
|
fn load(_: &mut (), from: Load<Infallible>) -> io::Result<Source> {
|
||||||
let path = match from {
|
let path = match from {
|
||||||
Load::File(path, _) => path,
|
Load::File(path, _) => path,
|
||||||
Load::Import(_, never) => match *never {},
|
Load::Import(_, never) => match *never {},
|
||||||
@@ -69,30 +63,19 @@ impl Engine for Mark {
|
|||||||
io::ErrorKind::PermissionDenied => io::Error::other(error),
|
io::ErrorKind::PermissionDenied => io::Error::other(error),
|
||||||
_ => error,
|
_ => error,
|
||||||
})?;
|
})?;
|
||||||
Ok(Opened {
|
Source::decode(bytes)
|
||||||
source: Source::decode(bytes)?,
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open(
|
fn open(
|
||||||
_: &Arc<Mutex<()>>,
|
_: &Arc<Mutex<()>>,
|
||||||
loaded: Opened,
|
loaded: Source,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> io::Result<(Doc, Entity<MarkView>, String)> {
|
) -> io::Result<(Doc, Entity<MarkView>, String)> {
|
||||||
let Opened {
|
|
||||||
source: loaded,
|
|
||||||
path,
|
|
||||||
} = loaded;
|
|
||||||
let folder = path
|
|
||||||
.parent()
|
|
||||||
.expect("a file sits in a folder")
|
|
||||||
.to_path_buf();
|
|
||||||
let mark = mark(&loaded.text);
|
let mark = mark(&loaded.text);
|
||||||
let doc = Arc::new(Mutex::new(Text::new(loaded)));
|
let doc = Arc::new(Mutex::new(Text::new(loaded)));
|
||||||
let saved = mark.clone();
|
let saved = mark.clone();
|
||||||
let view = cx.new(|cx| MarkView::new(Arc::clone(&doc), saved, folder, window, cx));
|
let view = cx.new(|cx| MarkView::new(Arc::clone(&doc), saved, window, cx));
|
||||||
Ok((doc, view, mark))
|
Ok((doc, view, mark))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Release builds open no console on Windows.
|
// Release builds open no console on Windows.
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod attach;
|
||||||
mod autosave;
|
mod autosave;
|
||||||
mod compare;
|
mod compare;
|
||||||
mod desk;
|
mod desk;
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ pub struct MarkView {
|
|||||||
/// The mark at the last load or save.
|
/// The mark at the last load or save.
|
||||||
saved: String,
|
saved: String,
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
/// The file's folder: pictures resolve here.
|
/// A saved file's folder: pictures resolve and go here.
|
||||||
pub(crate) folder: PathBuf,
|
pub(crate) folder: Option<PathBuf>,
|
||||||
/// The mode first took input (PERF-07).
|
/// The mode first took input (PERF-07).
|
||||||
pub(crate) shown: bool,
|
pub(crate) shown: bool,
|
||||||
pub focus: FocusHandle,
|
pub focus: FocusHandle,
|
||||||
@@ -85,13 +85,7 @@ impl EventEmitter<Reported> for MarkView {}
|
|||||||
impl EventEmitter<Failed> for MarkView {}
|
impl EventEmitter<Failed> for MarkView {}
|
||||||
|
|
||||||
impl MarkView {
|
impl MarkView {
|
||||||
pub fn new(
|
pub fn new(doc: Doc, saved: String, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
doc: Doc,
|
|
||||||
saved: String,
|
|
||||||
folder: PathBuf,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let text = doc.lock().expect("the text lock").current.clone();
|
let text = doc.lock().expect("the text lock").current.clone();
|
||||||
let mode = match text.len() > LARGEST_READ {
|
let mode = match text.len() > LARGEST_READ {
|
||||||
true => Mode::Source,
|
true => Mode::Source,
|
||||||
@@ -100,13 +94,14 @@ impl MarkView {
|
|||||||
let view = cx.weak_entity();
|
let view = cx.weak_entity();
|
||||||
let document = cx.new(|cx| {
|
let document = cx.new(|cx| {
|
||||||
let mut document = MarkdownDocument::new(text, mode.shown(), window, cx);
|
let mut document = MarkdownDocument::new(text, mode.shown(), window, cx);
|
||||||
document.set_base(folder.as_path(), cx);
|
|
||||||
document.on_link(move |url, _, cx| {
|
document.on_link(move |url, _, cx| {
|
||||||
view.update(cx, |view, cx| view.follow(url, cx)).ok();
|
view.update(cx, |view, cx| view.follow(url, cx)).ok();
|
||||||
});
|
});
|
||||||
document
|
document
|
||||||
});
|
});
|
||||||
cx.subscribe_in(&document, window, Self::heard).detach();
|
cx.subscribe_in(&document, window, Self::heard).detach();
|
||||||
|
cx.subscribe(&document, |view, _, attached, cx| view.attach(attached, cx))
|
||||||
|
.detach();
|
||||||
// The shell hears the load.
|
// The shell hears the load.
|
||||||
cx.defer_in(window, |view, _, cx| {
|
cx.defer_in(window, |view, _, cx| {
|
||||||
view.show_once(cx);
|
view.show_once(cx);
|
||||||
@@ -125,7 +120,7 @@ impl MarkView {
|
|||||||
changed: 0,
|
changed: 0,
|
||||||
saved,
|
saved,
|
||||||
mode,
|
mode,
|
||||||
folder,
|
folder: None,
|
||||||
shown: false,
|
shown: false,
|
||||||
focus,
|
focus,
|
||||||
}
|
}
|
||||||
@@ -251,14 +246,14 @@ impl MarkView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The file moved; pictures follow it.
|
/// The saved file's folder; pictures follow it.
|
||||||
pub fn moved(&mut self, folder: PathBuf, cx: &mut Context<Self>) {
|
pub fn moved(&mut self, folder: PathBuf, cx: &mut Context<Self>) {
|
||||||
if folder == self.folder {
|
if self.folder.as_ref() == Some(&folder) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.document
|
self.document
|
||||||
.update(cx, |document, cx| document.set_base(folder.as_path(), cx));
|
.update(cx, |document, cx| document.set_base(folder.as_path(), cx));
|
||||||
self.folder = folder;
|
self.folder = Some(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn heard(
|
fn heard(
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ fn open<'a>(
|
|||||||
let source = Source::decode(text.as_bytes().to_vec()).expect("reads");
|
let source = Source::decode(text.as_bytes().to_vec()).expect("reads");
|
||||||
let saved = mark(&source.text);
|
let saved = mark(&source.text);
|
||||||
let doc = Arc::new(Mutex::new(Text::new(source)));
|
let doc = Arc::new(Mutex::new(Text::new(source)));
|
||||||
let folder = std::env::temp_dir();
|
let (view, cx) = cx.add_window_view(move |window, cx| MarkView::new(doc, saved, window, cx));
|
||||||
let (view, cx) =
|
|
||||||
cx.add_window_view(move |window, cx| MarkView::new(doc, saved, folder, window, cx));
|
|
||||||
cx.update(|window, _| window.activate_window());
|
cx.update(|window, _| window.activate_window());
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
(view, cx)
|
(view, cx)
|
||||||
@@ -166,8 +164,7 @@ fn the_shell_hears_a_load_it_subscribed_to(cx: &mut TestAppContext) {
|
|||||||
let source = Source::decode(b"# Opened\n".to_vec()).expect("reads");
|
let source = Source::decode(b"# Opened\n".to_vec()).expect("reads");
|
||||||
let saved = mark(&source.text);
|
let saved = mark(&source.text);
|
||||||
let doc = Arc::new(Mutex::new(Text::new(source)));
|
let doc = Arc::new(Mutex::new(Text::new(source)));
|
||||||
let folder = std::env::temp_dir();
|
let view = cx.new(|cx| MarkView::new(doc, saved, window, cx));
|
||||||
let view = cx.new(|cx| MarkView::new(doc, saved, folder, window, cx));
|
|
||||||
// As the shell: subscribed after the view is made.
|
// As the shell: subscribed after the view is made.
|
||||||
let count = count.clone();
|
let count = count.clone();
|
||||||
cx.subscribe(&view, move |_, _, _: &Reported, _| {
|
cx.subscribe(&view, move |_, _, _: &Reported, _| {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub use task::RecoveryTask;
|
|||||||
|
|
||||||
pub use save::{
|
pub use save::{
|
||||||
SaveError, Saved, atomic_capable, claim_all, export, import_attachment, same_file, save,
|
SaveError, Saved, atomic_capable, claim_all, export, import_attachment, same_file, save,
|
||||||
|
write_attachment,
|
||||||
save_as, save_atomic, save_overwrite, save_with_backup, save_with_probe,
|
save_as, save_atomic, save_overwrite, save_with_backup, save_with_probe,
|
||||||
};
|
};
|
||||||
pub use session::{Fingerprint, Open, SaveState, SaveTicket, Session, Sessions, StaleTicket};
|
pub use session::{Fingerprint, Open, SaveState, SaveTicket, Session, Sessions, StaleTicket};
|
||||||
@@ -212,24 +212,38 @@ pub fn same_file(a: &Path, b: &Path) -> io::Result<bool> {
|
|||||||
|
|
||||||
/// Copies an attachment in; the source is never touched (FILE-07).
|
/// Copies an attachment in; the source is never touched (FILE-07).
|
||||||
pub fn import_attachment(source: &Path, dir: &Path) -> io::Result<PathBuf> {
|
pub fn import_attachment(source: &Path, dir: &Path) -> io::Result<PathBuf> {
|
||||||
std::fs::create_dir_all(dir).map_err(|e| with_path(dir, e))?;
|
|
||||||
let name = source.file_name().ok_or_else(|| {
|
let name = source.file_name().ok_or_else(|| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
format!("{} has no file name", source.display()),
|
format!("{} has no file name", source.display()),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let mut target = dir.join(name);
|
let mut from = std::fs::File::open(source).map_err(|e| with_path(source, e))?;
|
||||||
let stem = Path::new(name)
|
let target = attach(dir, Path::new(name), |to| io::copy(&mut from, to).map(drop))?;
|
||||||
.file_stem()
|
log::info!("files: imported {} to {}", source.display(), target.display());
|
||||||
.unwrap_or_default()
|
Ok(target)
|
||||||
.to_string_lossy()
|
}
|
||||||
.into_owned();
|
|
||||||
let ext = Path::new(name)
|
/// Writes `bytes` into `dir` as `name`, or a free variant.
|
||||||
|
pub fn write_attachment(name: &str, bytes: &[u8], dir: &Path) -> io::Result<PathBuf> {
|
||||||
|
let target = attach(dir, Path::new(name), |to| to.write_all(bytes))?;
|
||||||
|
log::info!("files: wrote {} bytes to {}", bytes.len(), target.display());
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claims a free name like `name`, then fills it.
|
||||||
|
fn attach(
|
||||||
|
dir: &Path,
|
||||||
|
name: &Path,
|
||||||
|
fill: impl FnOnce(&mut std::fs::File) -> io::Result<()>,
|
||||||
|
) -> io::Result<PathBuf> {
|
||||||
|
std::fs::create_dir_all(dir).map_err(|e| with_path(dir, e))?;
|
||||||
|
let stem = name.file_stem().unwrap_or_default().to_string_lossy().into_owned();
|
||||||
|
let ext = name
|
||||||
.extension()
|
.extension()
|
||||||
.map(|ext| ext.to_string_lossy().into_owned())
|
.map(|ext| ext.to_string_lossy().into_owned())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut from = std::fs::File::open(source).map_err(|e| with_path(source, e))?;
|
let mut target = dir.join(name);
|
||||||
let mut counter = 1;
|
let mut counter = 1;
|
||||||
// Claims the name; another writer's file is never replaced.
|
// Claims the name; another writer's file is never replaced.
|
||||||
let mut to = loop {
|
let mut to = loop {
|
||||||
@@ -242,17 +256,12 @@ pub fn import_attachment(source: &Path, dir: &Path) -> io::Result<PathBuf> {
|
|||||||
Err(e) => return Err(with_path(&target, e)),
|
Err(e) => return Err(with_path(&target, e)),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let copied = io::copy(&mut from, &mut to).and_then(|_| to.flush());
|
let filled = fill(&mut to).and_then(|()| to.flush());
|
||||||
if let Err(e) = copied {
|
if let Err(e) = filled {
|
||||||
// A half copy is not left behind.
|
// A half copy is not left behind.
|
||||||
std::fs::remove_file(&target).ok();
|
std::fs::remove_file(&target).ok();
|
||||||
return Err(with_path(&target, e));
|
return Err(with_path(&target, e));
|
||||||
}
|
}
|
||||||
log::info!(
|
|
||||||
"files: imported {} to {}",
|
|
||||||
source.display(),
|
|
||||||
target.display()
|
|
||||||
);
|
|
||||||
Ok(target)
|
Ok(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,15 @@ fn an_import_copies_and_never_deletes() {
|
|||||||
assert_eq!(read(&second), "png-bytes");
|
assert_eq!(read(&second), "png-bytes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn written_bytes_take_a_free_name() {
|
||||||
|
let dir = tempfile::tempdir().expect("a temp dir");
|
||||||
|
let first = write_attachment("pasted.png", b"one", dir.path()).expect("the first write");
|
||||||
|
let second = write_attachment("pasted.png", b"two", dir.path()).expect("the second write");
|
||||||
|
assert_ne!(first, second);
|
||||||
|
assert_eq!((read(&first), read(&second)), ("one".into(), "two".into()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_numbered_import_name_fits_long_file_names() {
|
fn a_numbered_import_name_fits_long_file_names() {
|
||||||
let dir = tempfile::tempdir().expect("a temp dir");
|
let dir = tempfile::tempdir().expect("a temp dir");
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fetch() { # name url rev [sparse paths...]
|
|||||||
echo "fetch $name $rev"
|
echo "fetch $name $rev"
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git cd16b204f0ba56473b9ffb831bf2483bc232a6bb
|
fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git d126a7e7f7977dddae598486dd99aba79ce1f5f0
|
||||||
fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b
|
fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b
|
||||||
fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \
|
fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \
|
||||||
include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \
|
include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \
|
||||||
|
|||||||
Reference in new issue
Block a user