UNPKG

vue-use-monaco

Version:

A Vue library for integrating Monaco Editor with Shiki syntax highlighting, supporting real-time updates.

360 lines (355 loc) 12.8 kB
//#region rolldown:runtime var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion const __shikijs_monaco = __toESM(require("@shikijs/monaco")); const monaco_editor = __toESM(require("monaco-editor")); const shiki_bundle_full = __toESM(require("shiki/bundle/full")); const vue = __toESM(require("vue")); const __vueuse_core = __toESM(require("@vueuse/core")); //#region src/code.detect.ts /** * Language detection definitions */ const languages = [ [ "bash", [/#!(\/usr)?\/bin\/bash/g, 500], [/\b(if|elif|then|fi|echo)\b|\$/g, 10] ], [ "html", [/<\/?[a-z-][^\n>]*>/g, 10], [/^\s+<!DOCTYPE\s+html/g, 500] ], ["http", [/^(GET|HEAD|POST|PUT|DELETE|PATCH|HTTP)\b/g, 500]], ["js", [/\b(console|await|async|function|export|import|this|class|for|let|const|map|join|require)\b/g, 10]], ["ts", [/\b(console|await|async|function|export|import|this|class|for|let|const|map|join|require|implements|interface|namespace)\b/g, 10]], ["py", [/\b(def|print|class|and|or|lambda)\b/g, 10]], ["sql", [/\b(SELECT|INSERT|FROM)\b/g, 50]], [ "pl", [/#!(\/usr)?\/bin\/perl/g, 500], [/\b(use|print)\b|\$/g, 10] ], ["lua", [/#!(\/usr)?\/bin\/lua/g, 500]], ["make", [/\b(ifneq|endif|if|elif|then|fi|echo|.PHONY|^[a-z]+ ?:$)\b|\$/gm, 10]], ["uri", [/https?:|mailto:|tel:|ftp:/g, 30]], ["css", [/^(@import|@page|@media|(\.|#)[a-z]+)/gm, 20]], [ "diff", [/^[+><-]/gm, 10], [/^@@[-+,0-9 ]+@@/gm, 25] ], [ "md", [/^(>|\t\*|\t\d+.|#{1,6} |-\s+|\*\s+)/gm, 25], [/\[.*\](.*)/g, 10] ], ["docker", [/^(FROM|ENTRYPOINT|RUN)/gm, 500]], [ "xml", [/<\/?[a-z-][^\n>]*>/g, 10], [/^<\?xml/g, 500] ], ["c", [/#include\b|\bprintf\s+\(/g, 100]], ["rs", [/^\s+(use|fn|mut|match)\b/gm, 100]], ["go", [/\b(func|fmt|package)\b/g, 100]], ["java", [/^import\s+java/gm, 500]], ["asm", [/^(section|global main|extern|\t(call|mov|ret))/gm, 100]], ["css", [/^(@import|@page|@media|(\.|#)[a-z]+)/gm, 20]], ["json", [/\b(true|false|null|\{\})\b|"[^"]+":/g, 10]], ["yaml", [/^(\s+)?[a-z][a-z0-9]*:/gim, 10]], [ "toml", [/^\s*\[.*\]\s*$/gm, 100], [/^\s*[\w-]+ *= */gm, 20] ], [ "mermaid", [/^(graph|flowchart|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|pie|mindmap)/gm, 500], [/\b(-->|--o|--x|=>|\[\]|[{}])\b/g, 10] ] ]; /** * Try to find the language the given code belongs to * * @param {string} code The code to analyze * @returns {CodeLanguage} The detected language of the code */ function detectLanguage(code) { var _languages$map$filter; return ((_languages$map$filter = languages.map(([lang, ...features]) => [lang, features.reduce((acc, [match, score]) => acc + [...code.matchAll(match)].length * score, 0)]).filter(([, score]) => score > 20).sort((a, b) => b[1] - a[1])[0]) === null || _languages$map$filter === void 0 ? void 0 : _languages$map$filter[0]) || "plain"; } function processedLanguage(language) { if (/^(?:shellscript|bash|sh|shell|zsh)/i.test(language)) return "shell"; if (/^(?:powershell|ps1?)/i.test(language)) return "powershell"; return language.split(":")[0]; } //#endregion //#region src/isDark.ts const isDark = (0, __vueuse_core.useDark)(); //#endregion //#region src/index.ts let themesRegistered = false; let languagesRegistered = false; let themeRegisterPromise = null; let currentThemes = []; let currentLanguages = []; const disposals = []; async function registerMonacoThemes(themes, languages$1) { registerMonacoLanguages(languages$1); if (themesRegistered && JSON.stringify(themes) === JSON.stringify(currentThemes) && JSON.stringify(languages$1) === JSON.stringify(currentLanguages)) return; themesRegistered = true; currentThemes = themes; currentLanguages = languages$1; const highlighter = await (0, shiki_bundle_full.createHighlighter)({ themes, langs: languages$1 }); (0, __shikijs_monaco.shikiToMonaco)(highlighter, monaco_editor); } function registerMonacoLanguages(languages$1) { if (languagesRegistered && JSON.stringify(languages$1) === JSON.stringify(currentLanguages)) return; languagesRegistered = true; currentLanguages = languages$1; for (const lang of languages$1) monaco_editor.languages.register({ id: lang }); } /** * useMonaco 组合式函数 * * 提供 Monaco 编辑器的创建、销毁、内容/主题/语言更新等能力。 * 支持主题自动切换、语言高亮、代码更新等功能。 * * @param {MonacoOptions} [monacoOptions] - 编辑器初始化配置,支持 Monaco 原生配置及扩展项 * @param {number} [monacoOptions.MAX_HEIGHT] - 编辑器最大高度(像素) * @param {boolean} [monacoOptions.readOnly] - 是否为只读模式 * @param {MonacoTheme[]} [monacoOptions.themes] - 主题数组,至少包含两个主题:[暗色主题, 亮色主题] * @param {MonacoLanguage[]} [monacoOptions.languages] - 支持的编程语言数组 * @param {string} [monacoOptions.theme] - 初始主题名称 * @param {boolean} [monacoOptions.isCleanOnBeforeCreate] - 是否在创建前清理之前注册的资源, 默认为 true * @param {(monaco: typeof import('monaco-editor')) => monaco.IDisposable[]} [monacoOptions.onBeforeCreate] - 编辑器创建前的钩子函数 * * @returns {{ * createEditor: (container: HTMLElement, code: string, language: string) => Promise<monaco.editor.IStandaloneCodeEditor>, * cleanupEditor: () => void, * updateCode: (newCode: string, codeLanguage: string) => void, * setTheme: (theme: MonacoTheme) => void, * setLanguage: (language: MonacoLanguage) => void, * getCurrentTheme: () => string, * getEditor: () => typeof monaco.editor, * getEditorView: () => monaco.editor.IStandaloneCodeEditor | null * }} 返回对象包含以下方法和属性: * * @property {Function} createEditor - 创建并挂载 Monaco 编辑器到指定容器 * @property {Function} cleanupEditor - 销毁编辑器并清理容器 * @property {Function} updateCode - 更新编辑器内容和语言,必要时滚动到底部 * @property {Function} setTheme - 切换编辑器主题 * @property {Function} setLanguage - 切换编辑器语言 * @property {Function} getCurrentTheme - 获取当前主题名称 * @property {Function} getEditor - 获取 Monaco 的静态 editor 对象(用于静态方法调用) * @property {Function} getEditorView - 获取当前编辑器实例 * * @throws {Error} 当主题数组不是数组或长度小于2时抛出错误 * * @example * ```typescript * import { useMonaco } from 'vue-use-monaco' * * const { createEditor, updateCode, setTheme } = useMonaco({ * themes: ['vitesse-dark', 'vitesse-light'], * languages: ['javascript', 'typescript'], * readOnly: false * }) * * // 创建编辑器 * const editor = await createEditor(containerRef.value, 'console.log("hello")', 'javascript') * * // 更新代码 * updateCode('console.log("world")', 'javascript') * * // 切换主题 * setTheme('vitesse-light') * ``` */ function useMonaco(monacoOptions = {}) { if (monacoOptions.isCleanOnBeforeCreate ?? true) disposals.forEach((d) => d.dispose()); let editorView = null; const themes = monacoOptions.themes ?? ["vitesse-dark", "vitesse-light"]; if (!Array.isArray(themes) || themes.length < 2) throw new Error("Monaco themes must be an array with at least two themes: [darkTheme, lightTheme]"); const languages$1 = monacoOptions.languages ?? [ "jsx", "tsx", "vue", "csharp", "python", "java", "kotlin", "c", "cpp", "rust", "go", "powershell", "sql", "yaml", "json", "html", "css", "javascript", "typescript", "css", "markdown", "xml", "yaml", "toml", "dockerfile", "kotlin", "objective-c", "objective-cpp", "php", "ruby", "scala", "svelte", "swift", "erlang", "angular-html", "angular-ts", "dart", "lua", "mermaid", "cmake", "nginx" ]; const MAX_HEIGHT = monacoOptions.MAX_HEIGHT ?? 500; let lastContainer = null; const currentTheme = (0, vue.computed)(() => isDark.value ? typeof themes[0] === "string" ? themes[0] : themes[0].name : typeof themes[1] === "string" ? themes[1] : themes[1].name); let themeWatcher = null; if (monacoOptions.onBeforeCreate) { const disposal = monacoOptions.onBeforeCreate(monaco_editor); if (disposal) disposals.push(...disposal); } async function createEditor(container, code, language) { cleanupEditor(); lastContainer = container; if (themeWatcher) { themeWatcher(); themeWatcher = null; } if (!themeRegisterPromise) themeRegisterPromise = registerMonacoThemes(themes, languages$1); await themeRegisterPromise; container.style.overflow = "auto"; container.style.maxHeight = `${MAX_HEIGHT}px`; const defaultScrollbar = { verticalScrollbarSize: 8, horizontalScrollbarSize: 8, handleMouseWheel: true, alwaysConsumeMouseWheel: false }; editorView = monaco_editor.editor.create(container, { value: code, language, theme: currentTheme.value, scrollBeyondLastLine: false, minimap: { enabled: false }, automaticLayout: true, readOnly: monacoOptions.readOnly ?? true, contextmenu: false, scrollbar: { ...defaultScrollbar, ...monacoOptions.scrollbar || {} }, ...monacoOptions }); function updateHeight() { var _getModel; const lineCount$1 = ((_getModel = editorView.getModel()) === null || _getModel === void 0 ? void 0 : _getModel.getLineCount()) ?? 1; const lineHeight = editorView.getOption(monaco_editor.editor.EditorOption.lineHeight); const height = Math.min(lineCount$1 * lineHeight + 16, MAX_HEIGHT); container.style.height = `${height}px`; } updateHeight(); editorView.onDidChangeModelContent(updateHeight); const model = editorView.getModel(); const lineCount = (model === null || model === void 0 ? void 0 : model.getLineCount()) ?? 1; if (container.scrollHeight >= MAX_HEIGHT) editorView.revealLine(lineCount); themeWatcher = (0, vue.watch)(() => isDark.value, () => { monaco_editor.editor.setTheme(currentTheme.value); }, { flush: "post" }); return editorView; } (0, vue.onUnmounted)(cleanupEditor); function cleanupEditor() { if (editorView) { editorView.dispose(); editorView = null; } if (lastContainer) { lastContainer.innerHTML = ""; lastContainer = null; } if (themeWatcher) { themeWatcher(); themeWatcher = null; } } return { createEditor, cleanupEditor, updateCode(newCode, codeLanguage) { var _editorView$getContai; if (!editorView) return; const processedCodeLanguage = processedLanguage(codeLanguage); const model = editorView.getModel(); if (!model) return; if (model.getLanguageId() !== processedCodeLanguage) monaco_editor.editor.setModelLanguage(model, processedCodeLanguage); monaco_editor.editor.setTheme(currentTheme.value); const prevLineCount = model.getLineCount(); model.setValue(newCode); const newLineCount = model.getLineCount(); const container = (_editorView$getContai = editorView.getContainerDomNode) === null || _editorView$getContai === void 0 ? void 0 : _editorView$getContai.call(editorView); if (newLineCount !== prevLineCount && container && container.scrollHeight >= MAX_HEIGHT) editorView.revealLine(newLineCount); }, setTheme(theme) { if (themes.includes(theme)) monaco_editor.editor.setTheme(typeof theme === "string" ? theme : theme.name); else console.warn(`Theme "${theme}" is not registered. Available themes: ${themes.join(", ")}`); }, setLanguage(language) { if (languages$1.includes(language)) { if (editorView) { const model = editorView.getModel(); if (model && model.getLanguageId() !== language) monaco_editor.editor.setModelLanguage(model, language); } } else console.warn(`Language "${language}" is not registered. Available languages: ${languages$1.join(", ")}`); }, getCurrentTheme() { return currentTheme.value; }, getEditor() { return monaco_editor.editor; }, getEditorView() { return editorView; } }; } //#endregion exports.detectLanguage = detectLanguage; exports.useMonaco = useMonaco;