
重要提示 如果你不知道我下面发的三段代码怎么用,那就不建议用,ai写的,有没有错会不会造成影响 我也不知道!!!
重要提示 如果你不知道我下面发的三段代码怎么用,那就不建议用,ai写的,有没有错会不会造成影响 我也不知道!!!
重要提示 如果你不知道我下面发的三段代码怎么用,那就不建议用,ai写的,有没有错会不会造成影响 我也不知道!!!
main.js
/* main.js - Footnote Compass (Cursor Follow + Smart Persistence) */
const { Plugin, ItemView, debounce } = require('obsidian');
const VIEW_TYPE_FOOTNOTE = "footnote-compass-view";
class FootnoteListView extends ItemView {
constructor(leaf) {
super(leaf);
// 缓存当前的脚注数据
this.cachedRefs = [];
// 【新增】记忆最后一次活跃的 Markdown 视图
this.lastActiveMarkdownView = null;
}
getViewType() { return VIEW_TYPE_FOOTNOTE; }
getDisplayText() { return "脚注大纲"; }
getIcon() { return "message-circle-more"; }
async onOpen() {
this.renderStructure();
// 稍微延迟,等待工作区加载
setTimeout(() => {
this.checkAndUpdate();
}, 100);
}
renderStructure() {
const container = this.containerEl.children[1];
container.empty();
container.createEl("h4", { text: "" });
container.createDiv({ cls: "footnote-list-root" });
}
clearView(msg) {
const container = this.containerEl.children[1];
const listRoot = container.querySelector(".footnote-list-root");
if (listRoot) {
listRoot.empty();
if(msg) listRoot.createDiv({ cls: "footnote-empty", text: msg });
}
this.cachedRefs = [];
}
// 【新增】智能寻找最佳 Markdown 视图 (用于快捷键切换回来时恢复显示)
findBestMarkdownLeaf() {
// 1. 如果当前激活的就是 Markdown,直接返回
const active = this.app.workspace.activeLeaf;
if (active && active.view.getViewType() === 'markdown') {
return active;
}
// 2. 如果之前有记忆,且记忆还未关闭,返回记忆
if (this.lastActiveMarkdownView && this.lastActiveMarkdownView.leaf && this.lastActiveMarkdownView.leaf.view.getViewType() === 'markdown') {
return this.lastActiveMarkdownView.leaf;
}
// 3. 如果都没有,去工作区找“最近的一个 Markdown 文件”
const recent = this.app.workspace.getMostRecentLeaf && this.app.workspace.getMostRecentLeaf();
if (recent && recent.view.getViewType() === 'markdown') {
return recent;
}
// 4. 实在不行,找第一个 Markdown
const leaves = this.app.workspace.getLeavesOfType('markdown');
if (leaves.length > 0) return leaves[0];
return null;
}
// --- 主更新入口:逻辑已修改为“非白板即保留” ---
checkAndUpdate() {
const activeLeaf = this.app.workspace.activeLeaf;
if (!activeLeaf) return;
const viewType = activeLeaf.view.getViewType();
// 1. 白板 (Canvas) -> 必须清空
if (viewType === 'canvas') {
this.clearView("当前是白板 (Canvas)");
this.lastActiveMarkdownView = null; // 清空记忆
return;
}
// 2. Markdown -> 正常更新并记忆
if (viewType === 'markdown') {
this.lastActiveMarkdownView = activeLeaf.view;
this.updateView(activeLeaf.view);
return;
}
// 3. 其他所有情况 (快捷键切换到本插件、侧边栏、第三方插件)
// 策略:不要清空!尝试找回最近的 Markdown 内容显示
const bestLeaf = this.findBestMarkdownLeaf();
if (bestLeaf) {
this.updateView(bestLeaf.view);
} else {
// 如果真的连一个 Markdown 都找不到,那就保持原样不动
}
}
// 解析文本,构建列表
updateView(view) {
const container = this.containerEl.children[1];
const listRoot = container.querySelector(".footnote-list-root");
if (!listRoot) return;
// 这里的 view 可能是后台的 view,需要防错
let text = "";
try {
text = view.editor.getValue();
} catch (e) {
return;
}
// 先清空 DOM
listRoot.empty();
const lines = text.split("\n");
const definitionMap = new Map();
const defRegex = /^\[\^([^\]]+)\]:\s*(.*)$/;
// 1. 找定义
lines.forEach((line) => {
const match = line.match(defRegex);
if (match) definitionMap.set(match[1], match[2]);
});
// 2. 找引用
this.cachedRefs = [];
const refRegex = /\[\^([^\]]+)\](?!:)/g;
lines.forEach((line, lineIndex) => {
// 排除文末定义行
if (defRegex.test(line)) return;
let match;
while ((match = refRegex.exec(line)) !== null) {
const key = match[1];
const content = definitionMap.get(key) || "(未找到定义内容)";
this.cachedRefs.push({
key: key,
content: content,
line: lineIndex,
col: match.index,
el: null
});
}
});
if (this.cachedRefs.length === 0) {
listRoot.createDiv({ cls: "footnote-empty", text: "当前文档没有脚注引用" });
return;
}
const listContainer = listRoot.createDiv({ cls: "footnote-compass-container" });
// 3. 渲染 DOM
this.cachedRefs.forEach((ref, index) => {
const itemEl = listContainer.createDiv({ cls: "footnote-item" });
ref.el = itemEl;
itemEl.createDiv({ cls: "footnote-key", text: `[^${ref.key}]` });
itemEl.createDiv({ cls: "footnote-content", text: ref.content });
// 点击跳转
itemEl.addEventListener("click", () => {
this.app.workspace.setActiveLeaf(view.leaf, true, true);
view.editor.setCursor({ line: ref.line, ch: ref.col + 2 });
view.editor.scrollIntoView({ from: { line: ref.line, ch: ref.col }, to: { line: ref.line, ch: ref.col } }, true);
view.editor.focus();
this.highlightByRefIndex(index);
});
});
// 渲染完立刻做一次高亮检查
this.syncHighlightWithCursor(view);
}
// --- 核心功能:根据光标位置高亮 (保持你要求的逻辑) ---
syncHighlightWithCursor(view) {
if (!this.cachedRefs.length || !view || view.getViewType() !== 'markdown') return;
// 注意:如果是通过快捷键切换过来,view 可能不在前台,但 editor 还是可读的
const cursor = view.editor.getCursor();
const currentLine = cursor.line;
let activeIndex = -1;
for (let i = 0; i < this.cachedRefs.length; i++) {
const ref = this.cachedRefs[i];
if (ref.line <= currentLine) {
activeIndex = i;
} else {
break;
}
}
this.highlightByRefIndex(activeIndex);
}
highlightByRefIndex(index) {
this.cachedRefs.forEach((ref, i) => {
if (!ref.el) return;
if (i === index) {
if (!ref.el.classList.contains("is-active")) {
ref.el.addClass("is-active");
ref.el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
} else {
ref.el.removeClass("is-active");
}
});
}
}
module.exports = class FootnoteCompassPlugin extends Plugin {
async onload() {
this.registerView(VIEW_TYPE_FOOTNOTE, (leaf) => new FootnoteListView(leaf));
this.addRibbonIcon('message-circle-more', '打开脚注大纲', () => {
this.activateView();
});
// 注册命令,方便快捷键调用
this.addCommand({
id: 'open-sidebar',
name: '打开/显示侧边栏',
callback: () => { this.activateView(); }
});
// 1. 内容改变/视图切换/布局改变 -> 更新大纲内容
const debouncedUpdate = debounce(() => {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOOTNOTE);
leaves.forEach(leaf => {
if (leaf.view instanceof FootnoteListView) leaf.view.checkAndUpdate();
});
}, 200, true);
// 2. 光标移动 -> 只更新高亮 (保持你的逻辑)
const handleCursorActivity = debounce(() => {
// 这里我们不仅要找 activeLeaf,还要考虑如果是侧边栏激活的情况
// 简单处理:让所有存在的脚注插件实例去同步它目前持有 view 的光标
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOOTNOTE);
leaves.forEach(leaf => {
if (leaf.view instanceof FootnoteListView) {
// 如果插件当前显示的是某个 Markdown view 的内容,就去同步那个 view 的光标
const targetView = leaf.view.lastActiveMarkdownView || this.app.workspace.activeLeaf?.view;
if(targetView && targetView.getViewType() === 'markdown') {
leaf.view.syncHighlightWithCursor(targetView);
}
}
});
}, 50, true);
this.registerEvent(this.app.workspace.on('active-leaf-change', debouncedUpdate));
this.registerEvent(this.app.workspace.on('editor-change', debouncedUpdate));
this.registerEvent(this.app.workspace.on('layout-change', debouncedUpdate));
// --- 保持你要求的监听方式 ---
this.registerDomEvent(document, 'click', handleCursorActivity);
this.registerDomEvent(document, 'keyup', handleCursorActivity);
}
async activateView() {
const { workspace } = this.app;
let leaf = null;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_FOOTNOTE);
if (leaves.length > 0) {
leaf = leaves[0];
workspace.revealLeaf(leaf);
} else {
leaf = workspace.getRightLeaf(false);
await leaf.setViewState({ type: VIEW_TYPE_FOOTNOTE, active: true });
workspace.revealLeaf(leaf);
}
// 激活时强制刷新一次,确保快捷键切过来有内容
if (leaf.view instanceof FootnoteListView) leaf.view.checkAndUpdate();
}
};
styles.css
/* 整个容器的内边距 */
.footnote-compass-container {
padding: 12px;
}
/* 单个脚注卡片 */
.footnote-item {
/* 布局 */
display: flex;
flex-direction: column;
/* 外观 */
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 6px;
/* 左侧线条(默认灰色) */
/* 交互 */
cursor: pointer;
transition: all 0.2s ease;
}
/* 鼠标悬停时的效果 */
.footnote-item:hover {
background-color: var(--background-modifier-hover);
transform: translateX(2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* --- 关键修改:隐藏 [^1] --- */
.footnote-key {
display: none;
}
/* 脚注内容样式 */
.footnote-content {
font-size: 18px; /* 稍微调小一点,18px在大纲里太大了,你可以自己改回18 */
color: var(--text-muted);
line-height: 1.5;
word-wrap: break-word;
}
/* 悬停时文字颜色变深 */
.footnote-item:hover .footnote-content {
color: var(--text-normal);
}
/* 空状态提示 */
.footnote-empty {
color: var(--text-faint);
padding: 20px;
text-align: center;
font-size: 13px;
}
/* =========================================
新增:被选中的状态 (Active State)
========================================= */
/* 选中时,左侧线条变红 */
.footnote-item.is-active {
background-color: var(--background-primary); /* 稍微突出的背景 */
box-shadow: 0 2px 8px rgba(255, 0, 0, 0.1); /* 微微的红色光晕 */
}
/* 选中时,文字内容强制变红 */
.footnote-item.is-active .footnote-content {
color: rgb(0, 157, 255) !important;
font-weight: bold; /* 可选:加粗 */
}
manifest.json
{
"id": "footnote-compass",
"name": "Footnote Compass",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "在侧边栏显示所有脚注,展示脚注内容,点击跳转到正文引用位置。",
"author": "AI Assistant",
"authorUrl": "",
"isDesktopOnly": false
}
美化css
/* 隐藏 Obsidian 核心 Page Preview (页面预览/脚注预览) 弹窗的标题栏 */
/* 1. 针对核心弹窗的标题栏,使用您提供的类名 */
div.popover-titlebar {
display: none !important;
}
/* 2. 确保内容区域向上移动,填补被隐藏标题栏的位置 */
/* 核心弹窗通常使用 .popover.hover-popover-note 作为父容器 */
.popover.hover-popover-note .popover-content {
/* 移除内容区域顶部的默认内边距,确保内容贴边 */
padding-top: 0 !important;
}
/*
* CSS 片段名称: increase-footnote-popup-text-size.css
* 描述: 增大页面预览弹窗(包括脚注弹窗)中的文字大小
*/
.popover.hover-popover .markdown-preview-view {
/*
* 调整为你希望的字体大小。
* 1.1em 比默认字体(1.0em)增大 10%。
*/
font-size: 1.4em;
/* 可选:如果你想同时增大行高,使阅读更舒适 */
line-height: 1.6;
}
/*
* 针对 编辑模式 (包括源文件模式和实时预览模式) 增大文字大小
* 对应的主体容器类是 .cm-editor 或 .markdown-source-view
*/
/*
* CSS 片段名称: increase-footnote-popup-text-size.css
* 描述: 仅增大悬停弹窗(包括脚注弹窗)内的文字大小。
*/
/*
* .popover.hover-popover 目标是 Obsidian 的所有悬停预览弹窗。
* .markdown-preview-view 目标是弹窗内的预览渲染内容。
*/
.popover.hover-popover .markdown-preview-view {
/*
* 调整为你希望的字体大小。
* 1.1em 比默认字体(1.0em)增大 10%。
*/
font-size: 1.5em;
/* 可选:如果你想同时增大行高 */
line-height: 1.6;
}
/*
* 针对所有同时是“悬停弹窗”且“包含编辑器”的元素
*/
.popover.hover-popover .cm-editor {
/*
* 增大弹窗内的编辑文本大小
*/
font-size: 1.7em !important;
}
/* 隐藏处于编辑状态的预览弹窗的边框和外轮廓 */
.popover.hover-popover.is-editing {
/* 隐藏外轮廓线 (outline) */
outline: none !important;
/* 隐藏边框线 (border) */
border: none !important;
/* 也可以单独设置边框颜色为透明,确保它不显示 */
border-color: transparent !important;
}
.cm-s-obsidian span.cm-footref.cm-hmd-barelink, .cm-s-obsidian span.cm-blockid {
color: var(--text-muted);
}
/*
* 最终修复方案:隔离源码模式 + 解决符号重复问题。
* 我们使用 :not(.is-source-mode) + 假设 CodeMirror 编辑器中脚注引用的所有部分都是兄弟元素。
*/
/* ================= 隔离源码模式:使用更强的排除规则 ================= */
/* 选择所有【非源码模式】的 CodeMirror 编辑器内的脚注引用 span */
.markdown-source-view:not(.is-source-mode) span.cm-footref.cm-hmd-barelink {
/* 1. 隐藏原文本 ([^2] 的每个部分) */
font-size: 8px !important;
line-height: 3 !important;
color: #1E1E1E;
/* 2. 确保不占用空间,并为伪元素定位做准备 */
margin: 0 !important;
padding: 0 !important;
position: relative;
display: inline-block; /* 确保它是一个可操作的块级元素 */
}
/* ================= 解决符号重复问题:只在最后一个子元素后插入 ================= */
/* 1. 只在【非源码模式】下,且为【兄弟元素中的最后一个】后面插入符号 */
.markdown-source-view:not(.is-source-mode) span.cm-footref.cm-hmd-barelink:last-child::after {
/* 插入符号 */
content: "💬";
/* 恢复符号的样式 */
font-size: 26px;
line-height: 1.1;
margin-left: -15px;
color: var(--text-normal);
/* 确保符号能覆盖任何剩余的边框或背景 */
background-color: transparent;
}
/* 2. 确保在第一个元素前没有符号(以防万一) */
.markdown-source-view:not(.is-source-mode) span.cm-footref.cm-hmd-barelink::before {
content: none !important;
}
/* 3. 移除其他元素的::after,以防不是 last-child 的元素也生成符号 */
.markdown-source-view:not(.is-source-mode) span.cm-footref.cm-hmd-barelink:not(:last-child)::after {
content: none !important;
}
/* 针对原生页面预览弹窗 */
.popover.hover-popover {
/*
* 这是一个魔法数值。
* 原生弹窗通常会为了不遮挡文字而留出一段距离。
* 我们用负边距把它拉回来,让它离鼠标更近。
*/
margin-top: -95px !important;
margin-left: -80px !important;
margin-bottom: 100px !important;
/* 加上弹窗出现的动画,看起来更丝滑 */
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}