/**
 * Monaco Editor 封装模块
 *
 *  - 全局单例 MonacoEditorManager,切换文件时 setModel() 复用实例,不销毁重建。
 *  - 中文 NLS:通过 require.config 的 availableLanguages 加载 Monaco 自带的 zh-cn 包。
 *  - 行高亮:点击行后高亮该行并将 #L{n} 写入 URL,刷新后自动恢复。
 */

import { monacoTheme } from './util.js';

// ─────────────────────────────────────────────────────────────────────────────
//  常量
// ─────────────────────────────────────────────────────────────────────────────

const MONACO_VS = 'https://dpangzi.com/library/monaco-editor/min/vs';
const MONACO_FONT_FAMILY = '"Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, "Cascadia Mono", "Courier New", monospace';
const MONACO_FONT_LIGATURES = true;

/**
 * 服务端语言 ID / Prism 语言 ID → Monaco 语言 ID
 */
const LANG_MAP = {
    // Prism 别名
    markup: 'html',
    sh: 'shell',
    bash: 'shell',
    ps1: 'powershell',
    // 服务端配置值 → Monaco 语言 ID
    docker: 'dockerfile',   // ExtensionToLanguage: "dockerfile": "docker"
    ignore: 'plaintext',    // .dockerignore / .gitignore
    git: 'plaintext',    // .gitattributes
    sln: 'ini',          // .sln 格式近似 INI
    text: 'plaintext',    // .txt
};

/** Monaco 语言 ID → 中文显示名称 */
const LANG_DISPLAY = {
    csharp: 'C#',
    javascript: 'JavaScript',
    typescript: 'TypeScript',
    html: 'HTML',
    css: 'CSS',
    less: 'Less',
    scss: 'SCSS',
    json: 'JSON',
    xml: 'XML',
    sql: 'SQL',
    python: 'Python',
    java: 'Java',
    cpp: 'C++',
    c: 'C',
    go: 'Go',
    rust: 'Rust',
    php: 'PHP',
    ruby: 'Ruby',
    shell: 'Shell',
    powershell: 'PowerShell',
    yaml: 'YAML',
    markdown: 'Markdown',
    razor: 'Razor',
    ini: 'INI',
    dockerfile: 'Dockerfile',
    plaintext: '纯文本',
};

// ─────────────────────────────────────────────────────────────────────────────
//  MonacoEditorManager
// ─────────────────────────────────────────────────────────────────────────────

class MonacoEditorManager {

    constructor() {
        /** @type {Promise<typeof import('monaco-editor')> | null} */
        this._loadPromise = null;
        /** @type {import('monaco-editor').editor.IStandaloneCodeEditor | null} */
        this._editor = null;
        /** @type {HTMLElement | null} */
        this._hostEl = null;
        /** @type {import('monaco-editor').editor.ITextModel | null} */
        this._currentModel = null;
        /** @type {import('monaco-editor').editor.IEditorDecorationsCollection | null} */
        this._decorations = null;
        /** @type {typeof import('monaco-editor') | null} */
        this._monaco = null;
        /** @type {import('monaco-editor').IDisposable | null} 鼠标点击监听器 */
        this._clickDisposable = null;
    }

    // ── 公开 API ──────────────────────────────────────────────────────────────

    /**
     * 在指定容器内显示代码。
     * @param {HTMLElement} hostEl  挂载容器(.monaco-host 元素)
     * @param {string}      code    源代码文本
     * @param {string}      lang    Prism 语言 ID,自动映射为 Monaco 语言 ID
     */
    async show(hostEl, code, lang) {
        const monaco = await this._loadMonaco();
        this._monaco = monaco;
        const monacoLang = this._toMonacoLang(lang);

        // 根据行数自适应高度(避免 Monaco 渲染到 0px 容器)
        this._setHeight(hostEl, code);

        // ── 创建或重新挂载 Editor ──────────────────────────────────────────
        if (!this._editor || this._hostEl !== hostEl) {
            if (this._editor) {
                this._clickDisposable?.dispose();
                this._editor.dispose();
                this._editor = null;
            }
            if (this._currentModel) {
                this._currentModel.dispose();
                this._currentModel = null;
            }

            this._editor = monaco.editor.create(hostEl, {
                model: null,   // 稍后 setModel
                readOnly: true,
                theme: monacoTheme(),
                minimap: { enabled: false },
                scrollBeyondLastLine: false,
                fontSize: 13,
                fontFamily: MONACO_FONT_FAMILY,
                fontLigatures: MONACO_FONT_LIGATURES,
                fontWeight: 'normal',
                lineNumbers: 'on',
                automaticLayout: true,
                renderWhitespace: 'none',
                contextmenu: false,
                wordWrap: 'off',
                smoothScrolling: true,
                padding: { top: 8, bottom: 8 },
            });

            this._decorations = this._editor.createDecorationsCollection();
            this._clickDisposable = this._bindLineClick();
            this._hostEl = hostEl;
        }

        // ── 恢复 URL Hash 行号(在 setModel 前解析,避免遗漏事件) ────────
        const hashLine = this._parseHashLine();
        if (hashLine) {
            // onDidContentSizeChange 在 setModel 完成内容测量后触发。
            // 注册一次性监听,滚动完成后立即销毁,避免后续切换时重复触发。
            const disposable = this._editor.onDidContentSizeChange(() => {
                disposable.dispose();
                this._editor.revealLineInCenter(hashLine);
            });
        }

        // ── 替换 Model(核心:避免 dispose+create)────────────────────────
        const oldModel = this._currentModel;
        this._currentModel = monaco.editor.createModel(code, monacoLang);
        this._editor.setModel(this._currentModel);   // ← 触发 onDidContentSizeChange
        if (oldModel) oldModel.dispose();

        // Chromium 下偶发字符宽度测量偏差:切换模型后主动重新测量一次。
        this._stabilizeTextMetrics();

        // decoration 在 setModel 后立即生效,无需等待布局
        if (hashLine) this._highlightLine(hashLine);

        // ── 同步主题 ──────────────────────────────────────────────────────
        monaco.editor.setTheme(monacoTheme());

        // ── 语言徽章 ──────────────────────────────────────────────────────
        this._renderLangBadge(hostEl, monacoLang);
    }

    /** 销毁 Editor 实例(切换到非代码页面时调用)。 */
    destroy() {
        this._clickDisposable?.dispose();
        this._clickDisposable = null;

        if (this._currentModel) {
            this._currentModel.dispose();
            this._currentModel = null;
        }
        if (this._editor) {
            this._editor.dispose();
            this._editor = null;
        }
        this._hostEl = null;
        this._decorations = null;
        this._monaco = null;
    }

    // ── 内部方法 ──────────────────────────────────────────────────────────────

    /**
     * 动态加载 Monaco AMD loader
     * @returns {Promise<typeof import('monaco-editor')>}
     */
    _loadMonaco() {
        if (this._loadPromise) return this._loadPromise;

        this._loadPromise = new Promise((resolve, reject) => {
            const bootstrap = () => {
                window.require.config({
                    paths: { vs: MONACO_VS },
                    // 中文 NLS:Monaco 会自动加载 *.nls.zh-cn.js 文件
                    'vs/nls': { availableLanguages: { '*': 'zh-cn' } },
                });
                window.require(['vs/editor/editor.main'], () => resolve(window.monaco), reject);
            };

            if (window.require?.config) {
                bootstrap();
                return;
            }

            const s = document.createElement('script');
            s.src = `${MONACO_VS}/loader.js`;
            s.onload = bootstrap;
            s.onerror = () => {
                this._loadPromise = null; // 允许下次重试
                reject(new Error('Monaco loader.js 加载失败'));
            };
            document.head.appendChild(s);
        });

        return this._loadPromise;
    }

    /**
     * 根据代码行数自适应设置容器高度。
     * @param {HTMLElement} hostEl
     * @param {string}      code
     */
    _setHeight(hostEl, code) {
        const lineCount = code.split('\n').length;
        const lineHeight = 19;
        const maxHeight = Math.floor(window.innerHeight * 0.8);
        const minHeight = 300;
        const height = Math.max(minHeight, Math.min(lineCount * lineHeight + 40, maxHeight));
        hostEl.style.height = `${height}px`;
    }

    /** Prism 语言 ID → Monaco 语言 ID */
    _toMonacoLang(lang) {
        return LANG_MAP[lang] ?? lang;
    }

    /** Monaco 语言 ID → 中文显示名称 */
    _getDisplayName(monacoLang) {
        return LANG_DISPLAY[monacoLang] ?? monacoLang;
    }

    /**
     * 在容器右下角渲染语言徽章。
     * @param {HTMLElement} hostEl
     * @param {string}      monacoLang
     */
    _renderLangBadge(hostEl, monacoLang) {
        let badge = hostEl.querySelector('.monaco-lang-badge');
        if (!badge) {
            badge = document.createElement('div');
            badge.className = 'monaco-lang-badge';
            hostEl.appendChild(badge);
        }
        badge.textContent = this._getDisplayName(monacoLang);
    }

    // ── 行高亮 & URL Hash ─────────────────────────────────────────────────────

    /**
     * 绑定编辑器的鼠标点击事件。
     * @returns {import('monaco-editor').IDisposable}
     */
    _bindLineClick() {
        return this._editor.onMouseDown((e) => {
            const pos = e.target?.position;
            if (!pos) return;
            this._highlightLine(pos.lineNumber);
            // 更新 URL hash,不触发页面跳转
            const newUrl = `${window.location.pathname}${window.location.search}#L${pos.lineNumber}`;
            window.history.replaceState(null, '', newUrl);
        });
    }

    /**
     * 解析 URL Hash 中的行号(格式 #L42),无效时返回 null。
     * @returns {number | null}
     */
    _parseHashLine() {
        const match = window.location.hash.match(/^#L(\d+)$/);
        if (!match) return null;
        const n = parseInt(match[1], 10);
        return n >= 1 ? n : null;
    }

    /**
     * 高亮指定行(替换之前的高亮)。
     * @param {number} lineNumber
     */
    _highlightLine(lineNumber) {
        if (!this._editor || !this._monaco || !this._decorations) return;

        this._decorations.set([{
            range: new this._monaco.Range(lineNumber, 1, lineNumber, 1),
            options: {
                isWholeLine: true,
                className: 'monaco-highlighted-line',
                linesDecorationsClassName: 'monaco-highlighted-line-gutter',
            },
        }]);
    }

    /**
     * 强制编辑器重新进行字体与布局测量,修复 Chromium 选区/光标偶发偏移。
     */
    _stabilizeTextMetrics() {
        if (!this._editor) return;

        this._editor.updateOptions({
            fontFamily: MONACO_FONT_FAMILY,
            fontLigatures: MONACO_FONT_LIGATURES,
        });
        this._editor.layout();

        // 等待字体就绪后再做一次,避免 Web 字体异步加载导致的宽度漂移。
        const applyFinalLayout = () => {
            if (!this._editor) return;
            this._editor.layout();
        };

        if (document.fonts?.ready) {
            document.fonts.ready.then(() => {
                requestAnimationFrame(applyFinalLayout);
            });
        } else {
            requestAnimationFrame(applyFinalLayout);
        }
    }
}

const _manager = new MonacoEditorManager();

/**
 * @param {HTMLElement} hostEl
 * @param {string}      code
 * @param {string}      lang
 */
export const show = (hostEl, code, lang) => _manager.show(hostEl, code, lang);
export const destroy = () => _manager.destroy();
评论加载中...