mirror of
https://github.com/xcloud-ai/quick-codeblock.git
synced 2026-07-22 08:32:36 +00:00
v1.0.0 初始发布:代码块快捷插入插件
This commit is contained in:
commit
73ec526625
7 changed files with 1351 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Obsidian runtime data (用户本地设置,不入库)
|
||||
data.json
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Build output (if using TypeScript compilation)
|
||||
# 注意:本插件的 main.js 是手写纯 JS,需要入库
|
||||
# 如果改用 TS 编译流程,取消下面注释让 main.js 成为编译产物
|
||||
# main.js
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 旭说云原生
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
91
README.md
Normal file
91
README.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Quick CodeBlock / 代码块快捷插入
|
||||
|
||||
快捷插入 Markdown 代码块,光标自动定位到块内。支持自定义语言、选中文本一键包裹、快捷键配置与冲突检测。
|
||||
|
||||
替代 Templater 的代码块模板,无需 Templater 依赖。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **快捷插入代码块**:插入 ` ```lang ` 代码块,光标自动定位到块内空行,直接开始写代码
|
||||
- **选中文本包裹**:选中一段文字,触发命令,自动用代码块包裹
|
||||
- **自定义语言列表**:默认含 python / javascript / bash / shell / sql / json / yaml / go / rust / java / html / css,可自行增删
|
||||
- **语言选择弹窗**:不想记快捷键?用「选择语言」命令弹出列表点选
|
||||
- **快捷键自定义**:在插件设置面板里直接为需要的语言配置快捷键,无需去系统设置里翻
|
||||
- **快捷键冲突检测**:设置快捷键时即时检测是否与其他命令冲突,冲突显示警告并提供跳转入口
|
||||
- **智能换行**:光标前后有内容时自动处理换行,不会粘连到已有文字
|
||||
|
||||
## 安装
|
||||
|
||||
### 方式一:社区插件(审核通过后)
|
||||
|
||||
Obsidian → 设置 → 第三方插件 → 社区插件 → 搜索「Quick CodeBlock」→ 安装
|
||||
|
||||
### 方式二:手动安装
|
||||
|
||||
1. 下载最新 Release 的 `main.js`、`manifest.json`、`styles.css`
|
||||
2. 在 Obsidian 仓库下创建 `.obsidian/plugins/quick-codeblock/` 目录
|
||||
3. 将 3 个文件放入该目录
|
||||
4. Obsidian → 设置 → 第三方插件 → 关闭安全模式 → 找到「代码块快捷插入」→ 开启
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 插入代码块
|
||||
|
||||
1. `Ctrl+P` 打开命令面板,搜索「插入代码块」
|
||||
2. 选择对应语言的命令(如「插入代码块(python)」)
|
||||
3. 无选中文本时:插入空代码块,光标自动定位到块内
|
||||
4. 有选中文本时:用 ` ```lang ` 包裹选中的内容
|
||||
|
||||
### 配置快捷键
|
||||
|
||||
1. 打开插件设置(设置 → 第三方插件 → 代码块快捷插入 → 选项)
|
||||
2. 在「快捷键设置」区域,默认有一项(python)
|
||||
3. 点击「+ 添加快捷键配置」增加新项
|
||||
4. 每项可选择语言(dropdown)+ 设置快捷键 + 删除
|
||||
5. 点击快捷键输入框,按下组合键即可设置;按 `Backspace` 或 `Esc` 清除
|
||||
6. 设置后自动检测冲突,有冲突会显示红色警告和「前往修改」链接
|
||||
|
||||
### 替代 Templater
|
||||
|
||||
如果你之前用 Templater 的代码块模板(如 `codepython.md`),本插件完全覆盖该功能:
|
||||
|
||||
| | Templater | 本插件 |
|
||||
|---|---|---|
|
||||
| 依赖 | 需要 Templater 插件 | 无依赖 |
|
||||
| 插入 ```python + 光标定位 | `tp.file.cursor()` | 自动计算行号定位 |
|
||||
| 多语言 | 要写多个模板 | 一个插件搞定 |
|
||||
| 选中文本包裹 | 不支持 | 支持 |
|
||||
| 快捷键管理 | 不支持 | 内置配置 + 冲突检测 |
|
||||
|
||||
## 设置说明
|
||||
|
||||
| 设置项 | 说明 |
|
||||
|--------|------|
|
||||
| 默认语言 | 主命令「插入代码块」使用的语言 |
|
||||
| 语言列表 | 每行一个,为每个语言注册独立命令 |
|
||||
| 保留选中内容缩进 | 选中文本包裹时是否保留原有缩进 |
|
||||
| 快捷键设置 | 为需要的语言配置快捷键(默认一项,可自行添加) |
|
||||
|
||||
## 命令列表
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| 插入代码块(python) | 主命令,用默认语言 |
|
||||
| 插入代码块(选择语言) | 弹出列表选择语言 |
|
||||
| 插入代码块(javascript) | 每个预设语言各一个命令 |
|
||||
| ... | 语言列表里的每个语言都会注册 |
|
||||
|
||||
## 技术说明
|
||||
|
||||
- 纯 JavaScript 实现(`main.js`),无需编译,直接可用
|
||||
- `main.ts` 为 TypeScript 源码参考,供二次开发使用
|
||||
- 快捷键通过 `hotkeyManager.setHotkeys` 绑定,与 Obsidian 系统快捷键互通
|
||||
- 卸载插件时自动清除快捷键绑定,保持 `hotkeys.json` 干净
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT License](./LICENSE)
|
||||
|
||||
## 作者
|
||||
|
||||
**旭说云原生**
|
||||
538
main.js
Normal file
538
main.js
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
/*
|
||||
* 代码块快捷插入 - main.js
|
||||
* 纯 JavaScript 实现,无需编译,直接放入插件目录即可运行
|
||||
*
|
||||
* 功能:
|
||||
* 1. 快捷插入代码块 ```lang ... ```,光标自动定位到块内
|
||||
* 2. 选中文本时,用代码块包裹选中的内容
|
||||
* 3. 自定义默认语言 + 常用语言列表
|
||||
* 4. 每个语言注册独立命令
|
||||
* 5. 「选择语言」命令弹出列表供选择
|
||||
* 6. 快捷键配置:默认一项(默认语言),用户可自行添加更多
|
||||
* 7. 设置快捷键后即时检测冲突,冲突时显示警告 + 跳转入口
|
||||
*
|
||||
* 替代 Templater 的 codepython.md 模板,无需 Templater 依赖。
|
||||
*/
|
||||
const { Plugin, Notice, PluginSettingTab, Setting, Modal } = require("obsidian");
|
||||
|
||||
const PLUGIN_ID = "quick-codeblock";
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
defaultLanguage: "python",
|
||||
languages: "python\njavascript\nbash\nshell\nsql\njson\nyaml\ngo\nrust\njava\nhtml\ncss",
|
||||
keepIndent: true,
|
||||
// 快捷键配置列表:用户主动添加,默认只有一项(默认语言)
|
||||
// 每项: { lang: "python", hotkey: { modifiers, key } | null }
|
||||
hotkeyConfigs: [{ lang: "python", hotkey: null }],
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// 工具函数(快捷键格式化 / 冲突检测 / 跳转)
|
||||
// ================================================================
|
||||
|
||||
function formatHotkey(hotkey) {
|
||||
if (!hotkey) return "";
|
||||
const modMap = { Mod: "Ctrl", Ctrl: "Ctrl", Alt: "Alt", Shift: "Shift", Meta: "Win" };
|
||||
const mods = (hotkey.modifiers || []).slice().sort().map((m) => modMap[m] || m);
|
||||
return mods.length > 0 ? `${mods.join(" + ")} + ${hotkey.key}` : hotkey.key;
|
||||
}
|
||||
|
||||
function getEffectiveHotkeys(hotkeyManager, commandId) {
|
||||
try {
|
||||
if (typeof hotkeyManager.getEffectiveHotkeys === "function") {
|
||||
return hotkeyManager.getEffectiveHotkeys(commandId) || [];
|
||||
}
|
||||
const custom = hotkeyManager.getHotkeys ? hotkeyManager.getHotkeys(commandId) || [] : [];
|
||||
const baked = hotkeyManager.getBakedHotkeys ? hotkeyManager.getBakedHotkeys(commandId) || [] : [];
|
||||
return custom.length > 0 ? custom : baked;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function findConflict(app, ownCommandId, hotkey) {
|
||||
if (!hotkey) return null;
|
||||
const targetCombo = formatHotkey(hotkey);
|
||||
const hotkeyManager = app.hotkeyManager;
|
||||
const commands = app.commands.commands;
|
||||
for (const id of Object.keys(commands)) {
|
||||
if (id === ownCommandId) continue;
|
||||
const effective = getEffectiveHotkeys(hotkeyManager, id);
|
||||
for (const h of effective) {
|
||||
if (formatHotkey(h) === targetCombo) {
|
||||
return { commandId: id, commandName: commands[id].name || id };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openHotkeysSettings(app) {
|
||||
let opened = false;
|
||||
try {
|
||||
if (app.setting && typeof app.setting.openTabById === "function") {
|
||||
app.setting.openTabById("hotkeys");
|
||||
opened = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
if (!opened) {
|
||||
try {
|
||||
if (typeof app.openSettings === "function") {
|
||||
app.openSettings();
|
||||
opened = true;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!opened) {
|
||||
try {
|
||||
app.commands.executeCommandById("app:open-settings");
|
||||
opened = true;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (opened) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (app.setting && typeof app.setting.openTabById === "function") {
|
||||
app.setting.openTabById("hotkeys");
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 200);
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 插件主类
|
||||
// ================================================================
|
||||
class QuickCodeBlockPlugin extends Plugin {
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.registerCommands();
|
||||
this.addSettingTab(new CodeBlockSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// 卸载时清除所有快捷键绑定
|
||||
for (const config of this.settings.hotkeyConfigs) {
|
||||
const fullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
this.app.hotkeyManager.removeHotkeys(fullId);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
registerCommands() {
|
||||
const def = this.settings.defaultLanguage;
|
||||
|
||||
// 1. 主命令
|
||||
this.addCommand({
|
||||
id: "insert-codeblock-default",
|
||||
name: `插入代码块(${def})`,
|
||||
editorCallback: (editor) => this.insertCodeBlock(editor, def),
|
||||
});
|
||||
|
||||
// 2. 选择语言命令
|
||||
this.addCommand({
|
||||
id: "insert-codeblock-pick",
|
||||
name: "插入代码块(选择语言)",
|
||||
editorCallback: (editor) => {
|
||||
const langs = this.getLanguages();
|
||||
new LanguagePickerModal(this.app, langs, (lang) => {
|
||||
this.insertCodeBlock(editor, lang);
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
// 3. 为语言列表的每个语言注册命令(命令始终注册,快捷键按需绑定)
|
||||
for (const lang of this.getLanguages()) {
|
||||
this.addCommand({
|
||||
id: `insert-codeblock-${lang}`,
|
||||
name: `插入代码块(${lang})`,
|
||||
editorCallback: (editor) => this.insertCodeBlock(editor, lang),
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 为 hotkeyConfigs 里的配置绑定快捷键
|
||||
for (const config of this.settings.hotkeyConfigs) {
|
||||
if (config.hotkey) {
|
||||
const fullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
this.app.hotkeyManager.setHotkeys(fullId, [config.hotkey]);
|
||||
} catch (e) {
|
||||
console.error(`[quick-codeblock] 绑定 ${config.lang} 快捷键失败:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 核心:插入代码块
|
||||
// ================================================================
|
||||
insertCodeBlock(editor, lang) {
|
||||
const selection = editor.getSelection();
|
||||
|
||||
if (selection) {
|
||||
const content = this.settings.keepIndent ? selection : selection.trim();
|
||||
const block = "```" + lang + "\n" + content + "\n```";
|
||||
editor.replaceSelection(block);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
const beforeCursor = line.substring(0, cursor.ch);
|
||||
const afterCursor = line.substring(cursor.ch);
|
||||
|
||||
let block = "```" + lang + "\n\n```";
|
||||
if (beforeCursor.trim() !== "") block = "\n" + block;
|
||||
if (afterCursor.trim() !== "") block = block + "\n";
|
||||
|
||||
editor.replaceSelection(block);
|
||||
|
||||
const newCursor = editor.getCursor();
|
||||
const targetLine =
|
||||
afterCursor.trim() !== "" ? newCursor.line - 2 : newCursor.line - 1;
|
||||
editor.setCursor({ line: targetLine, ch: 0 });
|
||||
}
|
||||
|
||||
getLanguages() {
|
||||
return this.settings.languages
|
||||
.split(/[\n,,]/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
// 兼容旧格式 hotkeys 对象 → 迁移到 hotkeyConfigs 数组
|
||||
if (!this.settings.hotkeyConfigs) {
|
||||
if (this.settings.hotkeys && typeof this.settings.hotkeys === "object") {
|
||||
this.settings.hotkeyConfigs = Object.entries(this.settings.hotkeys)
|
||||
.filter(([_, h]) => h)
|
||||
.map(([lang, hotkey]) => ({ lang, hotkey }));
|
||||
} else {
|
||||
this.settings.hotkeyConfigs = [{ lang: this.settings.defaultLanguage, hotkey: null }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 语言选择 Modal
|
||||
// ================================================================
|
||||
class LanguagePickerModal extends Modal {
|
||||
constructor(app, languages, onChoose) {
|
||||
super(app);
|
||||
this.languages = languages;
|
||||
this.onChoose = onChoose;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("qcb-picker");
|
||||
contentEl.createEl("h2", { text: "选择代码块语言" });
|
||||
|
||||
const grid = contentEl.createEl("div", { cls: "qcb-lang-grid" });
|
||||
for (const lang of this.languages) {
|
||||
const item = grid.createEl("div", { cls: "qcb-lang-item", text: lang });
|
||||
item.addEventListener("click", () => {
|
||||
this.onChoose(lang);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 设置面板
|
||||
// ================================================================
|
||||
class CodeBlockSettingTab extends PluginSettingTab {
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "代码块快捷插入" });
|
||||
|
||||
// ---- 基础设置 ----
|
||||
new Setting(containerEl)
|
||||
.setName("默认语言")
|
||||
.setDesc("主命令「插入代码块」使用的语言")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("python")
|
||||
.setValue(this.plugin.settings.defaultLanguage)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultLanguage = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("语言列表")
|
||||
.setDesc("每行一个语言,会为每个语言注册独立命令。修改后重新打开设置面板可刷新下方选项。")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setPlaceholder("python\njavascript\nbash\n...")
|
||||
.setValue(this.plugin.settings.languages)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.languages = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.rows = 8;
|
||||
text.inputEl.cols = 30;
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("保留选中内容缩进")
|
||||
.setDesc("选中文本包裹成代码块时,是否保留原有缩进")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.keepIndent)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.keepIndent = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
// ---- 快捷键设置区域 ----
|
||||
containerEl.createEl("hr", { cls: "qcb-divider" });
|
||||
containerEl.createEl("h3", { text: "快捷键设置" });
|
||||
|
||||
const desc = containerEl.createEl("p", { cls: "qcb-desc" });
|
||||
desc.innerHTML =
|
||||
"为需要的语言配置快捷键。点击输入框后按下组合键即可设置," +
|
||||
"按 <code>Backspace</code> 或 <code>Esc</code> 清除。设置后自动检测冲突。";
|
||||
|
||||
// 配置列表
|
||||
const configs = this.plugin.settings.hotkeyConfigs;
|
||||
for (let i = 0; i < configs.length; i++) {
|
||||
this.createHotkeyConfigItem(i);
|
||||
}
|
||||
|
||||
// 添加按钮
|
||||
new Setting(containerEl).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("+ 添加快捷键配置")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.hotkeyConfigs.push({
|
||||
lang: this.plugin.settings.defaultLanguage,
|
||||
hotkey: null,
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
// ---- 更多入口 ----
|
||||
containerEl.createEl("hr", { cls: "qcb-divider" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("更多快捷键设置")
|
||||
.setDesc("打开 Obsidian 系统的快捷键设置页面,可查看 / 修改所有命令的快捷键")
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("打开 Obsidian 快捷键设置")
|
||||
.onClick(() => {
|
||||
const ok = openHotkeysSettings(this.app);
|
||||
if (ok) {
|
||||
new Notice("已打开快捷键设置,可在搜索框输入命令名查找", 6000);
|
||||
} else {
|
||||
new Notice("无法自动打开,请手动进入:设置 → 快捷键", 6000);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// ---- 使用说明 ----
|
||||
const tip = containerEl.createEl("div", { cls: "qcb-tip" });
|
||||
tip.innerHTML =
|
||||
"<b>使用方法</b><br>" +
|
||||
"1. <code>Ctrl+P</code> 打开命令面板,搜索「插入代码块」<br>" +
|
||||
"2. 无选中文本时:插入空代码块,光标自动定位到块内<br>" +
|
||||
"3. 有选中文本时:用 <code>```lang</code> 包裹选中的内容<br>" +
|
||||
"4. 在上方「快捷键设置」区域,点击「+ 添加」为需要的语言配置快捷键<br>" +
|
||||
"5. 修改语言列表后,关闭再打开设置面板即可刷新选项";
|
||||
}
|
||||
|
||||
// 创建单个快捷键配置项:语言选择 + 快捷键输入 + 冲突检测 + 删除
|
||||
createHotkeyConfigItem(index) {
|
||||
const plugin = this.plugin;
|
||||
const config = plugin.settings.hotkeyConfigs[index];
|
||||
const fullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
|
||||
const setting = new Setting(this.containerEl);
|
||||
|
||||
// 语言选择 dropdown
|
||||
const select = setting.controlEl.createEl("select", { cls: "qcb-lang-select" });
|
||||
const langs = plugin.getLanguages();
|
||||
// 确保当前 lang 在选项里(即使不在列表中)
|
||||
if (!langs.includes(config.lang)) langs.unshift(config.lang);
|
||||
for (const lang of langs) {
|
||||
const option = select.createEl("option", { value: lang, text: lang });
|
||||
if (lang === config.lang) option.selected = true;
|
||||
}
|
||||
|
||||
// 快捷键输入框
|
||||
const inputEl = setting.controlEl.createEl("input", {
|
||||
type: "text",
|
||||
cls: "qcb-hotkey-input",
|
||||
attr: { readonly: true, placeholder: "点击设置…" },
|
||||
});
|
||||
inputEl.value = formatHotkey(config.hotkey);
|
||||
|
||||
// 冲突提示
|
||||
const warningEl = setting.descEl.createEl("div", { cls: "qcb-warning" });
|
||||
|
||||
const updateWarning = (hotkey) => {
|
||||
warningEl.empty();
|
||||
warningEl.removeClass("has-conflict");
|
||||
if (!hotkey) return;
|
||||
const conflict = findConflict(this.app, fullId, hotkey);
|
||||
if (conflict) {
|
||||
warningEl.addClass("has-conflict");
|
||||
warningEl.createEl("span", { text: "⚠️ 冲突:", cls: "qcb-conflict-label" });
|
||||
warningEl.createEl("span", { text: conflict.commandName, cls: "qcb-conflict-name" });
|
||||
warningEl.createEl("span", { text: " " });
|
||||
const link = warningEl.createEl("a", {
|
||||
text: "前往修改 ›",
|
||||
cls: "qcb-conflict-link",
|
||||
attr: { href: "#" },
|
||||
});
|
||||
link.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
openHotkeysSettings(this.app);
|
||||
new Notice(`请在快捷键设置中搜索「${conflict.commandName}」查看并修改`, 8000);
|
||||
});
|
||||
} else {
|
||||
warningEl.createEl("span", { text: "✓ 无冲突", cls: "qcb-ok" });
|
||||
}
|
||||
};
|
||||
|
||||
updateWarning(config.hotkey);
|
||||
|
||||
// 语言变更:清除旧绑定,绑定到新语言
|
||||
select.addEventListener("change", async () => {
|
||||
// 清除旧语言的快捷键
|
||||
if (config.hotkey) {
|
||||
try {
|
||||
this.app.hotkeyManager.removeHotkeys(`${PLUGIN_ID}:insert-codeblock-${config.lang}`);
|
||||
} catch (e) {}
|
||||
}
|
||||
config.lang = select.value;
|
||||
// 绑定到新语言
|
||||
if (config.hotkey) {
|
||||
const newFullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
this.app.hotkeyManager.setHotkeys(newFullId, [config.hotkey]);
|
||||
} catch (e) {}
|
||||
}
|
||||
await plugin.saveSettings();
|
||||
// 更新 fullId 和冲突检测
|
||||
fullId; // 注意:fullId 是 const,这里用闭包里的 config.lang 重新检测
|
||||
const currentFullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
warningEl.empty();
|
||||
if (config.hotkey) {
|
||||
const conflict = findConflict(this.app, currentFullId, config.hotkey);
|
||||
if (conflict) {
|
||||
warningEl.addClass("has-conflict");
|
||||
warningEl.createEl("span", { text: "⚠️ 冲突:", cls: "qcb-conflict-label" });
|
||||
warningEl.createEl("span", { text: conflict.commandName, cls: "qcb-conflict-name" });
|
||||
} else {
|
||||
warningEl.createEl("span", { text: "✓ 无冲突", cls: "qcb-ok" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 录制状态
|
||||
inputEl.addEventListener("focus", () => inputEl.addClass("recording"));
|
||||
inputEl.addEventListener("blur", () => inputEl.removeClass("recording"));
|
||||
|
||||
// 捕获按键
|
||||
inputEl.addEventListener("keydown", async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const key = ev.key;
|
||||
|
||||
// 清除
|
||||
if (key === "Backspace" || key === "Escape" || key === "Delete") {
|
||||
if (config.hotkey) {
|
||||
try {
|
||||
this.app.hotkeyManager.removeHotkeys(`${PLUGIN_ID}:insert-codeblock-${config.lang}`);
|
||||
} catch (e) {}
|
||||
}
|
||||
config.hotkey = null;
|
||||
inputEl.value = "";
|
||||
await plugin.saveSettings();
|
||||
updateWarning(null);
|
||||
new Notice(`已清除:${config.lang} 快捷键`, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (["Control", "Alt", "Shift", "Meta", "Tab"].includes(key)) return;
|
||||
|
||||
const modifiers = [];
|
||||
if (ev.ctrlKey) modifiers.push("Mod");
|
||||
if (ev.altKey) modifiers.push("Alt");
|
||||
if (ev.shiftKey) modifiers.push("Shift");
|
||||
if (ev.metaKey) modifiers.push("Meta");
|
||||
|
||||
let displayKey = key;
|
||||
if (key.length === 1) displayKey = key.toUpperCase();
|
||||
|
||||
const hotkey = { modifiers, key: displayKey };
|
||||
config.hotkey = hotkey;
|
||||
inputEl.value = formatHotkey(hotkey);
|
||||
|
||||
// 绑定
|
||||
const bindId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
this.app.hotkeyManager.setHotkeys(bindId, [hotkey]);
|
||||
} catch (e) {
|
||||
console.error(`[quick-codeblock] 设置 ${config.lang} 快捷键失败:`, e);
|
||||
new Notice(`设置失败:${e.message}`, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
await plugin.saveSettings();
|
||||
updateWarning(hotkey);
|
||||
|
||||
const conflict = findConflict(this.app, bindId, hotkey);
|
||||
if (conflict) {
|
||||
new Notice(
|
||||
`⚠️ 「${formatHotkey(hotkey)}」与「${conflict.commandName}」冲突,点击警告中的「前往修改」处理`,
|
||||
8000
|
||||
);
|
||||
} else {
|
||||
new Notice(`已设置:${config.lang} → ${formatHotkey(hotkey)}`, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除按钮
|
||||
setting.addExtraButton((btn) =>
|
||||
btn
|
||||
.setIcon("trash")
|
||||
.setTooltip("删除此配置")
|
||||
.onClick(async () => {
|
||||
if (config.hotkey) {
|
||||
try {
|
||||
this.app.hotkeyManager.removeHotkeys(`${PLUGIN_ID}:insert-codeblock-${config.lang}`);
|
||||
} catch (e) {}
|
||||
}
|
||||
plugin.settings.hotkeyConfigs.splice(index, 1);
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = QuickCodeBlockPlugin;
|
||||
504
main.ts
Normal file
504
main.ts
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
/*
|
||||
* 代码块快捷插入 - main.ts (TypeScript 源码参考)
|
||||
*
|
||||
* 这是 main.js 的 TypeScript 版本,供需要类型检查或二次开发的用户参考。
|
||||
* 编译方法(需要 obsidian、@types/node、esbuild):
|
||||
* npm install obsidian @types/node esbuild
|
||||
* esbuild main.ts --bundle --external:obsidian --format=cjs --outfile=main.js
|
||||
*
|
||||
* 已提供编译好的 main.js,通常无需自行编译。
|
||||
*/
|
||||
import { App, Editor, Modal, Notice, Plugin, PluginSettingTab, Setting } from "obsidian";
|
||||
|
||||
interface Hotkey {
|
||||
modifiers: string[];
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface HotkeyConfig {
|
||||
lang: string;
|
||||
hotkey: Hotkey | null;
|
||||
}
|
||||
|
||||
interface ConflictInfo {
|
||||
commandId: string;
|
||||
commandName: string;
|
||||
}
|
||||
|
||||
interface PluginSettings {
|
||||
defaultLanguage: string;
|
||||
languages: string;
|
||||
keepIndent: boolean;
|
||||
hotkeyConfigs: HotkeyConfig[];
|
||||
}
|
||||
|
||||
const PLUGIN_ID = "quick-codeblock";
|
||||
|
||||
const DEFAULT_SETTINGS: PluginSettings = {
|
||||
defaultLanguage: "python",
|
||||
languages: "python\njavascript\nbash\nshell\nsql\njson\nyaml\ngo\nrust\njava\nhtml\ncss",
|
||||
keepIndent: true,
|
||||
hotkeyConfigs: [{ lang: "python", hotkey: null }],
|
||||
};
|
||||
|
||||
function formatHotkey(hotkey: Hotkey | null): string {
|
||||
if (!hotkey) return "";
|
||||
const modMap: Record<string, string> = { Mod: "Ctrl", Ctrl: "Ctrl", Alt: "Alt", Shift: "Shift", Meta: "Win" };
|
||||
const mods = (hotkey.modifiers || []).slice().sort().map((m) => modMap[m] || m);
|
||||
return mods.length > 0 ? `${mods.join(" + ")} + ${hotkey.key}` : hotkey.key;
|
||||
}
|
||||
|
||||
function getEffectiveHotkeys(hotkeyManager: any, commandId: string): any[] {
|
||||
try {
|
||||
if (typeof hotkeyManager.getEffectiveHotkeys === "function") {
|
||||
return hotkeyManager.getEffectiveHotkeys(commandId) || [];
|
||||
}
|
||||
const custom = hotkeyManager.getHotkeys ? hotkeyManager.getHotkeys(commandId) || [] : [];
|
||||
const baked = hotkeyManager.getBakedHotkeys ? hotkeyManager.getBakedHotkeys(commandId) || [] : [];
|
||||
return custom.length > 0 ? custom : baked;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function findConflict(app: App, ownCommandId: string, hotkey: Hotkey | null): ConflictInfo | null {
|
||||
if (!hotkey) return null;
|
||||
const targetCombo = formatHotkey(hotkey);
|
||||
const hotkeyManager = (app as any).hotkeyManager;
|
||||
const commands = (app as any).commands.commands as Record<string, { name: string }>;
|
||||
for (const id of Object.keys(commands)) {
|
||||
if (id === ownCommandId) continue;
|
||||
const effective = getEffectiveHotkeys(hotkeyManager, id);
|
||||
for (const h of effective) {
|
||||
if (formatHotkey(h) === targetCombo) {
|
||||
return { commandId: id, commandName: commands[id].name || id };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openHotkeysSettings(app: App): boolean {
|
||||
let opened = false;
|
||||
try {
|
||||
const setting = (app as any).setting;
|
||||
if (setting && typeof setting.openTabById === "function") {
|
||||
setting.openTabById("hotkeys");
|
||||
opened = true;
|
||||
}
|
||||
} catch {}
|
||||
if (!opened) {
|
||||
try {
|
||||
(app as any).openSettings();
|
||||
opened = true;
|
||||
} catch {}
|
||||
}
|
||||
if (!opened) {
|
||||
try {
|
||||
app.commands.executeCommandById("app:open-settings" as any);
|
||||
opened = true;
|
||||
} catch {}
|
||||
}
|
||||
if (opened) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const setting = (app as any).setting;
|
||||
if (setting && typeof setting.openTabById === "function") {
|
||||
setting.openTabById("hotkeys");
|
||||
}
|
||||
} catch {}
|
||||
}, 200);
|
||||
}
|
||||
return opened;
|
||||
}
|
||||
|
||||
export default class QuickCodeBlockPlugin extends Plugin {
|
||||
settings!: PluginSettings;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
this.registerCommands();
|
||||
this.addSettingTab(new CodeBlockSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
for (const config of this.settings.hotkeyConfigs) {
|
||||
const fullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
(this.app as any).hotkeyManager.removeHotkeys(fullId);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
private registerCommands(): void {
|
||||
const def = this.settings.defaultLanguage;
|
||||
this.addCommand({
|
||||
id: "insert-codeblock-default",
|
||||
name: `插入代码块(${def})`,
|
||||
editorCallback: (editor: Editor) => this.insertCodeBlock(editor, def),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-codeblock-pick",
|
||||
name: "插入代码块(选择语言)",
|
||||
editorCallback: (editor: Editor) => {
|
||||
const langs = this.getLanguages();
|
||||
new LanguagePickerModal(this.app, langs, (lang: string) => {
|
||||
this.insertCodeBlock(editor, lang);
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
for (const lang of this.getLanguages()) {
|
||||
this.addCommand({
|
||||
id: `insert-codeblock-${lang}`,
|
||||
name: `插入代码块(${lang})`,
|
||||
editorCallback: (editor: Editor) => this.insertCodeBlock(editor, lang),
|
||||
});
|
||||
}
|
||||
|
||||
for (const config of this.settings.hotkeyConfigs) {
|
||||
if (config.hotkey) {
|
||||
const fullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
(this.app as any).hotkeyManager.setHotkeys(fullId, [config.hotkey]);
|
||||
} catch (e) {
|
||||
console.error(`[quick-codeblock] 绑定 ${config.lang} 快捷键失败:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insertCodeBlock(editor: Editor, lang: string): void {
|
||||
const selection = editor.getSelection();
|
||||
if (selection) {
|
||||
const content = this.settings.keepIndent ? selection : selection.trim();
|
||||
const block = "```" + lang + "\n" + content + "\n```";
|
||||
editor.replaceSelection(block);
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
const beforeCursor = line.substring(0, cursor.ch);
|
||||
const afterCursor = line.substring(cursor.ch);
|
||||
|
||||
let block = "```" + lang + "\n\n```";
|
||||
if (beforeCursor.trim() !== "") block = "\n" + block;
|
||||
if (afterCursor.trim() !== "") block = block + "\n";
|
||||
|
||||
editor.replaceSelection(block);
|
||||
|
||||
const newCursor = editor.getCursor();
|
||||
const targetLine = afterCursor.trim() !== "" ? newCursor.line - 2 : newCursor.line - 1;
|
||||
editor.setCursor({ line: targetLine, ch: 0 });
|
||||
}
|
||||
|
||||
private getLanguages(): string[] {
|
||||
return this.settings.languages
|
||||
.split(/[\n,,]/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
if (!this.settings.hotkeyConfigs) {
|
||||
if ((this.settings as any).hotkeys && typeof (this.settings as any).hotkeys === "object") {
|
||||
this.settings.hotkeyConfigs = Object.entries((this.settings as any).hotkeys)
|
||||
.filter(([_, h]: [string, any]) => h)
|
||||
.map(([lang, hotkey]: [string, any]) => ({ lang, hotkey }));
|
||||
} else {
|
||||
this.settings.hotkeyConfigs = [{ lang: this.settings.defaultLanguage, hotkey: null }];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguagePickerModal extends Modal {
|
||||
private languages: string[];
|
||||
private onChoose: (lang: string) => void;
|
||||
|
||||
constructor(app: App, languages: string[], onChoose: (lang: string) => void) {
|
||||
super(app);
|
||||
this.languages = languages;
|
||||
this.onChoose = onChoose;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.addClass("qcb-picker");
|
||||
contentEl.createEl("h2", { text: "选择代码块语言" });
|
||||
const grid = contentEl.createEl("div", { cls: "qcb-lang-grid" });
|
||||
for (const lang of this.languages) {
|
||||
const item = grid.createEl("div", { cls: "qcb-lang-item", text: lang });
|
||||
item.addEventListener("click", () => {
|
||||
this.onChoose(lang);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class CodeBlockSettingTab extends PluginSettingTab {
|
||||
plugin: QuickCodeBlockPlugin;
|
||||
|
||||
constructor(app: App, plugin: QuickCodeBlockPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "代码块快捷插入" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("默认语言")
|
||||
.setDesc("主命令「插入代码块」使用的语言")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("python")
|
||||
.setValue(this.plugin.settings.defaultLanguage)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultLanguage = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("语言列表")
|
||||
.setDesc("每行一个语言,会为每个语言注册独立命令。修改后重新打开设置面板可刷新下方选项。")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setPlaceholder("python\njavascript\nbash\n...")
|
||||
.setValue(this.plugin.settings.languages)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.languages = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.rows = 8;
|
||||
text.inputEl.cols = 30;
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("保留选中内容缩进")
|
||||
.setDesc("选中文本包裹成代码块时,是否保留原有缩进")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.keepIndent)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.keepIndent = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
containerEl.createEl("hr", { cls: "qcb-divider" });
|
||||
containerEl.createEl("h3", { text: "快捷键设置" });
|
||||
|
||||
const desc = containerEl.createEl("p", { cls: "qcb-desc" });
|
||||
desc.innerHTML =
|
||||
"为需要的语言配置快捷键。点击输入框后按下组合键即可设置," +
|
||||
"按 <code>Backspace</code> 或 <code>Esc</code> 清除。设置后自动检测冲突。";
|
||||
|
||||
const configs = this.plugin.settings.hotkeyConfigs;
|
||||
for (let i = 0; i < configs.length; i++) {
|
||||
this.createHotkeyConfigItem(i);
|
||||
}
|
||||
|
||||
new Setting(containerEl).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("+ 添加快捷键配置")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.hotkeyConfigs.push({
|
||||
lang: this.plugin.settings.defaultLanguage,
|
||||
hotkey: null,
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
containerEl.createEl("hr", { cls: "qcb-divider" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("更多快捷键设置")
|
||||
.setDesc("打开 Obsidian 系统的快捷键设置页面,可查看 / 修改所有命令的快捷键")
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("打开 Obsidian 快捷键设置")
|
||||
.onClick(() => {
|
||||
const ok = openHotkeysSettings(this.app);
|
||||
if (ok) {
|
||||
new Notice("已打开快捷键设置,可在搜索框输入命令名查找", 6000);
|
||||
} else {
|
||||
new Notice("无法自动打开,请手动进入:设置 → 快捷键", 6000);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const tip = containerEl.createEl("div", { cls: "qcb-tip" });
|
||||
tip.innerHTML =
|
||||
"<b>使用方法</b><br>" +
|
||||
"1. <code>Ctrl+P</code> 打开命令面板,搜索「插入代码块」<br>" +
|
||||
"2. 无选中文本时:插入空代码块,光标自动定位到块内<br>" +
|
||||
"3. 有选中文本时:用 <code>```lang</code> 包裹选中的内容<br>" +
|
||||
"4. 在上方「快捷键设置」区域,点击「+ 添加」为需要的语言配置快捷键<br>" +
|
||||
"5. 修改语言列表后,关闭再打开设置面板即可刷新选项";
|
||||
}
|
||||
|
||||
private createHotkeyConfigItem(index: number): void {
|
||||
const plugin = this.plugin;
|
||||
const config = plugin.settings.hotkeyConfigs[index];
|
||||
|
||||
const setting = new Setting(this.containerEl);
|
||||
|
||||
const select = setting.controlEl.createEl("select", { cls: "qcb-lang-select" });
|
||||
const langs = plugin.getLanguages();
|
||||
if (!langs.includes(config.lang)) langs.unshift(config.lang);
|
||||
for (const lang of langs) {
|
||||
const option = select.createEl("option", { value: lang, text: lang });
|
||||
if (lang === config.lang) option.selected = true;
|
||||
}
|
||||
|
||||
const inputEl = setting.controlEl.createEl("input", {
|
||||
type: "text",
|
||||
cls: "qcb-hotkey-input",
|
||||
attr: { readonly: true, placeholder: "点击设置…" },
|
||||
});
|
||||
inputEl.value = formatHotkey(config.hotkey);
|
||||
|
||||
const warningEl = setting.descEl.createEl("div", { cls: "qcb-warning" });
|
||||
|
||||
const updateWarning = (hotkey: Hotkey | null, lang: string) => {
|
||||
warningEl.empty();
|
||||
warningEl.removeClass("has-conflict");
|
||||
if (!hotkey) return;
|
||||
const currentFullId = `${PLUGIN_ID}:insert-codeblock-${lang}`;
|
||||
const conflict = findConflict(this.app, currentFullId, hotkey);
|
||||
if (conflict) {
|
||||
warningEl.addClass("has-conflict");
|
||||
warningEl.createEl("span", { text: "⚠️ 冲突:", cls: "qcb-conflict-label" });
|
||||
warningEl.createEl("span", { text: conflict.commandName, cls: "qcb-conflict-name" });
|
||||
warningEl.createEl("span", { text: " " });
|
||||
const link = warningEl.createEl("a", {
|
||||
text: "前往修改 ›",
|
||||
cls: "qcb-conflict-link",
|
||||
attr: { href: "#" },
|
||||
});
|
||||
link.addEventListener("click", (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
openHotkeysSettings(this.app);
|
||||
new Notice(`请在快捷键设置中搜索「${conflict.commandName}」查看并修改`, 8000);
|
||||
});
|
||||
} else {
|
||||
warningEl.createEl("span", { text: "✓ 无冲突", cls: "qcb-ok" });
|
||||
}
|
||||
};
|
||||
|
||||
updateWarning(config.hotkey, config.lang);
|
||||
|
||||
select.addEventListener("change", async () => {
|
||||
if (config.hotkey) {
|
||||
try {
|
||||
(this.app as any).hotkeyManager.removeHotkeys(`${PLUGIN_ID}:insert-codeblock-${config.lang}`);
|
||||
} catch {}
|
||||
}
|
||||
config.lang = select.value;
|
||||
if (config.hotkey) {
|
||||
const newFullId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
(this.app as any).hotkeyManager.setHotkeys(newFullId, [config.hotkey]);
|
||||
} catch {}
|
||||
}
|
||||
await plugin.saveSettings();
|
||||
updateWarning(config.hotkey, config.lang);
|
||||
});
|
||||
|
||||
inputEl.addEventListener("focus", () => inputEl.addClass("recording"));
|
||||
inputEl.addEventListener("blur", () => inputEl.removeClass("recording"));
|
||||
|
||||
inputEl.addEventListener("keydown", async (ev: KeyboardEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const key = ev.key;
|
||||
|
||||
if (key === "Backspace" || key === "Escape" || key === "Delete") {
|
||||
if (config.hotkey) {
|
||||
try {
|
||||
(this.app as any).hotkeyManager.removeHotkeys(`${PLUGIN_ID}:insert-codeblock-${config.lang}`);
|
||||
} catch {}
|
||||
}
|
||||
config.hotkey = null;
|
||||
inputEl.value = "";
|
||||
await plugin.saveSettings();
|
||||
updateWarning(null, config.lang);
|
||||
new Notice(`已清除:${config.lang} 快捷键`, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (["Control", "Alt", "Shift", "Meta", "Tab"].includes(key)) return;
|
||||
|
||||
const modifiers: string[] = [];
|
||||
if (ev.ctrlKey) modifiers.push("Mod");
|
||||
if (ev.altKey) modifiers.push("Alt");
|
||||
if (ev.shiftKey) modifiers.push("Shift");
|
||||
if (ev.metaKey) modifiers.push("Meta");
|
||||
|
||||
let displayKey = key;
|
||||
if (key.length === 1) displayKey = key.toUpperCase();
|
||||
|
||||
const hotkey: Hotkey = { modifiers, key: displayKey };
|
||||
config.hotkey = hotkey;
|
||||
inputEl.value = formatHotkey(hotkey);
|
||||
|
||||
const bindId = `${PLUGIN_ID}:insert-codeblock-${config.lang}`;
|
||||
try {
|
||||
(this.app as any).hotkeyManager.setHotkeys(bindId, [hotkey]);
|
||||
} catch (e: any) {
|
||||
console.error(`[quick-codeblock] 设置 ${config.lang} 快捷键失败:`, e);
|
||||
new Notice(`设置失败:${e.message}`, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
await plugin.saveSettings();
|
||||
updateWarning(hotkey, config.lang);
|
||||
|
||||
const conflict = findConflict(this.app, bindId, hotkey);
|
||||
if (conflict) {
|
||||
new Notice(
|
||||
`⚠️ 「${formatHotkey(hotkey)}」与「${conflict.commandName}」冲突,点击警告中的「前往修改」处理`,
|
||||
8000
|
||||
);
|
||||
} else {
|
||||
new Notice(`已设置:${config.lang} → ${formatHotkey(hotkey)}`, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
setting.addExtraButton((btn) =>
|
||||
btn
|
||||
.setIcon("trash")
|
||||
.setTooltip("删除此配置")
|
||||
.onClick(async () => {
|
||||
if (config.hotkey) {
|
||||
try {
|
||||
(this.app as any).hotkeyManager.removeHotkeys(`${PLUGIN_ID}:insert-codeblock-${config.lang}`);
|
||||
} catch {}
|
||||
}
|
||||
plugin.settings.hotkeyConfigs.splice(index, 1);
|
||||
await plugin.saveSettings();
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "quick-codeblock",
|
||||
"name": "代码块快捷插入",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "快捷插入代码块并自动定位光标。支持自定义语言、选中文本包裹、快捷键配置与冲突检测。",
|
||||
"author": "旭说云原生",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
164
styles.css
Normal file
164
styles.css
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/* 代码块快捷插入 - 样式 */
|
||||
|
||||
/* 语言选择 Modal */
|
||||
.qcb-picker .qcb-lang-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.qcb-picker .qcb-lang-item {
|
||||
padding: 10px 12px;
|
||||
text-align: center;
|
||||
font-family: var(--font-monospace, monospace);
|
||||
font-size: 0.92em;
|
||||
background: var(--background-modifier-form-field, #f5f5f5);
|
||||
border: 1px solid var(--background-modifier-border, #ddd);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
color: var(--text-normal, currentColor);
|
||||
}
|
||||
|
||||
.qcb-picker .qcb-lang-item:hover {
|
||||
border-color: var(--interactive-accent, #7c3aed);
|
||||
background: var(--interactive-accent-hover, rgba(124, 58, 237, 0.1));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.qcb-picker .qcb-lang-item:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 分隔线与描述 */
|
||||
.qcb-divider {
|
||||
margin: 24px 0 16px 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--background-modifier-border, #ddd);
|
||||
}
|
||||
|
||||
.qcb-desc {
|
||||
color: var(--text-muted, #999);
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qcb-desc code {
|
||||
background: var(--background-modifier-border, #e0e0e0);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
/* 语言选择 dropdown */
|
||||
.qcb-lang-select {
|
||||
padding: 6px 8px;
|
||||
font-family: var(--font-monospace, monospace);
|
||||
font-size: 0.88em;
|
||||
background: var(--background-modifier-form-field, #fff);
|
||||
border: 1px solid var(--background-modifier-border, #ddd);
|
||||
border-radius: 4px;
|
||||
color: var(--text-normal, currentColor);
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.qcb-lang-select:hover {
|
||||
border-color: var(--interactive-accent, #7c3aed);
|
||||
}
|
||||
|
||||
.qcb-lang-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent, #7c3aed);
|
||||
}
|
||||
|
||||
/* 快捷键输入框 */
|
||||
.qcb-hotkey-input {
|
||||
width: 130px;
|
||||
padding: 6px 10px;
|
||||
text-align: center;
|
||||
font-family: var(--font-monospace, monospace);
|
||||
font-size: 0.88em;
|
||||
background: var(--background-modifier-form-field, #fff);
|
||||
border: 1px solid var(--background-modifier-border, #ddd);
|
||||
border-radius: 4px;
|
||||
color: var(--text-normal, currentColor);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.qcb-hotkey-input:hover {
|
||||
border-color: var(--interactive-accent, #7c3aed);
|
||||
}
|
||||
|
||||
.qcb-hotkey-input.recording {
|
||||
border-color: var(--interactive-accent, #7c3aed);
|
||||
box-shadow: 0 0 0 2px var(--interactive-accent-hover, rgba(124, 58, 237, 0.25));
|
||||
}
|
||||
|
||||
.qcb-hotkey-input::placeholder {
|
||||
color: var(--text-faint, #bbb);
|
||||
}
|
||||
|
||||
/* 冲突提示 */
|
||||
.qcb-warning {
|
||||
margin-top: 6px;
|
||||
font-size: 0.85em;
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
.qcb-warning.has-conflict {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(224, 82, 82, 0.1);
|
||||
border-left: 3px solid var(--text-error, #e05252);
|
||||
}
|
||||
|
||||
.qcb-conflict-label {
|
||||
color: var(--text-error, #e05252);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.qcb-conflict-name {
|
||||
color: var(--text-normal, currentColor);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.qcb-conflict-link {
|
||||
color: var(--interactive-accent, #7c3aed);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.qcb-conflict-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.qcb-ok {
|
||||
color: var(--text-success, #4caf50);
|
||||
}
|
||||
|
||||
/* 使用说明 */
|
||||
.qcb-tip {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--background-secondary, #f9f9f9);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--interactive-accent, #7c3aed);
|
||||
color: var(--text-muted, #666);
|
||||
font-size: 0.88em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.qcb-tip code {
|
||||
background: var(--background-modifier-border, #e0e0e0);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.qcb-tip b {
|
||||
color: var(--text-normal, currentColor);
|
||||
}
|
||||
Loading…
Reference in a new issue