Better Link Clicker
使用Obsidian的Typescript API 进行开发, 使用Pnpm构建
环境搭建与项目初始化
- 确保已安装
Node.js和npm。 - 使用 Obsidian 官方提供的插件模板仓库创建一个新的插件项目。
- 在本地克隆你的插件仓库。
- 进入项目目录,运行
npm install安装所需的依赖(如 Obsidian API 的 TypeScript 类型定义)。 - 运行
npm run dev编译插件,并准备在 Obsidian 中进行测试。 - 在你的 Obsidian Vault 的
.obsidian/plugins目录下创建插件文件夹,并将编译好的main.js,manifest.json,styles.css文件放入其中。 - 在 Obsidian 的设置中启用该插件以进行调试。
核心功能:拦截并处理链接点击事件
- 在
main.ts的onload方法中,使用this.registerDomEvent来监听整个工作区 (document) 的点击事件。 - 在事件回调函数中,检查被点击的元素或其父元素是否为内部链接。
- 调研并确定需要处理的所有内部链接相关的 CSS class (例如,
cm-hmd-internal-link,is-unresolved)。
- 调研并确定需要处理的所有内部链接相关的 CSS class (例如,
- 在回调函数中,判断
event.ctrlKey(对于 Windows/Linux) 或event.metaKey(对于 macOS) 是否被按下。 - 实现 Ctrl/Cmd + 点击逻辑:
- 如果 Ctrl/Cmd 被按下,则允许默认的跳转行为。
- 实现直接点击逻辑:
- 如果 Ctrl/Cmd 没有被按下,调用
event.preventDefault()来阻止 Obsidian 默认的跳转行为。 - 实现“编辑链接”的功能。这可能需要将光标定位到链接文本处,这部分可能需要进一步研究 API 来实现精确的光标控制。
- 如果 Ctrl/Cmd 没有被按下,调用
处理不存在的笔记
- 当用户通过 Ctrl/Cmd + 点击一个链接时,首先获取链接指向的笔记路径。
- 直接判断是否有
span.is-unresolved的HTML元素来判断文件是否存在。 - 如果文件不存在:
- 检查设置中是否允许弹出创建对话框。
- 如果允许,则创建一个自定义的
Modal弹窗,询问用户是否要创建该文件。 - 在 Modal 中提供“创建”和“取消”按钮。
- 如果用户点击“创建”,使用
this.app.vault.create(filePath, '')创建一个新的空文件。 - 文件创建成功后,使用
this.app.workspace.getLead("tab").openFile(newFile)跳转到新创建的笔记。
- 如果文件存在:
- 不阻止, 由obsidian自行处理。
开发设置界面
- 创建一个新的
SettingTab子类。 - 在
main.ts中定义一个接口 (interface) 来描述插件的设置数据结构,例如interface MyPluginSettings { confirmCreateFile: boolean; }。 - 实现
loadSettings和saveSettings方法来加载和保存插件的配置。 - 在
SettingTab的display方法中,使用new Setting(containerEl)来添加设置项。 - 添加一个开关 (
addToggle) 组件,用于控制是否在链接到不存在的笔记时弹出创建文件的确认对话框。 - 将开关组件的值与设置对象中的
confirmCreateFile属性绑定。当开关状态改变时,更新设置对象并调用saveSettings。 - 在
onload方法中,通过this.addSettingTab(new MySettingTab(this.app, this))来注册设置页面。
完善与发布
- 在不同的视图模式(实时预览、源码模式、阅读模式)下全面测试插件功能。
- 确保在 Windows 和 macOS 系统上,Ctrl 和 Cmd 键的逻辑都能正常工作。
- 编写插件的
README.md文件,清晰地说明插件的功能和使用方法。 - 根据 Obsidian 社区的要求,更新
manifest.json中的版本号和信息,准备发布。 - 通过git 上传到远程仓库
- 通过Github PR创建要求更新official formu
Refactor
- 使用LinkCache进行重建
- 使用CodeMirror 6获取当前pos
- 遍历LinkCache, 根据pos判断点击的是否是Link, 是哪一个Link
- 获取Link的原始信息
- 根据原始信息解析需要创建的文件位置
Rebuild
这个插件的核心在于拦截点击事件并根据配置决定跳转、创建或移动光标。大致逻辑可分为几个步骤:
-
监听点击
- 注册一个
clickDOM 事件,捕获所有页面上的鼠标点击,并在处理函数handleLinkClick中执行后续逻辑。
- 注册一个
-
定位链接
- 获取当前激活的 Markdown 视图和编辑器。
- 利用编辑器的
posAtCoords方法把点击坐标转换成文档偏移,再转换为行/列位置。 - 从元数据缓存中检索所有链接和嵌入(
fileCache.links、fileCache.embeds),循环检查被点击位置是否落在某个链接的范围内。
-
处理点击
- 分析按键修饰符(
Keymap.isModEvent)决定打开目标的位置(新标签、窗口等)以及是否 “跳转”。 - 如果当前是实时预览模式并且光标已经在链接内,只更新光标位置而不跳转。
- 根据配置
jumpOnlyWithModifier与平台按键决定是否允许跳转,否则仅将光标移到链接起点。
- 分析按键修饰符(
-
解析目标
- 提取链接的基本目标(去掉别名和锚点)。
- 尝试通过
metadataCache.getFirstLinkpathDest查找已存在的文件。- 若存在则直接调用
workspace.openLinkText以合适的目标打开。
- 若存在则直接调用
- 若目标文件不存在,则调用
buildNewFilePath按 vault 的newFileLocation配置生成新路径。
-
创建 / 打开新文件
- 根据设置决定是否弹出确认模态框。
- 使用
vault.create创建空文件,并在指定 leaf(标签/窗口)中打开。
-
辅助函数
- 若跳转被禁止或光标需调整,
moveCursorToLinkStart将编辑器光标置于链接起始位置。 isCursorInsideLink用于检测当前光标是否在链接范围内(查询后避免不必要跳转)。
- 若跳转被禁止或光标需调整,
方法上主要用到的是 Obsidian 的 API(app.workspace、metadataCache、vault 等)、TypeScript 类型保护、字符串处理以及事件/键盘修饰符的判断,形成了一套点击链接时智能跳转或创建文件的功能流程。