@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
JavaScript
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