UNPKG

vue-use-monaco

Version:

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

336 lines (332 loc) 11.6 kB
import { shikiToMonaco } from "@shikijs/monaco"; import * as monaco from "monaco-editor"; import { createHighlighter } from "shiki/bundle/full"; import { computed, onUnmounted, watch } from "vue"; import { useDark } from "@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 = 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 createHighlighter({ themes, langs: languages$1 }); shikiToMonaco(highlighter, monaco); } 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.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 = 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); 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.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.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 = watch(() => isDark.value, () => { monaco.editor.setTheme(currentTheme.value); }, { flush: "post" }); return editorView; } 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.setModelLanguage(model, processedCodeLanguage); monaco.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.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.setModelLanguage(model, language); } } else console.warn(`Language "${language}" is not registered. Available languages: ${languages$1.join(", ")}`); }, getCurrentTheme() { return currentTheme.value; }, getEditor() { return monaco.editor; }, getEditorView() { return editorView; } }; } //#endregion export { detectLanguage, useMonaco };