@ks89/ngx-codemirror6
Version:
Codemirror 6 library for Angular
1,061 lines (1,058 loc) • 69.8 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, inject, ErrorHandler, forwardRef, ViewChild, Output, Input, Component, NgModule } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, indentUnit, indentOnInput, codeFolding, foldGutter, foldKeymap, foldState, foldCode, unfoldCode, toggleFold, foldAll, unfoldAll, foldedRanges } from '@codemirror/language';
import { GutterMarker, EditorView, placeholder, lineNumbers, highlightActiveLine, highlightActiveLineGutter, keymap, highlightSpecialChars, highlightWhitespace, highlightTrailingWhitespace, scrollPastEnd, drawSelection, gutter, gutters, showPanel, tooltips, showTooltip, hoverTooltip, Decoration, hasHoverTooltips, activateHover, closeHoverTooltips, repositionTooltips, getPanel } from '@codemirror/view';
import { EditorState, EditorSelection, Compartment, StateEffect, StateField } from '@codemirror/state';
class BreakpointMarker extends GutterMarker {
toDOM() {
const marker = document.createElement('span');
marker.className = 'cm-breakpoint-marker';
marker.setAttribute('aria-hidden', 'true');
return marker;
}
}
const breakpointMarker = new BreakpointMarker();
/** Creates the EditorState.readOnly facet extension from readOnly and disabled state. */
function createReadOnlyExtensions(readOnly, disabled) {
return [EditorState.readOnly.of(readOnly || disabled)];
}
/** Creates the EditorView.editable facet extension from readOnly, editable override, and disabled state. */
function createEditableExtensions(readOnly, editable, disabled) {
return [EditorView.editable.of((editable ?? !readOnly) && !disabled)];
}
/** Normalizes a single theme extension or a theme extension array. */
function createThemeExtensions(theme) {
return [syntaxHighlighting(defaultHighlightStyle, { fallback: true }), ...(Array.isArray(theme) ? theme : [theme])];
}
/** Creates display-related extensions such as line wrapping, line numbers, active line, and whitespace markers. */
async function createDisplayExtensions(config, optionalPackages) {
const extensions = [];
if (config.lineWrapping) {
extensions.push(EditorView.lineWrapping);
}
if (config.placeholder) {
extensions.push(placeholder(config.placeholder));
}
if (config.lineNumbers) {
extensions.push(lineNumbers(config.lineNumberFormatter ? { formatNumber: config.lineNumberFormatter } : undefined));
}
if (config.highlightActiveLine) {
extensions.push(highlightActiveLine());
}
if (config.highlightActiveLineGutter) {
extensions.push(highlightActiveLineGutter());
}
if (config.highlightSelectionMatches) {
extensions.push((await optionalPackages.loadSearch()).highlightSelectionMatches());
}
if (config.bracketMatching) {
extensions.push(bracketMatching());
}
if (config.closeBrackets) {
const autocomplete = await optionalPackages.loadAutocomplete();
extensions.push(autocomplete.closeBrackets(), keymap.of(autocomplete.closeBracketsKeymap));
}
if (config.highlightSpecialChars) {
extensions.push(highlightSpecialChars());
}
if (config.highlightWhitespace) {
extensions.push(highlightWhitespace());
}
if (config.highlightTrailingWhitespace) {
extensions.push(highlightTrailingWhitespace());
}
if (config.scrollPastEnd) {
extensions.push(scrollPastEnd());
}
return extensions;
}
/** Creates indentation state and language-aware indentation extensions. */
function createIndentationExtensions(config) {
return [
EditorState.tabSize.of(config.tabSize),
indentUnit.of(config.indentUnit),
...(config.indentOnInput ? [indentOnInput()] : [])
];
}
/** Creates keymap extensions, including optional Tab indentation when @codemirror/commands is installed. */
async function createKeymapExtensions(config, optionalPackages) {
const bindings = flattenKeymaps(config.keymaps);
if (config.indentWithTab) {
bindings.push((await optionalPackages.loadCommands()).indentWithTab);
}
return bindings.length > 0 ? [keymap.of(bindings)] : [];
}
/** Creates selection behavior extensions, including multiple selection and drawn selections. */
function createSelectionExtensions(config) {
const extensions = [EditorState.allowMultipleSelections.of(config.multipleSelections)];
if (config.drawSelection) {
extensions.push(drawSelection());
}
return extensions;
}
/** Creates autocompletion extensions. Requires @codemirror/autocomplete when enabled. */
async function createAutocompletionExtensions(config, optionalPackages) {
if (!config.autocompletion) {
return [];
}
const autocomplete = await optionalPackages.loadAutocomplete();
const customSources = Array.isArray(config.completions) ? config.completions : config.completions ? [config.completions] : [];
const extensions = [autocomplete.autocompletion(config.autocompletionConfig ?? undefined)];
if (customSources.length > 0) {
extensions.push(EditorState.languageData.of(() => customSources.map((source) => ({ autocomplete: source }))));
}
return extensions;
}
/** Creates lint extensions. Requires @codemirror/lint when linting or lint gutter is enabled. */
async function createLintExtensions(config, optionalPackages) {
const extensions = [];
if (config.lint && config.linter) {
extensions.push((await optionalPackages.loadLint()).linter(config.linter, config.lintConfig ?? undefined));
}
if (config.lintGutter) {
extensions.push((await optionalPackages.loadLint()).lintGutter());
}
if (config.lintKeymap) {
extensions.push(keymap.of((await optionalPackages.loadLint()).lintKeymap));
}
return extensions;
}
/** Creates search extensions. Requires @codemirror/search when search features are enabled. */
async function createSearchExtensions(config, optionalPackages) {
const extensions = [];
if (config.search) {
extensions.push((await optionalPackages.loadSearch()).search(config.searchConfig ?? undefined));
}
if (config.searchKeymap) {
extensions.push(keymap.of((await optionalPackages.loadSearch()).searchKeymap));
}
return extensions;
}
/** Creates code folding and fold gutter extensions. */
function createFoldingExtensions(config) {
const extensions = [];
if (config.codeFolding) {
extensions.push(codeFolding(config.codeFoldingConfig ?? undefined));
}
if (config.foldGutter) {
extensions.push(foldGutter(config.foldGutterConfig ?? undefined));
}
if (config.foldKeymap) {
extensions.push(keymap.of(foldKeymap));
}
return extensions;
}
/** Creates custom gutters and the wrapper breakpoint gutter extension. */
function createGutterExtensions(config) {
const extensions = [];
const customGutterExtensions = config.customGutters.map((gutterConfig) => gutter(gutterConfig));
if (config.guttersFixed !== null && (customGutterExtensions.length > 0 || config.breakpointGutter)) {
extensions.push(gutters({ fixed: config.guttersFixed }));
}
if (config.breakpointGutter) {
extensions.push(createBreakpointGutter(config));
}
extensions.push(...customGutterExtensions);
return extensions;
}
/** Creates direct decoration extensions and wrapper marked range decorations. */
function createDecorationExtensions(config) {
const extensions = [];
for (const decoration of normalizeArray(config.decorations)) {
extensions.push(EditorView.decorations.of(decoration));
}
return extensions;
}
/** Creates panel extensions from panel constructors. */
function createPanelExtensions(config) {
const panelConstructors = normalizeArray(config.panels);
return panelConstructors.length > 0 ? panelConstructors.map((panel) => showPanel.of(panel)) : [];
}
/** Creates tooltip, tooltip configuration, and hover tooltip extensions. */
function createTooltipExtensions(config) {
const extensions = [];
if (config.tooltipConfig) {
extensions.push(tooltips(config.tooltipConfig));
}
for (const tooltip of normalizeArray(config.tooltips)) {
extensions.push(showTooltip.of(tooltip));
}
if (config.hoverTooltip) {
extensions.push(hoverTooltip(config.hoverTooltip, config.hoverTooltipOptions ?? undefined));
}
return extensions;
}
/** Creates editor attributes, content attributes, and base theme extensions. */
function createStylingExtensions(config) {
const extensions = [];
if (config.baseTheme) {
extensions.push(EditorView.baseTheme(config.baseTheme));
}
if (config.editorAttributes) {
extensions.push(EditorView.editorAttributes.of(config.editorAttributes));
}
if (config.contentAttributes) {
extensions.push(EditorView.contentAttributes.of(config.contentAttributes));
}
return extensions;
}
/** Creates the CodeMirror CSP nonce extension when a nonce is provided. */
function createCspNonceExtensions(cspNonce) {
return cspNonce ? [EditorView.cspNonce.of(cspNonce)] : [];
}
/** Creates a minimal JSON representation before an EditorView exists. */
function createPendingEditorJSON(content) {
return {
doc: content,
selection: EditorSelection.single(0).toJSON()
};
}
function createBreakpointGutter(config) {
return gutter({
class: 'cm-breakpoint-gutter',
lineMarker: (view, line) => (view.state.field(config.breakpointField).includes(line.from) ? breakpointMarker : null),
domEventHandlers: {
mousedown: (view, line, event) => {
event.preventDefault();
config.toggleBreakpoint(view.state.doc.lineAt(line.from).number);
return true;
}
},
initialSpacer: () => breakpointMarker
});
}
function createMarkedRangeDecorations(markedRanges, docLength) {
return Decoration.set(markedRanges.flatMap((range) => {
const from = Math.max(0, range.from);
const to = Math.min(docLength, Math.max(from, range.to));
if (from >= to) {
return [];
}
return Decoration.mark({
class: range.class,
attributes: range.attributes,
tagName: range.tagName,
inclusive: range.inclusive,
inclusiveStart: range.inclusiveStart,
inclusiveEnd: range.inclusiveEnd
}).range(from, to);
}), true);
}
function flattenKeymaps(keymaps) {
if (keymaps.length === 0) {
return [];
}
const first = keymaps[0];
return Array.isArray(first)
? keymaps.flatMap((bindings) => [...bindings])
: [...keymaps];
}
function normalizeArray(value) {
if (!value) {
return [];
}
return Array.isArray(value) ? value : [value];
}
class MissingOptionalCodemirrorDependencyError extends Error {
constructor(packageName, cause) {
super(`@ks89/ngx-codemirror6 optional feature requires ${packageName}. Install it in your application to use this API.`, {
cause
});
}
}
/** Lazy loader and cache for optional CodeMirror packages used by advanced wrapper APIs. */
class OptionalCodemirrorPackages {
constructor() {
this.autocompleteModule = null;
this.commandsModule = null;
this.lintModule = null;
this.searchModule = null;
}
/** Cached @codemirror/autocomplete module, or null until it has been loaded. */
get autocomplete() {
return this.autocompleteModule;
}
/** Loads @codemirror/autocomplete or throws a package-specific installation error. */
async loadAutocomplete() {
try {
return (this.autocompleteModule ??= await import('@codemirror/autocomplete'));
}
catch (error) {
if (this.isModuleResolutionError(error, '@codemirror/autocomplete')) {
throw this.createMissingOptionalDependencyError('@codemirror/autocomplete', error);
}
throw error;
}
}
/** Cached @codemirror/commands module, or null until it has been loaded. */
get commands() {
return this.commandsModule;
}
/** Loads @codemirror/commands or throws a package-specific installation error. */
async loadCommands() {
try {
return (this.commandsModule ??= await import('@codemirror/commands'));
}
catch (error) {
if (this.isModuleResolutionError(error, '@codemirror/commands')) {
throw this.createMissingOptionalDependencyError('@codemirror/commands', error);
}
throw error;
}
}
/** Cached @codemirror/lint module, or null until it has been loaded. */
get lint() {
return this.lintModule;
}
/** Loads @codemirror/lint or throws a package-specific installation error. */
async loadLint() {
try {
return (this.lintModule ??= await import('@codemirror/lint'));
}
catch (error) {
if (this.isModuleResolutionError(error, '@codemirror/lint')) {
throw this.createMissingOptionalDependencyError('@codemirror/lint', error);
}
throw error;
}
}
/** Cached @codemirror/search module, or null until it has been loaded. */
get search() {
return this.searchModule;
}
/** Loads @codemirror/search or throws a package-specific installation error. */
async loadSearch() {
try {
return (this.searchModule ??= await import('@codemirror/search'));
}
catch (error) {
if (this.isModuleResolutionError(error, '@codemirror/search')) {
throw this.createMissingOptionalDependencyError('@codemirror/search', error);
}
throw error;
}
}
createMissingOptionalDependencyError(packageName, error) {
return new MissingOptionalCodemirrorDependencyError(packageName, error);
}
isModuleResolutionError(error, packageName) {
if (!(error instanceof Error)) {
return false;
}
const code = this.getErrorCode(error);
if ((code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') && error.message.includes(packageName)) {
return true;
}
return (error.message.includes(packageName) &&
[
'Cannot find module',
'Cannot find package',
'Failed to resolve module specifier',
'could not be resolved',
'Could not resolve'
].some((messagePart) => error.message.includes(messagePart)));
}
getErrorCode(error) {
return 'code' in error && typeof error.code === 'string' ? error.code : undefined;
}
}
/**
* The MIT License (MIT)
*
* Copyright (c) 2023-2026 Stefano Cappa (Ks89)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/**
* Angular wrapper component for CodeMirror 6.
* Provides declarative inputs for common CodeMirror extensions plus imperative methods for editor commands.
*/
class CodemirrorComponent {
constructor() {
/** Initial and externally controlled editor document text. */
this.content = '';
/** Additional CodeMirror extensions appended after the wrapper-managed extensions. */
this.appendExtensions = [];
/** Language support extension, such as one returned by a @codemirror/lang-* package. */
this.language = [];
/**
* Main switch for read-only mode.
* Maps to CodeMirror's EditorState.readOnly facet, which editing commands and extensions consult before changing the document.
* In most applications this is the only input you need: true makes the editor read-only, false makes it editable.
*/
this.readOnly = true;
/**
* Advanced DOM editability override.
* Maps to CodeMirror's EditorView.editable facet, which controls whether the content DOM is browser-editable/focusable.
* Leave null for normal use; the wrapper then derives it from readOnly with editable = !readOnly.
*/
this.editable = null;
/** Theme extension or extensions applied to the editor view. */
this.theme = [];
/** Enables CodeMirror's line wrapping extension for long visual lines. */
this.lineWrapping = false;
/** Placeholder text shown when the editor document is empty. */
this.placeholder = '';
/** Shows a line-number gutter. */
this.lineNumbers = false;
/** Custom formatter for displayed line numbers in the line-number gutter. */
this.lineNumberFormatter = null;
/** Highlights the visual line containing the primary selection. */
this.highlightActiveLine = false;
/** Highlights the gutter for the active line when a gutter is visible. */
this.highlightActiveLineGutter = false;
/** Enables bracket matching around the cursor through @codemirror/language. */
this.bracketMatching = false;
/** Highlights unusual or special characters with CodeMirror's special-character highlighter. */
this.highlightSpecialChars = false;
/** Renders visible markers for whitespace characters. */
this.highlightWhitespace = false;
/** Renders visible markers for trailing whitespace at line ends. */
this.highlightTrailingWhitespace = false;
/** Allows scrolling past the end of the document. */
this.scrollPastEnd = false;
/** Number of columns used to display tab characters in the editor state. */
this.tabSize = 4;
/** Indentation unit used by language-aware indentation services. */
this.indentUnit = ' ';
/** Custom key bindings or groups of key bindings registered with CodeMirror's keymap extension. */
this.keymaps = [];
/** Allows the editor state to hold multiple selection ranges. */
this.multipleSelections = false;
/** Draws the selection with editor-managed DOM so multiple selections and custom selection rendering are visible. */
this.drawSelection = false;
/**
* Non-read-only editing inputs.
* These inputs affect editing behavior and are useful only when the editor can accept document edits.
*/
/** Enables automatic indentation when typed input matches language indentation rules. */
this.indentOnInput = false;
/**
* Optional package: @codemirror/autocomplete.
* Install @codemirror/autocomplete in the consuming application to use these inputs.
* These inputs are also non-read-only editing inputs.
*/
/** Enables automatic closing of brackets through @codemirror/autocomplete when that optional package is installed. */
this.closeBrackets = false;
/** Enables the completion UI through @codemirror/autocomplete. */
this.autocompletion = false;
/** Completion source or sources added to language completions through CodeMirror language data. */
this.completions = null;
/** Configuration object forwarded to @codemirror/autocomplete's autocompletion extension. */
this.autocompletionConfig = null;
/**
* Optional package: @codemirror/commands.
* Install @codemirror/commands in the consuming application to use these inputs.
* These inputs are also non-read-only editing inputs.
*/
/** Adds CodeMirror's Tab indentation key binding through @codemirror/commands. */
this.indentWithTab = false;
/**
* Optional package: @codemirror/lint.
* Install @codemirror/lint in the consuming application to use these inputs.
*/
/** Enables lint diagnostics when a linter source is provided. */
this.lint = false;
/** Lint source used by @codemirror/lint to produce diagnostics for the current editor view. */
this.linter = null;
/** Configuration object forwarded to @codemirror/lint's linter extension. */
this.lintConfig = null;
/** Shows CodeMirror's lint gutter for diagnostic markers. */
this.lintGutter = false;
/** Registers CodeMirror's default lint key bindings. */
this.lintKeymap = false;
/**
* Optional package: @codemirror/search.
* Install @codemirror/search in the consuming application to use these inputs.
*/
/** Highlights other occurrences of the current selection through @codemirror/search. */
this.highlightSelectionMatches = false;
/** Enables CodeMirror's search state and search panel support through @codemirror/search. */
this.search = false;
/** Configuration object forwarded to @codemirror/search's search extension. */
this.searchConfig = null;
/** Registers the default search key bindings. */
this.searchKeymap = false;
/**
* Core dependency: @codemirror/language.
* These inputs do not require optional packages.
*/
/** Enables code folding commands and folded range state through @codemirror/language. */
this.codeFolding = false;
/** Configuration object forwarded to CodeMirror's codeFolding extension. */
this.codeFoldingConfig = null;
/** Shows a gutter with fold controls. */
this.foldGutter = false;
/** Configuration object forwarded to CodeMirror's foldGutter extension. */
this.foldGutterConfig = null;
/** Registers CodeMirror's default folding key bindings. */
this.foldKeymap = false;
/**
* Core dependency: @codemirror/view gutters, decorations, panels, and tooltips.
* These inputs do not require optional packages.
*/
/** Custom gutter configurations forwarded to CodeMirror's gutter extension. */
this.customGutters = [];
/** Controls whether configured gutters are fixed during horizontal scrolling; null keeps CodeMirror's default. */
this.guttersFixed = null;
/** Adds the wrapper's clickable breakpoint gutter. */
this.breakpointGutter = false;
/** One-based line numbers that should display breakpoint markers in the breakpoint gutter. */
this.breakpoints = [];
/** Decoration set or dynamic decoration sources registered with EditorView.decorations. */
this.decorations = null;
/** Simple marked document ranges converted to CodeMirror mark decorations. */
this.markedRanges = [];
/** Panel constructor or constructors shown with CodeMirror's showPanel facet. */
this.panels = null;
/** Tooltip descriptor or descriptors shown with CodeMirror's showTooltip facet. */
this.tooltips = null;
/** Tooltip positioning configuration forwarded to CodeMirror's tooltips extension. */
this.tooltipConfig = null;
/** Hover tooltip source used to create contextual tooltips from document positions. */
this.hoverTooltip = null;
/** Options forwarded to CodeMirror's hoverTooltip extension, such as hover delay. */
this.hoverTooltipOptions = null;
/**
* Core dependency: @codemirror/view styling and security.
* These inputs do not require optional packages.
*/
/** CSP nonce attached to CodeMirror style elements created by the editor view. */
this.cspNonce = null;
/** Static attributes or an attribute provider applied to the outer .cm-editor element. */
this.editorAttributes = null;
/** Static attributes or an attribute provider applied to the editable content element. */
this.contentAttributes = null;
/** Base theme rules applied with EditorView.baseTheme for structural or feature-specific styling. */
this.baseTheme = null;
/** Emits the created CodeMirror EditorView after the editor is initialized. */
this.editorReady = new EventEmitter();
/** Emits the full document text after user/editor transactions change the document. */
this.contentChange = new EventEmitter();
/** Emits every CodeMirror ViewUpdate produced by the editor view. */
this.update = new EventEmitter();
/** Emits the current EditorSelection whenever a transaction changes the selection. */
this.selectionChange = new EventEmitter();
/** Emits the updated one-based breakpoint line numbers after the breakpoint gutter toggles a marker. */
this.breakpointsChange = new EventEmitter();
this.languageCompartment = new Compartment();
this.readOnlyCompartment = new Compartment();
this.editableCompartment = new Compartment();
this.themeCompartment = new Compartment();
this.displayCompartment = new Compartment();
this.indentationCompartment = new Compartment();
this.keymapCompartment = new Compartment();
this.selectionCompartment = new Compartment();
this.autocompleteCompartment = new Compartment();
this.lintCompartment = new Compartment();
this.searchCompartment = new Compartment();
this.foldingCompartment = new Compartment();
this.gutterCompartment = new Compartment();
this.decorationCompartment = new Compartment();
this.panelCompartment = new Compartment();
this.tooltipCompartment = new Compartment();
this.stylingCompartment = new Compartment();
this.appendExtensionsCompartment = new Compartment();
this.cspNonceCompartment = new Compartment();
this.breakpointSetEffect = StateEffect.define();
this.breakpointToggleEffect = StateEffect.define();
this.breakpointStateField = StateField.define({
create: (state) => this.createBreakpointPositions(state, this.breakpoints),
update: (positions, tr) => {
let next = positions.map((position) => tr.changes.mapPos(position, 1));
for (const effect of tr.effects) {
if (effect.is(this.breakpointSetEffect)) {
next = this.createBreakpointPositions(tr.state, effect.value);
}
else if (effect.is(this.breakpointToggleEffect)) {
const line = effect.value;
if (Number.isInteger(line) && line >= 1 && line <= tr.state.doc.lines) {
const position = tr.state.doc.line(line).from;
next = this.toggleBreakpointPosition(next, position);
}
}
}
return this.normalizeBreakpointPositions(next);
}
});
this.markedRangesSetEffect = StateEffect.define();
this.markedRangesStateField = StateField.define({
create: (state) => createMarkedRangeDecorations(this.markedRanges, state.doc.length),
update: (decorations, tr) => {
let next = decorations.map(tr.changes);
for (const effect of tr.effects) {
if (effect.is(this.markedRangesSetEffect)) {
next = createMarkedRangeDecorations(effect.value, tr.state.doc.length);
}
}
return next;
},
provide: (field) => EditorView.decorations.from(field)
});
this.disabled = false;
this.destroyed = false;
this.editorCreationVersion = 0;
this.reconfigureVersion = 0;
this.updatingContentFromModel = false;
this.onChange = () => undefined;
this.onTouched = () => undefined;
this.optionalPackages = new OptionalCodemirrorPackages();
this.errorHandler = inject(ErrorHandler);
}
/** Creates the CodeMirror editor after Angular has initialized the host element. */
ngAfterViewInit() {
this.destroyed = false;
void this.createEditor(this.content).catch((error) => this.errorHandler.handleError(error));
}
/** Reconfigures the live editor when Angular input bindings change. */
ngOnChanges(changes) {
void this.reconfigureEditor(changes).catch((error) => this.errorHandler.handleError(error));
}
async reconfigureEditor(changes) {
if (this.destroyed || !this.editorView) {
return;
}
if (changes['content']) {
this.setEditorContent(this.content);
}
const reconfigureVersion = ++this.reconfigureVersion;
const effects = await this.getReconfigureEffects(changes);
if (!this.destroyed && reconfigureVersion === this.reconfigureVersion && this.editorView && effects.length > 0) {
this.editorView.dispatch({ effects });
}
}
/** Destroys the CodeMirror editor view and prevents pending async initialization from recreating it. */
ngOnDestroy() {
this.destroyed = true;
this.editorCreationVersion += 1;
this.reconfigureVersion += 1;
this.destroyEditor();
}
/**
* Implements ControlValueAccessor.writeValue.
* Angular forms call this method when the external form model writes a value into the editor.
*/
writeValue(value) {
this.content = value ?? '';
this.setEditorContent(this.content);
}
/**
* Implements ControlValueAccessor.registerOnChange.
* Angular forms call this method to register the callback that receives editor document changes.
*/
registerOnChange(fn) {
this.onChange = fn;
}
/**
* Implements ControlValueAccessor.registerOnTouched.
* Angular forms call this method to register the callback emitted when the editor is touched.
*/
registerOnTouched(fn) {
this.onTouched = fn;
}
/**
* Implements ControlValueAccessor.setDisabledState.
* Angular forms call this method when the form control is enabled or disabled.
*/
setDisabledState(isDisabled) {
this.disabled = isDisabled;
if (this.editorView) {
this.editorView.dispatch({
effects: [
this.readOnlyCompartment.reconfigure(createReadOnlyExtensions(this.readOnly, this.disabled)),
this.editableCompartment.reconfigure(createEditableExtensions(this.readOnly, this.editable, this.disabled))
]
});
}
}
/**
* Recreates the editor from a CodeMirror state configuration while preserving wrapper-managed behavior.
* User-provided extensions are appended after the wrapper extensions.
*/
async resetEditor(config) {
const editorCreationVersion = ++this.editorCreationVersion;
this.reconfigureVersion += 1;
const state = EditorState.create({
...config,
doc: config.doc ?? this.content,
extensions: [await this.createExtensions(), config.extensions ?? []]
});
if (!this.destroyed && editorCreationVersion === this.editorCreationVersion) {
this.content = state.doc.toString();
this.mountEditorState(state);
}
}
/** Moves focus to the CodeMirror editor when it exists. */
focus() {
this.editorView?.focus();
}
/** Returns the current editor document text, or the pending content input before initialization. */
getValue() {
return this.editorView?.state.doc.toString() ?? this.content;
}
/** Replaces the full editor document and synchronizes the content input cache. */
setValue(value) {
this.content = value;
this.setEditorContent(value);
}
/** Dispatches one or more raw CodeMirror transaction specs to the editor. */
dispatch(...specs) {
this.editorView?.dispatch(...specs);
}
/** Replaces the current selection ranges with the provided text. */
replaceSelection(text) {
if (!this.editorView) {
return;
}
this.editorView.dispatch(this.editorView.state.replaceSelection(text));
}
/** Scrolls a document position into view, clamping the position to the document bounds. */
scrollToPosition(position) {
if (!this.editorView) {
return;
}
const boundedPosition = Math.max(0, Math.min(position, this.editorView.state.doc.length));
this.editorView.dispatch({
effects: EditorView.scrollIntoView(boundedPosition, { y: 'center' })
});
}
/** Scrolls a one-based document line into view, clamping the line to the document bounds. */
scrollToLine(line) {
if (!this.editorView) {
return;
}
const boundedLine = Math.max(1, Math.min(line, this.editorView.state.doc.lines));
this.scrollToPosition(this.editorView.state.doc.line(boundedLine).from);
}
/** Scrolls the primary selection range into view. */
scrollSelectionIntoView() {
if (!this.editorView) {
return;
}
this.editorView.dispatch({
effects: EditorView.scrollIntoView(this.editorView.state.selection.main, { y: 'nearest' })
});
}
/** Serializes the editor state, including history and fold state when those extensions are active. */
toJSON() {
if (!this.editorView) {
return createPendingEditorJSON(this.content);
}
const fields = {};
if (this.optionalPackages.commands && this.editorView.state.field(this.optionalPackages.commands.historyField, false)) {
fields['history'] = this.optionalPackages.commands.historyField;
}
if (this.editorView.state.field(foldState, false)) {
fields['fold'] = foldState;
}
return Object.keys(fields).length > 0 ? this.editorView.state.toJSON(fields) : this.editorView.state.toJSON();
}
/** Restores a serialized CodeMirror state produced by toJSON. */
async restoreFromJSON(json) {
const commands = json && typeof json === 'object' && 'history' in json ? await this.optionalPackages.loadCommands() : null;
const fields = {
...(commands ? { history: commands.historyField } : {}),
...(json && typeof json === 'object' && 'fold' in json ? { fold: foldState } : {})
};
const state = EditorState.fromJSON(json, {
extensions: await this.createExtensions()
}, Object.keys(fields).length > 0 ? fields : undefined);
this.content = state.doc.toString();
if (this.editorView) {
this.editorView.setState(state);
}
else {
this.mountEditorState(state);
}
}
/** Runs the @codemirror/commands undo command. Requires @codemirror/commands. */
async undo() {
return this.editorView ? (await this.optionalPackages.loadCommands()).undo(this.editorView) : false;
}
/** Runs the @codemirror/commands redo command. Requires @codemirror/commands. */
async redo() {
return this.editorView ? (await this.optionalPackages.loadCommands()).redo(this.editorView) : false;
}
/** Opens CodeMirror's search panel. Requires @codemirror/search. */
async openSearchPanel() {
return this.editorView ? (await this.optionalPackages.loadSearch()).openSearchPanel(this.editorView) : false;
}
/** Closes CodeMirror's search panel. Requires @codemirror/search. */
async closeSearchPanel() {
return this.editorView ? (await this.optionalPackages.loadSearch()).closeSearchPanel(this.editorView) : false;
}
/** Returns true when the search panel is open and @codemirror/search has been loaded. */
isSearchPanelOpen() {
return this.editorView && this.optionalPackages.search ? this.optionalPackages.search.searchPanelOpen(this.editorView.state) : false;
}
/** Selects the next search match. Requires @codemirror/search. */
async findNext() {
return this.editorView ? (await this.optionalPackages.loadSearch()).findNext(this.editorView) : false;
}
/** Selects the previous search match. Requires @codemirror/search. */
async findPrevious() {
return this.editorView ? (await this.optionalPackages.loadSearch()).findPrevious(this.editorView) : false;
}
/** Replaces the current search match. Requires @codemirror/search. */
async replaceNext() {
return this.editorView ? (await this.optionalPackages.loadSearch()).replaceNext(this.editorView) : false;
}
/** Replaces all search matches. Requires @codemirror/search. */
async replaceAll() {
return this.editorView ? (await this.optionalPackages.loadSearch()).replaceAll(this.editorView) : false;
}
/** Opens CodeMirror's go-to-line command UI. Requires @codemirror/search. */
async gotoLine() {
return this.editorView ? (await this.optionalPackages.loadSearch()).gotoLine(this.editorView) : false;
}
/** Adds the next occurrence of the current selection to the selection set. Requires @codemirror/search. */
async selectNextOccurrence() {
return this.editorView ? (await this.optionalPackages.loadSearch()).selectNextOccurrence(this.editorView) : false;
}
/** Selects matches for the current selection. Requires @codemirror/search. */
async selectSelectionMatches() {
return this.editorView ? (await this.optionalPackages.loadSearch()).selectSelectionMatches(this.editorView) : false;
}
/** Selects all matches for the active search query. Requires @codemirror/search. */
async selectMatches() {
return this.editorView ? (await this.optionalPackages.loadSearch()).selectMatches(this.editorView) : false;
}
/** Folds the foldable range at the current selection. */
foldCode() {
return this.editorView ? foldCode(this.editorView) : false;
}
/** Unfolds the folded range at the current selection. */
unfoldCode() {
return this.editorView ? unfoldCode(this.editorView) : false;
}
/** Toggles folding at the current selection. */
toggleFold() {
return this.editorView ? toggleFold(this.editorView) : false;
}
/** Folds all foldable ranges in the document. */
foldAll() {
return this.editorView ? foldAll(this.editorView) : false;
}
/** Unfolds all folded ranges in the document. */
unfoldAll() {
return this.editorView ? unfoldAll(this.editorView) : false;
}
/** Returns the current folded document ranges. */
getFoldedRanges() {
if (!this.editorView) {
return [];
}
const ranges = [];
foldedRanges(this.editorView.state).between(0, this.editorView.state.doc.length, (from, to) => {
ranges.push({ from, to });
});
return ranges;
}
/** Opens the completion UI. Requires @codemirror/autocomplete. */
async startCompletion() {
return this.editorView ? (await this.optionalPackages.loadAutocomplete()).startCompletion(this.editorView) : false;
}
/** Closes the completion UI. Requires @codemirror/autocomplete. */
async closeCompletion() {
return this.editorView ? (await this.optionalPackages.loadAutocomplete()).closeCompletion(this.editorView) : false;
}
/** Accepts the selected completion. Requires @codemirror/autocomplete. */
async acceptCompletion() {
return this.editorView ? (await this.optionalPackages.loadAutocomplete()).acceptCompletion(this.editorView) : false;
}
/** Returns the current completion UI status, or null before @codemirror/autocomplete is loaded. */
completionStatus() {
return this.editorView && this.optionalPackages.autocomplete ? this.optionalPackages.autocomplete.completionStatus(this.editorView.state) : null;
}
/** Returns the currently available completion options, or an empty array when completion is inactive. */
currentCompletions() {
return this.editorView && this.optionalPackages.autocomplete ? this.optionalPackages.autocomplete.currentCompletions(this.editorView.state) : [];
}
/** Returns the currently selected completion option, or null when none is selected. */
selectedCompletion() {
return this.editorView && this.optionalPackages.autocomplete ? this.optionalPackages.autocomplete.selectedCompletion(this.editorView.state) : null;
}
/** Returns the selected completion option index, or null when completion is inactive. */
selectedCompletionIndex() {
return this.editorView && this.optionalPackages.autocomplete
? this.optionalPackages.autocomplete.selectedCompletionIndex(this.editorView.state)
: null;
}
/** Toggles a breakpoint marker for a one-based line number and emits breakpointsChange. */
toggleBreakpoint(line) {
if (!Number.isInteger(line) || line < 1) {
return;
}
if (!this.editorView) {
const current = new Set(this.breakpoints);
if (current.has(line)) {
current.delete(line);
}
else {
current.add(line);
}
this.breakpoints = this.normalizeBreakpointLineNumbers([...current]);
this.breakpointsChange.emit([...this.breakpoints]);
return;
}
this.editorView?.dispatch({
effects: this.breakpointToggleEffect.of(line)
});
}
/** Returns true when the one-based line number has a breakpoint marker. */
hasBreakpoint(line) {
return this.getBreakpointLineNumbers().includes(line);
}
/** Adds a cursor on the line above the current selection. Requires @codemirror/commands. */
async addCursorAbove() {
return this.editorView ? (await this.optionalPackages.loadCommands()).addCursorAbove(this.editorView) : false;
}
/** Adds a cursor on the line below the current selection. Requires @codemirror/commands. */
async addCursorBelow() {
return this.editorView ? (await this.optionalPackages.loadCommands()).addCursorBelow(this.editorView) : false;
}
/** Toggles line or block comments for the current selection. Requires @codemirror/commands. */
async toggleComment() {
return this.editorView ? (await this.optionalPackages.loadCommands()).toggleComment(this.editorView) : false;
}
/** Toggles line comments for the current selection. Requires @codemirror/commands. */
async toggleLineComment() {
return this.editorView ? (await this.optionalPackages.loadCommands()).toggleLineComment(this.editorView) : false;
}
/** Adds line comments to the current selection. Requires @codemirror/commands. */
async lineComment() {
return this.editorView ? (await this.optionalPackages.loadCommands()).lineComment(this.editorView) : false;
}
/** Removes line comments from the current selection. Requires @codemirror/commands. */
async lineUncomment() {
return this.editorView ? (await this.optionalPackages.loadCommands()).lineUncomment(this.editorView) : false;
}
/** Toggles block comments for the current selection. Requires @codemirror/commands. */
async toggleBlockComment() {
return this.editorView ? (await this.optionalPackages.loadCommands()).toggleBlockComment(this.editorView) : false;
}
/** Indents the current selection one level deeper. Requires @codemirror/commands. */
async indentMore() {
return this.editorView ? (await this.optionalPackages.loadCommands()).indentMore(this.editorView) : false;
}
/** Reduces indentation for the current selection by one level. Requires @codemirror/commands. */
async indentLess() {
return this.editorView ? (await this.optionalPackages.loadCommands()).indentLess(this.editorView) : false;
}
/** Applies language-aware indentation to the current selection. Requires @codemirror/commands. */
async indentSelection() {
return this.editorView ? (await this.optionalPackages.loadCommands()).indentSelection(this.editorView) : false;
}
/** Deletes the selected lines. Requires @codemirror/commands. */
async deleteLine() {
return this.editorView ? (await this.optionalPackages.loadCommands()).deleteLine(this.editorView) : false;
}
/** Moves the selected lines up. Requires @codemirror/commands. */
async moveLineUp() {
return this.editorView ? (await this.optionalPackages.loadCommands()).moveLineUp(this.editorView) : false;
}
/** Moves the selected lines down. Requires @codemirror/commands. */
async moveLineDown() {
return this.editorView ? (await this.optionalPackages.loadCommands()).moveLineDown(this.editorView) : false;
}
/** Selects the current line. Requires @codemirror/commands. */
async selectLine() {
return this.editorView ? (await this.optionalPackages.loadCommands()).selectLine(this.editorView) : false;
}
/** Moves the cursor to the matching bracket when one is available. Requires @codemirror/commands. */
async cursorMatchingBracket() {
return this.editorView ? (await this.optionalPackages.loadCommands()).cursorMatchingBracket(this.editorView) : false;
}
/** Toggles Tab focus mode. Requires @codemirror/commands. */
async toggleTabFocusMode() {
return this.editorView ? (await this.optionalPackages.loadCommands()).toggleTabFocusMode(this.editorView) : false;
}
/** Returns true when hover tooltip sources are active in the editor state. */
hasHoverTooltips() {
return this.editorView ? hasHoverTooltips(this.editorView.state) : false;
}
/** Activates hover tooltips at a document position. */
activateHover(position, side = 1) {
if (this.editorView) {
activateHover(this.editorView, position, side);
}
}
/** Closes currently visible hover tooltips. */
closeHoverTooltips() {
this.editorView?.dispatch({
effects: closeHoverTooltips
});
}
/** Recomputes tooltip positions for the current editor layout. */
repositionTooltips() {
if (this.editorView) {
repositionTooltips(this.editorView);
}
}
/** Returns the active panel instance for the given panel constructor, when mounted. */
getPanel(panel) {
return this.editorView ? getPanel(this.editorView, panel) : null;
}
/** Recreates the editor with the same document and selection, clearing undo/redo history. */
async clearHistory() {
if (!this.editorView) {
return;
}
const state = this.editorView.state;
await this.createEditor(state.doc.toString(), state.selection);
}
async createEditor(doc, selection) {
const editorCreationVersion = ++this.editorCreationVersion;
const state = EditorState.create({
doc,
selection,
extensions: await this.createExtensions()
});
if (!this.destroyed && editorCreationVersion === this.editorCreationVersion) {
this.mountEditorState(state);
}
}
mountEditorState(state) {
if (!this.host) {
throw new Error('Internal ngx-codemirror6 error - host must be defined');
}
if (this.destroyed) {
return;
}
this.destroyEditor();
this.editorView = new EditorView({
parent: this.host.nativeElement,
state
});
this.editorReady.emit(this.editorView);
}
async createExtensions() {
return [
this.languageCompartment.of(this.language),
this.readOnlyCompartment.of(createReadOnlyExtensions(this.readOnly, this.disabled)),
this.editableCompartment.of(createEditableExtensions(this.readOnly, this.editable, this.disabled)),
this.themeCompartment.of(createThemeExtensions(this.theme)),
this.displayCompartment.of(await createDisplayExtensions(this.getDisplayConfig(), this.optionalPackages)),
this.indentationCompartment.of(createIndentationExtensions(this.getIndentationConfig())),
this.keymapCompartment.of(await createKeymapExtensions(this.getKeymapConfig(), this.optionalPackages)),
this.selectionCompartment.of(createSelectionExtensions(this.getSelectionConfig())),
this.autocompleteCompartment.of(await createAutocompletionExtensions(this.getAutocompletionConfig(), this.optionalPackages)),
this.lintCompartment.of(await createLintExtensions(this.getLintConfig(), this.optionalPackages)),
this.searchCompartment.of(await createSearchExte