/*
* 代码块快捷插入 - 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 =
"为需要的语言配置快捷键。点击输入框后按下组合键即可设置," +
"按 Backspace 或 Esc 清除。设置后自动检测冲突。";
// 配置列表
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 =
"使用方法
" +
"1. Ctrl+P 打开命令面板,搜索「插入代码块」
" +
"2. 无选中文本时:插入空代码块,光标自动定位到块内
" +
"3. 有选中文本时:用 ```lang 包裹选中的内容
" +
"4. 在上方「快捷键设置」区域,点击「+ 添加」为需要的语言配置快捷键
" +
"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;