UNPKG

@ks89/ngx-codemirror6

Version:
1,061 lines (1,058 loc) 69.8 kB
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