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
JavaScript
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 };