关于记录滚轮位置的自写插件simply scroll

第一个是 只记录正文最后的位置 不记录光标的位置
simply scroll
好出就是 不会闪现 不会乱跑

插件市场的会记录光标,导致闪现很烦

manifest.json文件

{
	"id": "simply-scroll",
	"name": "Simply Scroll",
	"version": "1.0.0",
	"minAppVersion": "0.15.0",
	"description": "只记录文件的滚动位置,不记录光标位置。无闪烁,无跳转。",
	"author": "Custom",
	"authorUrl": "",
	"isDesktopOnly": false
}

main.js文件

const { Plugin, WorkspaceLeaf, debounce } = require('obsidian');

// 劫持函数
function around(obj, factories) {
    const removers = Object.keys(factories).map(key => {
        const original = obj[key];
        const factory = factories[key];
        const wrapped = factory(original);
        wrapped && (wrapped.container = original);
        obj[key] = wrapped;
        return () => { if (obj[key] === wrapped) obj[key] = original; };
    });
    return () => removers.forEach(r => r());
}

module.exports = class SimplyScrollPlugin extends Plugin {
    async onload() {
        this.data = Object.assign({}, await this.loadData());
        const plugin = this;

        // --- 第一部分:注入式恢复(解决闪烁) ---
        this.register(around(WorkspaceLeaf.prototype, {
            setViewState(next) {
                return async function (state, eState) {
                    // 在 Obsidian 渲染之前,强制注入滚动位置
                    if (state.type === 'markdown' && state.state?.file) {
                        const path = state.state.file;
                        const saved = plugin.data[path];

                        // 只有在非跳转(没有 subpath)且有保存数据时才介入
                        if (saved > 1 && !state.state.subpath) {
                            eState = eState || {};
                            eState.scroll = saved;
                            // 必须删除 line 和 cursor,否则 Obsidian 会优先滚动到光标,导致闪烁
                            delete eState.line;
                            delete eState.cursor;
                        }
                    }
                    return next.call(this, state, eState);
                };
            }
        }));

        // --- 第二部分:安全保存逻辑(解决假数据) ---
        
        // 只有正在滚动的活动文件才保存(防抖)
        this.saveActiveScroll = debounce(() => {
            const view = this.app.workspace.getActiveViewOfType(require('obsidian').MarkdownView);
            if (view?.file) {
                const scroll = view.currentMode?.getScroll();
                // !!!硬性过滤:绝对不存小于 5 像素的数值,彻底杀掉 0.666
                if (scroll && scroll > 5) {
                    this.data[view.file.path] = scroll;
                    this.saveData(this.data);
                }
            }
        }, 500);

        this.registerDomEvent(window, 'scroll', (e) => {
            if (e.target.classList?.contains('cm-scroller')) {
                this.saveActiveScroll();
            }
        }, true);

        // 切换保护:切换瞬间禁止任何扫描,防止滑落中的残影值被记入
        this.registerEvent(this.app.workspace.on('active-leaf-change', (leaf) => {
            // 这里不再做任何 saveScrollImmediate 的全量扫描
            // 数据的更新全部交给上面的 scroll 事件
        }));
    }

    async onunload() {
        console.log('Simply Scroll Unloaded');
    }
};

第二个是脚注插件 footnote compass

main.js

/* main.js - Footnote Compass (Modified Version) */
const { Plugin, ItemView, debounce, setIcon } = require('obsidian');

const VIEW_TYPE_FOOTNOTE = "footnote-compass-view";

class FootnoteListView extends ItemView {
    constructor(leaf) {
        super(leaf);
        // 缓存原始数据(始终保持文档中的出现顺序,用于光标定位)
        this.cachedRefs = []; 
        // 记忆最后一次活跃的 Markdown 视图
        this.lastActiveMarkdownView = null;
        // 排序状态:false=原样(文档顺序), true=排序(序号顺序)
        this.isSortByKey = false; 
        // 界面元素引用
        this.listRoot = null;
        this.sortBtn = null;
    }

    getViewType() { return VIEW_TYPE_FOOTNOTE; }
    getDisplayText() { return "脚注大纲"; }
    getIcon() { return "message-circle-more"; }

    async onOpen() {
        this.renderStructure();
        // 稍微延迟,等待工作区加载
        setTimeout(() => {
            this.checkAndUpdate();
        }, 100);
    }

    // --- 1. 构建界面结构 (工具栏 + 列表区) ---
    renderStructure() {
        const container = this.containerEl.children[1];
        container.empty();
        // 添加容器类名,用于 CSS Flex 布局
        container.addClass("footnote-compass-view-container");

        // === 创建顶部工具栏 ===
        const toolbar = container.createDiv({ cls: "footnote-toolbar" });
        
        // 创建排序按钮
        this.sortBtn = toolbar.createDiv({ 
            cls: "footnote-btn", 
            attr: { "aria-label": "按序号重新排列" } 
        });
        
        // 固定使用一个图标 "arrow-down-0-1" (表示数字排序)
        setIcon(this.sortBtn, "arrow-down-0-1"); 

        // 绑定点击事件
        this.sortBtn.addEventListener("click", () => {
            this.toggleSort();
        });

        // === 创建列表滚动区 ===
        this.listRoot = container.createDiv({ cls: "footnote-list-root" });
    }

    // --- 2. 切换排序逻辑 ---
    toggleSort() {
        this.isSortByKey = !this.isSortByKey;
        
        // 只切换 class 类名,不换图标
        if (this.isSortByKey) {
            this.sortBtn.addClass("is-active");
            this.sortBtn.setAttribute("aria-label", "恢复文档顺序");
        } else {
            this.sortBtn.removeClass("is-active");
            this.sortBtn.setAttribute("aria-label", "按序号重新排列");
        }

        // 重新渲染列表
        this.renderRefList();
    }

    clearView(msg) {
        if (this.listRoot) {
            this.listRoot.empty();
            if(msg) this.listRoot.createDiv({ cls: "footnote-empty", text: msg });
        }
        this.cachedRefs = [];
    }

    // 智能寻找 Markdown 视图
    findBestMarkdownLeaf() {
        const active = this.app.workspace.activeLeaf;
        if (active && active.view.getViewType() === 'markdown') return active;

        if (this.lastActiveMarkdownView && this.lastActiveMarkdownView.leaf && this.lastActiveMarkdownView.leaf.view.getViewType() === 'markdown') {
            return this.lastActiveMarkdownView.leaf;
        }

        const recent = this.app.workspace.getMostRecentLeaf && this.app.workspace.getMostRecentLeaf();
        if (recent && recent.view.getViewType() === 'markdown') return recent;

        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. 白板 -> 清空
        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);
        }
    }

    // --- 3. 数据解析 (Parse) ---
    updateView(view) {
        if (!this.listRoot) return;
        
        let text = "";
        try {
            text = view.editor.getValue();
        } catch (e) { return; }
        
        const lines = text.split("\n");
        const definitionMap = new Map();
        const defRegex = /^\[\^([^\]]+)\]:\s*(.*)$/;

        // Step 1: 提取定义
        lines.forEach((line) => {
            const match = line.match(defRegex);
            if (match) definitionMap.set(match[1], match[2]);
        });

        // Step 2: 提取引用 (这里必须按文档顺序存储,以便 syncHighlightWithCursor 计算位置)
        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 // DOM 元素稍后在 renderRefList 中绑定
                });
            }
        });

        // Step 3: 渲染界面
        this.renderRefList();
        
        // Step 4: 渲染完立刻做一次高亮检查
        this.syncHighlightWithCursor(view);
    }

    // --- 4. 界面渲染 (Render) ---
    renderRefList() {
        if (!this.listRoot) return;
        this.listRoot.empty();

        if (this.cachedRefs.length === 0) {
            this.listRoot.createDiv({ cls: "footnote-empty", text: "当前文档没有脚注引用" });
            return;
        }

        // 决定展示顺序 (副本排序,不影响原数据)
        let displayRefs = [];
        
        if (this.isSortByKey) {
            // 自然排序: 1, 2, 10 (而不是 1, 10, 2)
            displayRefs = [...this.cachedRefs].sort((a, b) => {
                return a.key.localeCompare(b.key, undefined, { numeric: true, sensitivity: 'base' });
            });
        } else {
            // 默认: 按文档出现顺序
            displayRefs = this.cachedRefs;
        }

        const listContainer = this.listRoot.createDiv({ cls: "footnote-compass-container" });

        displayRefs.forEach((ref) => {
            const itemEl = listContainer.createDiv({ cls: "footnote-item" });
            // 【关键】将 DOM 绑定回数据对象,这样无论怎么排序,高亮逻辑都能通过 ref 找到 DOM
            ref.el = itemEl; 

            itemEl.createDiv({ cls: "footnote-key", text: `[^${ref.key}]` });
            
            // 【修改部分】在这里加上了序号前缀
            // 原来是: text: ref.content
            // 现在是: text: `${ref.key}. ${ref.content}`
            itemEl.createDiv({ cls: "footnote-content", text: `${ref.key}. ${ref.content}` });

// 点击跳转逻辑
            itemEl.addEventListener("click", () => {
                const view = this.lastActiveMarkdownView || this.findBestMarkdownLeaf()?.view;
                if (view) {
                    // 1. 激活叶面
                    this.app.workspace.setActiveLeaf(view.leaf, true, true);

                    // 2. 构造状态对象 (仿照 Quiet Outline)
                    // line: 目标行号
                    // cursor: 光标位置范围
                    const state = {
                        line: ref.line,
                        cursor: {
                            from: { line: ref.line, ch: ref.col },
                            to: { line: ref.line, ch: ref.col + 2 }
                        }
                    };

                    // 3. 使用 Obsidian 原生 API 跳转
                    // 这会自动处理滚动(通常是居中)、光标放置和底层的渲染刷新
                    view.setEphemeralState(state);

                    // 4. 手动高亮侧边栏对应项
                    this.highlightSpecificRef(ref);
                }
            });
        });
    }

    // --- 5. 光标高亮逻辑 ---
    syncHighlightWithCursor(view) {
        if (!this.cachedRefs.length || !view || view.getViewType() !== 'markdown') return;

        const cursor = view.editor.getCursor();
        const currentLine = cursor.line;

        let activeRef = null;

        // 使用 cachedRefs (有序的) 来查找当前行对应的脚注
        for (let i = 0; i < this.cachedRefs.length; i++) {
            const ref = this.cachedRefs[i];
            if (ref.line <= currentLine) {
                activeRef = ref;
            } else {
                break;
            }
        }

        if (activeRef) {
            this.highlightSpecificRef(activeRef);
        }
    }

    highlightSpecificRef(targetRef) {
        // 遍历所有引用,只高亮目标
        this.cachedRefs.forEach((ref) => {
            if (!ref.el) return; // 还没渲染出来
            
            if (ref === targetRef) {
                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(); }
        });

        // 事件防抖
        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);

        const handleCursorActivity = debounce(() => {
            const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOOTNOTE);
            leaves.forEach(leaf => {
                if (leaf.view instanceof FootnoteListView) {
                    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();
    }
};

manifest.json

{
	"id": "footnote-compass",
	"name": "Footnote Compass",
	"version": "1.0.0",
	"minAppVersion": "0.15.0",
	"description": "脚注",
	"author": "AI Assistant",
	"authorUrl": "",
	"isDesktopOnly": false
}

styles.css

/* 容器布局 */
.footnote-compass-view-container {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* 工具栏样式 (模仿 Quiet Outline) */
.footnote-toolbar {
    display: flex;
    padding: 6px 10px;
    border-bottom: 1px solid var(--background-modifier-border);
    gap: 8px;
    align-items: center;
}

/* 按钮基础样式 */
.footnote-btn {
    

    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 4px;
    color: var(--text-muted);
    cursor: pointer;
    transition: all 0.2s ease;
}

.footnote-btn:hover {
    color: var(--text-normal);
}

.footnote-btn.is-active {
    color: var(--text-on-accent);
}

/* 列表区域 */
.footnote-list-root {
    flex: 1;
    overflow-y: auto;
    padding: 10px;
}

.footnote-item {
    padding: 4px 8px;
    margin-bottom: 4px;
    border-radius: 4px;
    cursor: pointer;
    display: flex;
    gap: 8px;
}

.footnote-item:hover {
    background-color: var(--background-modifier-hover);
}

.footnote-item.is-active {
    background-color: var(--interactive-accent);
    color: var(--text-on-accent);
}

.footnote-key {
    font-weight: bold;
    color: var(--text-accent);
    flex-shrink: 0;
}
.footnote-item.is-active .footnote-key {
    color: inherit;
}

.footnote-content {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--text-muted);
}
.footnote-item.is-active .footnote-content {
    color: inherit;
}

/* 容器布局 */
.footnote-compass-view-container {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* 工具栏样式 */
.footnote-toolbar {
    display: flex;
    padding: 6px 10px;
    border-bottom: 1px solid var(--background-modifier-border);
    gap: 8px;
    align-items: center;
    flex-shrink: 0; /* 防止被压缩 */
}

/* 按钮基础样式 */
.footnote-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: 4px;
    color: var(--text-muted);
    cursor: pointer;
    transition: all 0.2s ease;
}

/* 图标尺寸修正 */
.footnote-btn svg {
    width: 16px;
    height: 16px;
}

.footnote-btn:hover {
    background-color: var(--background-modifier-hover);
    color: var(--text-normal);
}

/* 按钮激活态 */
.footnote-btn.is-active {
    color: var(--text-on-accent);
}

/* 列表区域 */
.footnote-list-root {
    flex: 1;
    overflow-y: auto;
    padding: 10px;
}

/* 列表项 */
.footnote-item {
    padding: 4px 8px;
    margin-bottom: 4px;
    border-radius: 4px;
    cursor: pointer;
    display: flex;
    gap: 8px;
}

.footnote-item:hover { background-color: var(--background-modifier-hover); }

.footnote-item.is-active {
    background-color: var(--interactive-accent);
    color: var(--text-on-accent);
}

.footnote-key { font-weight: bold; color: var(--text-accent); flex-shrink: 0; }
.footnote-item.is-active .footnote-key { color: inherit; }

.footnote-content {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-muted);
}
.footnote-item.is-active .footnote-content { color: inherit; }

兼容跳转吗?比如:点击某条标题链接时,会跳转到标题,而不是文件上次的阅读位置
footnote compass 有说明吗?解决了什么问题

点标题 会跳标题
解决的问题就是 不闪
因为不记录光标位置
不会从第一行闪到当前行

正经发个插件吧,放到github上。你说的光标乱闪那个是啥情况?那个记录光标位置的插件我有