2 Commits

Author SHA1 Message Date
dependabot[bot]
24f1549730 ⬆️ Bump golang.org/x/crypto from 0.44.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 03:04:41 +00:00
4471441d6f ♻️ Refactor some code 2025-11-19 20:54:58 +08:00
40 changed files with 733 additions and 1639 deletions

View File

@@ -93,32 +93,13 @@ export default defineConfig({
items: [ items: [
{text: '简介', link: '/zh/guide/introduction'}, {text: '简介', link: '/zh/guide/introduction'},
{text: '安装', link: '/zh/guide/installation'}, {text: '安装', link: '/zh/guide/installation'},
{text: '快速开始', link: '/zh/guide/getting-started'}, {text: '快速开始', link: '/zh/guide/getting-started'}
{text: '界面总览', link: '/zh/guide/ui-overview'},
{text: '块语法与结构', link: '/zh/guide/block-syntax'}
] ]
}, },
{ {
text: '编辑与效率', text: '功能特性',
items: [ items: [
{text: '键盘快捷键', link: '/zh/guide/keyboard-shortcuts'}, {text: '功能概览', link: '/zh/guide/features'}
{text: '多窗口与标签页', link: '/zh/guide/multiwindow-tabs'},
{text: '扩展与插件', link: '/zh/guide/extensions'},
{text: 'HTTP 客户端', link: '/zh/guide/http-client'}
]
},
{
text: '个性化与数据',
items: [
{text: '设置与配置', link: '/zh/guide/settings'},
{text: '主题与外观', link: '/zh/guide/themes'},
{text: '备份与更新', link: '/zh/guide/backup-update'}
]
},
{
text: '问题处理',
items: [
{text: '常见问题与故障排查', link: '/zh/guide/troubleshooting'}
] ]
} }
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,60 +0,0 @@
# 备份与更新
![备份更新占位](/img/placeholder-backup.png)
> 替换为备份设置、推送状态、更新提示的截图。
## Git 备份
`BackupService``dataPath` 转化为 Git 仓库,并提供自动/手动推送。
### 初始化
1. 在设置 > 备份中开启「启用备份」。
2. 填写远程仓库 URLHTTPS 或 SSH
3. 选择认证方式:
- **Token**:适用于 GitHub/Gitea https 仓库。
- **SSH Key**:指定私钥路径和 passphrase。
- **用户名/密码**:适合自建 HTTP 仓库。
4. 点击“测试连接”(按钮在计划中,可先在终端测试)。
### 自动备份
- 勾选 “自动备份” + 设置 `BackupInterval`(分钟)。
- 服务会创建 ticker 定时 `git add -> commit -> push`
- Commit 消息形如 `Auto backup <timestamp>`,包含 `voidraft.db`, `extensions.json`, `config.json`, `voidraft_data.bin`
### 手动推送
- 打开工具栏消息中心或设置页点击“立即推送”。
- `backupStore.pushToRemote` 会显示状态气泡(成功/失败提示 3~5 秒)。
### 常见问题
| 提示 | 解决 |
| --- | --- |
| `repository not found` | 检查 Repo URL 与权限,必要时创建空仓库 |
| `authentication required` | 选择正确认证方式,确认 token scope需 repo 权限) |
| `auto backup stopped` | 查看日志,可能是网络不通或凭据失效;修改配置后服务会自动重启 |
## 自动更新
`SelfUpdateService` 负责检测、下载、应用新版。
### 检查更新
- 启动时若勾选 “自动更新” 会自动检查。
- 也可在设置 > 更新点击 “检查更新” 或在工具栏更新图标处触发。
- 服务优先访问 `primarySource`,失败时回退 `backupSource`
### 下载与应用
1. 检测到更新后,界面提示版本号与变更信息(从 Release Notes 获取)。
2. 点击 “下载并安装” 后,后台执行下载,完成后提示“准备重启”。
3. 选择 “立即重启” 将调用 `RestartApplication`,自动重新打开上次的文档。
4. 若启用了 “更新前备份”,在下载前会触发一次 Git push。
### 失败处理
| 场景 | 建议 |
| --- | --- |
| 下载失败 | 检查网络/代理,切换至备用源 |
| 校验失败 | 删除 `%LOCALAPPDATA%/voidraft/update-cache` 再重试 |
| 应用后无法启动 | 从 Git 备份回滚数据,下载旧版本安装包覆盖 |
## 发布渠道
- 官方 GitHub Releases`https://github.com/landaiqing/voidraft/releases`
- 自建 Gitea`https://git.landaiqing.cn/landaiqing/voidraft`
- 可在设置中替换 Owner/Repo/BaseURL以指向企业私有镜像。
> 建议将备份仓库设为私有,并在更新前后验证数据完整性。若要接入 S3/OSS 等备份方式,可关注 roadmap 或自行扩展。

View File

@@ -1,78 +0,0 @@
# 块语法与结构
![块语法占位](/img/placeholder-block-flow.png)
> 替换为展示分隔符(`∞∞∞language`)、块内容、语言标签的截图。
## 结构定义
每个块都由 **分隔符 + 内容** 组成:
```
∞∞∞language[-a]\n
<内容>
```
- `language``lang-parser/languages.ts` 中的 token例如 `text``javascript``python``md``http``math``sql`
- `-a`:可选自动检测后缀,表示忽略显式语言,由 `lang-detect/autodetect.ts` 根据内容猜测。
- 内容允许空行;块之间无需额外空格。
`parser.ts` 会将块解析为:
```ts
{
language: { name: "javascript", auto: false },
delimiter: { from, to },
content: { from, to },
range: { from, to }
}
```
这些字段供格式化、HTTP 运行、块操作等扩展示例使用。
## 快捷命令
| 命令 | 默认快捷键 | 说明 |
| --- | --- | --- |
| blockAddAfterCurrent | `Ctrl+Enter` | 插入新块(下方) |
| blockAddBeforeCurrent | `Ctrl+Shift+Enter` | 插入新块(上方) |
| blockGotoPrevious/Next | `Alt+↑ / Alt+↓` | 在块之间跳转 |
| blockSelectAll | `Ctrl+Shift+A` | 选中当前块(含分隔符) |
| blockDelete | `Alt+Delete` | 删除整个块 |
| blockMoveUp/Down | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | 重排块顺序 |
| blockFormat | `Ctrl+Shift+F` | 针对当前块执行 Prettier |
## 语言与能力矩阵
| 语言 | 适配特性 |
| --- | --- |
| `text`/`note` | 基础文本,没有特殊扩展 |
| `md` | Markdown 预览、checkbox、高亮 |
| `javascript`/`typescript`/`json` | Prettier 格式化、彩虹括号、折叠、颜色选择 |
| `go`/`rust`/`python`/`java` | 高亮、折叠、自动缩进、语法跳转 |
| `http` | HTTP DSL、变量、响应插入详见 [HTTP 客户端](/zh/guide/http-client)|
| `math` | `mathBlock` 运行器,支持 `prev` 引用上一次结果 |
| `sql`/`yaml`/`toml` | 语法高亮、格式化(由 Prettier/插件支持) |
> 若需要不在列表中的语言,可先使用 `text` 块输入,再在工具栏搜索语言名;也可以在 `lang-parser/languages.ts` 中添加条目。
## 自动检测策略
- 当分隔符以 `∞∞∞text-a` 写成时,`AUTO_DETECT_SUFFIX` 生效,`lang-detect` 会基于内容统计 + Levenshtein 距离预测语言。
- 自动结果会写入块状态,但不会覆盖分隔符原文,因此可通过工具栏明确指定。
## 特殊块
### HTTP 块
- 语法位于 `extensions/httpclient/language`,支持 `@var/@json/@form/@multipart` 等指令。
- 运行器会在块尾生成 `### Response`包含状态码、耗时、headers、body。
### 数学块
- 语言设为 `math`,逐行计算。
- `prev` 变量表示上一行结果,可完成链式运算。
- 结果挂件可点击复制,或显示格式化/定点值。
### Markdown 块
- 工具栏中的 Preview 按钮会调用 `toggleMarkdownPreview`,右侧打开面板。
- 预览状态按文档隔离,不会影响其他文档。
## 分隔符校验
- `parser.ts` 暴露 `isValidDelimiter` 方法,格式错误时分隔符会以红色底纹标记。
- 复制/剪切操作会自动扩选到整个块,确保分隔符完整。
## 维护建议
- 保持每个逻辑主题占用一个块,并用 `md` 块写标题。
- 大量多语言内容时,可用 `text` + 自动检测,待语言确定后再改分隔符。
- 若文档出现“无法解析块”提示,可运行 `格式化文档` 或在命令面板触发“重建语法树”。

View File

@@ -1,42 +0,0 @@
# 扩展与插件
![扩展占位](/img/placeholder-extensions.png)
> 替换为展示扩展设置面板或功能合集(小地图、搜索、翻译)的截图。
voidraft 的扩展系统由 `internal/models/extensions.go` + 前端 `ExtensionManager` 驱动。扩展配置存储在 `%USERPROFILE%/.voidraft/data/extensions.json`,可在设置页面勾选启用或调整参数。
## 核心扩展
| 扩展 ID | 功能 | 关键文件 |
| --- | --- | --- |
| `editor` | 基础 CodeMirror 行为、光标保护、滚轮缩放 | `frontend/src/views/editor/basic/*` |
| `codeblock` | 块解析、拖拽、复制、格式化、数学、HTTP DSL | `extensions/codeblock` |
| `vscodeSearch` | VSCode 风格搜索替换面板 | `extensions/vscodeSearch` |
| `markdownPreview` | Markdown 实时预览 | `extensions/markdownPreview` |
## 编辑增强
- **Rainbow Brackets (`rainbowBrackets`)**:彩虹色括号匹配。
- **Fold (`fold`)**:代码折叠/展开,支持 `Ctrl+Alt+[`/`]`
- **Hyperlink (`hyperlink`)**:识别 URL/邮箱,`Ctrl+Click` 打开。
- **Color Selector (`colorSelector`)**:悬浮配色器,支持 HEX/RGB/HSL。
- **Checkbox (`checkbox`)**Markdown 任务列表交互式勾选。
- **Text Highlight (`textHighlight`)**`Mod+Shift+H` 快速标记重点,可自定义颜色/透明度。
## 工具扩展
- **Translator (`translator`)**:选区翻译;配置项包括默认翻译器、最短/最长字符数。后端集成 Bing/Google/Youdao/DeepL/TartuNLP。
- **Minimap (`minimap`)**:右侧迷你地图,支持悬浮/常驻、显示字符/块,突出当前选区。
- **Search (`search`)**:补充 VSCode 风格搜索,暴露命令给快捷键系统。
- **HTTP Client (`httpclient`)**DSL + 运行器,详见 [HTTP 客户端](/zh/guide/http-client)。
## 未来扩展(欢迎参与)
- Vim / Emacs 键位层(正在计划)。
- 自定义命令面板Command Palette
- 代码片段/模板库扩展。
- AI 助手(文生代码/注释)。
## 开发者指南概述
1. **注册扩展**:在 `extensionManager.registerFactory` 中添加自定义扩展工厂。
2. **配置项**:在 `extensions.json` 中声明默认配置,并在设置面板暴露 UIVue 组件)。
3. **热更新**:调用 `manager.updateExtensionImmediate(id, enabled, config)` 实时切换,无需刷新窗口。
4. **后端交互**:通过 `ExtensionService.UpdateExtensionState` 将配置写入 SQLite。
> 如果需要编写自用扩展,可 fork 项目在 `frontend/src/views/editor/extensions` 中添加文件,再通过 PR 贡献给社区。

View File

@@ -1,86 +1,163 @@
# 功能特性 # 功能特性
![扩展能力占位](/img/placeholder-extensions.png) 探索 voidraft 的强大功能,让它成为开发者的优秀工具。
> 替换为展示彩虹括号、小地图、搜索工具条等扩展组合的截图。
## 1. 编辑体验 ## 块状编辑
### 块状编辑全流程
- `∞∞∞language[-a]` 语法由 `codeblock/lang-parser` 解析,支持自动检测、分隔符校验、块范围缓存。
- `blockState` 暴露 API`getActiveBlock/getFirstBlock/getLastBlock`供格式化、重排、复制、HTTP 执行等插件共享。
- `mathBlock` 可在块尾展示计算结果,点击可复制;`CURRENCIES_LOADED` 注解在汇率更新时刷新缓存。
### 语言支持 voidraft 的核心功能是其块状编辑系统:
- 内建 30+ 语言模版(`lang-parser/languages.ts`),覆盖 JS/TS/HTML/CSS/Go/Rust/Python/SQL/YAML/HTTP/Markdown/Plain/Text/Math。
- 语言切换下拉实时更新分隔符;支持自定义别名(例如 `∞∞∞shell`)。
### 语法高亮与主题 - 每个块可以有不同的编程语言
- `rainbowBracket``fold``hyperlink``colorSelector` 等扩展组合提供接近 VSCode 的体验。 - 块之间由分隔符分隔(`∞∞∞语言`
- `ThemeService` 预置 12+ 暗/亮主题,可在设置中克隆、修改 JSON 色板,并立即生效。 - 快速在块之间导航
- 独立格式化每个块
### 文本统计与滚轮缩放 ## 语法高亮
- `statsExtension` 实时统计行数、字符数和选区,展示在状态栏。
- `wheelZoomExtension``Ctrl + 鼠标滚轮` 调整字体大小,同时同步 `configStore`
## 2. 高效工具箱 支持 30+ 种语言的专业语法高亮:
### VSCode 式搜索替换
- `extensions/vscodeSearch` 提供悬浮面板,支持大小写/整词/正则、向上/向下跳转、批量替换。
- 对应快捷键:`Ctrl+F``Ctrl+H``Alt+Enter`(替换全部)。
### Markdown 预览 - 自动语言检测
- `panelStore` 为每个文档维护预览状态,保证不同文档互不影响。 - 可自定义配色方案
- 选中 Markdown 块后点击工具栏预览按钮即可在右侧展开实时渲染面板。 - 支持嵌套语言
- 代码折叠支持
### HTTP 客户端 ## HTTP 客户端
- Request DSL + 运行器在 [专章](/zh/guide/http-client) 详细说明。
- 支持变量、响应插入、多种请求体、定制 header、复制 cURL。
### 翻译助手 用于 API 测试的内置 HTTP 客户端:
- `translator` 扩展监听选区,符合长度阈值后显示按钮;由 `TranslationService` 调用 Bing/Google/Youdao/DeepL/TartuNLP。
- 支持语种缓存、复制译文、切换译文方向。
### 颜色与高亮 ### 请求类型
- `colorSelector` 识别 `#fff/rgba/hsl`、打开取色器;`textHighlight``Mod+Shift+H` 标记重要行。 - GET、POST、PUT、DELETE、PATCH
- 自定义请求头
- 多种请求体格式JSON、FormData、URL 编码、XML、文本
## 3. 复杂布局能力 ### 请求变量
### 多窗口 定义和重用变量:
- `WindowService` 允许为任意文档创建独立 WebViewURL 自动携带 `?documentId=`
- `WindowSnapService` 根据主窗口位置吸附子窗口(上下左右+四角),并缓存尺寸、位置。
- 支持全局热键(默认 `Alt+X`)一键显示或隐藏所有窗口。
### 标签页 ```http
- `tabStore` 通过 `enableTabs` 控制;支持拖拽排序、关闭其他/左侧/右侧标签。 @var {
- 与多窗口互斥:当文档被新窗口接管后会从标签栏移除,避免重复。 baseUrl: "https://api.example.com",
token: "your-api-token"
}
### 系统托盘与置顶 GET "{{baseUrl}}/users" {
- `TrayService` 控制关闭时隐藏到托盘或直接退出。 authorization: "Bearer {{token}}"
- 工具栏提供图钉按钮,可即时切换 `AlwaysOnTop`(支持临时置顶和永久置顶)。 }
```
## 4. 数据守护 ### 响应处理
### SQLite + 自动迁移 - 查看格式化的 JSON 响应
- `DatabaseService` 启动时执行 PRAGMA + 表结构校验,缺失字段自动 `ALTER TABLE` - 查看响应时间和大小
- 默认生成 `documents/extensions/key_bindings/themes` 等表,支持软删除与锁定。 - 检查响应头
- 保存响应以供日后使用
### Git 备份 ## 代码格式化
- `BackupService``dataPath` 初始化为 Git 仓库,支持 Token/SSHKey/用户名密码三种方式。
- 自动任务按分钟运行(`BackupInterval`),包括 add/commit/push也可从 UI 触发一次性 push。
### 配置快照 集成 Prettier 支持:
- 所有设置存于 `config.json`,包含 `metadata.version/lastUpdated`,方便手工回滚。
- `ConfigService.Watch` 为窗口吸附、托盘、热键等服务提供实时响应。
### 自动更新 - 保存时格式化(可选)
- `SelfUpdateService` 先检查主源Gitea失败再回退到 GitHub下载完成后可一键「重启并更新」。 - 格式化选区或整个块
- 更新前可选自动触发 Git 备份(`backupBeforeUpdate`)。 - 支持 JavaScript、TypeScript、CSS、HTML、JSON 等
- 可自定义格式化规则
## 5. 自动化与集成 ## 编辑器扩展
- **启动时动作**:可开启开机自启(`StartupService`)、默认最小化至托盘。
- **HTTP 运行挂钩**`response-inserter` 可在响应块尾部插入 `// @timestamp` 等自定义标记。
- **Math/汇率**`mathBlock` 可引用上一次结果 (`prev`),配合 `CURRENCIES_LOADED` 注解支撑货币换算。
- **系统信息**`SystemService` 暴露内存、GC、Goroutine 数量,可在调试面板查看。
## 6. 可配置的快捷键 ### VSCode 风格搜索
- 详见 [键盘快捷键](/zh/guide/keyboard-shortcuts)。默认绑定定义在 `internal/models/key_bindings.go`,前端设置页可逐项修改、禁用。 - 查找和替换,支持正则表达式
- 区分大小写和全字匹配选项
- 跨所有块搜索
### 小地图
- 文档的鸟瞰图
- 快速导航
- 可自定义大小和位置
### 彩虹括号
- 彩色括号配对
- 更容易匹配括号
- 可自定义颜色
### 颜色选择器
- 可视化颜色选择
- 支持 hex、RGB、HSL
- 实时预览
### 翻译工具
- 翻译选定的文本
- 支持多种语言
- 快速键盘访问
### 文本高亮
- 高亮重要文本
- 多种高亮颜色
- 持久化高亮
## 多窗口支持
高效使用多个窗口:
- 每个窗口都是独立的
- 独立的文档
- 同步的设置
- 窗口状态持久化
## 主题自定义
完全控制编辑器外观:
### 内置主题
- 深色模式
- 浅色模式
- 根据系统自动切换
### 自定义主题
- 创建你自己的主题
- 自定义每种颜色
- 保存和分享主题
- 导入社区主题
## 自动更新系统
通过自动更新保持最新:
- 后台更新检查
- 新版本通知
- 一键更新
- 更新历史
- 支持多个更新源GitHub、Gitea
## 数据备份
使用基于 Git 的备份保护你的数据:
- 自动备份
- 手动触发备份
- 支持 GitHub 和 Gitea
- 多种认证方式SSH、Token、密码
- 可配置备份间隔
## 键盘快捷键
广泛的键盘支持:
- 可自定义快捷键
- Vim/Emacs 按键绑定(计划中)
- 快速命令面板
- 上下文感知快捷键
## 性能
专为速度而构建:
- 快速启动时间
- 流畅滚动
- 高效内存使用
- 支持大文件
## 隐私与安全
你的数据是安全的:
- 本地优先存储
- 可选云备份
- 无遥测或跟踪
- 开源代码库
## 7. 文档 & 帮助
- 文档站以 VitePress 构建(`frontend/docs`),内置中英双语导航,可一键部署到 GitHub Pages。
- `README` 与本文档同步介绍核心功能;建议将常用工作流截图补充到每个「图片占位」中。

View File

@@ -1,82 +1,107 @@
# 快速开始 # 快速开始
![块结构流程占位](/img/placeholder-block-flow.png) 学习使用 voidraft 的基础知识并创建你的第一个文档。
> 替换为展示块分隔符、语言标签、内容的截图,帮助读者直观理解 `∞∞∞language` 结构。
## 5 分钟上手流程 ## 编辑器界面
1. **启动应用**:等待加载动画结束,默认会打开 `default` 文档。
2. **新建文档**:点击工具栏的文档列表按钮,输入标题后创建;也可在设置里开启标签页,以便同时挂载多个文档。
3. **创建首个块**
- 在空白处输入 `∞∞∞javascript` 并回车。
- 输入代码或文本,`CodeBlockExtension` 会自动匹配语法高亮。
4. **格式化与预览**
- 选中块后点击工具栏的「Format」或使用 `Ctrl+Shift+F`
- 如果块语言是 `md`可点击「Preview」按钮开启 Markdown 侧栏。
5. **运行 HTTP 请求**:创建 `∞∞∞http` 块,填写请求,再点击行号旁的 Run 按钮即可获取响应。
6. **打开第二窗口**:在文档列表中右键文档 -> “在新窗口中打开”。`WindowService` 会创建无边框窗口并自动贴靠主窗口。
## 界面导览 当你打开 voidraft 时,你将看到:
- **主编辑区**CodeMirror 视图,支持鼠标滚轮 + `Ctrl` 缩放(`wheelZoomExtension`)。
- **右侧小地图**`extensions/minimap` 提供鸟瞰和选区同步。
- **底部状态**`editorStore.documentStats` 实时展示行数/字符/选区。
- **工具栏**`Toolbar.vue`包含文档切换、块语言下拉、窗口置顶、格式化、Markdown 预览、更新提示、进入设置等。
## 块的基本操作 - **主编辑器**:编写和编辑的中心区域
| 操作 | 快捷键 | 说明 | - **工具栏**:快速访问常用操作
| --- | --- | --- | - **状态栏**:显示当前块的语言和其他信息
| 新建块(下方) | `Ctrl+Enter` | 在当前块后插入 `∞∞∞text-a` 分隔符 |
| 新建块(上方) | `Ctrl+Shift+Enter` | 在当前块前插入 |
| 跳到上/下一个块 | `Alt+Up / Alt+Down` | 通过 `blockGotoPrevious/Next` 命令 |
| 删除块 | `Alt+Delete` | 仅删除块内容,不影响其他块 |
| 块排序 | `Ctrl+Shift+↑/↓` | `moveLines` 结合块范围移动 |
| 复制块 | `Ctrl+C`(光标在块上即可) | `copyPaste.ts` 自动扩展选区至整个块 |
## 自动语言与格式化 ## 创建代码块
- 当分隔符写成 `∞∞∞text-a` 时,会触发语言自动检测(`lang-detect/autodetect.ts`),常用于粘贴未知代码。
- `formatCode.ts` 调用 Prettier自动选择 parser若语言不支持会提示不可格式化。
- 块语言可在工具栏下拉中修改,列表由 `lang-parser/languages.ts` 提供。
## Markdown / 待办 voidraft 使用基于块的编辑系统。每个块可以有不同的语言:
1. 使用 `∞∞∞md` 分隔符。
2. 在块内写 Markdown点击工具栏预览按钮。
3. 勾选/取消 Checkbox`extensions/checkbox`)即可同步更新文本。
## 翻译与文本标注 1.`Ctrl+Enter` 创建新块
- 选中文本后会浮现翻译入口(`translator` 扩展),点击即可在块内查看结果、复制、切换目标语言。 2. 输入 `∞∞∞` 后跟语言名称(例如 `∞∞∞javascript`
- `textHighlight` 扩展提供 `Mod+Shift+H` 高亮当前选区,颜色可在扩展设置中调整。 3. 在该块中开始编码
### 支持的语言
voidraft 支持 30+ 种编程语言,包括:
- JavaScript、TypeScript
- Python、Go、Rust
- HTML、CSS、Sass
- SQL、YAML、JSON
- 以及更多...
## 基本操作
### 导航
- `Ctrl+Up/Down`:在块之间移动
- `Ctrl+Home/End`:跳转到第一个/最后一个块
- `Ctrl+F`:在文档中搜索
### 编辑
- `Ctrl+D`:复制当前行
- `Ctrl+/`:切换注释
- `Alt+Up/Down`:向上/向下移动行
- `Ctrl+Shift+F`:格式化代码(如果语言支持 Prettier
### 块管理
- `Ctrl+Enter`:创建新块
- `Ctrl+Shift+Enter`:在上方创建块
- `Alt+Delete`:删除当前块
## 使用 HTTP 客户端
voidraft 包含用于测试 API 的内置 HTTP 客户端:
1. 创建一个 HTTP 语言的块
2. 编写你的 HTTP 请求:
## HTTP 客户端概览
```http ```http
http POST "https://api.example.com/users" {
@var {
baseUrl: "https://api.example.com",
token: "{{secrets.token}}"
}
POST "{{baseUrl}}/users" {
authorization: "Bearer {{token}}"
content-type: "application/json" content-type: "application/json"
@json { @json {
name: "voidraft", name: "",
role: "developer" email: "zhangsan@example.com"
} }
} }
``` ```
- `parser/request-parser.ts` 会将变量与请求体解析为结构化对象。
- 点击 gutter Run 获取响应,`response-inserter.ts` 会将结果写入 `### Response` 区块。
## 自动保存与版本安全 3. 点击运行按钮执行请求
- `editorStore` 为每个文档维护 `autoSaveTimer`,默认 2000 ms可在设置 > 编辑 调整。 4. 内联查看响应
- `documentStates` 记录每个文档的光标位置,切换文档或重启应用都会恢复。
- 若开启 Git 备份,可在工具栏或设置中查看最近一次 `push` 是否成功。
## 最佳实践 ## 多窗口支持
- 使用 Markdown 块为每组代码加标题/注释,便于导航。
- 重要文档启用“锁定”以避免被删除(文档右键菜单)。 同时处理多个文档:
- 多窗口 + 吸附用于常驻参考资料,标签页用于在一个窗口内快速切换。
- 善用「窗口置顶」图钉,让 voidraft 叠放在 VSCode/浏览器之上。 1. 转到 `文件 > 新建窗口`(或 `Ctrl+Shift+N`
2. 每个窗口都是独立的
3. 更改会自动保存
## 自定义主题
个性化你的编辑器:
1. 打开设置(`Ctrl+,`
2. 转到外观
3. 选择主题或创建自己的主题
4. 根据你的偏好自定义颜色
## 键盘快捷键
学习基本快捷键:
| 操作 | 快捷键 |
|-----|--------|
| 新建窗口 | `Ctrl+Shift+N` |
| 搜索 | `Ctrl+F` |
| 替换 | `Ctrl+H` |
| 格式化代码 | `Ctrl+Shift+F` |
| 切换主题 | `Ctrl+Shift+T` |
| 命令面板 | `Ctrl+Shift+P` |
## 下一步
现在你已经了解了基础知识:
- 详细探索[功能特性](/zh/guide/features)
接下来:
- [界面总览](/zh/guide/ui-overview)
- [块语法与结构](/zh/guide/block-syntax)

View File

@@ -1,72 +0,0 @@
# HTTP 客户端
![HTTP 客户端占位](/img/placeholder-http.png)
> 替换为 HTTP 块 + 响应卡片的截图。
voidraft 将 HTTP 测试写成块(`∞∞∞http`),语法与 JetBrains Http Client 类似。解析与执行由 `frontend/src/views/editor/extensions/httpclient` 完成。
## 基本语法
```http
http
GET "https://api.example.com/users" {
accept: "application/json"
}
```
- 方法 + URL 必须用双引号包裹。
- Header 以 `key: "value"` 格式编写。
- 请求体使用内联指令(见下文)。
## 变量与环境
```http
@var {
baseUrl: "https://api.example.com",
token: "{{secrets.token}}"
}
GET "{{baseUrl}}/users" {
authorization: "Bearer {{token}}"
}
```
- `@var` 块使用 JSON 语法。
- 变量在任意请求中以 `{{name}}` 引用。
- `variable-resolver.ts` 支持嵌套、默认值、外部 secrets 映射。
## 请求体助手
| 指令 | 示例 | 说明 |
| --- | --- | --- |
| `@json` | `@json { "name": "voidraft" }` | 自动 `content-type: application/json` 并格式化 |
| `@form` | `@form { username: "demo" }` | 转为 `application/x-www-form-urlencoded` |
| `@multipart` | `@multipart { file: @"C:\tmp\a.txt" }` | 读取文件、多段表单 |
| `@text` | `@text <raw body>` | 自由文本 |
## 运行与响应
1. 将光标置于 HTTP 块内。
2. 点击行号左侧的 Run三角形图标或按下自定义快捷键。
3. 运行结果会插入到块尾的 `### Response` 中,包含:
- 状态行 + 响应时间 + 体积。
- Headers可折叠
- 响应体(自动格式化 JSON / XML / HTML / Text
- 复制、另存为、再次发送等快捷按钮。
## 多请求文档
- 每个 `∞∞∞http` 块被视为独立请求。
- `request-parser.ts` 会解析同一块内的多个请求(以 `###` 分隔)。
- 使用 Markdown 块写注释或分组标题。
## 变量注入顺序
1. 块内 `@var`
2. 文档级变量(计划中)。
3. 环境变量(`EnvironmentService` 预留)。
## 调试技巧
- 运行器会在控制台打印完整请求信息,可通过 `wails3 dev` 查看。
- 如果响应过大,可右键响应块选择“折叠正文”或“导出到文件”。
- 网络错误会在响应卡片顶部以红条展示,内容来自 `HttpClientService`
- 需要代理时确保系统代理已设置voidraft 会自动继承。
## 与其他功能配合
- 运行结果可直接与 Markdown/代码块混排,形成 API 使用手册。
- 配合 Git 备份可版本化 API 调试记录。
- 可将响应复制到其他块(例如 JSON → Prettier 之后用于 mock
> 欢迎在 Issue 中提交你希望支持的额外 DSL 指令(例如 GraphQL、WebSocket、gRPC

View File

@@ -1,77 +1,63 @@
# 安装 # 安装
![安装流程占位](/img/placeholder-settings.png) 本指南将帮助你在系统上安装 voidraft。
> 替换为安装向导或设置页截图,展示关键开关(置顶、数据目录、自动更新等)。
## 系统要求2025.11 ## 系统要求
| 项目 | 最低配置 | 推荐配置 |
| --- | --- | --- |
| 操作系统 | Windows 10 19045 / Windows 11 21H2 | Windows 11 23H2macOS/Linux 版本开发中) |
| CPU | x86_64 双核 | 4 核以上 |
| 内存 | 4 GB | ≥ 8 GB |
| 磁盘空间 | 200 MB含 SQLite 数据) | 1 GB 以上以保存附件/备份 |
| 运行环境 | Go 1.21+, Node.js 18+(仅开发者编译时需要) | 同左 + pnpm 8 用于前端 |
## 获取发行版 - **操作系统**Windows 10 或更高版本macOS 和 Linux 支持计划中)
1. 打开 [GitHub Releases](https://github.com/landaiqing/voidraft/releases) 或自建 Gitea 镜像。 - **内存**:最低 4GB推荐 8GB
2. 下载 `voidraft-windows-amd64-installer.exe`(安装版)或 `voidraft-portable.zip`(绿色版)。 - **磁盘空间**200MB 可用空间
3. (可选)验证 SHA256
```powershell
Get-FileHash .\voidraft-windows-amd64-installer.exe -Algorithm SHA256
```
4. 双击安装包,按向导完成安装;或解压绿色版至任意目录并创建快捷方式。
## 首次启动流程 ## 下载
1. 启动后将创建数据目录:`%USERPROFILE%\.voidraft\data`(含 `voidraft.db`、`config.json`、`extensions.json`)。
2. 默认会生成 `default` 文档和一段示例块 `∞∞∞text-a`。
3. 若检测到旧版本数据,`ConfigMigrationService` 会自动迁移字段;`DataMigrationService` 确保表结构一致。
4. 首次运行建议立刻打开「设置 > 备份」配置远程 Git 仓库。
## 开发者手动构建 访问[发布页面](https://github.com/landaiqing/voidraft/releases)并下载适合你平台的最新版本:
```bash
# 克隆项目
git clone https://github.com/landaiqing/voidraft.git
cd voidraft
# 安装前端依赖 - **Windows**`voidraft-windows-amd64-installer.exe`
cd frontend
npm install
npm run build
cd ..
# 构建/运行桌面应用 ## 安装步骤
wails3 dev # 启动调试
wails3 package # 生成安装包(输出位于 bin/
```
> 若遇到 `wails3` 未找到,请先执行 `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`。
## 数据目录与可执行文件 ### Windows
| 类型 | 默认位置 | 说明 |
| --- | --- | --- |
| 安装目录 | `C:\Program Files\voidraft` | 包含主程序与嵌入式前端资源 |
| 数据目录 | `C:\Users\<you>\.voidraft\data` | 可在设置 > 通用修改 `dataPath`,修改后需重启 |
| 备份仓库 | `dataPath/.git` | `BackupService` 初始化或使用现有仓库 |
| 日志 | `%LOCALAPPDATA%/voidraft/logs/*.log` | 通过 Wails `application.Log` 输出 |
## 常用 CLI 检查 1. 从发布页面下载安装程序
```powershell 2. 运行 `voidraft-windows-amd64-installer.exe` 文件
# 查看版本 3. 按照安装向导操作
& "C:\Program Files\voidraft\voidraft.exe" --version 4. 从开始菜单或桌面快捷方式启动 voidraft
# 清理缓存(若前端异常) ## 首次启动
Remove-Item "$env:APPDATA\voidraft\Cache" -Recurse -Force
```
## 防火墙与代理 首次启动 voidraft 时:
- voidraft 仅在使用 HTTP 客户端、更新检测、REST 翻译器时发起网络请求。
- 若处于企业代理,请在系统代理中放行 `voidraft.exe` 或设置环境变量 `HTTP(S)_PROXY`HTTP 客户端会继承系统代理。
## 常见安装问题 1. 应用程序将创建一个数据目录来存储你的文档
| 症状 | 处理方案 | 2. 你将看到带有欢迎块的主编辑器界面
| --- | --- | 3. 开始输入或创建你的第一个代码块!
| 安装向导被安全策略阻止 | 使用签名哈希进行白名单设置或改用便携版 |
| 启动后白屏 | 删除 `%APPDATA%/voidraft/Cache`,确保显卡驱动支持 WebView2 | ## 配置
| `wails3 dev` 报错缺少 WebView2 | 安装 [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) |
| 便携版无法写入 | 检查解压目录是否具有写权限,或在设置内切换 `dataPath` 至可写分区 | voidraft 将其配置和数据存储在:
- **Windows**`%APPDATA%/voidraft/`
你可以自定义各种设置,包括:
- 编辑器主题(深色/浅色模式)
- 代码格式化偏好
- 备份设置
- 键盘快捷键
## 更新
voidraft 包含自动更新功能,会在有新版本时通知你。你可以:
- 从设置中手动检查更新
- 启用自动更新
- 选择首选的更新源
## 故障排除
如果在安装过程中遇到任何问题:
1. 确保你有管理员权限
2. 检查杀毒软件是否阻止了安装
3. 访问我们的 [GitHub issues](https://github.com/landaiqing/voidraft/issues) 页面寻求帮助
下一步:[快速开始 →](/zh/guide/getting-started)
> 继续阅读:[快速开始](/zh/guide/getting-started)

View File

@@ -1,73 +1,50 @@
# 简介 # 简介
> voidraft 是一款面向开发者的「块式工作台」,用 CodeMirror 6 打造 Heynote 风格的体验,并结合 Wails3 + Go 后端提供系统托盘、全局热键、自动备份等桌面级能力 欢迎使用 voidraft —— 一个专为开发者设计的优雅文本片段记录工具
![主界面总览占位](/img/placeholder-main-ui.png) ## 什么是 voidraft
> 将 `/img/placeholder-main-ui.png` 替换为真实的应用主界面截图,演示数据面板、工具栏和右侧小地图。
## 产品定位 voidraft 是一个现代化的桌面应用程序帮助开发者管理文本片段、代码块、API 响应、会议笔记和日常待办事项。它为开发工作流程提供了流畅而优雅的编辑体验和强大的功能。
- **核心诉求**:在一处快速记录代码/配置/API 响应/待办清单,并能随时重排、格式化、运行或搜索。
- **目标用户**需要跨项目管理零碎文本的开发者、DevOps、测试或产品技术写作者。
- **设计理念**:所有内容都拆成可重排的块(`∞∞∞language`);每个块拥有独立语言、格式化器与扩展;多窗口/多标签保证同一份数据的不同视角。
## 面向场景 ## 核心特性
1. **临时代码/脚本草稿**:支持 30+ 语言高亮、Prettier 格式化、彩虹括号、文本高亮。
2. **API 调试台**HTTP 块内置运行器、变量解析、响应插入;请求和响应始终和文档共存。
3. **会议 & 需求记录**Markdown 块 + Checkbox 扩展 + 颜色标注快速整理想法。
4. **翻译与研究**:选中文本即可调 Bing/Google/DeepL/TartuNLP/有道翻译,结果内联呈现。
5. **多窗口资料墙**重要文档可弹出独立无边框窗口依附Snap在主窗口侧边。
## 核心概念 ### 块状编辑模式
### 块式编辑器
- 解析器位于 `frontend/src/views/editor/extensions/codeblock`,依赖自研 Lezer 语法树确保 `∞∞∞` 分隔符稳定。
- 块结构(语言、是否自动检测、正文范围)存入 `blockState`供格式化、移动、复制、HTTP 执行等扩展复用。
- `math` 块使用 `math.js` 运行器,`http` 块调用 request parser + gutter run widget。
### 扩展驱动 voidraft 使用受 Heynote 启发的独特块状编辑系统。你可以将内容分割为独立的代码块,每个块具有:
- 后端通过 `internal/models/extensions.go` 定义扩展 ID/配置,`ExtensionService` 负责持久化。 - 不同的编程语言设置
- 前端 `ExtensionManager` 根据扩展配置动态拼装 CodeMirror Extension pipeline小地图、VSCode Search、Translator、Color Picker 等)。 - 语法高亮
- 所有扩展都可在设置页热切换,立即同步到当前与所有已打开的编辑器实例。 - 独立格式化
- 轻松在块之间导航
### 数据与安全 ### 开发者工具
- SQLite 数据保存在 `%USERPROFILE%/.voidraft/data/voidraft.db`(可在设置中自定义 dataPath
- `DatabaseService` 自动迁移表结构,`DocumentService` 提供软删除/锁定机制避免误删默认草稿。
- `BackupService` 基于 go-gitSSH/Token/用户名密码)把 dataPath git 化,可按分钟全量提交、推送到 GitHub/Gitea 等。
- `SelfUpdateService` 同时轮询 GitHub/Gitea Release支持自动下载 + 一键重启。
## 系统架构概览 - **HTTP 客户端**:直接在编辑器中测试 API
| 层级 | 说明 | 关键路径 | - **代码格式化**:内置 Prettier 支持多种语言
| --- | --- | --- | - **语法高亮**:支持 30+ 种编程语言
| 桌面容器 | Wails3 + Go 1.21,负责窗口、托盘、热键、服务注入 | `main.go`, `internal/services` | - **自动语言检测**:自动识别代码块语言类型
| 后端服务 | Config/Document/Extension/Theme/Backup/Window/Hotkey/Translation 等 | `internal/services/*.go` |
| 数据模型 | Document、Theme、KeyBinding、GitBackup、Config | `internal/models` |
| 前端应用 | Vue 3 + Vite + Pinia + vue-router | `frontend/src` |
| 编辑器内核 | CodeMirror 6 扩展及自研块解析、HTTP DSL、Markdown 预览 | `frontend/src/views/editor` |
| 文档站点 | VitePress多语言导航 | `frontend/docs` |
## 模块速览 ### 自定义
- **文档存储**`DocumentService` 支持创建/重命名/软删除/恢复、多窗口并发打开同一文档。
- **编辑器实例管理**`editorStore` 使用 LRU 缓存 + 自动保存计时器,确保在多文档切换时保留光标位置、未保存内容。
- **HTTP 客户端**`extensions/httpclient` 包括 Lezer 语法、变量解析、响应插入与运行 gutter支持 JSON/FormData/GraphQL 等多体格式。
- **Markdown 预览**`panelStore` 管理逐文档的预览状态,可随块实时刷新。
- **多窗口/吸附**`WindowService` + `WindowSnapService` 根据主窗口位置智能吸附子窗口、自动记忆尺寸。
- **全局热键**`HotkeyService` 监听系统级组合键,切换窗口显隐(默认 Alt+X可配置
- **系统托盘**`systray.SetupSystemTray` 注入显示/隐藏、退出、开机启动等操作。
- **翻译生态**`TranslationService` 聚合 Bing/Google/Youdao/DeepL/TartuNLP前端 `translator` 扩展提供 Tooltip + 复制。
- **主题与外观**`ThemeService` 预置 12+ 主题,可重置/克隆;前端 `createThemeExtension` 即时应用。
## 数据流(从键盘到持久化) - **自定义主题**:创建并保存你自己的编辑器主题
1. 用户按键 -> CodeMirror extensions 更新文档。 - **扩展功能**:丰富的编辑器扩展,包括小地图、彩虹括号、颜色选择器等
2. `contentChangeExtension` 记录脏状态并刷新 `documentStats`(行数、字符数、选区字符数)。 - **多窗口**:同时处理多个文档
3. 触发自动保存计时器(默认 2s -> `DocumentService.UpdateDocumentContent` 写入 SQLite。
4. 若开启 Git 自动备份,每次 Commit 会序列化数据库 + 附带 `voidraft_data.bin`
5. 配置变更Pinia store通过 `ConfigService.Set` 传回 Go并触发观察者如 WindowSnap/Hotkey/Backup
## 版本节奏与路线图 ### 数据管理
- ✅ 当前实现多窗口、标签页、HTTP 客户端、Markdown Preview、数学块、彩虹括号、翻译、Git 备份、自动更新。
- 🚧 进行中自定义扩展导入、键位模版、Linux/macOS 原生打包。 - **Git 备份**:使用 Git 仓库自动备份
- 🗺️ 规划中:剪贴板历史、团队同步、云端模板市场。 - **云同步**:跨设备同步你的数据
- **自动更新**:及时获取最新功能
## 为什么选择 voidraft
- **专注开发者**:考虑开发者需求而构建
- **现代技术栈**使用前沿技术Wails3、Vue 3、CodeMirror 6
- **跨平台**:支持 WindowsmacOS 和 Linux 支持计划中)
- **开源**MIT 许可证,社区驱动开发
## 开始使用
准备好开始了吗?从我们的[发布页面](https://github.com/landaiqing/voidraft/releases)下载最新版本,或继续阅读文档了解更多。
下一步:[安装 →](/zh/guide/installation)
## 下一步
- [安装 voidraft](/zh/guide/installation)
- [界面总览](/zh/guide/ui-overview)
- [快速开始](/zh/guide/getting-started)

View File

@@ -1,74 +0,0 @@
# 键盘快捷键
![快捷键占位](/img/placeholder-shortcuts.png)
> 替换为展示快捷键设置界面或常用快捷键速查表的截图。
快捷键定义源自 `internal/models/key_bindings.go`,在设置 > 键位 中可以启用/禁用或改写。下表列出常用组合:
## 块管理
| 功能 | 默认快捷键 | 备注 |
| --- | --- | --- |
| 新建块(下方) | `Ctrl+Enter` | `blockAddAfterCurrent` |
| 新建块(上方) | `Ctrl+Shift+Enter` | `blockAddBeforeCurrent` |
| 跳到上/下一个块 | `Alt+↑ / Alt+↓` | `blockGotoPrevious/Next` |
| 选择当前块 | `Ctrl+Shift+A` | `blockSelectAll` |
| 删除块 | `Alt+Delete` | `blockDelete` |
| 块上移/下移 | `Ctrl+Shift+↑ / Ctrl+Shift+↓` | `blockMoveUp/Down` |
| 复制块 | `Ctrl+C`(块获得焦点) | `blockCopy` |
| 剪切块 | `Ctrl+X` | `blockCut` |
| 粘贴块 | `Ctrl+V` | `blockPaste` |
## 行与文本编辑
| 功能 | 快捷键 |
| --- | --- |
| 行复制(上/下) | `Shift+Alt+↑ / Shift+Alt+↓` |
| 行移动(上/下) | `Alt+↑ / Alt+↓`(在块内部) |
| 插入空行 | `Ctrl+Enter`(块尾后仍可插入) |
| 选择整行 | `Alt+L` |
| 语法级跳转 | `Ctrl+Alt+Left / Ctrl+Alt+Right` |
| 匹配括号 | `Shift+Ctrl+\` |
| 注释/块注释 | `Ctrl+/` / `Shift+Alt+A` |
| Tab 缩进/反向缩进 | `Ctrl+]` / `Ctrl+[` |
| 删除单词(向前/向后) | `Ctrl+Backspace` / `Ctrl+Delete` |
## 搜索与替换
| 功能 | 快捷键 | 描述 |
| --- | --- | --- |
| 打开搜索 | `Ctrl+F` | `showSearch` |
| 打开替换 | `Ctrl+H` | `searchShowReplace` |
| 切换大小写/整词/正则 | `Alt+C / Alt+W / Alt+R` | `searchToggleCase/Word/Regex` |
| 替换全部 | `Alt+Enter` | `searchReplaceAll` |
## Markdown/预览/格式化
| 功能 | 快捷键 |
| --- | --- |
| 格式化块 | `Ctrl+Shift+F` |
| 打开 Markdown 预览 | 工具栏按钮(建议映射到 `Ctrl+Shift+M` |
| 高亮文本 | `Mod+Shift+H` |
## 窗口与系统
| 功能 | 快捷键 |
| --- | --- |
| 新建窗口 | `Ctrl+Shift+N`(命令面板) |
| 全局显示/隐藏所有窗口 | 默认 `Alt+X`(可在设置 > 通用 > 全局热键中修改) |
| 打开设置 | `Ctrl+,` |
| 切换主题 | `Ctrl+Shift+T`(可自定义) |
## HTTP 客户端
| 功能 | 快捷键 |
| --- | --- |
| 运行请求 | 点击行号旁 Run 或自定义 `Ctrl+Alt+R` |
| 复制响应正文 | `Ctrl+Alt+C`(响应块聚焦时) |
## 翻译工具
| 功能 | 快捷键 |
| --- | --- |
| 显示翻译浮层 | 选中 ≥ `minSelectionLength` 的文本后按 `Ctrl+'`(可自定义) |
| 复制译文 | 在浮层中按 `Ctrl+C` |
## 自定义与导出
1. 打开设置 > 键位,列表会加载来自 `ExtensionService.GetAllKeyBindings()` 的数据。
2. 可单独禁用某个绑定或录入新组合;存储在 `%USERPROFILE%/.voidraft/data/key_bindings.json`
3. 需要与系统级快捷键冲突时,可勾选“忽略系统修饰键”。
> 建议将以上表格打印贴在工作区,或在文档中保留常用组合,方便新同事查阅。

View File

@@ -1,67 +0,0 @@
# 多窗口与标签页
![多窗口占位](/img/placeholder-multiwindow.png)
> 替换为展示主窗口 + 侧边浮窗(子窗口)或标签页齐开的截图。
## 多窗口工作流
- `WindowService.OpenDocumentWindow` 会根据文档 ID 创建新 WebView 窗口URL 自动附加 `?documentId=<id>`
- `windowStore` 通过查询字符串判断当前是否为子窗口(非主窗口)。
- 子窗口具备:
- 独立的 CodeMirror 实例与扩展栈。
- 与主窗口共享的 Document/Config Store因此编辑内容实时同步SQLite 数据库为唯一来源)。
- `WindowSnapService` 提供吸附:拖动靠近主窗口边缘时自动贴靠;支持上下左右以及四个角。
- 关闭时自动注销吸附状态,避免悬挂引用。
### 操作步骤
1. 打开文档列表(工具栏图标或 `Ctrl+Shift+O`)。
2. 右键目标文档 → 选择 “在新窗口中打开”。
3. 若文档已在标签页打开,会自动从标签栏移除,防止重复。
4. 关闭窗口:
- 点击自定义标题栏关闭按钮。
- 系统托盘菜单选择退出。
### 使用建议
- 将参考资料或检查清单放在子窗口中,配合“窗口置顶”保持常驻。
- 通过 Windows Snap + voidraft Snap 组合,可快速排版 2-4 个窗口。
- 若想在多窗口之间同步滚动,可尝试启用“共享视图状态”扩展(计划中)。
## 标签页模式
- 在设置 > 通用中开启“启用标签页”(`config.general.enableTabs`)。
- `tabStore` 维护 `tabsMap` + `tabOrder`,支持:
- 拖拽排序(拖动标签即可)。
- 关闭单个/其他/左侧/右侧标签。
- 检测当前文档是否已存在标签。
- 标签栏位于主窗口顶部,紧贴工具栏下方。
### 常用操作
| 操作 | 方法 |
| --- | --- |
| 关闭标签 | 点击标签上的叉号或中键 |
| 关闭其他 | 右键标签 → “关闭其他标签” |
| 关闭右侧/左侧 | 右键标签 → 选择对应菜单 |
| 固定标签(计划中) | 将在后续版本中提供 pin 功能 |
### Tabs vs 窗口
| 项目 | 标签页 | 新窗口 |
| --- | --- | --- |
| UI 占用 | 集成在一个窗口中 | 独立操作系统窗口 |
| 跨屏 | 不方便 | 可拖到其他显示器 |
| 独立置顶 | 不可单独置顶 | 每个窗口可单独置顶 |
| 推荐场景 | 同一背景的多个文档 | 跨项目/跨显示器对比 |
## 系统托盘与热键
- 勾选 “启用系统托盘” 后,关闭窗口默认隐藏至托盘。
- 全局热键(默认 `Alt+X`)由 `HotkeyService` 控制:
- 若 main window 可见 → 隐藏所有 voidraft 窗口。
- 若 main window 隐藏 → Show + Restore + Focus。
- `TrayService` 还提供“最小化到托盘”“显示主窗口”等菜单项。
## 常见问题
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 新窗口无法打开 | 文档 ID 不存在或被锁定 | 在文档列表确认状态,必要时解锁 |
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
| 关闭窗口直接退出应用 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
| 标签页切换慢 | 同时开启标签 + 多窗口导致资源占用 | 关闭暂不需要的窗口或减少标签 |
> 如果需要更复杂的布局(如平铺窗口、快捷布局),欢迎在 Issue 中提出建议。

View File

@@ -1,71 +0,0 @@
# 设置与配置
![设置占位](/img/placeholder-settings.png)
> 替换为设置页截图,突出通用/编辑/外观/更新/备份等分栏。
所有设置都映射到 `internal/models/config.go`,持久化文件位于 `%USERPROFILE%/.voidraft/data/config.json`。前端 `configStore` 负责与后端 `ConfigService` 同步。
## 通用General
| 选项 | 说明 | 后端键 |
| --- | --- | --- |
| 窗口置顶 (`alwaysOnTop`) | 永久置顶主窗口 | `general.alwaysOnTop` |
| 数据目录 (`dataPath`) | SQLite + 备份所在目录,修改后需重启 | `general.dataPath` |
| 系统托盘 (`enableSystemTray`) | 关闭窗口后隐藏到托盘而非退出 | `general.enableSystemTray` |
| 开机自启 (`startAtLogin`) | 调用 `StartupService` 注册 | `general.startAtLogin` |
| 窗口吸附 (`enableWindowSnap`) | `WindowSnapService` 是否启用 | `general.enableWindowSnap` |
| 全局热键 (`enableGlobalHotkey` + `globalHotkey`) | 默认 Alt+X控制显隐 | `general.globalHotkey` |
| 标签页 (`enableTabs`) | 启用多标签界面 | `general.enableTabs` |
| 加载动画 (`enableLoadingAnimation`) | 切换文档时显示动画 | `general.enableLoadingAnimation` |
## 编辑Editing
| 选项 | 说明 |
| --- | --- |
| Font Size/Family/Weight/Line Height | 立即作用于所有编辑器实例 |
| Tab Size/Tab Type/Enable Tab Indent | 映射 `tabExtension` 行为 |
| Auto Save Delay | ms影响 `editorStore` 自动保存周期 |
## 外观Appearance
| 选项 | 说明 |
| --- | --- |
| Language | UI 语言(`zh-CN`/`en-US` |
| System Theme | 深色/浅色/跟随系统 |
| Current Theme | 选择预设或自定义主题(详见 [主题与外观](/zh/guide/themes) |
## 更新Updates
| 选项 | 说明 |
| --- | --- |
| Auto Update | 启动时自动检查更新 |
| Primary/Backup Source | `github``gitea`,对应 `UpdatesConfig` |
| Backup Before Update | 下载更新前执行 Git 备份 |
| Update Timeout | HTTP 请求超时 |
| GitHub/Gitea 仓库 | owner/repo/baseURL可指向自建镜像 |
## 备份Backup
| 选项 | 说明 |
| --- | --- |
| Enabled | 开关 Git 备份 |
| Repo URL | 远程仓库地址HTTPS 或 SSH |
| Auth Method | `token` / `ssh_key` / `user_pass` |
| Username/Password/Token/SSH Key Path | 根据认证方式填写 |
| Backup Interval | 自动备份间隔(分钟) |
| Auto Backup | 是否按间隔自动推送 |
## 键位Key Bindings
- 列表由 `ExtensionService.GetAllKeyBindings()` 提供。可搜索命令 ID 或组合。
- 允许将命令禁用(关闭开关)或录入新组合。
- 更改立即影响所有编辑器实例。
## 扩展Extensions
- 显示 `ExtensionSettings` 中的所有扩展。
- 每项可开关并展示 JSON 配置(背景色、最小选区、最小化提示等)。
- 修改后调用 `ExtensionService.UpdateExtensionState` 并通知 `ExtensionManager` 热更新。
## 配置文件备份
- 每次修改配置都会更新 `metadata.lastUpdated`,可用 Git 备份追踪历史。
- 若出现配置损坏,可删除 `config.json`,应用会写入 `NewDefaultAppConfig`
## 导入/导出(建议)
- 目前可手动复制 `config.json`/`extensions.json`/`key_bindings.json`
- 计划提供 UI 层面的导入导出按钮,便于跨设备同步。
> 修改高级选项(如 dataPath后建议重启以确保后台服务数据库、备份、窗口吸附等读取到最新配置。

View File

@@ -1,44 +0,0 @@
# 主题与外观
![主题占位](/img/placeholder-themes.png)
> 替换为主题切换界面或自定义主题编辑器的截图。
voidraft 的主题由后端 `ThemeService` 管理,存储在 `themes` 表。前端通过 `themeStore` + `createThemeExtension` 应用色板。
## 预设主题
| 名称 | 类型 | 说明 |
| --- | --- | --- |
| default-dark | Dark | 默认暗色,适合低光环境 |
| default-light | Light | 默认亮色 |
| dracula | Dark | 高对比度紫色系 |
| aura | Dark | 柔和霓虹风 |
| github-dark / github-light | Dark/Light | 与 GitHub 主题接近 |
| material-dark / light | Dark/Light | Material Design 色板 |
| one-dark | Dark | VSCode 经典主题 |
| solarized-dark / light | Dark/Light | Solarized 配色 |
| tokyo-night / storm / day | Dark/Light | Tokyo Night 三件套 |
## 自定义主题
1. 打开设置 > 外观,选择「创建主题」。
2. 颜色字段对应 `ThemeColorConfig`,包含 `editor.background`, `editor.foreground`, `gutter`, `selection`, `bracket`, `keyword`, `string`, `comment`, `accent` 等。
3. 保存后立即写入数据库,可通过 `Reset` 按钮恢复为预设值。
4. 前端 `themeExtension` 会向 CodeMirror 注入新的 `EditorView.theme`
## 动态切换
- 切换主题会立即影响所有已打开的编辑器实例;`updateEditorTheme` 逐个更新 `EditorView`
- `SystemTheme` 设为 `auto`voidraft 会监听操作系统深浅模式并自动切换到 `default-dark``default-light`
## 字体与行高
- 字体配置来自设置 > 编辑,`createFontExtensionFromBackend` 会同步 `fontFamily/fontSize/fontWeight/lineHeight`
- 可在通用设置中的“滚轮缩放”手势下临时调整字号。
## 小地图/装饰色
- `minimap` 扩展读取主题中的 `accent` 颜色,用于高亮当前视区。
- `textHighlight` 扩展的默认背景色可在扩展设置中配置。
## 截图建议
- 展示暗/亮主题对比。
- 展示主题编辑对话框,标出关键字段。
- 展示自定义主题应用后的编辑器界面。
> 如果希望导入 VSCode `.json` 主题,可将颜色映射到 `ThemeColorConfig` 后写入数据库,或等待官方导入工具上线。

View File

@@ -1,53 +0,0 @@
# 常见问题与故障排查
![故障排查占位](/img/placeholder-troubleshooting.png)
> 替换为错误提示或日志查看界面的截图。
## 安装与启动
| 问题 | 可能原因 | 解决步骤 |
| --- | --- | --- |
| 启动白屏 | WebView2 缺失或缓存损坏 | 安装 WebView2 Runtime删除 `%APPDATA%/voidraft/Cache` 后重启 |
| 双击无反应 | 被安全策略拦截 | 以管理员运行或使用便携版;验证 SHA256 后加入白名单 |
| `wails3 dev` 报错 | Go/Node 版本不符或缺少 WebView2 | 确保 Go 1.21+、Node 18+,安装 WebView2 |
## 编辑器
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 格式化按钮灰色 | 当前块语言无对应 Prettier parser | 更换语言或安装支持的语言扩展(未来版本) |
| 块解析错误 | 分隔符格式不正确 | 确认 `∞∞∞language` 后跟换行;可用自动重建语法树命令 |
| 翻译浮层不出现 | 选区过短或超过最大长度 | 在设置 > 扩展 > translator 中调整阈值 |
| 小地图不同步 | 编辑器实例未刷新 | 切换文档或重开应用,检查扩展是否被禁用 |
## 窗口与多实例
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 子窗口未吸附 | WindowSnap 未启用 | 设置 > 通用 → 勾选“窗口吸附” |
| 全局热键无效 | 与系统/其他软件冲突 | 在设置 > 通用改用非系统占用组合,比如 `Ctrl+Alt+Space` |
| 关闭窗口直接退出 | 未启用托盘模式 | 设置 > 通用 → 启用系统托盘 |
## HTTP 客户端
| 问题 | 可能原因 | 解决 |
| --- | --- | --- |
| 发送失败,提示 `proxy` | 系统代理配置异常 | 在系统代理或环境变量中设置 HTTP(S)_PROXY或关闭代理再试 |
| 响应乱码 | 服务器未声明编码 | 手动在请求头中加 `accept-charset: utf-8`,或在响应视图切换编码(计划) |
| 变量未替换 | 变量名拼写或作用域错误 | 确认 `@var` 定义位置,使用 `{{name}}` 语法 |
## 数据与备份
| 问题 | 可能原因 | 解决 |
| --- | --- | --- |
| 自动备份停在 “未初始化” | Repo URL/认证缺失 | 补全备份配置或关闭自动备份 |
| Push 失败 | Token 权限不足或网络问题 | 为 Token 开启 `repo` scope检查代理稍后再试 |
| 数据目录迁移后文件缺失 | 未重启或权限不足 | 修改 `dataPath` 后重启应用;确保目标目录可写 |
## 更新
| 问题 | 原因 | 解决 |
| --- | --- | --- |
| 检查更新超时 | 主源不可达 | 切换到备用源或关闭代理重试 |
| 下载完成但未重启 | 权限或文件被占用 | 以管理员运行,关闭杀毒软件后重试 |
## 收集日志
- Wails 日志:`%LOCALAPPDATA%/voidraft/logs/*.log`
- 终端调试:运行 `wails3 dev` 并观察控制台输出。
- 若提交 Issue请附上系统版本、voidraft 版本、日志片段、复现步骤。
> 仍未解决?请到 GitHub Issues 提交反馈,并尽可能附上截图和日志。

View File

@@ -1,46 +0,0 @@
# 界面总览
![界面截图占位](/img/placeholder-main-ui.png)
> 替换为包含顶部工具栏、块区域、右侧小地图、底部状态栏的完整截图。
voidraft 的主窗口由四个区域组成:
| 区域 | 位置 | 作用 | 相关代码 |
| --- | --- | --- | --- |
| 工具栏 | 顶部浮层 | 文档切换、块语言选择、格式化、Markdown 预览、窗口置顶、更新提示、进入设置 | `frontend/src/components/toolbar/Toolbar.vue` |
| 编辑器主体 | 中央 | CodeMirror 6 视图承载块编辑、HTTP 运行器、翻译按钮等 | `frontend/src/views/editor/Editor.vue` + `extensions` |
| 导航辅助 | 右侧 | 小地图、滚动条、块徽标、HTTP 运行按钮 | `extensions/minimap`, `codeblock/decorations.ts` |
| 底部状态 | 左下角 | 行数、字符数、选区统计、文档脏状态 | `editorStore.documentStats` |
## 工具栏详解
| 项 | 说明 | 快捷入口 |
| --- | --- | --- |
| 文档切换器 | 展开后列出全部文档,支持搜索、创建、在新窗口打开 | 同步 `DocumentService.ListAllDocumentsMeta` |
| 块语言下拉 | 当前块语言,列表取自 `lang-parser/languages.ts`,支持搜索 | 鼠标选择或输入语言 token |
| Pin窗口置顶 | 临时 / 永久置顶切换,调用 `SystemService.SetWindowOnTop``config.general.alwaysOnTop` | Alt+Space自定义 |
| Format / Preview | 对当前块执行 Prettier 或打开 Markdown 预览 | `Ctrl+Shift+F` / 工具栏按钮 |
| 更新提示 | 轮询 `SelfUpdateService`,有更新时显示小点,可直接“检查/下载/重启” | 设置 > 更新 |
| 设置入口 | 跳转到 Vue Router 的 `/settings` 页面 | `Ctrl+,` |
## 多文档视图
- **标签页(可选)**:在设置 > 通用中启用“标签页模式”,`tabStore` 将当前文档加入 tab bar支持拖拽、批量关闭。
- **多窗口**:以文档列表右键「在新窗口中打开」或命令面板为入口。`WindowService` 会根据文档 ID 命名窗口,`WindowSnapService` 自动吸附。
- **系统托盘**:关闭窗口时默认最小化到托盘,可在托盘图标中重新唤醒或彻底退出。
## 面板与浮层
- **Markdown 预览**:针对选中的 Markdown 块,面板会贴在右侧,支持实时滚动同步、关闭动画。
- **HTTP 响应**:运行后在块底部自动插入 `### Response`,可展开查看头部/体/耗时。
- **翻译浮层**:选中文本后自动出现按钮,点击后显示结果卡片,附带复制、语种切换。
## 快捷状态
- **底部统计**
- `Ln`:当前块内行号。
- `Ch`:字符数。
- `Sel`:选区字符数。
- **右上角加载动画**:当编辑器实例加载或切换文档时显示,遵循 `enableLoadingAnimation` 设置。
## 建议截图
1. 默认深色主题 + 多块示例。
2. 打开 Markdown 预览 + 小地图。
3. 展示 HTTP 块运行按钮与响应卡片。
4. 展示标签页或多窗口。

View File

@@ -14,6 +14,7 @@ import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtensi
import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension'; import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/basic/fontExtension';
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension'; import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension'; import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
import {createWheelZoomExtension} from '@/views/editor/basic/wheelZoomExtension';
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap'; import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
import { import {
createDynamicExtensions, createDynamicExtensions,
@@ -242,6 +243,11 @@ export const useEditorStore = defineStore('editor', () => {
fontWeight: configStore.config.editing.fontWeight fontWeight: configStore.config.editing.fontWeight
}); });
const wheelZoomExtension = createWheelZoomExtension(
() => configStore.increaseFontSize(),
() => configStore.decreaseFontSize()
);
// 统计扩展 // 统计扩展
const statsExtension = createStatsUpdateExtension(updateDocumentStats); const statsExtension = createStatsUpdateExtension(updateDocumentStats);
@@ -287,6 +293,7 @@ export const useEditorStore = defineStore('editor', () => {
themeExtension, themeExtension,
...tabExtensions, ...tabExtensions,
fontExtension, fontExtension,
wheelZoomExtension,
statsExtension, statsExtension,
contentChangeExtension, contentChangeExtension,
codeBlockExtension, codeBlockExtension,
@@ -707,12 +714,15 @@ export const useEditorStore = defineStore('editor', () => {
// 更新前端编辑器扩展 - 应用于所有实例 // 更新前端编辑器扩展 - 应用于所有实例
const manager = getExtensionManager(); const manager = getExtensionManager();
if (manager) { if (manager) {
// 使用立即更新模式,跳过防抖 // 直接更新前端扩展至所有视图
manager.updateExtensionImmediate(id, enabled, config || {}); manager.updateExtension(id, enabled, config);
} }
// 重新加载扩展配置 // 重新加载扩展配置
await extensionStore.loadExtensions(); await extensionStore.loadExtensions();
if (manager) {
manager.initExtensions(extensionStore.extensions);
}
await applyKeymapSettings(); await applyKeymapSettings();
}; };

View File

@@ -3,7 +3,6 @@ import {computed, onBeforeUnmount, onMounted, ref} from 'vue';
import {useEditorStore} from '@/stores/editorStore'; import {useEditorStore} from '@/stores/editorStore';
import {useDocumentStore} from '@/stores/documentStore'; import {useDocumentStore} from '@/stores/documentStore';
import {useConfigStore} from '@/stores/configStore'; import {useConfigStore} from '@/stores/configStore';
import {createWheelZoomHandler} from './basic/wheelZoomExtension';
import Toolbar from '@/components/toolbar/Toolbar.vue'; import Toolbar from '@/components/toolbar/Toolbar.vue';
import {useWindowStore} from "@/stores/windowStore"; import {useWindowStore} from "@/stores/windowStore";
import LoadingScreen from '@/components/loading/LoadingScreen.vue'; import LoadingScreen from '@/components/loading/LoadingScreen.vue';
@@ -19,12 +18,6 @@ const editorElement = ref<HTMLElement | null>(null);
const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation); const enableLoadingAnimation = computed(() => configStore.config.general.enableLoadingAnimation);
// 创建滚轮缩放处理器
const wheelHandler = createWheelZoomHandler(
configStore.increaseFontSize,
configStore.decreaseFontSize
);
onMounted(async () => { onMounted(async () => {
if (!editorElement.value) return; if (!editorElement.value) return;
@@ -38,16 +31,9 @@ onMounted(async () => {
editorStore.setEditorContainer(editorElement.value); editorStore.setEditorContainer(editorElement.value);
await tabStore.initializeTab(); await tabStore.initializeTab();
// 添加滚轮事件监听
editorElement.value.addEventListener('wheel', wheelHandler, {passive: false});
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
// 移除滚轮事件监听
if (editorElement.value) {
editorElement.value.removeEventListener('wheel', wheelHandler);
}
editorStore.clearAllEditors(); editorStore.clearAllEditors();
}); });
@@ -88,7 +74,6 @@ onBeforeUnmount(() => {
overflow: auto; overflow: auto;
} }
// 加载动画过渡效果
.loading-fade-enter-active, .loading-fade-enter-active,
.loading-fade-leave-active { .loading-fade-leave-active {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
@@ -99,3 +84,4 @@ onBeforeUnmount(() => {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@@ -1,22 +1,40 @@
// 处理滚轮缩放字体的事件处理函数 import {EditorView} from '@codemirror/view';
export const createWheelZoomHandler = ( import type {Extension} from '@codemirror/state';
increaseFontSize: () => void,
decreaseFontSize: () => void type FontAdjuster = () => Promise<void> | void;
) => {
return (event: WheelEvent) => { const runAdjuster = (adjuster: FontAdjuster) => {
// 检查是否按住了Ctrl键 try {
if (event.ctrlKey) { const result = adjuster();
// 阻止默认行为(防止页面缩放) if (result && typeof (result as Promise<void>).then === 'function') {
(result as Promise<void>).catch((error) => {
console.error('Failed to adjust font size:', error);
});
}
} catch (error) {
console.error('Failed to adjust font size:', error);
}
};
export const createWheelZoomExtension = (
increaseFontSize: FontAdjuster,
decreaseFontSize: FontAdjuster
): Extension => {
return EditorView.domEventHandlers({
wheel(event) {
if (!event.ctrlKey) {
return false;
}
event.preventDefault(); event.preventDefault();
// 根据滚轮方向增大或减小字体
if (event.deltaY < 0) { if (event.deltaY < 0) {
// 向上滚动,增大字体 runAdjuster(increaseFontSize);
increaseFontSize(); } else if (event.deltaY > 0) {
} else { runAdjuster(decreaseFontSize);
// 向下滚动,减小字体
decreaseFontSize();
} }
return true;
} }
}; });
}; };

View File

@@ -1,7 +1,7 @@
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { useKeybindingStore } from '@/stores/keybindingStore'; import { useKeybindingStore } from '@/stores/keybindingStore';
import { useExtensionStore } from '@/stores/extensionStore'; import { useExtensionStore } from '@/stores/extensionStore';
import { KeymapManager } from './keymapManager'; import { Manager } from './manager';
/** /**
* 异步创建快捷键扩展 * 异步创建快捷键扩展
@@ -23,7 +23,7 @@ export const createDynamicKeymapExtension = async (): Promise<Extension> => {
// 获取启用的扩展ID列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
return KeymapManager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds); return Manager.createKeymapExtension(keybindingStore.keyBindings, enabledExtensionIds);
}; };
/** /**
@@ -37,10 +37,10 @@ export const updateKeymapExtension = (view: any): void => {
// 获取启用的扩展ID列表 // 获取启用的扩展ID列表
const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id); const enabledExtensionIds = extensionStore.enabledExtensions.map(ext => ext.id);
KeymapManager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds); Manager.updateKeymap(view, keybindingStore.keyBindings, enabledExtensionIds);
}; };
// 导出相关模块 // 导出相关模块
export { KeymapManager } from './keymapManager'; export { Manager } from './manager';
export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands'; export { commands, getCommandHandler, getCommandDescription, isCommandRegistered, getRegisteredCommands } from './commands';
export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types'; export type { KeyBinding, CommandHandler, CommandDefinition, KeymapResult } from './types';

View File

@@ -8,7 +8,7 @@ import {getCommandHandler, isCommandRegistered} from './commands';
* *
* CodeMirror快捷键扩展 * CodeMirror快捷键扩展
*/ */
export class KeymapManager { export class Manager {
private static compartment = new Compartment(); private static compartment = new Compartment();
/** /**

View File

@@ -1,299 +0,0 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionState, EditorViewInfo, ExtensionFactory} from './types'
import {createDebounce} from '@/common/utils/debounce';
/**
* 扩展管理器
* 负责管理所有动态扩展的注册、启用、禁用和配置更新
* 采用统一配置,多视图同步的设计模式
*/
export class ExtensionManager {
// 统一的扩展状态存储
private extensionStates = new Map<ExtensionID, ExtensionState>();
// 编辑器视图管理
private viewsMap = new Map<number, EditorViewInfo>();
private activeViewId: number | null = null;
// 注册的扩展工厂
private extensionFactories = new Map<ExtensionID, ExtensionFactory>();
// 防抖处理
private debouncedUpdateFunctions = new Map<ExtensionID, {
debouncedFn: (enabled: boolean, config: any) => void;
cancel: () => void;
flush: () => void;
}>();
/**
* 注册扩展工厂
* @param id 扩展ID
* @param factory 扩展工厂
*/
registerExtension(id: ExtensionID, factory: ExtensionFactory): void {
this.extensionFactories.set(id, factory);
// 创建初始状态
if (!this.extensionStates.has(id)) {
const compartment = new Compartment();
const defaultConfig = factory.getDefaultConfig();
this.extensionStates.set(id, {
id,
factory,
config: defaultConfig,
enabled: false,
compartment,
extension: [] // 默认为空扩展(禁用状态)
});
}
// 为每个扩展创建防抖函数
if (!this.debouncedUpdateFunctions.has(id)) {
const { debouncedFn, cancel, flush } = createDebounce(
(enabled: boolean, config: any) => {
this.updateExtensionImmediate(id, enabled, config);
},
{ delay: 300 }
);
this.debouncedUpdateFunctions.set(id, {
debouncedFn,
cancel,
flush
});
}
}
/**
* 获取所有注册的扩展ID列表
*/
getRegisteredExtensions(): ExtensionID[] {
return Array.from(this.extensionFactories.keys());
}
/**
* 检查扩展是否已注册
* @param id 扩展ID
*/
isExtensionRegistered(id: ExtensionID): boolean {
return this.extensionFactories.has(id);
}
/**
* 从后端配置初始化扩展状态
* @param extensionConfigs 后端扩展配置列表
*/
initializeExtensionsFromConfig(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const factory = this.extensionFactories.get(config.id);
if (!factory) continue;
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config.config)) {
continue;
}
try {
// 创建扩展实例
const extension = config.enabled ? factory.create(config.config) : [];
// 如果状态已存在则更新,否则创建新状态
if (this.extensionStates.has(config.id)) {
const state = this.extensionStates.get(config.id)!;
state.config = config.config;
state.enabled = config.enabled;
state.extension = extension;
} else {
const compartment = new Compartment();
this.extensionStates.set(config.id, {
id: config.id,
factory,
config: config.config,
enabled: config.enabled,
compartment,
extension
});
}
} catch (error) {
console.error(`Failed to initialize extension ${config.id}:`, error);
}
}
}
/**
* 获取初始扩展配置数组(用于创建编辑器)
* @returns CodeMirror扩展数组
*/
getInitialExtensions(): Extension[] {
const extensions: Extension[] = [];
// 为每个注册的扩展添加compartment
for (const state of this.extensionStates.values()) {
extensions.push(state.compartment.of(state.extension));
}
return extensions;
}
/**
* 设置编辑器视图
* @param view 编辑器视图实例
* @param documentId 文档ID
*/
setView(view: EditorView, documentId: number): void {
// 保存视图信息
this.viewsMap.set(documentId, {
view,
documentId,
registered: true
});
// 设置当前活动视图
this.activeViewId = documentId;
}
/**
* 获取当前活动视图
*/
private getActiveView(): EditorView | null {
if (this.activeViewId === null) return null;
const viewInfo = this.viewsMap.get(this.activeViewId);
return viewInfo ? viewInfo.view : null;
}
/**
* 更新单个扩展配置并应用到所有视图(带防抖功能)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtension(id: ExtensionID, enabled: boolean, config: any = {}): void {
const debouncedUpdate = this.debouncedUpdateFunctions.get(id);
if (debouncedUpdate) {
debouncedUpdate.debouncedFn(enabled, config);
} else {
// 如果没有防抖函数,直接执行
this.updateExtensionImmediate(id, enabled, config);
}
}
/**
* 立即更新扩展(无防抖)
* @param id 扩展ID
* @param enabled 是否启用
* @param config 扩展配置
*/
updateExtensionImmediate(id: ExtensionID, enabled: boolean, config: any = {}): void {
// 获取扩展状态
const state = this.extensionStates.get(id);
if (!state) return;
// 获取工厂
const factory = state.factory;
// 验证配置
if (factory.validateConfig && !factory.validateConfig(config)) {
return;
}
try {
// 创建新的扩展实例
const extension = enabled ? factory.create(config) : [];
// 更新内部状态
state.config = config;
state.enabled = enabled;
state.extension = extension;
// 应用到所有视图
this.applyExtensionToAllViews(id);
} catch (error) {
console.error(`Failed to update extension ${id}:`, error);
}
}
/**
* 将指定扩展的当前状态应用到所有视图
* @param id 扩展ID
*/
private applyExtensionToAllViews(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
// 遍历所有视图并应用更改
for (const viewInfo of this.viewsMap.values()) {
try {
if (!viewInfo.registered) continue;
viewInfo.view.dispatch({
effects: state.compartment.reconfigure(state.extension)
});
} catch (error) {
console.error(`Failed to apply extension ${id} to document ${viewInfo.documentId}:`, error);
}
}
}
/**
* 获取扩展当前状态
* @param id 扩展ID
*/
getExtensionState(id: ExtensionID): {
enabled: boolean
config: any
} | null {
const state = this.extensionStates.get(id);
if (!state) return null;
return {
enabled: state.enabled,
config: state.config
};
}
/**
* 重置扩展到默认配置
* @param id 扩展ID
*/
resetExtensionToDefault(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
const defaultConfig = state.factory.getDefaultConfig();
this.updateExtension(id, true, defaultConfig);
}
/**
* 从管理器中移除视图
* @param documentId 文档ID
*/
removeView(documentId: number): void {
if (this.activeViewId === documentId) {
this.activeViewId = null;
}
this.viewsMap.delete(documentId);
}
/**
* 销毁管理器
*/
destroy(): void {
// 清除所有防抖函数
for (const { cancel } of this.debouncedUpdateFunctions.values()) {
cancel();
}
this.debouncedUpdateFunctions.clear();
this.viewsMap.clear();
this.activeViewId = null;
this.extensionFactories.clear();
this.extensionStates.clear();
}
}

View File

@@ -1,301 +1,152 @@
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import i18n from '@/i18n'; import i18n from '@/i18n';
import {ExtensionFactory} from './types' import {ExtensionDefinition} from './types';
// 导入现有扩展的创建函数
import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension';
import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension'; import {createTextHighlighter} from '../extensions/textHighlight/textHighlightExtension';
import {color} from '../extensions/colorSelector'; import {color} from '../extensions/colorSelector';
import {hyperLink} from '../extensions/hyperlink'; import {hyperLink} from '../extensions/hyperlink';
import {minimap} from '../extensions/minimap'; import {minimap} from '../extensions/minimap';
import {vscodeSearch} from '../extensions/vscodeSearch'; import {vscodeSearch} from '../extensions/vscodeSearch';
import {createCheckboxExtension} from '../extensions/checkbox'; import {createCheckboxExtension} from '../extensions/checkbox';
import {createTranslatorExtension} from '../extensions/translator'; import {createTranslatorExtension} from '../extensions/translator';
import {foldingOnIndent} from '../extensions/fold/foldExtension'; import {foldingOnIndent} from '../extensions/fold/foldExtension';
/** type ExtensionEntry = {
* 彩虹括号扩展工厂 definition: ExtensionDefinition
*/ displayNameKey: string
export const rainbowBracketsFactory: ExtensionFactory = { descriptionKey: string
create(_config: any) {
return rainbowBracketsExtension();
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
}; };
/** type RegisteredExtensionID = Exclude<ExtensionID, ExtensionID.$zero | ExtensionID.ExtensionEditor>;
* 文本高亮扩展工厂
*/
export const textHighlightFactory: ExtensionFactory = {
create(config: any) {
return createTextHighlighter({
backgroundColor: config.backgroundColor || '#FFD700',
opacity: config.opacity || 0.3
});
},
getDefaultConfig() {
return {
backgroundColor: '#FFD700', // 金黄色
opacity: 0.3 // 透明度
};
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.backgroundColor || typeof config.backgroundColor === 'string') &&
(!config.opacity || (typeof config.opacity === 'number' && config.opacity >= 0 && config.opacity <= 1));
}
};
/** const defineExtension = (create: (config: any) => any, defaultConfig: Record<string, any> = {}): ExtensionDefinition => ({
* 小地图扩展工厂 create,
*/ defaultConfig
export const minimapFactory: ExtensionFactory = { });
create(config: any) {
const options = {
displayText: config.displayText || 'characters',
showOverlay: config.showOverlay || 'always',
autohide: config.autohide || false
};
return minimap(options);
},
getDefaultConfig() {
return {
displayText: 'characters',
showOverlay: 'always',
autohide: false
};
},
validateConfig(config: any) {
return typeof config === 'object' &&
(!config.displayText || typeof config.displayText === 'string') &&
(!config.showOverlay || typeof config.showOverlay === 'string') &&
(!config.autohide || typeof config.autohide === 'boolean');
}
};
/** const EXTENSION_REGISTRY: Record<RegisteredExtensionID, ExtensionEntry> = {
* 超链接扩展工厂
*/
export const hyperlinkFactory: ExtensionFactory = {
create(_config: any) {
return hyperLink;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 颜色选择器扩展工厂
*/
export const colorSelectorFactory: ExtensionFactory = {
create(_config: any) {
return color;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 搜索扩展工厂
*/
export const searchFactory: ExtensionFactory = {
create(_config: any) {
return vscodeSearch;
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
export const foldFactory: ExtensionFactory = {
create(_config: any) {
return foldingOnIndent;
},
getDefaultConfig(): any {
return {};
},
validateConfig(config: any): boolean {
return typeof config === 'object';
}
};
/**
* 选择框扩展工厂
*/
export const checkboxFactory: ExtensionFactory = {
create(_config: any) {
return createCheckboxExtension();
},
getDefaultConfig() {
return {};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 翻译扩展工厂
*/
export const translatorFactory: ExtensionFactory = {
create(config: any) {
return createTranslatorExtension({
minSelectionLength: config.minSelectionLength || 2,
maxTranslationLength: config.maxTranslationLength || 5000,
});
},
getDefaultConfig() {
return {
minSelectionLength: 2,
maxTranslationLength: 5000,
};
},
validateConfig(config: any) {
return typeof config === 'object';
}
};
/**
* 所有扩展的统一配置
* 排除$zero值以避免TypeScript类型错误
*/
const EXTENSION_CONFIGS = {
// 编辑增强扩展
[ExtensionID.ExtensionRainbowBrackets]: { [ExtensionID.ExtensionRainbowBrackets]: {
factory: rainbowBracketsFactory, definition: defineExtension(() => rainbowBracketsExtension()),
displayNameKey: 'extensions.rainbowBrackets.name', displayNameKey: 'extensions.rainbowBrackets.name',
descriptionKey: 'extensions.rainbowBrackets.description' descriptionKey: 'extensions.rainbowBrackets.description'
}, },
[ExtensionID.ExtensionHyperlink]: { [ExtensionID.ExtensionHyperlink]: {
factory: hyperlinkFactory, definition: defineExtension(() => hyperLink),
displayNameKey: 'extensions.hyperlink.name', displayNameKey: 'extensions.hyperlink.name',
descriptionKey: 'extensions.hyperlink.description' descriptionKey: 'extensions.hyperlink.description'
}, },
[ExtensionID.ExtensionColorSelector]: { [ExtensionID.ExtensionColorSelector]: {
factory: colorSelectorFactory, definition: defineExtension(() => color),
displayNameKey: 'extensions.colorSelector.name', displayNameKey: 'extensions.colorSelector.name',
descriptionKey: 'extensions.colorSelector.description' descriptionKey: 'extensions.colorSelector.description'
}, },
[ExtensionID.ExtensionTranslator]: { [ExtensionID.ExtensionTranslator]: {
factory: translatorFactory, definition: defineExtension((config: any) => createTranslatorExtension({
minSelectionLength: config?.minSelectionLength ?? 2,
maxTranslationLength: config?.maxTranslationLength ?? 5000
}), {
minSelectionLength: 2,
maxTranslationLength: 5000
}),
displayNameKey: 'extensions.translator.name', displayNameKey: 'extensions.translator.name',
descriptionKey: 'extensions.translator.description' descriptionKey: 'extensions.translator.description'
}, },
// UI增强扩展
[ExtensionID.ExtensionMinimap]: { [ExtensionID.ExtensionMinimap]: {
factory: minimapFactory, definition: defineExtension((config: any) => minimap({
displayText: config?.displayText ?? 'characters',
showOverlay: config?.showOverlay ?? 'always',
autohide: config?.autohide ?? false
}), {
displayText: 'characters',
showOverlay: 'always',
autohide: false
}),
displayNameKey: 'extensions.minimap.name', displayNameKey: 'extensions.minimap.name',
descriptionKey: 'extensions.minimap.description' descriptionKey: 'extensions.minimap.description'
}, },
// 工具扩展
[ExtensionID.ExtensionSearch]: { [ExtensionID.ExtensionSearch]: {
factory: searchFactory, definition: defineExtension(() => vscodeSearch),
displayNameKey: 'extensions.search.name', displayNameKey: 'extensions.search.name',
descriptionKey: 'extensions.search.description' descriptionKey: 'extensions.search.description'
}, },
[ExtensionID.ExtensionFold]: { [ExtensionID.ExtensionFold]: {
factory: foldFactory, definition: defineExtension(() => foldingOnIndent),
displayNameKey: 'extensions.fold.name', displayNameKey: 'extensions.fold.name',
descriptionKey: 'extensions.fold.description' descriptionKey: 'extensions.fold.description'
}, },
[ExtensionID.ExtensionTextHighlight]: { [ExtensionID.ExtensionTextHighlight]: {
factory: textHighlightFactory, definition: defineExtension((config: any) => createTextHighlighter({
backgroundColor: config?.backgroundColor ?? '#FFD700',
opacity: config?.opacity ?? 0.3
}), {
backgroundColor: '#FFD700',
opacity: 0.3
}),
displayNameKey: 'extensions.textHighlight.name', displayNameKey: 'extensions.textHighlight.name',
descriptionKey: 'extensions.textHighlight.description' descriptionKey: 'extensions.textHighlight.description'
}, },
[ExtensionID.ExtensionCheckbox]: { [ExtensionID.ExtensionCheckbox]: {
factory: checkboxFactory, definition: defineExtension(() => createCheckboxExtension()),
displayNameKey: 'extensions.checkbox.name', displayNameKey: 'extensions.checkbox.name',
descriptionKey: 'extensions.checkbox.description' descriptionKey: 'extensions.checkbox.description'
} }
} as const;
const isRegisteredExtension = (id: ExtensionID): id is RegisteredExtensionID =>
Object.prototype.hasOwnProperty.call(EXTENSION_REGISTRY, id);
const getRegistryEntry = (id: ExtensionID): ExtensionEntry | undefined => {
if (!isRegisteredExtension(id)) {
return undefined;
}
return EXTENSION_REGISTRY[id];
}; };
/** export function registerAllExtensions(manager: Manager): void {
* 注册所有扩展工厂到管理器 (Object.entries(EXTENSION_REGISTRY) as [RegisteredExtensionID, ExtensionEntry][]).forEach(([id, entry]) => {
* @param manager 扩展管理器实例 manager.registerExtension(id, entry.definition);
*/
export function registerAllExtensions(manager: ExtensionManager): void {
Object.entries(EXTENSION_CONFIGS).forEach(([id, config]) => {
manager.registerExtension(id as ExtensionID, config.factory);
}); });
} }
/**
* 获取扩展工厂的显示名称
* @param id 扩展ID
* @returns 显示名称
*/
export function getExtensionDisplayName(id: ExtensionID): string { export function getExtensionDisplayName(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.displayNameKey ? i18n.global.t(config.displayNameKey) : id; return entry?.displayNameKey ? i18n.global.t(entry.displayNameKey) : id;
} }
/**
* 获取扩展工厂的描述
* @param id 扩展ID
* @returns 描述
*/
export function getExtensionDescription(id: ExtensionID): string { export function getExtensionDescription(id: ExtensionID): string {
const config = EXTENSION_CONFIGS[id as ExtensionID]; const entry = getRegistryEntry(id);
return config?.descriptionKey ? i18n.global.t(config.descriptionKey) : ''; return entry?.descriptionKey ? i18n.global.t(entry.descriptionKey) : '';
} }
/** function getExtensionDefinition(id: ExtensionID): ExtensionDefinition | undefined {
* 获取扩展工厂实例 return getRegistryEntry(id)?.definition;
* @param id 扩展ID
* @returns 扩展工厂实例
*/
export function getExtensionFactory(id: ExtensionID): ExtensionFactory | undefined {
return EXTENSION_CONFIGS[id as ExtensionID]?.factory;
} }
/**
* 获取扩展的默认配置
* @param id 扩展ID
* @returns 默认配置对象
*/
export function getExtensionDefaultConfig(id: ExtensionID): any { export function getExtensionDefaultConfig(id: ExtensionID): any {
const factory = getExtensionFactory(id); const definition = getExtensionDefinition(id);
return factory?.getDefaultConfig() || {}; if (!definition) return {};
return cloneConfig(definition.defaultConfig);
} }
/**
* 检查扩展是否有配置项
* @param id 扩展ID
* @returns 是否有配置项
*/
export function hasExtensionConfig(id: ExtensionID): boolean { export function hasExtensionConfig(id: ExtensionID): boolean {
const defaultConfig = getExtensionDefaultConfig(id); return Object.keys(getExtensionDefaultConfig(id)).length > 0;
return Object.keys(defaultConfig).length > 0;
} }
/**
* 获取所有可用扩展的ID列表
* @returns 扩展ID数组
*/
export function getAllExtensionIds(): ExtensionID[] { export function getAllExtensionIds(): ExtensionID[] {
return Object.keys(EXTENSION_CONFIGS) as ExtensionID[]; return Object.keys(EXTENSION_REGISTRY) as RegisteredExtensionID[];
} }
const cloneConfig = (config: any) => {
if (Array.isArray(config)) {
return config.map(cloneConfig);
}
if (config && typeof config === 'object') {
return Object.keys(config).reduce((acc, key) => {
acc[key] = cloneConfig(config[key]);
return acc;
}, {} as Record<string, any>);
}
return config;
};

View File

@@ -1,13 +1,13 @@
import {Extension} from '@codemirror/state'; import {Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {useExtensionStore} from '@/stores/extensionStore'; import {useExtensionStore} from '@/stores/extensionStore';
import {ExtensionManager} from './extensionManager'; import {Manager} from './manager';
import {registerAllExtensions} from './extensions'; import {registerAllExtensions} from './extensions';
/** /**
* 全局扩展管理器实例 * 全局扩展管理器实例
*/ */
const extensionManager = new ExtensionManager(); const extensionManager = new Manager();
/** /**
* 异步创建动态扩展 * 异步创建动态扩展
@@ -26,7 +26,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
} }
// 初始化扩展管理器配置 // 初始化扩展管理器配置
extensionManager.initializeExtensionsFromConfig(extensionStore.extensions); extensionManager.initExtensions(extensionStore.extensions);
// 获取初始扩展配置 // 获取初始扩展配置
return extensionManager.getInitialExtensions(); return extensionManager.getInitialExtensions();
@@ -36,7 +36,7 @@ export const createDynamicExtensions = async (_documentId?: number): Promise<Ext
* 获取扩展管理器实例 * 获取扩展管理器实例
* @returns 扩展管理器 * @returns 扩展管理器
*/ */
export const getExtensionManager = (): ExtensionManager => { export const getExtensionManager = (): Manager => {
return extensionManager; return extensionManager;
}; };
@@ -58,5 +58,5 @@ export const removeExtensionManagerView = (documentId: number): void => {
}; };
// 导出相关模块 // 导出相关模块
export {ExtensionManager} from './extensionManager'; export {Manager} from './manager';
export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions'; export {registerAllExtensions, getExtensionDisplayName, getExtensionDescription} from './extensions';

View File

@@ -0,0 +1,135 @@
import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {Extension as ExtensionConfig, ExtensionID} from '@/../bindings/voidraft/internal/models/models';
import {ExtensionDefinition, ExtensionState} from './types';
/**
* 扩展管理器
* 负责注册、初始化与同步所有动态扩展
*/
export class Manager {
private extensionStates = new Map<ExtensionID, ExtensionState>();
private views = new Map<number, EditorView>();
registerExtension(id: ExtensionID, definition: ExtensionDefinition): void {
const existingState = this.extensionStates.get(id);
if (existingState) {
existingState.definition = definition;
if (existingState.config === undefined) {
existingState.config = this.cloneConfig(definition.defaultConfig ?? {});
}
} else {
const compartment = new Compartment();
const defaultConfig = this.cloneConfig(definition.defaultConfig ?? {});
this.extensionStates.set(id, {
id,
definition,
config: defaultConfig,
enabled: false,
compartment,
extension: []
});
}
}
initExtensions(extensionConfigs: ExtensionConfig[]): void {
for (const config of extensionConfigs) {
const state = this.extensionStates.get(config.id);
if (!state) continue;
const resolvedConfig = this.cloneConfig(config.config ?? state.definition.defaultConfig ?? {});
this.commitExtensionState(state, config.enabled, resolvedConfig);
}
}
getInitialExtensions(): Extension[] {
const extensions: Extension[] = [];
for (const state of this.extensionStates.values()) {
extensions.push(state.compartment.of(state.extension));
}
return extensions;
}
setView(view: EditorView, documentId: number): void {
this.views.set(documentId, view);
this.applyAllExtensionsToView(view);
}
updateExtension(id: ExtensionID, enabled: boolean, config?: any): void {
const state = this.extensionStates.get(id);
if (!state) return;
const resolvedConfig = this.resolveConfig(state, config);
this.commitExtensionState(state, enabled, resolvedConfig);
}
removeView(documentId: number): void {
this.views.delete(documentId);
}
destroy(): void {
this.views.clear();
this.extensionStates.clear();
}
private resolveConfig(state: ExtensionState, config?: any): any {
if (config !== undefined) {
return this.cloneConfig(config);
}
if (state.config !== undefined) {
return this.cloneConfig(state.config);
}
return this.cloneConfig(state.definition.defaultConfig ?? {});
}
private commitExtensionState(state: ExtensionState, enabled: boolean, config: any): void {
try {
const runtimeExtension = enabled ? state.definition.create(config) : [];
state.enabled = enabled;
state.config = config;
state.extension = runtimeExtension;
this.applyExtensionToAllViews(state.id);
} catch (error) {
console.error(`Failed to update extension ${state.id}:`, error);
}
}
private applyExtensionToAllViews(id: ExtensionID): void {
const state = this.extensionStates.get(id);
if (!state) return;
for (const [documentId, view] of this.views.entries()) {
try {
view.dispatch({effects: state.compartment.reconfigure(state.extension)});
} catch (error) {
console.error(`Failed to apply extension ${id} to document ${documentId}:`, error);
}
}
}
private applyAllExtensionsToView(view: EditorView): void {
const effects: any[] = [];
for (const state of this.extensionStates.values()) {
effects.push(state.compartment.reconfigure(state.extension));
}
if (effects.length === 0) return;
try {
view.dispatch({effects});
} catch (error) {
console.error('Failed to register extensions on view:', error);
}
}
private cloneConfig<T>(config: T): T {
if (Array.isArray(config)) {
return config.map(item => this.cloneConfig(item)) as unknown as T;
}
if (config && typeof config === 'object') {
return Object.keys(config as Record<string, any>).reduce((acc, key) => {
(acc as any)[key] = this.cloneConfig((config as Record<string, any>)[key]);
return acc;
}, {} as Record<string, any>) as T;
}
return config;
}
}

View File

@@ -1,49 +1 @@
import {Compartment, Extension} from '@codemirror/state'; import {Compartment, Extension} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {ExtensionID} from '@/../bindings/voidraft/internal/models/models';
/**
* 扩展工厂接口
* 每个扩展需要实现此接口来创建和配置扩展
*/
export interface ExtensionFactory {
/**
* 创建扩展实例
* @param config 扩展配置
* @returns CodeMirror扩展
*/
create(config: any): Extension
/**
* 获取默认配置
* @returns 默认配置对象
*/
getDefaultConfig(): any
/**
* 验证配置
* @param config 配置对象
* @returns 是否有效
*/
validateConfig?(config: any): boolean
}
/**
* 扩展状态
*/
export interface ExtensionState {
id: ExtensionID
factory: ExtensionFactory
config: any
enabled: boolean
compartment: Compartment
extension: Extension
}
/**
* 视图信息
*/
export interface EditorViewInfo {
view: EditorView
documentId: number
registered: boolean
}

View File

@@ -66,10 +66,12 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
if (!extension) return; if (!extension) return;
// 更新配置 // 更新配置
const updatedConfig = {...extension.config, [configKey]: value}; const updatedConfig = {...extension.config};
if (value === undefined) {
console.log(`[ExtensionsPage] 更新扩展 ${extensionId} 配置, ${configKey}=${value}`); delete updatedConfig[configKey];
} else {
updatedConfig[configKey] = value;
}
// 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例 // 使用editorStore的updateExtension方法更新确保应用到所有编辑器实例
await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig); await editorStore.updateExtension(extensionId, extension.enabled, updatedConfig);
@@ -81,7 +83,7 @@ const updateExtensionConfig = async (extensionId: ExtensionID, configKey: string
// 重置扩展到默认配置 // 重置扩展到默认配置
const resetExtension = async (extensionId: ExtensionID) => { const resetExtension = async (extensionId: ExtensionID) => {
try { try {
// 重置到默认配置(后端) // 重置到默认配置
await ExtensionService.ResetExtensionToDefault(extensionId); await ExtensionService.ResetExtensionToDefault(extensionId);
// 重新加载扩展状态以获取最新配置 // 重新加载扩展状态以获取最新配置
@@ -92,63 +94,65 @@ const resetExtension = async (extensionId: ExtensionID) => {
if (extension) { if (extension) {
// 通过editorStore更新确保所有视图都能同步 // 通过editorStore更新确保所有视图都能同步
await editorStore.updateExtension(extensionId, extension.enabled, extension.config); await editorStore.updateExtension(extensionId, extension.enabled, extension.config);
console.log(`[ExtensionsPage] 重置扩展 ${extensionId} 配置,同步应用到所有编辑器实例`);
} }
} catch (error) { } catch (error) {
console.error('Failed to reset extension:', error); console.error('Failed to reset extension:', error);
} }
}; };
// 配置项类型定义 const getConfigValue = (
type ConfigItemType = 'toggle' | 'number' | 'text' | 'select' config: Record<string, any> | undefined,
configKey: string,
interface SelectOption { defaultValue: any
value: any ) => {
label: string if (config && Object.prototype.hasOwnProperty.call(config, configKey)) {
} return config[configKey];
interface ConfigItemMeta {
type: ConfigItemType
options?: SelectOption[]
}
// 只保留 select 类型的配置项元数据
const extensionConfigMeta: Partial<Record<ExtensionID, Record<string, ConfigItemMeta>>> = {
[ExtensionID.ExtensionMinimap]: {
displayText: {
type: 'select',
options: [
{value: 'characters', label: 'Characters'},
{value: 'blocks', label: 'Blocks'}
]
},
showOverlay: {
type: 'select',
options: [
{value: 'always', label: 'Always'},
{value: 'mouse-over', label: 'Mouse Over'}
]
}
} }
return defaultValue;
}; };
// 获取配置项类型
const getConfigItemType = (extensionId: ExtensionID, configKey: string, defaultValue: any): string => { const formatConfigValue = (value: any): string => {
const meta = extensionConfigMeta[extensionId]?.[configKey]; if (value === undefined) return '';
if (meta?.type) { try {
return meta.type; const serialized = JSON.stringify(value);
return serialized ?? '';
} catch (error) {
console.warn('Failed to stringify config value', error);
return '';
} }
// 根据默认值类型自动推断
if (typeof defaultValue === 'boolean') return 'toggle';
if (typeof defaultValue === 'number') return 'number';
return 'text';
}; };
// 获取选择框的选项列表
const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOption[] => { const handleConfigInput = async (
return extensionConfigMeta[extensionId]?.[configKey]?.options || []; extensionId: ExtensionID,
configKey: string,
defaultValue: any,
event: Event
) => {
const target = event.target as HTMLInputElement | null;
if (!target) return;
const rawValue = target.value;
const trimmedValue = rawValue.trim();
if (!trimmedValue.length) {
await updateExtensionConfig(extensionId, configKey, undefined);
return;
}
try {
const parsedValue = JSON.parse(trimmedValue);
await updateExtensionConfig(extensionId, configKey, parsedValue);
} catch (_error) {
const extension = extensionStore.extensions.find(ext => ext.id === extensionId);
const fallbackValue = getConfigValue(extension?.config, configKey, defaultValue);
target.value = formatConfigValue(fallbackValue);
}
}; };
</script> </script>
<template> <template>
@@ -204,58 +208,28 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
</button> </button>
</div> </div>
<div <div class="config-table-wrapper">
v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)" <table class="config-table">
:key="configKey" <tbody>
class="config-item" <tr
> v-for="[configKey, configValue] in Object.entries(extension.defaultConfig)"
<SettingItem :key="configKey"
:title="configKey"
>
<!-- 布尔值切换开关 -->
<ToggleSwitch
v-if="getConfigItemType(extension.id, configKey, configValue) === 'toggle'"
:model-value="extension.config[configKey] ?? configValue"
@update:model-value="updateExtensionConfig(extension.id, configKey, $event)"
/>
<!-- 数字输入框 -->
<input
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'number'"
type="number"
class="config-input"
:value="extension.config[configKey] ?? configValue"
:min="configKey === 'opacity' ? 0 : undefined"
:max="configKey === 'opacity' ? 1 : undefined"
:step="configKey === 'opacity' ? 0.1 : 1"
@input="updateExtensionConfig(extension.id, configKey, parseFloat(($event.target as HTMLInputElement).value))"
/>
<!-- 选择框 -->
<select
v-else-if="getConfigItemType(extension.id, configKey, configValue) === 'select'"
class="config-select"
:value="extension.config[configKey] ?? configValue"
@change="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLSelectElement).value)"
> >
<option <th scope="row" class="config-table-key">
v-for="option in getSelectOptions(extension.id, configKey)" {{ configKey }}
:key="option.value" </th>
:value="option.value" <td class="config-table-value">
> <input
{{ option.label }} class="config-value-input"
</option> type="text"
</select> :value="formatConfigValue(getConfigValue(extension.config, configKey, configValue))"
@change="handleConfigInput(extension.id, configKey, configValue, $event)"
<!-- 文本输入框 --> @keyup.enter.prevent="handleConfigInput(extension.id, configKey, configValue, $event)"
<input />
v-else </td>
type="text" </tr>
class="config-input" </tbody>
:value="extension.config[configKey] ?? configValue" </table>
@input="updateExtensionConfig(extension.id, configKey, ($event.target as HTMLInputElement).value)"
/>
</SettingItem>
</div> </div>
</div> </div>
</div> </div>
@@ -361,37 +335,65 @@ const getSelectOptions = (extensionId: ExtensionID, configKey: string): SelectOp
} }
} }
.config-item { .config-table-wrapper {
&:not(:last-child) {
margin-bottom: 12px;
}
/* 配置项标题和描述字体大小 */
:deep(.setting-item-title) {
font-size: 12px;
}
:deep(.setting-item-description) {
font-size: 11px;
}
}
.config-input, .config-select {
min-width: 120px;
padding: 4px 8px;
border: 1px solid var(--settings-input-border); border: 1px solid var(--settings-input-border);
border-radius: 3px; border-radius: 6px;
background-color: var(--settings-input-bg); margin-top: 8px;
color: var(--settings-text); overflow: hidden;
font-size: 11px; background-color: var(--settings-panel, var(--settings-input-bg));
&:focus {
outline: none;
border-color: var(--settings-accent);
}
} }
.config-select { .config-table {
cursor: pointer; width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.config-table tr + tr {
border-top: 1px solid var(--settings-input-border);
}
.config-table th,
.config-table td {
padding: 10px 12px;
vertical-align: middle;
}
.config-table-key {
width: 36%;
text-align: left;
font-weight: 600;
color: var(--settings-text-secondary);
border-right: 1px solid var(--settings-input-border);
background-color: var(--settings-input-bg);
}
.config-table-value {
padding: 6px;
}
.config-value-input {
width: 100%;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--settings-text);
font-size: 12px;
line-height: 1.4;
box-sizing: border-box;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.config-value-input:hover {
border-color: var(--settings-hover-border, var(--settings-input-border));
background-color: var(--settings-hover);
}
.config-value-input:focus {
outline: none;
border-color: var(--settings-accent);
background-color: var(--settings-input-bg);
} }
</style> </style>

2
go.mod
View File

@@ -77,7 +77,7 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/go-gitlab v0.115.0 // indirect github.com/xanzy/go-gitlab v0.115.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.44.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/image v0.33.0 // indirect golang.org/x/image v0.33.0 // indirect
golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect

4
go.sum
View File

@@ -176,8 +176,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=