UNPKG

@ym-han/monaco-error-lens

Version:

Error Lens for Monaco Editor, adapted from the VSCode Error Lens extension: makes diagnostics more visible with inline messages, line highlighting, and gutter icons. WARNING: Very experimental; use with caution.

751 lines (737 loc) 21.1 kB
const SEVERITY_LEVELS = { ERROR: 8, WARNING: 4, INFO: 2, HINT: 1 }; const CSS_CLASSES = { MESSAGE: "monaco-error-lens-message", LINE: "monaco-error-lens-line", GUTTER: "monaco-error-lens-gutter", ERROR: "error", WARNING: "warning", INFO: "info", HINT: "hint" }; const DEFAULT_OPTIONS = { enableInlineMessages: true, enableLineHighlights: true, enableGutterIcons: true, messageTemplate: "{message}", followCursor: "allLines", maxMessageLength: 200, maxMarkersPerLine: 3, severityFilter: [8, 4, 2, 1], // All severities updateDelay: 100, enabled: true, colors: { error: { background: "rgba(228, 85, 84, 0.15)", foreground: "#ff6464" }, warning: { background: "rgba(255, 148, 47, 0.15)", foreground: "#fa973a" }, info: { background: "rgba(0, 183, 228, 0.15)", foreground: "#00b7e4" }, hint: { background: "rgba(119, 136, 153, 0.15)", foreground: "#778899" } } }; function getSeverityClass(severity) { switch (severity) { case SEVERITY_LEVELS.ERROR: return "error"; case SEVERITY_LEVELS.WARNING: return "warning"; case SEVERITY_LEVELS.INFO: return "info"; case SEVERITY_LEVELS.HINT: return "hint"; default: return "unknown"; } } function formatMessage(marker, template = "{message}", maxLength) { let formatted = template.replace("{message}", marker.message ?? "").replace("{source}", marker.source ?? "").replace("{code}", marker.code ? String(marker.code) : ""); if (maxLength && formatted.length > maxLength) { formatted = `${formatted.substring(0, maxLength - 3)}...`; } return formatted; } function debounce(func, wait) { let timeout = null; const debouncedFn = (...args) => { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => func(...args), wait); }; const cancel = () => { if (timeout) { clearTimeout(timeout); timeout = null; } }; return { debouncedFn, cancel }; } function throttle(func, limit) { let inThrottle = false; return (...args) => { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } function mergeOptions(defaultOptions, userOptions = {}) { return { ...defaultOptions, ...userOptions, colors: { error: { background: userOptions.colors?.error?.background ?? defaultOptions.colors.error?.background ?? "rgba(228, 85, 84, 0.15)", foreground: userOptions.colors?.error?.foreground ?? defaultOptions.colors.error?.foreground ?? "#ff6464" }, warning: { background: userOptions.colors?.warning?.background ?? defaultOptions.colors.warning?.background ?? "rgba(255, 148, 47, 0.15)", foreground: userOptions.colors?.warning?.foreground ?? defaultOptions.colors.warning?.foreground ?? "#fa973a" }, info: { background: userOptions.colors?.info?.background ?? defaultOptions.colors.info?.background ?? "rgba(0, 183, 228, 0.15)", foreground: userOptions.colors?.info?.foreground ?? defaultOptions.colors.info?.foreground ?? "#00b7e4" }, hint: { background: userOptions.colors?.hint?.background ?? defaultOptions.colors.hint?.background ?? "rgba(119, 136, 153, 0.15)", foreground: userOptions.colors?.hint?.foreground ?? defaultOptions.colors.hint?.foreground ?? "#778899" } } }; } function sortMarkers(markers) { return markers.sort((a, b) => { if (a.startLineNumber !== b.startLineNumber) { return a.startLineNumber - b.startLineNumber; } return b.severity - a.severity; }); } function groupMarkersByLine(markers) { const grouped = /* @__PURE__ */ new Map(); for (const marker of markers) { const line = marker.startLineNumber; if (!grouped.has(line)) { grouped.set(line, []); } const lineMarkers = grouped.get(line); if (lineMarkers) { lineMarkers.push(marker); } } return grouped; } function countMarkersBySeverity(markers) { return markers.reduce( (acc, marker) => { switch (marker.severity) { case SEVERITY_LEVELS.ERROR: acc.errors++; break; case SEVERITY_LEVELS.WARNING: acc.warnings++; break; case SEVERITY_LEVELS.INFO: acc.infos++; break; case SEVERITY_LEVELS.HINT: acc.hints++; break; } return acc; }, { errors: 0, warnings: 0, infos: 0, hints: 0 } ); } class DecorationManager { constructor(editor, config) { this.editor = editor; this.config = config; this.decorations = []; this.lastDecorationCount = 0; } /** * Update all decorations based on current markers */ updateDecorations(markers) { const decorations = this.createDecorations(markers); this.decorations = this.editor.deltaDecorations(this.decorations, decorations); this.lastDecorationCount = decorations.length; } /** * Clear all decorations */ clearDecorations() { this.decorations = this.editor.deltaDecorations(this.decorations, []); this.lastDecorationCount = 0; } /** * Get the current decoration count */ getDecorationCount() { return this.lastDecorationCount; } /** * Create Monaco decorations from markers */ createDecorations(markers) { const decorations = []; const groupedMarkers = this.groupMarkersByLine(markers); for (const [lineNumber, lineMarkers] of groupedMarkers) { const primaryMarker = lineMarkers[0]; if (!primaryMarker) continue; const decoration = this.createLineDecoration(lineNumber, lineMarkers); if (decoration) { decorations.push(decoration); } } return decorations; } /** * Create a decoration for an entire line */ createLineDecoration(lineNumber, markers) { const primaryMarker = markers[0]; if (!primaryMarker) return null; const severityClass = getSeverityClass(primaryMarker.severity); const options = { stickiness: 1 // NeverGrowsWhenTypingAtEdges }; if (this.config.enableLineHighlights) { options.isWholeLine = true; options.className = `${CSS_CLASSES.LINE} ${CSS_CLASSES.LINE}-${severityClass}`; } if (this.config.enableInlineMessages) { const formattedMessage = formatMessage( primaryMarker, this.config.messageTemplate, this.config.maxMessageLength ); options.after = { content: formattedMessage, inlineClassName: `${CSS_CLASSES.MESSAGE} ${CSS_CLASSES.MESSAGE}-${severityClass}` }; } if (this.config.enableGutterIcons) { options.glyphMarginClassName = `${CSS_CLASSES.GUTTER} ${CSS_CLASSES.GUTTER}-${severityClass}`; } return { range: { startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 1 }, options }; } /** * Group markers by line number with sorted markers per line */ groupMarkersByLine(markers) { const grouped = /* @__PURE__ */ new Map(); for (const marker of markers) { const line = marker.startLineNumber; if (!grouped.has(line)) { grouped.set(line, []); } const lineMarkers = grouped.get(line); if (lineMarkers) { lineMarkers.push(marker); } } for (const lineMarkers of grouped.values()) { lineMarkers.sort((a, b) => b.severity - a.severity); } return grouped; } /** * Update configuration */ updateConfig(config) { this.config = config; } } class SimpleEventEmitter { constructor() { this.listeners = /* @__PURE__ */ new Map(); } /** * Add an event listener */ on(event, listener) { if (!this.listeners.has(event)) { this.listeners.set(event, /* @__PURE__ */ new Set()); } const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.add(listener); } return () => this.off(event, listener); } /** * Remove an event listener */ off(event, listener) { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.delete(listener); if (eventListeners.size === 0) { this.listeners.delete(event); } } } /** * Emit an event */ emit(event, data) { const eventListeners = this.listeners.get(event); if (eventListeners) { const listeners = Array.from(eventListeners); listeners.forEach((listener) => { try { listener(data); } catch { } }); } } /** * Remove all listeners */ removeAllListeners(event) { if (event) { this.listeners.delete(event); } else { this.listeners.clear(); } } } class MonacoErrorLens { constructor(editor, monaco, options = {}) { this.disposables = []; this.isDisposed = false; this.editor = editor; this.monaco = monaco; this.config = mergeOptions(DEFAULT_OPTIONS, options); this.eventEmitter = new SimpleEventEmitter(); this.decorationManager = new DecorationManager(this.editor, this.config); const { debouncedFn, cancel } = debounce( () => this.updateDecorationsInternal(), this.config.updateDelay ); this.updateDecorations = debouncedFn; this.cancelUpdateDecorations = cancel; this.initialize(); } /** * Initialize the Error Lens instance */ initialize() { if (!this.config.enabled) return; this.injectStyles(); this.setupEventListeners(); if (this.isMonacoInitialized()) { this.updateDecorations(); } } /** * Check if Monaco Editor is fully initialized with required APIs */ isMonacoInitialized() { return !!(typeof this.monaco?.editor?.getModelMarkers === "function" && typeof this.monaco?.editor?.onDidChangeMarkers === "function" && this.editor.getModel()); } /** * Inject CSS styles for Error Lens */ injectStyles() { if (typeof document === "undefined") return; const existingStyle = document.getElementById("monaco-error-lens-styles"); if (existingStyle) return; const style = document.createElement("style"); style.id = "monaco-error-lens-styles"; style.textContent = this.generateCSS(); document.head.appendChild(style); } /** * Generate CSS styles for Error Lens */ generateCSS() { const { colors } = this.config; const customProps = this.generateCustomProperties(colors); return ` ${customProps} /* Monaco Error Lens Styles */ .monaco-error-lens-message { opacity: 0.8; font-style: italic; margin-left: 1ch; pointer-events: none; } .monaco-error-lens-line-error { background-color: var(--monaco-error-lens-error-bg, rgba(228, 85, 84, 0.15)); } .monaco-error-lens-line-warning { background-color: var(--monaco-error-lens-warning-bg, rgba(255, 148, 47, 0.15)); } .monaco-error-lens-line-info { background-color: var(--monaco-error-lens-info-bg, rgba(0, 183, 228, 0.15)); } .monaco-error-lens-line-hint { background-color: var(--monaco-error-lens-hint-bg, rgba(119, 136, 153, 0.15)); } .monaco-error-lens-message-error { color: var(--monaco-error-lens-error-fg, #ff6464); } .monaco-error-lens-message-warning { color: var(--monaco-error-lens-warning-fg, #fa973a); } .monaco-error-lens-message-info { color: var(--monaco-error-lens-info-fg, #00b7e4); } .monaco-error-lens-message-hint { color: var(--monaco-error-lens-hint-fg, #778899); } .monaco-error-lens-gutter-error::before { content: '✖'; font-weight: bold; font-size: 10px; display: flex; align-items: center; justify-content: center; width: 14px; height: 14px; border-radius: 50%; background-color: var(--monaco-error-lens-error-fg, #ff6464); color: white; line-height: 1; margin: auto; } .monaco-error-lens-gutter-warning::before { content: '⚠'; color: var(--monaco-error-lens-warning-fg, #fa973a); font-weight: bold; font-size: 12px; display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; margin: auto; } .monaco-error-lens-gutter-info::before { content: 'ⓘ'; color: var(--monaco-error-lens-info-fg, #00b7e4); font-weight: bold; font-size: 12px; display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; margin: auto; } .monaco-error-lens-gutter-hint::before { content: '●'; color: var(--monaco-error-lens-hint-fg, #778899); font-weight: bold; font-size: 12px; display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; margin: auto; opacity: 0.7; } `; } /** * Generate CSS custom properties for user-defined colors */ generateCustomProperties(colors) { const props = []; if (colors.error?.background) { props.push(`--monaco-error-lens-error-bg: ${colors.error.background};`); } if (colors.error?.foreground) { props.push(`--monaco-error-lens-error-fg: ${colors.error.foreground};`); } if (colors.warning?.background) { props.push(`--monaco-error-lens-warning-bg: ${colors.warning.background};`); } if (colors.warning?.foreground) { props.push(`--monaco-error-lens-warning-fg: ${colors.warning.foreground};`); } if (colors.info?.background) { props.push(`--monaco-error-lens-info-bg: ${colors.info.background};`); } if (colors.info?.foreground) { props.push(`--monaco-error-lens-info-fg: ${colors.info.foreground};`); } if (colors.hint?.background) { props.push(`--monaco-error-lens-hint-bg: ${colors.hint.background};`); } if (colors.hint?.foreground) { props.push(`--monaco-error-lens-hint-fg: ${colors.hint.foreground};`); } return props.length > 0 ? `:root { ${props.join("\n ")} }` : ""; } /** * Set up Monaco editor event listeners */ setupEventListeners() { if (!this.isMonacoInitialized()) return; const markerListener = this.monaco.editor.onDidChangeMarkers( (resources) => { const model = this.editor.getModel(); if (model && resources.some((resource) => resource.toString() === model.uri.toString())) { this.updateDecorations(); } } ); this.disposables.push(markerListener); if (this.config.followCursor === "activeLine") { const cursorListener = this.editor.onDidChangeCursorPosition(() => { this.updateDecorations(); }); this.disposables.push(cursorListener); } const modelListener = this.editor.onDidChangeModel(() => { this.clearDecorations(); this.updateDecorations(); }); this.disposables.push(modelListener); } /** * Update decorations based on current markers */ updateDecorationsInternal() { if (this.isDisposed || !this.config.enabled) return; const model = this.editor.getModel(); if (!model) return; if (!this.isMonacoInitialized()) return; try { const markers = this.monaco.editor.getModelMarkers({ resource: model.uri }); const markersArray = markers ?? []; const filteredMarkers = this.filterMarkers(markersArray); this.decorationManager.updateDecorations(filteredMarkers); this.eventEmitter.emit("decorations-updated", { decorationCount: this.decorationManager.getDecorationCount(), markerCount: markersArray.length, timestamp: /* @__PURE__ */ new Date() }); } catch (error) { this.eventEmitter.emit("error", { error: error instanceof Error ? error : new Error(String(error)), context: "updateDecorationsInternal", timestamp: /* @__PURE__ */ new Date() }); } } /** * Filter markers based on configuration */ filterMarkers(markers) { let filtered = [...markers]; filtered = filtered.filter( (marker) => this.config.severityFilter.includes(marker.severity) ); filtered = sortMarkers(filtered); if (this.config.followCursor === "activeLine") { const currentLine = this.editor.getPosition()?.lineNumber; if (currentLine) { filtered = filtered.filter((marker) => marker.startLineNumber === currentLine); } } if (this.config.maxMarkersPerLine > 0) { const lineGroups = groupMarkersByLine(filtered); filtered = []; for (const lineMarkers of lineGroups.values()) { const limited = lineMarkers.slice(0, this.config.maxMarkersPerLine); filtered.push(...limited); } } return filtered; } /** * Clear all decorations */ clearDecorations() { this.decorationManager.clearDecorations(); } /** * Update configuration options */ updateOptions(newOptions) { const oldEnabled = this.config.enabled; this.config = { ...this.config, ...mergeOptions(this.config, newOptions) }; this.decorationManager.updateConfig(this.config); if (newOptions.updateDelay !== void 0) { this.cancelUpdateDecorations(); const { debouncedFn, cancel } = debounce( () => this.updateDecorationsInternal(), this.config.updateDelay ); this.updateDecorations = debouncedFn; this.cancelUpdateDecorations = cancel; } if (newOptions.enabled !== void 0) { if (newOptions.enabled && !oldEnabled) { this.initialize(); this.eventEmitter.emit("status-changed", { enabled: true, timestamp: /* @__PURE__ */ new Date() }); } else if (!newOptions.enabled && oldEnabled) { this.clearDecorations(); this.eventEmitter.emit("status-changed", { enabled: false, timestamp: /* @__PURE__ */ new Date() }); } } else { this.updateDecorations(); } this.eventEmitter.emit("config-updated", { config: this.config, timestamp: /* @__PURE__ */ new Date() }); } /** * Enable the Error Lens */ enable() { if (!this.config.enabled) { this.config.enabled = true; this.initialize(); } } /** * Disable the Error Lens */ disable() { if (this.config.enabled) { this.config.enabled = false; this.clearDecorations(); } } /** * Toggle Error Lens on/off */ toggle() { if (this.config.enabled) { this.disable(); } else { this.enable(); } return this.config.enabled; } /** * Force update decorations immediately */ refresh() { this.updateDecorationsInternal(); } /** * Get current configuration */ getConfig() { return { ...this.config }; } /** * Get the event emitter for listening to Error Lens events */ getEventEmitter() { return this.eventEmitter; } /** * Check if Error Lens is currently active */ isActive() { return !this.isDisposed && this.config.enabled; } /** * Dispose of the Error Lens instance */ dispose() { if (this.isDisposed) return; this.isDisposed = true; this.cancelUpdateDecorations(); this.clearDecorations(); this.disposables.forEach((disposable) => disposable.dispose()); this.disposables = []; this.eventEmitter.removeAllListeners(); } } const VERSION = "1.0.0"; function isMonacoAvailable() { return typeof window !== "undefined" && !!window.monaco; } function getMonaco() { return typeof window !== "undefined" ? window.monaco ?? null : null; } function createErrorLens(editor, monaco, options) { return new MonacoErrorLens(editor, monaco, options); } function setupErrorLens(editor, monaco, options) { const errorLens = new MonacoErrorLens(editor, monaco, { enableInlineMessages: true, enableLineHighlights: true, enableGutterIcons: true, followCursor: "allLines", ...options }); return errorLens; } function createMinimalErrorLens(editor, monaco, options) { return new MonacoErrorLens(editor, monaco, { enableInlineMessages: true, enableLineHighlights: false, enableGutterIcons: false, maxMarkersPerLine: 1, followCursor: "activeLine", ...options }); } export { CSS_CLASSES, DEFAULT_OPTIONS, DecorationManager, MonacoErrorLens, SEVERITY_LEVELS, SimpleEventEmitter, VERSION, countMarkersBySeverity, createErrorLens, createMinimalErrorLens, debounce, MonacoErrorLens as default, formatMessage, getMonaco, getSeverityClass, groupMarkersByLine, isMonacoAvailable, mergeOptions, setupErrorLens, sortMarkers, throttle }; //# sourceMappingURL=index.js.map