From 197b5649ffa2efa1a979cbdbf8f276b24ce8a488 Mon Sep 17 00:00:00 2001 From: bhsd <2545473905@qq.com> Date: Mon, 29 Apr 2024 10:30:02 +0800 Subject: [PATCH] CodeMirrorModeMediaWiki: autocompletion Autocomplete magic words, tag names and url protocols. This patch also enables block comment using ``. Bug: T95100 Change-Id: If37da956ac1eb945b96753e6728c0247b1a68b66 --- extension.json | 14 +- i18n/en.json | 1 + i18n/qqq.json | 1 + includes/DataScript.php | 1 + package-lock.json | 20 + package.json | 1 + resources/codemirror.bundle.js | 2 + resources/codemirror.less | 7 +- .../codemirror.mediawiki.autocomplete.js | 20 + resources/codemirror.mediawiki.js | 124 +- .../codemirror.mediawiki.templateFolding.js | 8 +- resources/codemirror.preferences.js | 2 +- resources/lib/codemirror6.bundle.dist.js | 2036 ++++++++++++++++- tests/jest/codemirror.bidiIsolation.test.js | 7 +- tests/jest/codemirror.mediawiki.test.js | 2 +- tests/phpunit/DataScriptTest.php | 1 + 16 files changed, 2214 insertions(+), 33 deletions(-) create mode 100644 resources/codemirror.mediawiki.autocomplete.js diff --git a/extension.json b/extension.json index 1ca99f0b..972316ce 100644 --- a/extension.json +++ b/extension.json @@ -33,6 +33,11 @@ "description": "List of namespace IDs where template folding should be enabled, or `null` to enable for all namespaces. Set to [] to disable everywhere.", "public": true }, + "CodeMirrorAutocompleteNamespaces": { + "value": null, + "description": "List of namespace IDs where autocompletion should be enabled, or `null` to enable for all namespaces. Set to [] to disable everywhere.", + "public": true + }, "CodeMirrorLineNumberingNamespaces": { "value": null, "description": "List of namespace IDs where line numbering should be enabled, or `null` to enable for all namespaces. Set to [] to disable everywhere.", @@ -46,7 +51,8 @@ "lineNumbering": true, "lineWrapping": true, "specialChars": true, - "templateFolding": true + "templateFolding": true, + "autocomplete": true }, "description": "Defaults for CodeMirror user preferences. See https://w.wiki/BwzZ for more information.", "public": true @@ -284,7 +290,8 @@ "codemirror.mediawiki.js", "codemirror.mediawiki.config.js", "codemirror.mediawiki.bidiIsolation.js", - "codemirror.mediawiki.templateFolding.js" + "codemirror.mediawiki.templateFolding.js", + "codemirror.mediawiki.autocomplete.js" ], "styles": [ "codemirror.mediawiki.less", @@ -297,7 +304,8 @@ "messages": [ "codemirror-fold-template", "codemirror-prefs-bidiisolation", - "codemirror-prefs-templatefolding" + "codemirror-prefs-templatefolding", + "codemirror-prefs-autocomplete" ] }, "ext.CodeMirror.v6.WikiEditor": { diff --git a/i18n/en.json b/i18n/en.json index aa34e7e6..9ebdab49 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -12,6 +12,7 @@ "codemirror-prefs-enable": "Enable syntax highlighting for wikitext", "codemirror-prefs-title": "Syntax highlighting preferences", "codemirror-prefs-templatefolding": "Enable folding of template parameters", + "codemirror-prefs-autocomplete": "Enable autocompletion", "codemirror-prefs-bidiisolation": "Isolate bidirectional text", "codemirror-prefs-bracketmatching": "Enable bracket matching", "codemirror-prefs-linenumbering": "Show line numbers", diff --git a/i18n/qqq.json b/i18n/qqq.json index 78556725..42a5f783 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -17,6 +17,7 @@ "codemirror-prefs-enable": "Used in user preferences as label for enabling syntax highlighting.", "codemirror-prefs-title": "Syntax highlighting preferences", "codemirror-prefs-templatefolding": "Label for the option to enable folding of template parameters in the CodeMirror preferences panel.", + "codemirror-prefs-autocomplete": "Label for the option to enable autocompletion in the CodeMirror preferences panel.", "codemirror-prefs-bidiisolation": "Label for the option to enable bidirectional text isolation in the CodeMirror preferences panel.", "codemirror-prefs-bracketmatching": "Label for the option to enable bracket matching in the CodeMirror preferences panel.", "codemirror-prefs-linenumbering": "Label for the option to show line numbers in the CodeMirror preferences panel.", diff --git a/includes/DataScript.php b/includes/DataScript.php index 35e43d10..55662a8b 100644 --- a/includes/DataScript.php +++ b/includes/DataScript.php @@ -62,6 +62,7 @@ class DataScript { 'defaultPreferences' => $mwConfig->get( 'CodeMirrorDefaultPreferences' ), 'lineNumberingNamespaces' => $mwConfig->get( 'CodeMirrorLineNumberingNamespaces' ), 'templateFoldingNamespaces' => $mwConfig->get( 'CodeMirrorTemplateFoldingNamespaces' ), + 'autocompleteNamespaces' => $mwConfig->get( 'CodeMirrorAutocompleteNamespaces' ), 'pluginModules' => $registry->getAttribute( 'CodeMirrorPluginModules' ), 'tagModes' => $tagModes, 'tags' => array_fill_keys( $tagNames, true ), diff --git a/package-lock.json b/package-lock.json index 7b75d786..de6e822a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "codemirror", "devDependencies": { + "@codemirror/autocomplete": "6.12.0", "@codemirror/commands": "6.2.5", "@codemirror/language": "6.9.3", "@codemirror/search": "6.5.6", @@ -499,6 +500,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@codemirror/autocomplete": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz", + "integrity": "sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, "node_modules/@codemirror/commands": { "version": "6.2.5", "dev": true, diff --git a/package.json b/package.json index fd6d70ad..97124eea 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "node": "18.20.4" }, "devDependencies": { + "@codemirror/autocomplete": "6.12.0", "@codemirror/commands": "6.2.5", "@codemirror/language": "6.9.3", "@codemirror/search": "6.5.6", diff --git a/resources/codemirror.bundle.js b/resources/codemirror.bundle.js index 09342f74..eae69b47 100644 --- a/resources/codemirror.bundle.js +++ b/resources/codemirror.bundle.js @@ -2,6 +2,7 @@ * This file is managed by Rollup and bundles all the CodeMirror dependencies * into the single file resources/lib/codemirror6.bundle.dist.js. */ +import '@codemirror/autocomplete'; import '@codemirror/commands'; import '@codemirror/language'; import '@codemirror/search'; @@ -10,6 +11,7 @@ import '@codemirror/view'; import '@lezer/highlight'; /* eslint-disable es-x/no-export-ns-from */ +export * from '@codemirror/autocomplete'; export * from '@codemirror/commands'; export * from '@codemirror/language'; export * from '@codemirror/search'; diff --git a/resources/codemirror.less b/resources/codemirror.less index df3603b3..6d7d0105 100644 --- a/resources/codemirror.less +++ b/resources/codemirror.less @@ -35,9 +35,10 @@ line-height: 1.2; padding: 0 1px; opacity: 0.6; -} -.cm-tooltip-fold:hover { - opacity: 1; + + &:hover { + opacity: 1; + } } .cm-editor .cm-foldPlaceholder { diff --git a/resources/codemirror.mediawiki.autocomplete.js b/resources/codemirror.mediawiki.autocomplete.js new file mode 100644 index 00000000..502d84fb --- /dev/null +++ b/resources/codemirror.mediawiki.autocomplete.js @@ -0,0 +1,20 @@ +const { + autocompletion, + acceptCompletion, + keymap +} = require( 'ext.CodeMirror.v6.lib' ); + +/** + * CodeMirror extension providing + * autocompletion + * for the MediaWiki mode. This automatically applied when using {@link CodeMirrorModeMediaWiki}. + * + * @module CodeMirrorAutocomplete + * @type {Extension} + */ +const autocompleteExtension = [ + autocompletion( { defaultKeymap: true } ), + keymap.of( [ { key: 'Tab', run: acceptCompletion } ] ) +]; + +module.exports = autocompleteExtension; diff --git a/resources/codemirror.mediawiki.js b/resources/codemirror.mediawiki.js index e98a1ab2..ce3c4b2a 100644 --- a/resources/codemirror.mediawiki.js +++ b/resources/codemirror.mediawiki.js @@ -1,15 +1,18 @@ const { + CompletionSource, HighlightStyle, LanguageSupport, StreamLanguage, StreamParser, StringStream, Tag, + ensureSyntaxTree, syntaxHighlighting } = require( 'ext.CodeMirror.v6.lib' ); const mwModeConfig = require( './codemirror.mediawiki.config.js' ); const bidiIsolationExtension = require( './codemirror.mediawiki.bidiIsolation.js' ); const templateFoldingExtension = require( './codemirror.mediawiki.templateFolding.js' ); +const autocompleteExtension = require( './codemirror.mediawiki.autocomplete.js' ); /** * MediaWiki language support for CodeMirror 6. @@ -35,8 +38,7 @@ class CodeMirrorModeMediaWiki { */ constructor( config ) { this.config = config; - - this.urlProtocols = new RegExp( `^(?:${ this.config.urlProtocols })(?=[^\\s\u00a0{[\\]<>~).,'])`, 'i' ); + this.urlProtocols = new RegExp( `^(?:${ config.urlProtocols })(?=[^\\s\u00a0{[\\]<>~).,'])`, 'i' ); this.isBold = false; this.wasBold = false; this.isItalic = false; @@ -51,7 +53,25 @@ class CodeMirrorModeMediaWiki { this.registerGroundTokens(); // Dynamically register any tags that aren't already in CodeMirrorModeMediaWikiConfig - Object.keys( this.config.tags ).forEach( ( tag ) => mwModeConfig.addTag( tag ) ); + Object.keys( config.tags ).forEach( ( tag ) => mwModeConfig.addTag( tag ) ); + + this.functionSynonyms = [ + ...Object.keys( config.functionSynonyms[ 0 ] ) + .map( ( label ) => ( { type: 'function', label } ) ), + ...Object.keys( config.functionSynonyms[ 1 ] ) + .map( ( label ) => ( { type: 'constant', label } ) ) + ]; + this.doubleUnderscore = [ + ...Object.keys( config.doubleUnderscore[ 0 ] ), + ...Object.keys( config.doubleUnderscore[ 1 ] ) + ].map( ( label ) => ( { type: 'constant', label } ) ); + const extTags = Object.keys( config.tags ); + this.extTags = extTags.map( ( label ) => ( { type: 'type', label } ) ); + this.htmlTags = Object.keys( mwModeConfig.permittedHtmlTags ) + .filter( ( tag ) => !extTags.includes( tag ) ) + .map( ( label ) => ( { type: 'type', label } ) ); + this.protocols = config.urlProtocols.split( '|' ) + .map( ( label ) => ( { type: 'namespace', label: label.replace( /\\([:/])/g, '$1' ) } ) ); } /** @@ -1097,6 +1117,87 @@ class CodeMirrorModeMediaWiki { this.wasItalic = this.isItalic; } + /** + * Autocompletion for magic words and tag names. + * + * @return {CompletionSource} + * @private + */ + get completionSource() { + return ( context ) => { + const { state, pos, explicit } = context, + tree = ensureSyntaxTree( state, pos ), + node = tree && tree.resolve( pos, -1 ); + if ( !node ) { + return null; + } + const types = new Set( node.name.split( '_' ) ), + isParserFunction = types.has( mwModeConfig.tags.parserFunctionName ), + { from, to } = node, + search = state.sliceDoc( from, to ); + if ( explicit || isParserFunction && search.includes( '#' ) ) { + const validFor = /^[^|{}<>[\]#]*$/; + if ( isParserFunction || types.has( mwModeConfig.tags.templateName ) && !search.includes( ':' ) ) { + return { + from, + options: this.functionSynonyms, + validFor + }; + } + } else if ( !types.has( mwModeConfig.tags.comment ) && + !types.has( mwModeConfig.tags.templateVariableName ) && + !types.has( mwModeConfig.tags.templateName ) && + !types.has( mwModeConfig.tags.linkPageName ) && + !types.has( mwModeConfig.tags.linkToSection ) && + !types.has( mwModeConfig.tags.extLink ) + ) { + let mt = context.matchBefore( /__(?:(?!__)[^\s<>[\]{}|#])*$/ ); + if ( mt ) { + return { + from: mt.from, + options: this.doubleUnderscore, + validFor: /^[^\s<>[\]{}|#]*$/ + }; + } + mt = context.matchBefore( /<\/?[a-z\d]*$/i ); + const extTags = [ ...types ].filter( ( t ) => t.startsWith( 'mw-tag-' ) ).map( ( s ) => s.slice( 7 ) ); + if ( mt && mt.to - mt.from > 1 ) { + const validFor = /^[a-z\d]*$/i; + if ( mt.text[ 1 ] === '/' ) { + const extTag = extTags[ extTags.length - 1 ], + options = [ + ...this.htmlTags.filter( ( { label } ) => !( + label in mwModeConfig.implicitlyClosedHtmlTags + ) ), + ...extTag ? [ { type: 'type', label: extTag, boost: 50 } ] : [] + ]; + return { from: mt.from + 2, options, validFor }; + } + return { + from: mt.from + 1, + options: [ + ...this.htmlTags, + ...this.extTags.filter( ( { label } ) => !extTags.includes( label ) ) + ], + validFor + }; + } + if ( !types.has( mwModeConfig.tags.linkText ) && + !types.has( mwModeConfig.tags.extLinkText ) ) { + mt = context.matchBefore( /(?:^|[^[])\[[a-z:/]+$/i ); + if ( mt ) { + return { + from: mt.from + ( mt.text[ 1 ] === '[' ? 2 : 1 ), + options: this.protocols, + validFor: /^[a-z:/]*$/i + }; + } + } + } + return null; + }; + } + /** * @see https://codemirror.net/docs/ref/#language.StreamParser * @return {StreamParser} @@ -1257,7 +1358,18 @@ class CodeMirrorModeMediaWiki { * @return {Object} * @private */ - tokenTable: this.tokenTable + tokenTable: this.tokenTable, + + /** + * @see https://codemirror.net/docs/ref/#language.StreamParser.languageData + * @return {Object} + * @private + */ + languageData: { + // TODO: Rewrite the comment command using jQuery.textSelection + commentTokens: { block: { open: '' } }, + autocomplete: this.completionSource + } }; } } @@ -1271,6 +1383,7 @@ class CodeMirrorModeMediaWiki { * @param {boolean} [config.bidiIsolation=false] Enable bidi isolation around HTML tags. * This should generally always be enabled on RTL pages, but it comes with a performance cost. * @param {boolean} [config.templateFolding=true] Enable template folding. + * @param {boolean} [config.autocomplete=true] Enable autocompletion. * @param {Object|null} [mwConfig] Ignore; used only by unit tests. * @return {LanguageSupport} * @stable to call @@ -1292,6 +1405,9 @@ const mediaWikiLang = ( config = { bidiIsolation: false }, mwConfig = null ) => if ( config.templateFolding !== false ) { cm.preferences.registerExtension( 'templateFolding', templateFoldingExtension, cm.view ); } + if ( config.autocomplete !== false ) { + cm.preferences.registerExtension( 'autocomplete', autocompleteExtension, cm.view ); + } if ( config.bidiIsolation ) { cm.preferences.registerExtension( 'bidiIsolation', bidiIsolationExtension, cm.view ); } diff --git a/resources/codemirror.mediawiki.templateFolding.js b/resources/codemirror.mediawiki.templateFolding.js index d3ecafe2..97a3451d 100644 --- a/resources/codemirror.mediawiki.templateFolding.js +++ b/resources/codemirror.mediawiki.templateFolding.js @@ -16,7 +16,7 @@ const { unfoldAll, unfoldEffect } = require( 'ext.CodeMirror.v6.lib' ); -const modeConfig = require( './codemirror.mediawiki.config.js' ); +const mwModeConfig = require( './codemirror.mediawiki.config.js' ); /** * Check if a SyntaxNode is a template bracket (`{{` or `}}`) @@ -25,7 +25,7 @@ const modeConfig = require( './codemirror.mediawiki.config.js' ); * @return {boolean} * @private */ -const isBracket = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.templateBracket ), +const isBracket = ( node ) => node.name.split( '_' ).includes( mwModeConfig.tags.templateBracket ), /** * Check if a SyntaxNode is a template delimiter (`|`) * @@ -33,7 +33,7 @@ const isBracket = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.t * @return {boolean} * @private */ - isDelimiter = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.templateDelimiter ), + isDelimiter = ( node ) => node.name.split( '_' ).includes( mwModeConfig.tags.templateDelimiter ), /** * Check if a SyntaxNode is part of a template, except for the brackets * @@ -41,7 +41,7 @@ const isBracket = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.t * @return {boolean} * @private */ - isTemplate = ( node ) => /-template[a-z\d-]+ground/u.test( node.name ) && !isBracket( node ), + isTemplate = ( node ) => /-template[a-z\d-]+ground/.test( node.name ) && !isBracket( node ), /** * Update the stack of opening (+) or closing (-) brackets * diff --git a/resources/codemirror.preferences.js b/resources/codemirror.preferences.js index a2c0e1f8..6bb9fd7f 100644 --- a/resources/codemirror.preferences.js +++ b/resources/codemirror.preferences.js @@ -182,7 +182,7 @@ class CodeMirrorPreferences extends CodeMirrorPanel { // Some preferences can be set per-namespace through wiki configuration. // Values are an array of namespace IDs, [] to disable everywhere, // or null to enable everywhere. - const namespacePrefs = [ 'lineNumbering', 'templateFolding' ]; + const namespacePrefs = [ 'lineNumbering', 'templateFolding', 'autocomplete' ]; if ( namespacePrefs.includes( prefName ) ) { const namespaces = mw.config.get( 'extCodeMirrorConfig' )[ prefName + 'Namespaces' ]; return !namespaces || namespaces.includes( mw.config.get( 'wgNamespaceNumber' ) ); diff --git a/resources/lib/codemirror6.bundle.dist.js b/resources/lib/codemirror6.bundle.dist.js index 6a16dcaf..86976db3 100644 --- a/resources/lib/codemirror6.bundle.dist.js +++ b/resources/lib/codemirror6.bundle.dist.js @@ -2479,9 +2479,9 @@ function extendTransaction(tr) { } return spec == tr ? tr : Transaction.create(state, tr.changes, tr.selection, spec.effects, spec.annotations, spec.scrollIntoView); } -const none$1 = []; +const none$2 = []; function asArray$1(value) { - return value == null ? none$1 : Array.isArray(value) ? value : [value]; + return value == null ? none$2 : Array.isArray(value) ? value : [value]; } /** @@ -5963,7 +5963,7 @@ const dragMovesSelection$1 = /*@__PURE__*/Facet.define(); const mouseSelectionStyle = /*@__PURE__*/Facet.define(); const exceptionSink = /*@__PURE__*/Facet.define(); const updateListener = /*@__PURE__*/Facet.define(); -const inputHandler = /*@__PURE__*/Facet.define(); +const inputHandler$1 = /*@__PURE__*/Facet.define(); const focusChangeEffect = /*@__PURE__*/Facet.define(); const perLineTextDirection = /*@__PURE__*/Facet.define({ combine: values => values.some(x => x) @@ -5996,7 +5996,7 @@ class ScrollTarget { new ScrollTarget(EditorSelection.cursor(state.doc.length), this.y, this.x, this.yMargin, this.xMargin, this.isSnapshot); } } -const scrollIntoView = /*@__PURE__*/StateEffect.define({ map: (t, ch) => t.map(ch) }); +const scrollIntoView$1 = /*@__PURE__*/StateEffect.define({ map: (t, ch) => t.map(ch) }); /** Log or report an unhandled exception in client code. Should probably only be used by extension code that allows client code to @@ -10429,7 +10429,7 @@ function applyDOMChange(view, domChange) { view.inputState.composing++; let defaultTr; let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel)); - if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert))) + if (!view.state.facet(inputHandler$1).some(h => h(view, change.from, change.to, text, defaultInsert))) view.dispatch(defaultInsert()); return true; } @@ -11124,7 +11124,7 @@ class EditorView { this.dispatch = this.dispatch.bind(this); this._root = (config.root || getRoot(config.parent) || document); this.viewState = new ViewState(config.state || EditorState.create(config)); - if (config.scrollTo && config.scrollTo.is(scrollIntoView)) + if (config.scrollTo && config.scrollTo.is(scrollIntoView$1)) this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state); this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec)); for (let plugin of this.plugins) @@ -11212,7 +11212,7 @@ class EditorView { scrollTarget = new ScrollTarget(main.empty ? main : EditorSelection.cursor(main.head, main.head > main.anchor ? -1 : 1)); } for (let e of tr.effects) - if (e.is(scrollIntoView)) + if (e.is(scrollIntoView$1)) scrollTarget = e.value.clip(this.state); } this.viewState.update(update, scrollTarget); @@ -11825,7 +11825,7 @@ class EditorView { cause it to scroll the given position or range into view. */ static scrollIntoView(pos, options = {}) { - return scrollIntoView.of(new ScrollTarget(typeof pos == "number" ? EditorSelection.cursor(pos) : pos, options.y, options.x, options.yMargin, options.xMargin)); + return scrollIntoView$1.of(new ScrollTarget(typeof pos == "number" ? EditorSelection.cursor(pos) : pos, options.y, options.x, options.yMargin, options.xMargin)); } /** Return an effect that resets the editor to its current (at the @@ -11842,7 +11842,7 @@ class EditorView { scrollSnapshot() { let { scrollTop, scrollLeft } = this.scrollDOM; let ref = this.viewState.scrollAnchorAt(scrollTop); - return scrollIntoView.of(new ScrollTarget(EditorSelection.cursor(ref.from), "start", "start", ref.top - scrollTop, scrollLeft, true)); + return scrollIntoView$1.of(new ScrollTarget(EditorSelection.cursor(ref.from), "start", "start", ref.top - scrollTop, scrollLeft, true)); } /** Returns an extension that can be used to add DOM event handlers. @@ -11935,7 +11935,7 @@ The `insert` argument can be used to get the default transaction that would be applied for this input. This can be useful when dispatching the custom behavior as a separate transaction. */ -EditorView.inputHandler = inputHandler; +EditorView.inputHandler = inputHandler$1; /** This facet can be used to provide functions that create effects to be dispatched when the editor's focus state changes. @@ -13598,7 +13598,7 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class { scroll() { this.maybeMeasure(); } } }); -const baseTheme$3 = /*@__PURE__*/EditorView.baseTheme({ +const baseTheme$4 = /*@__PURE__*/EditorView.baseTheme({ ".cm-tooltip": { zIndex: 100, boxSizing: "border-box" @@ -13665,7 +13665,7 @@ const noOffset = { x: 0, y: 0 }; Facet to which an extension can add a value to show a tooltip. */ const showTooltip = /*@__PURE__*/Facet.define({ - enables: [tooltipPlugin, baseTheme$3] + enables: [tooltipPlugin, baseTheme$4] }); const showHoverTooltip = /*@__PURE__*/Facet.define(); class HoverTooltipHost { @@ -14164,7 +14164,7 @@ Markers given to this facet should _only_ define an in all gutters for the line). */ const gutterLineClass = /*@__PURE__*/Facet.define(); -const defaults = { +const defaults$1 = { class: "", renderEmptyElements: false, elementStyle: "", @@ -14182,7 +14182,7 @@ Define an editor gutter. The order in which the gutters appear is determined by their extension priority. */ function gutter(config) { - return [gutters(), activeGutters.of(Object.assign(Object.assign({}, defaults), config))]; + return [gutters(), activeGutters.of(Object.assign(Object.assign({}, defaults$1), config))]; } const unfixGutters = /*@__PURE__*/Facet.define({ combine: values => values.some(x => x) @@ -19058,7 +19058,7 @@ const defaultHighlightStyle = /*@__PURE__*/HighlightStyle.define([ color: "#f00" } ]); -const baseTheme$2 = /*@__PURE__*/EditorView.baseTheme({ +const baseTheme$3 = /*@__PURE__*/EditorView.baseTheme({ "&.cm-focused .cm-matchingBracket": { backgroundColor: "#328c8252" }, "&.cm-focused .cm-nonmatchingBracket": { backgroundColor: "#bb555544" } }); @@ -19106,7 +19106,7 @@ const bracketMatchingState = /*@__PURE__*/StateField.define({ }); const bracketMatchingUnique = [ bracketMatchingState, - baseTheme$2 + baseTheme$3 ]; /** Create an extension that enables bracket matching. Whenever the @@ -19766,6 +19766,1980 @@ function docID(data) { return type; } +/** +An instance of this is passed to completion source functions. +*/ +class CompletionContext { + /** + Create a new completion context. (Mostly useful for testing + completion sources—in the editor, the extension will create + these for you.) + */ + constructor( + /** + The editor state that the completion happens in. + */ + state, + /** + The position at which the completion is happening. + */ + pos, + /** + Indicates whether completion was activated explicitly, or + implicitly by typing. The usual way to respond to this is to + only return completions when either there is part of a + completable entity before the cursor, or `explicit` is true. + */ + explicit) { + this.state = state; + this.pos = pos; + this.explicit = explicit; + /** + @internal + */ + this.abortListeners = []; + } + /** + Get the extent, content, and (if there is a token) type of the + token before `this.pos`. + */ + tokenBefore(types) { + let token = syntaxTree(this.state).resolveInner(this.pos, -1); + while (token && types.indexOf(token.name) < 0) + token = token.parent; + return token ? { from: token.from, to: this.pos, + text: this.state.sliceDoc(token.from, this.pos), + type: token.type } : null; + } + /** + Get the match of the given expression directly before the + cursor. + */ + matchBefore(expr) { + let line = this.state.doc.lineAt(this.pos); + let start = Math.max(line.from, this.pos - 250); + let str = line.text.slice(start - line.from, this.pos - line.from); + let found = str.search(ensureAnchor(expr, false)); + return found < 0 ? null : { from: start + found, to: this.pos, text: str.slice(found) }; + } + /** + Yields true when the query has been aborted. Can be useful in + asynchronous queries to avoid doing work that will be ignored. + */ + get aborted() { return this.abortListeners == null; } + /** + Allows you to register abort handlers, which will be called when + the query is + [aborted](https://codemirror.net/6/docs/ref/#autocomplete.CompletionContext.aborted). + */ + addEventListener(type, listener) { + if (type == "abort" && this.abortListeners) + this.abortListeners.push(listener); + } +} +function toSet(chars) { + let flat = Object.keys(chars).join(""); + let words = /\w/.test(flat); + if (words) + flat = flat.replace(/\w/g, ""); + return `[${words ? "\\w" : ""}${flat.replace(/[^\w\s]/g, "\\$&")}]`; +} +function prefixMatch(options) { + let first = Object.create(null), rest = Object.create(null); + for (let { label } of options) { + first[label[0]] = true; + for (let i = 1; i < label.length; i++) + rest[label[i]] = true; + } + let source = toSet(first) + toSet(rest) + "*$"; + return [new RegExp("^" + source), new RegExp(source)]; +} +/** +Given a a fixed array of options, return an autocompleter that +completes them. +*/ +function completeFromList(list) { + let options = list.map(o => typeof o == "string" ? { label: o } : o); + let [validFor, match] = options.every(o => /^\w+$/.test(o.label)) ? [/\w*$/, /\w+$/] : prefixMatch(options); + return (context) => { + let token = context.matchBefore(match); + return token || context.explicit ? { from: token ? token.from : context.pos, options, validFor } : null; + }; +} +/** +Wrap the given completion source so that it will only fire when the +cursor is in a syntax node with one of the given names. +*/ +function ifIn(nodes, source) { + return (context) => { + for (let pos = syntaxTree(context.state).resolveInner(context.pos, -1); pos; pos = pos.parent) { + if (nodes.indexOf(pos.name) > -1) + return source(context); + if (pos.type.isTop) + break; + } + return null; + }; +} +/** +Wrap the given completion source so that it will not fire when the +cursor is in a syntax node with one of the given names. +*/ +function ifNotIn(nodes, source) { + return (context) => { + for (let pos = syntaxTree(context.state).resolveInner(context.pos, -1); pos; pos = pos.parent) { + if (nodes.indexOf(pos.name) > -1) + return null; + if (pos.type.isTop) + break; + } + return source(context); + }; +} +class Option { + constructor(completion, source, match, score) { + this.completion = completion; + this.source = source; + this.match = match; + this.score = score; + } +} +function cur(state) { return state.selection.main.from; } +// Make sure the given regexp has a $ at its end and, if `start` is +// true, a ^ at its start. +function ensureAnchor(expr, start) { + var _a; + let { source } = expr; + let addStart = start && source[0] != "^", addEnd = source[source.length - 1] != "$"; + if (!addStart && !addEnd) + return expr; + return new RegExp(`${addStart ? "^" : ""}(?:${source})${addEnd ? "$" : ""}`, (_a = expr.flags) !== null && _a !== void 0 ? _a : (expr.ignoreCase ? "i" : "")); +} +/** +This annotation is added to transactions that are produced by +picking a completion. +*/ +const pickedCompletion = /*@__PURE__*/Annotation.define(); +/** +Helper function that returns a transaction spec which inserts a +completion's text in the main selection range, and any other +selection range that has the same text in front of it. +*/ +function insertCompletionText(state, text, from, to) { + let { main } = state.selection, fromOff = from - main.from, toOff = to - main.from; + return Object.assign(Object.assign({}, state.changeByRange(range => { + if (range != main && from != to && + state.sliceDoc(range.from + fromOff, range.from + toOff) != state.sliceDoc(from, to)) + return { range }; + return { + changes: { from: range.from + fromOff, to: to == main.from ? range.to : range.from + toOff, insert: text }, + range: EditorSelection.cursor(range.from + fromOff + text.length) + }; + })), { scrollIntoView: true, userEvent: "input.complete" }); +} +const SourceCache = /*@__PURE__*/new WeakMap(); +function asSource(source) { + if (!Array.isArray(source)) + return source; + let known = SourceCache.get(source); + if (!known) + SourceCache.set(source, known = completeFromList(source)); + return known; +} +const startCompletionEffect = /*@__PURE__*/StateEffect.define(); +const closeCompletionEffect = /*@__PURE__*/StateEffect.define(); + +// A pattern matcher for fuzzy completion matching. Create an instance +// once for a pattern, and then use that to match any number of +// completions. +class FuzzyMatcher { + constructor(pattern) { + this.pattern = pattern; + this.chars = []; + this.folded = []; + // Buffers reused by calls to `match` to track matched character + // positions. + this.any = []; + this.precise = []; + this.byWord = []; + this.score = 0; + this.matched = []; + for (let p = 0; p < pattern.length;) { + let char = codePointAt(pattern, p), size = codePointSize(char); + this.chars.push(char); + let part = pattern.slice(p, p + size), upper = part.toUpperCase(); + this.folded.push(codePointAt(upper == part ? part.toLowerCase() : upper, 0)); + p += size; + } + this.astral = pattern.length != this.chars.length; + } + ret(score, matched) { + this.score = score; + this.matched = matched; + return true; + } + // Matches a given word (completion) against the pattern (input). + // Will return a boolean indicating whether there was a match and, + // on success, set `this.score` to the score, `this.matched` to an + // array of `from, to` pairs indicating the matched parts of `word`. + // + // The score is a number that is more negative the worse the match + // is. See `Penalty` above. + match(word) { + if (this.pattern.length == 0) + return this.ret(-100 /* Penalty.NotFull */, []); + if (word.length < this.pattern.length) + return false; + let { chars, folded, any, precise, byWord } = this; + // For single-character queries, only match when they occur right + // at the start + if (chars.length == 1) { + let first = codePointAt(word, 0), firstSize = codePointSize(first); + let score = firstSize == word.length ? 0 : -100 /* Penalty.NotFull */; + if (first == chars[0]) ; + else if (first == folded[0]) + score += -200 /* Penalty.CaseFold */; + else + return false; + return this.ret(score, [0, firstSize]); + } + let direct = word.indexOf(this.pattern); + if (direct == 0) + return this.ret(word.length == this.pattern.length ? 0 : -100 /* Penalty.NotFull */, [0, this.pattern.length]); + let len = chars.length, anyTo = 0; + if (direct < 0) { + for (let i = 0, e = Math.min(word.length, 200); i < e && anyTo < len;) { + let next = codePointAt(word, i); + if (next == chars[anyTo] || next == folded[anyTo]) + any[anyTo++] = i; + i += codePointSize(next); + } + // No match, exit immediately + if (anyTo < len) + return false; + } + // This tracks the extent of the precise (non-folded, not + // necessarily adjacent) match + let preciseTo = 0; + // Tracks whether there is a match that hits only characters that + // appear to be starting words. `byWordFolded` is set to true when + // a case folded character is encountered in such a match + let byWordTo = 0, byWordFolded = false; + // If we've found a partial adjacent match, these track its state + let adjacentTo = 0, adjacentStart = -1, adjacentEnd = -1; + let hasLower = /[a-z]/.test(word), wordAdjacent = true; + // Go over the option's text, scanning for the various kinds of matches + for (let i = 0, e = Math.min(word.length, 200), prevType = 0 /* Tp.NonWord */; i < e && byWordTo < len;) { + let next = codePointAt(word, i); + if (direct < 0) { + if (preciseTo < len && next == chars[preciseTo]) + precise[preciseTo++] = i; + if (adjacentTo < len) { + if (next == chars[adjacentTo] || next == folded[adjacentTo]) { + if (adjacentTo == 0) + adjacentStart = i; + adjacentEnd = i + 1; + adjacentTo++; + } + else { + adjacentTo = 0; + } + } + } + let ch, type = next < 0xff + ? (next >= 48 && next <= 57 || next >= 97 && next <= 122 ? 2 /* Tp.Lower */ : next >= 65 && next <= 90 ? 1 /* Tp.Upper */ : 0 /* Tp.NonWord */) + : ((ch = fromCodePoint(next)) != ch.toLowerCase() ? 1 /* Tp.Upper */ : ch != ch.toUpperCase() ? 2 /* Tp.Lower */ : 0 /* Tp.NonWord */); + if (!i || type == 1 /* Tp.Upper */ && hasLower || prevType == 0 /* Tp.NonWord */ && type != 0 /* Tp.NonWord */) { + if (chars[byWordTo] == next || (folded[byWordTo] == next && (byWordFolded = true))) + byWord[byWordTo++] = i; + else if (byWord.length) + wordAdjacent = false; + } + prevType = type; + i += codePointSize(next); + } + if (byWordTo == len && byWord[0] == 0 && wordAdjacent) + return this.result(-100 /* Penalty.ByWord */ + (byWordFolded ? -200 /* Penalty.CaseFold */ : 0), byWord, word); + if (adjacentTo == len && adjacentStart == 0) + return this.ret(-200 /* Penalty.CaseFold */ - word.length + (adjacentEnd == word.length ? 0 : -100 /* Penalty.NotFull */), [0, adjacentEnd]); + if (direct > -1) + return this.ret(-700 /* Penalty.NotStart */ - word.length, [direct, direct + this.pattern.length]); + if (adjacentTo == len) + return this.ret(-200 /* Penalty.CaseFold */ + -700 /* Penalty.NotStart */ - word.length, [adjacentStart, adjacentEnd]); + if (byWordTo == len) + return this.result(-100 /* Penalty.ByWord */ + (byWordFolded ? -200 /* Penalty.CaseFold */ : 0) + -700 /* Penalty.NotStart */ + + (wordAdjacent ? 0 : -1100 /* Penalty.Gap */), byWord, word); + return chars.length == 2 ? false + : this.result((any[0] ? -700 /* Penalty.NotStart */ : 0) + -200 /* Penalty.CaseFold */ + -1100 /* Penalty.Gap */, any, word); + } + result(score, positions, word) { + let result = [], i = 0; + for (let pos of positions) { + let to = pos + (this.astral ? codePointSize(codePointAt(word, pos)) : 1); + if (i && result[i - 1] == pos) + result[i - 1] = to; + else { + result[i++] = pos; + result[i++] = to; + } + } + return this.ret(score - word.length, result); + } +} + +const completionConfig = /*@__PURE__*/Facet.define({ + combine(configs) { + return combineConfig(configs, { + activateOnTyping: true, + activateOnTypingDelay: 100, + selectOnOpen: true, + override: null, + closeOnBlur: true, + maxRenderedOptions: 100, + defaultKeymap: true, + tooltipClass: () => "", + optionClass: () => "", + aboveCursor: false, + icons: true, + addToOptions: [], + positionInfo: defaultPositionInfo, + compareCompletions: (a, b) => a.label.localeCompare(b.label), + interactionDelay: 75, + updateSyncTime: 100 + }, { + defaultKeymap: (a, b) => a && b, + closeOnBlur: (a, b) => a && b, + icons: (a, b) => a && b, + tooltipClass: (a, b) => c => joinClass(a(c), b(c)), + optionClass: (a, b) => c => joinClass(a(c), b(c)), + addToOptions: (a, b) => a.concat(b) + }); + } +}); +function joinClass(a, b) { + return a ? b ? a + " " + b : a : b; +} +function defaultPositionInfo(view, list, option, info, space, tooltip) { + let rtl = view.textDirection == exports.Direction.RTL, left = rtl, narrow = false; + let side = "top", offset, maxWidth; + let spaceLeft = list.left - space.left, spaceRight = space.right - list.right; + let infoWidth = info.right - info.left, infoHeight = info.bottom - info.top; + if (left && spaceLeft < Math.min(infoWidth, spaceRight)) + left = false; + else if (!left && spaceRight < Math.min(infoWidth, spaceLeft)) + left = true; + if (infoWidth <= (left ? spaceLeft : spaceRight)) { + offset = Math.max(space.top, Math.min(option.top, space.bottom - infoHeight)) - list.top; + maxWidth = Math.min(400 /* Info.Width */, left ? spaceLeft : spaceRight); + } + else { + narrow = true; + maxWidth = Math.min(400 /* Info.Width */, (rtl ? list.right : space.right - list.left) - 30 /* Info.Margin */); + let spaceBelow = space.bottom - list.bottom; + if (spaceBelow >= infoHeight || spaceBelow > list.top) { // Below the completion + offset = option.bottom - list.top; + } + else { // Above it + side = "bottom"; + offset = list.bottom - option.top; + } + } + let scaleY = (list.bottom - list.top) / tooltip.offsetHeight; + let scaleX = (list.right - list.left) / tooltip.offsetWidth; + return { + style: `${side}: ${offset / scaleY}px; max-width: ${maxWidth / scaleX}px`, + class: "cm-completionInfo-" + (narrow ? (rtl ? "left-narrow" : "right-narrow") : left ? "left" : "right") + }; +} + +function optionContent(config) { + let content = config.addToOptions.slice(); + if (config.icons) + content.push({ + render(completion) { + let icon = document.createElement("div"); + icon.classList.add("cm-completionIcon"); + if (completion.type) + icon.classList.add(...completion.type.split(/\s+/g).map(cls => "cm-completionIcon-" + cls)); + icon.setAttribute("aria-hidden", "true"); + return icon; + }, + position: 20 + }); + content.push({ + render(completion, _s, _v, match) { + let labelElt = document.createElement("span"); + labelElt.className = "cm-completionLabel"; + let label = completion.displayLabel || completion.label, off = 0; + for (let j = 0; j < match.length;) { + let from = match[j++], to = match[j++]; + if (from > off) + labelElt.appendChild(document.createTextNode(label.slice(off, from))); + let span = labelElt.appendChild(document.createElement("span")); + span.appendChild(document.createTextNode(label.slice(from, to))); + span.className = "cm-completionMatchedText"; + off = to; + } + if (off < label.length) + labelElt.appendChild(document.createTextNode(label.slice(off))); + return labelElt; + }, + position: 50 + }, { + render(completion) { + if (!completion.detail) + return null; + let detailElt = document.createElement("span"); + detailElt.className = "cm-completionDetail"; + detailElt.textContent = completion.detail; + return detailElt; + }, + position: 80 + }); + return content.sort((a, b) => a.position - b.position).map(a => a.render); +} +function rangeAroundSelected(total, selected, max) { + if (total <= max) + return { from: 0, to: total }; + if (selected < 0) + selected = 0; + if (selected <= (total >> 1)) { + let off = Math.floor(selected / max); + return { from: off * max, to: (off + 1) * max }; + } + let off = Math.floor((total - selected) / max); + return { from: total - (off + 1) * max, to: total - off * max }; +} +class CompletionTooltip { + constructor(view, stateField, applyCompletion) { + this.view = view; + this.stateField = stateField; + this.applyCompletion = applyCompletion; + this.info = null; + this.infoDestroy = null; + this.placeInfoReq = { + read: () => this.measureInfo(), + write: (pos) => this.placeInfo(pos), + key: this + }; + this.space = null; + this.currentClass = ""; + let cState = view.state.field(stateField); + let { options, selected } = cState.open; + let config = view.state.facet(completionConfig); + this.optionContent = optionContent(config); + this.optionClass = config.optionClass; + this.tooltipClass = config.tooltipClass; + this.range = rangeAroundSelected(options.length, selected, config.maxRenderedOptions); + this.dom = document.createElement("div"); + this.dom.className = "cm-tooltip-autocomplete"; + this.updateTooltipClass(view.state); + this.dom.addEventListener("mousedown", (e) => { + let { options } = view.state.field(stateField).open; + for (let dom = e.target, match; dom && dom != this.dom; dom = dom.parentNode) { + if (dom.nodeName == "LI" && (match = /-(\d+)$/.exec(dom.id)) && +match[1] < options.length) { + this.applyCompletion(view, options[+match[1]]); + e.preventDefault(); + return; + } + } + }); + this.dom.addEventListener("focusout", (e) => { + let state = view.state.field(this.stateField, false); + if (state && state.tooltip && view.state.facet(completionConfig).closeOnBlur && + e.relatedTarget != view.contentDOM) + view.dispatch({ effects: closeCompletionEffect.of(null) }); + }); + this.showOptions(options, cState.id); + } + mount() { this.updateSel(); } + showOptions(options, id) { + if (this.list) + this.list.remove(); + this.list = this.dom.appendChild(this.createListBox(options, id, this.range)); + this.list.addEventListener("scroll", () => { + if (this.info) + this.view.requestMeasure(this.placeInfoReq); + }); + } + update(update) { + var _a; + let cState = update.state.field(this.stateField); + let prevState = update.startState.field(this.stateField); + this.updateTooltipClass(update.state); + if (cState != prevState) { + let { options, selected, disabled } = cState.open; + if (!prevState.open || prevState.open.options != options) { + this.range = rangeAroundSelected(options.length, selected, update.state.facet(completionConfig).maxRenderedOptions); + this.showOptions(options, cState.id); + } + this.updateSel(); + if (disabled != ((_a = prevState.open) === null || _a === void 0 ? void 0 : _a.disabled)) + this.dom.classList.toggle("cm-tooltip-autocomplete-disabled", !!disabled); + } + } + updateTooltipClass(state) { + let cls = this.tooltipClass(state); + if (cls != this.currentClass) { + for (let c of this.currentClass.split(" ")) + if (c) + this.dom.classList.remove(c); + for (let c of cls.split(" ")) + if (c) + this.dom.classList.add(c); + this.currentClass = cls; + } + } + positioned(space) { + this.space = space; + if (this.info) + this.view.requestMeasure(this.placeInfoReq); + } + updateSel() { + let cState = this.view.state.field(this.stateField), open = cState.open; + if (open.selected > -1 && open.selected < this.range.from || open.selected >= this.range.to) { + this.range = rangeAroundSelected(open.options.length, open.selected, this.view.state.facet(completionConfig).maxRenderedOptions); + this.showOptions(open.options, cState.id); + } + if (this.updateSelectedOption(open.selected)) { + this.destroyInfo(); + let { completion } = open.options[open.selected]; + let { info } = completion; + if (!info) + return; + let infoResult = typeof info === "string" ? document.createTextNode(info) : info(completion); + if (!infoResult) + return; + if ("then" in infoResult) { + infoResult.then(obj => { + if (obj && this.view.state.field(this.stateField, false) == cState) + this.addInfoPane(obj, completion); + }).catch(e => logException(this.view.state, e, "completion info")); + } + else { + this.addInfoPane(infoResult, completion); + } + } + } + addInfoPane(content, completion) { + this.destroyInfo(); + let wrap = this.info = document.createElement("div"); + wrap.className = "cm-tooltip cm-completionInfo"; + if (content.nodeType != null) { + wrap.appendChild(content); + this.infoDestroy = null; + } + else { + let { dom, destroy } = content; + wrap.appendChild(dom); + this.infoDestroy = destroy || null; + } + this.dom.appendChild(wrap); + this.view.requestMeasure(this.placeInfoReq); + } + updateSelectedOption(selected) { + let set = null; + for (let opt = this.list.firstChild, i = this.range.from; opt; opt = opt.nextSibling, i++) { + if (opt.nodeName != "LI" || !opt.id) { + i--; // A section header + } + else if (i == selected) { + if (!opt.hasAttribute("aria-selected")) { + opt.setAttribute("aria-selected", "true"); + set = opt; + } + } + else { + if (opt.hasAttribute("aria-selected")) + opt.removeAttribute("aria-selected"); + } + } + if (set) + scrollIntoView(this.list, set); + return set; + } + measureInfo() { + let sel = this.dom.querySelector("[aria-selected]"); + if (!sel || !this.info) + return null; + let listRect = this.dom.getBoundingClientRect(); + let infoRect = this.info.getBoundingClientRect(); + let selRect = sel.getBoundingClientRect(); + let space = this.space; + if (!space) { + let win = this.dom.ownerDocument.defaultView || window; + space = { left: 0, top: 0, right: win.innerWidth, bottom: win.innerHeight }; + } + if (selRect.top > Math.min(space.bottom, listRect.bottom) - 10 || + selRect.bottom < Math.max(space.top, listRect.top) + 10) + return null; + return this.view.state.facet(completionConfig).positionInfo(this.view, listRect, selRect, infoRect, space, this.dom); + } + placeInfo(pos) { + if (this.info) { + if (pos) { + if (pos.style) + this.info.style.cssText = pos.style; + this.info.className = "cm-tooltip cm-completionInfo " + (pos.class || ""); + } + else { + this.info.style.cssText = "top: -1e6px"; + } + } + } + createListBox(options, id, range) { + const ul = document.createElement("ul"); + ul.id = id; + ul.setAttribute("role", "listbox"); + ul.setAttribute("aria-expanded", "true"); + ul.setAttribute("aria-label", this.view.state.phrase("Completions")); + let curSection = null; + for (let i = range.from; i < range.to; i++) { + let { completion, match } = options[i], { section } = completion; + if (section) { + let name = typeof section == "string" ? section : section.name; + if (name != curSection && (i > range.from || range.from == 0)) { + curSection = name; + if (typeof section != "string" && section.header) { + ul.appendChild(section.header(section)); + } + else { + let header = ul.appendChild(document.createElement("completion-section")); + header.textContent = name; + } + } + } + const li = ul.appendChild(document.createElement("li")); + li.id = id + "-" + i; + li.setAttribute("role", "option"); + let cls = this.optionClass(completion); + if (cls) + li.className = cls; + for (let source of this.optionContent) { + let node = source(completion, this.view.state, this.view, match); + if (node) + li.appendChild(node); + } + } + if (range.from) + ul.classList.add("cm-completionListIncompleteTop"); + if (range.to < options.length) + ul.classList.add("cm-completionListIncompleteBottom"); + return ul; + } + destroyInfo() { + if (this.info) { + if (this.infoDestroy) + this.infoDestroy(); + this.info.remove(); + this.info = null; + } + } + destroy() { + this.destroyInfo(); + } +} +function completionTooltip(stateField, applyCompletion) { + return (view) => new CompletionTooltip(view, stateField, applyCompletion); +} +function scrollIntoView(container, element) { + let parent = container.getBoundingClientRect(); + let self = element.getBoundingClientRect(); + let scaleY = parent.height / container.offsetHeight; + if (self.top < parent.top) + container.scrollTop -= (parent.top - self.top) / scaleY; + else if (self.bottom > parent.bottom) + container.scrollTop += (self.bottom - parent.bottom) / scaleY; +} + +// Used to pick a preferred option when two options with the same +// label occur in the result. +function score(option) { + return (option.boost || 0) * 100 + (option.apply ? 10 : 0) + (option.info ? 5 : 0) + + (option.type ? 1 : 0); +} +function sortOptions(active, state) { + let options = []; + let sections = null; + let addOption = (option) => { + options.push(option); + let { section } = option.completion; + if (section) { + if (!sections) + sections = []; + let name = typeof section == "string" ? section : section.name; + if (!sections.some(s => s.name == name)) + sections.push(typeof section == "string" ? { name } : section); + } + }; + for (let a of active) + if (a.hasResult()) { + let getMatch = a.result.getMatch; + if (a.result.filter === false) { + for (let option of a.result.options) { + addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], 1e9 - options.length)); + } + } + else { + let matcher = new FuzzyMatcher(state.sliceDoc(a.from, a.to)); + for (let option of a.result.options) + if (matcher.match(option.label)) { + let matched = !option.displayLabel ? matcher.matched : getMatch ? getMatch(option, matcher.matched) : []; + addOption(new Option(option, a.source, matched, matcher.score + (option.boost || 0))); + } + } + } + if (sections) { + let sectionOrder = Object.create(null), pos = 0; + let cmp = (a, b) => { var _a, _b; return ((_a = a.rank) !== null && _a !== void 0 ? _a : 1e9) - ((_b = b.rank) !== null && _b !== void 0 ? _b : 1e9) || (a.name < b.name ? -1 : 1); }; + for (let s of sections.sort(cmp)) { + pos -= 1e5; + sectionOrder[s.name] = pos; + } + for (let option of options) { + let { section } = option.completion; + if (section) + option.score += sectionOrder[typeof section == "string" ? section : section.name]; + } + } + let result = [], prev = null; + let compare = state.facet(completionConfig).compareCompletions; + for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) { + let cur = opt.completion; + if (!prev || prev.label != cur.label || prev.detail != cur.detail || + (prev.type != null && cur.type != null && prev.type != cur.type) || + prev.apply != cur.apply || prev.boost != cur.boost) + result.push(opt); + else if (score(opt.completion) > score(prev)) + result[result.length - 1] = opt; + prev = opt.completion; + } + return result; +} +class CompletionDialog { + constructor(options, attrs, tooltip, timestamp, selected, disabled) { + this.options = options; + this.attrs = attrs; + this.tooltip = tooltip; + this.timestamp = timestamp; + this.selected = selected; + this.disabled = disabled; + } + setSelected(selected, id) { + return selected == this.selected || selected >= this.options.length ? this + : new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected, this.disabled); + } + static build(active, state, id, prev, conf) { + let options = sortOptions(active, state); + if (!options.length) { + return prev && active.some(a => a.state == 1 /* State.Pending */) ? + new CompletionDialog(prev.options, prev.attrs, prev.tooltip, prev.timestamp, prev.selected, true) : null; + } + let selected = state.facet(completionConfig).selectOnOpen ? 0 : -1; + if (prev && prev.selected != selected && prev.selected != -1) { + let selectedValue = prev.options[prev.selected].completion; + for (let i = 0; i < options.length; i++) + if (options[i].completion == selectedValue) { + selected = i; + break; + } + } + return new CompletionDialog(options, makeAttrs(id, selected), { + pos: active.reduce((a, b) => b.hasResult() ? Math.min(a, b.from) : a, 1e8), + create: createTooltip, + above: conf.aboveCursor, + }, prev ? prev.timestamp : Date.now(), selected, false); + } + map(changes) { + return new CompletionDialog(this.options, this.attrs, Object.assign(Object.assign({}, this.tooltip), { pos: changes.mapPos(this.tooltip.pos) }), this.timestamp, this.selected, this.disabled); + } +} +class CompletionState { + constructor(active, id, open) { + this.active = active; + this.id = id; + this.open = open; + } + static start() { + return new CompletionState(none$1, "cm-ac-" + Math.floor(Math.random() * 2e6).toString(36), null); + } + update(tr) { + let { state } = tr, conf = state.facet(completionConfig); + let sources = conf.override || + state.languageDataAt("autocomplete", cur(state)).map(asSource); + let active = sources.map(source => { + let value = this.active.find(s => s.source == source) || + new ActiveSource(source, this.active.some(a => a.state != 0 /* State.Inactive */) ? 1 /* State.Pending */ : 0 /* State.Inactive */); + return value.update(tr, conf); + }); + if (active.length == this.active.length && active.every((a, i) => a == this.active[i])) + active = this.active; + let open = this.open; + if (open && tr.docChanged) + open = open.map(tr.changes); + if (tr.selection || active.some(a => a.hasResult() && tr.changes.touchesRange(a.from, a.to)) || + !sameResults(active, this.active)) + open = CompletionDialog.build(active, state, this.id, open, conf); + else if (open && open.disabled && !active.some(a => a.state == 1 /* State.Pending */)) + open = null; + if (!open && active.every(a => a.state != 1 /* State.Pending */) && active.some(a => a.hasResult())) + active = active.map(a => a.hasResult() ? new ActiveSource(a.source, 0 /* State.Inactive */) : a); + for (let effect of tr.effects) + if (effect.is(setSelectedEffect)) + open = open && open.setSelected(effect.value, this.id); + return active == this.active && open == this.open ? this : new CompletionState(active, this.id, open); + } + get tooltip() { return this.open ? this.open.tooltip : null; } + get attrs() { return this.open ? this.open.attrs : baseAttrs; } +} +function sameResults(a, b) { + if (a == b) + return true; + for (let iA = 0, iB = 0;;) { + while (iA < a.length && !a[iA].hasResult) + iA++; + while (iB < b.length && !b[iB].hasResult) + iB++; + let endA = iA == a.length, endB = iB == b.length; + if (endA || endB) + return endA == endB; + if (a[iA++].result != b[iB++].result) + return false; + } +} +const baseAttrs = { + "aria-autocomplete": "list" +}; +function makeAttrs(id, selected) { + let result = { + "aria-autocomplete": "list", + "aria-haspopup": "listbox", + "aria-controls": id + }; + if (selected > -1) + result["aria-activedescendant"] = id + "-" + selected; + return result; +} +const none$1 = []; +function getUserEvent(tr) { + return tr.isUserEvent("input.type") ? "input" : tr.isUserEvent("delete.backward") ? "delete" : null; +} +class ActiveSource { + constructor(source, state, explicitPos = -1) { + this.source = source; + this.state = state; + this.explicitPos = explicitPos; + } + hasResult() { return false; } + update(tr, conf) { + let event = getUserEvent(tr), value = this; + if (event) + value = value.handleUserEvent(tr, event, conf); + else if (tr.docChanged) + value = value.handleChange(tr); + else if (tr.selection && value.state != 0 /* State.Inactive */) + value = new ActiveSource(value.source, 0 /* State.Inactive */); + for (let effect of tr.effects) { + if (effect.is(startCompletionEffect)) + value = new ActiveSource(value.source, 1 /* State.Pending */, effect.value ? cur(tr.state) : -1); + else if (effect.is(closeCompletionEffect)) + value = new ActiveSource(value.source, 0 /* State.Inactive */); + else if (effect.is(setActiveEffect)) + for (let active of effect.value) + if (active.source == value.source) + value = active; + } + return value; + } + handleUserEvent(tr, type, conf) { + return type == "delete" || !conf.activateOnTyping ? this.map(tr.changes) : new ActiveSource(this.source, 1 /* State.Pending */); + } + handleChange(tr) { + return tr.changes.touchesRange(cur(tr.startState)) ? new ActiveSource(this.source, 0 /* State.Inactive */) : this.map(tr.changes); + } + map(changes) { + return changes.empty || this.explicitPos < 0 ? this : new ActiveSource(this.source, this.state, changes.mapPos(this.explicitPos)); + } +} +class ActiveResult extends ActiveSource { + constructor(source, explicitPos, result, from, to) { + super(source, 2 /* State.Result */, explicitPos); + this.result = result; + this.from = from; + this.to = to; + } + hasResult() { return true; } + handleUserEvent(tr, type, conf) { + var _a; + let from = tr.changes.mapPos(this.from), to = tr.changes.mapPos(this.to, 1); + let pos = cur(tr.state); + if ((this.explicitPos < 0 ? pos <= from : pos < this.from) || + pos > to || + type == "delete" && cur(tr.startState) == this.from) + return new ActiveSource(this.source, type == "input" && conf.activateOnTyping ? 1 /* State.Pending */ : 0 /* State.Inactive */); + let explicitPos = this.explicitPos < 0 ? -1 : tr.changes.mapPos(this.explicitPos), updated; + if (checkValid(this.result.validFor, tr.state, from, to)) + return new ActiveResult(this.source, explicitPos, this.result, from, to); + if (this.result.update && + (updated = this.result.update(this.result, from, to, new CompletionContext(tr.state, pos, explicitPos >= 0)))) + return new ActiveResult(this.source, explicitPos, updated, updated.from, (_a = updated.to) !== null && _a !== void 0 ? _a : cur(tr.state)); + return new ActiveSource(this.source, 1 /* State.Pending */, explicitPos); + } + handleChange(tr) { + return tr.changes.touchesRange(this.from, this.to) ? new ActiveSource(this.source, 0 /* State.Inactive */) : this.map(tr.changes); + } + map(mapping) { + return mapping.empty ? this : + new ActiveResult(this.source, this.explicitPos < 0 ? -1 : mapping.mapPos(this.explicitPos), this.result, mapping.mapPos(this.from), mapping.mapPos(this.to, 1)); + } +} +function checkValid(validFor, state, from, to) { + if (!validFor) + return false; + let text = state.sliceDoc(from, to); + return typeof validFor == "function" ? validFor(text, from, to, state) : ensureAnchor(validFor, true).test(text); +} +const setActiveEffect = /*@__PURE__*/StateEffect.define({ + map(sources, mapping) { return sources.map(s => s.map(mapping)); } +}); +const setSelectedEffect = /*@__PURE__*/StateEffect.define(); +const completionState = /*@__PURE__*/StateField.define({ + create() { return CompletionState.start(); }, + update(value, tr) { return value.update(tr); }, + provide: f => [ + showTooltip.from(f, val => val.tooltip), + EditorView.contentAttributes.from(f, state => state.attrs) + ] +}); +function applyCompletion(view, option) { + const apply = option.completion.apply || option.completion.label; + let result = view.state.field(completionState).active.find(a => a.source == option.source); + if (!(result instanceof ActiveResult)) + return false; + if (typeof apply == "string") + view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to)), { annotations: pickedCompletion.of(option.completion) })); + else + apply(view, option.completion, result.from, result.to); + return true; +} +const createTooltip = /*@__PURE__*/completionTooltip(completionState, applyCompletion); + +/** +Returns a command that moves the completion selection forward or +backward by the given amount. +*/ +function moveCompletionSelection(forward, by = "option") { + return (view) => { + let cState = view.state.field(completionState, false); + if (!cState || !cState.open || cState.open.disabled || + Date.now() - cState.open.timestamp < view.state.facet(completionConfig).interactionDelay) + return false; + let step = 1, tooltip; + if (by == "page" && (tooltip = getTooltip(view, cState.open.tooltip))) + step = Math.max(2, Math.floor(tooltip.dom.offsetHeight / + tooltip.dom.querySelector("li").offsetHeight) - 1); + let { length } = cState.open.options; + let selected = cState.open.selected > -1 ? cState.open.selected + step * (forward ? 1 : -1) : forward ? 0 : length - 1; + if (selected < 0) + selected = by == "page" ? 0 : length - 1; + else if (selected >= length) + selected = by == "page" ? length - 1 : 0; + view.dispatch({ effects: setSelectedEffect.of(selected) }); + return true; + }; +} +/** +Accept the current completion. +*/ +const acceptCompletion = (view) => { + let cState = view.state.field(completionState, false); + if (view.state.readOnly || !cState || !cState.open || cState.open.selected < 0 || cState.open.disabled || + Date.now() - cState.open.timestamp < view.state.facet(completionConfig).interactionDelay) + return false; + return applyCompletion(view, cState.open.options[cState.open.selected]); +}; +/** +Explicitly start autocompletion. +*/ +const startCompletion = (view) => { + let cState = view.state.field(completionState, false); + if (!cState) + return false; + view.dispatch({ effects: startCompletionEffect.of(true) }); + return true; +}; +/** +Close the currently active completion. +*/ +const closeCompletion = (view) => { + let cState = view.state.field(completionState, false); + if (!cState || !cState.active.some(a => a.state != 0 /* State.Inactive */)) + return false; + view.dispatch({ effects: closeCompletionEffect.of(null) }); + return true; +}; +class RunningQuery { + constructor(active, context) { + this.active = active; + this.context = context; + this.time = Date.now(); + this.updates = []; + // Note that 'undefined' means 'not done yet', whereas 'null' means + // 'query returned null'. + this.done = undefined; + } +} +const MaxUpdateCount = 50, MinAbortTime = 1000; +const completionPlugin = /*@__PURE__*/ViewPlugin.fromClass(class { + constructor(view) { + this.view = view; + this.debounceUpdate = -1; + this.running = []; + this.debounceAccept = -1; + this.pendingStart = false; + this.composing = 0 /* CompositionState.None */; + for (let active of view.state.field(completionState).active) + if (active.state == 1 /* State.Pending */) + this.startQuery(active); + } + update(update) { + let cState = update.state.field(completionState); + if (!update.selectionSet && !update.docChanged && update.startState.field(completionState) == cState) + return; + let doesReset = update.transactions.some(tr => { + return (tr.selection || tr.docChanged) && !getUserEvent(tr); + }); + for (let i = 0; i < this.running.length; i++) { + let query = this.running[i]; + if (doesReset || + query.updates.length + update.transactions.length > MaxUpdateCount && Date.now() - query.time > MinAbortTime) { + for (let handler of query.context.abortListeners) { + try { + handler(); + } + catch (e) { + logException(this.view.state, e); + } + } + query.context.abortListeners = null; + this.running.splice(i--, 1); + } + else { + query.updates.push(...update.transactions); + } + } + if (this.debounceUpdate > -1) + clearTimeout(this.debounceUpdate); + if (update.transactions.some(tr => tr.effects.some(e => e.is(startCompletionEffect)))) + this.pendingStart = true; + let delay = this.pendingStart ? 50 : update.state.facet(completionConfig).activateOnTypingDelay; + this.debounceUpdate = cState.active.some(a => a.state == 1 /* State.Pending */ && !this.running.some(q => q.active.source == a.source)) + ? setTimeout(() => this.startUpdate(), delay) : -1; + if (this.composing != 0 /* CompositionState.None */) + for (let tr of update.transactions) { + if (getUserEvent(tr) == "input") + this.composing = 2 /* CompositionState.Changed */; + else if (this.composing == 2 /* CompositionState.Changed */ && tr.selection) + this.composing = 3 /* CompositionState.ChangedAndMoved */; + } + } + startUpdate() { + this.debounceUpdate = -1; + this.pendingStart = false; + let { state } = this.view, cState = state.field(completionState); + for (let active of cState.active) { + if (active.state == 1 /* State.Pending */ && !this.running.some(r => r.active.source == active.source)) + this.startQuery(active); + } + } + startQuery(active) { + let { state } = this.view, pos = cur(state); + let context = new CompletionContext(state, pos, active.explicitPos == pos); + let pending = new RunningQuery(active, context); + this.running.push(pending); + Promise.resolve(active.source(context)).then(result => { + if (!pending.context.aborted) { + pending.done = result || null; + this.scheduleAccept(); + } + }, err => { + this.view.dispatch({ effects: closeCompletionEffect.of(null) }); + logException(this.view.state, err); + }); + } + scheduleAccept() { + if (this.running.every(q => q.done !== undefined)) + this.accept(); + else if (this.debounceAccept < 0) + this.debounceAccept = setTimeout(() => this.accept(), this.view.state.facet(completionConfig).updateSyncTime); + } + // For each finished query in this.running, try to create a result + // or, if appropriate, restart the query. + accept() { + var _a; + if (this.debounceAccept > -1) + clearTimeout(this.debounceAccept); + this.debounceAccept = -1; + let updated = []; + let conf = this.view.state.facet(completionConfig); + for (let i = 0; i < this.running.length; i++) { + let query = this.running[i]; + if (query.done === undefined) + continue; + this.running.splice(i--, 1); + if (query.done) { + let active = new ActiveResult(query.active.source, query.active.explicitPos, query.done, query.done.from, (_a = query.done.to) !== null && _a !== void 0 ? _a : cur(query.updates.length ? query.updates[0].startState : this.view.state)); + // Replay the transactions that happened since the start of + // the request and see if that preserves the result + for (let tr of query.updates) + active = active.update(tr, conf); + if (active.hasResult()) { + updated.push(active); + continue; + } + } + let current = this.view.state.field(completionState).active.find(a => a.source == query.active.source); + if (current && current.state == 1 /* State.Pending */) { + if (query.done == null) { + // Explicitly failed. Should clear the pending status if it + // hasn't been re-set in the meantime. + let active = new ActiveSource(query.active.source, 0 /* State.Inactive */); + for (let tr of query.updates) + active = active.update(tr, conf); + if (active.state != 1 /* State.Pending */) + updated.push(active); + } + else { + // Cleared by subsequent transactions. Restart. + this.startQuery(current); + } + } + } + if (updated.length) + this.view.dispatch({ effects: setActiveEffect.of(updated) }); + } +}, { + eventHandlers: { + blur(event) { + let state = this.view.state.field(completionState, false); + if (state && state.tooltip && this.view.state.facet(completionConfig).closeOnBlur) { + let dialog = state.open && getTooltip(this.view, state.open.tooltip); + if (!dialog || !dialog.dom.contains(event.relatedTarget)) + setTimeout(() => this.view.dispatch({ effects: closeCompletionEffect.of(null) }), 10); + } + }, + compositionstart() { + this.composing = 1 /* CompositionState.Started */; + }, + compositionend() { + if (this.composing == 3 /* CompositionState.ChangedAndMoved */) { + // Safari fires compositionend events synchronously, possibly + // from inside an update, so dispatch asynchronously to avoid reentrancy + setTimeout(() => this.view.dispatch({ effects: startCompletionEffect.of(false) }), 20); + } + this.composing = 0 /* CompositionState.None */; + } + } +}); + +const baseTheme$2 = /*@__PURE__*/EditorView.baseTheme({ + ".cm-tooltip.cm-tooltip-autocomplete": { + "& > ul": { + fontFamily: "monospace", + whiteSpace: "nowrap", + overflow: "hidden auto", + maxWidth_fallback: "700px", + maxWidth: "min(700px, 95vw)", + minWidth: "250px", + maxHeight: "10em", + height: "100%", + listStyle: "none", + margin: 0, + padding: 0, + "& > li, & > completion-section": { + padding: "1px 3px", + lineHeight: 1.2 + }, + "& > li": { + overflowX: "hidden", + textOverflow: "ellipsis", + cursor: "pointer" + }, + "& > completion-section": { + display: "list-item", + borderBottom: "1px solid silver", + paddingLeft: "0.5em", + opacity: 0.7 + } + } + }, + "&light .cm-tooltip-autocomplete ul li[aria-selected]": { + background: "#17c", + color: "white", + }, + "&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]": { + background: "#777", + }, + "&dark .cm-tooltip-autocomplete ul li[aria-selected]": { + background: "#347", + color: "white", + }, + "&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]": { + background: "#444", + }, + ".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after": { + content: '"···"', + opacity: 0.5, + display: "block", + textAlign: "center" + }, + ".cm-tooltip.cm-completionInfo": { + position: "absolute", + padding: "3px 9px", + width: "max-content", + maxWidth: `${400 /* Info.Width */}px`, + boxSizing: "border-box" + }, + ".cm-completionInfo.cm-completionInfo-left": { right: "100%" }, + ".cm-completionInfo.cm-completionInfo-right": { left: "100%" }, + ".cm-completionInfo.cm-completionInfo-left-narrow": { right: `${30 /* Info.Margin */}px` }, + ".cm-completionInfo.cm-completionInfo-right-narrow": { left: `${30 /* Info.Margin */}px` }, + "&light .cm-snippetField": { backgroundColor: "#00000022" }, + "&dark .cm-snippetField": { backgroundColor: "#ffffff22" }, + ".cm-snippetFieldPosition": { + verticalAlign: "text-top", + width: 0, + height: "1.15em", + display: "inline-block", + margin: "0 -0.7px -.7em", + borderLeft: "1.4px dotted #888" + }, + ".cm-completionMatchedText": { + textDecoration: "underline" + }, + ".cm-completionDetail": { + marginLeft: "0.5em", + fontStyle: "italic" + }, + ".cm-completionIcon": { + fontSize: "90%", + width: ".8em", + display: "inline-block", + textAlign: "center", + paddingRight: ".6em", + opacity: "0.6", + boxSizing: "content-box" + }, + ".cm-completionIcon-function, .cm-completionIcon-method": { + "&:after": { content: "'ƒ'" } + }, + ".cm-completionIcon-class": { + "&:after": { content: "'○'" } + }, + ".cm-completionIcon-interface": { + "&:after": { content: "'◌'" } + }, + ".cm-completionIcon-variable": { + "&:after": { content: "'𝑥'" } + }, + ".cm-completionIcon-constant": { + "&:after": { content: "'𝐶'" } + }, + ".cm-completionIcon-type": { + "&:after": { content: "'𝑡'" } + }, + ".cm-completionIcon-enum": { + "&:after": { content: "'∪'" } + }, + ".cm-completionIcon-property": { + "&:after": { content: "'□'" } + }, + ".cm-completionIcon-keyword": { + "&:after": { content: "'🔑\uFE0E'" } // Disable emoji rendering + }, + ".cm-completionIcon-namespace": { + "&:after": { content: "'▢'" } + }, + ".cm-completionIcon-text": { + "&:after": { content: "'abc'", fontSize: "50%", verticalAlign: "middle" } + } +}); + +class FieldPos { + constructor(field, line, from, to) { + this.field = field; + this.line = line; + this.from = from; + this.to = to; + } +} +class FieldRange { + constructor(field, from, to) { + this.field = field; + this.from = from; + this.to = to; + } + map(changes) { + let from = changes.mapPos(this.from, -1, exports.MapMode.TrackDel); + let to = changes.mapPos(this.to, 1, exports.MapMode.TrackDel); + return from == null || to == null ? null : new FieldRange(this.field, from, to); + } +} +class Snippet { + constructor(lines, fieldPositions) { + this.lines = lines; + this.fieldPositions = fieldPositions; + } + instantiate(state, pos) { + let text = [], lineStart = [pos]; + let lineObj = state.doc.lineAt(pos), baseIndent = /^\s*/.exec(lineObj.text)[0]; + for (let line of this.lines) { + if (text.length) { + let indent = baseIndent, tabs = /^\t*/.exec(line)[0].length; + for (let i = 0; i < tabs; i++) + indent += state.facet(indentUnit); + lineStart.push(pos + indent.length - tabs); + line = indent + line.slice(tabs); + } + text.push(line); + pos += line.length + 1; + } + let ranges = this.fieldPositions.map(pos => new FieldRange(pos.field, lineStart[pos.line] + pos.from, lineStart[pos.line] + pos.to)); + return { text, ranges }; + } + static parse(template) { + let fields = []; + let lines = [], positions = [], m; + for (let line of template.split(/\r\n?|\n/)) { + while (m = /[#$]\{(?:(\d+)(?::([^}]*))?|([^}]*))\}/.exec(line)) { + let seq = m[1] ? +m[1] : null, name = m[2] || m[3] || "", found = -1; + for (let i = 0; i < fields.length; i++) { + if (seq != null ? fields[i].seq == seq : name ? fields[i].name == name : false) + found = i; + } + if (found < 0) { + let i = 0; + while (i < fields.length && (seq == null || (fields[i].seq != null && fields[i].seq < seq))) + i++; + fields.splice(i, 0, { seq, name }); + found = i; + for (let pos of positions) + if (pos.field >= found) + pos.field++; + } + positions.push(new FieldPos(found, lines.length, m.index, m.index + name.length)); + line = line.slice(0, m.index) + name + line.slice(m.index + m[0].length); + } + for (let esc; esc = /\\([{}])/.exec(line);) { + line = line.slice(0, esc.index) + esc[1] + line.slice(esc.index + esc[0].length); + for (let pos of positions) + if (pos.line == lines.length && pos.from > esc.index) { + pos.from--; + pos.to--; + } + } + lines.push(line); + } + return new Snippet(lines, positions); + } +} +let fieldMarker = /*@__PURE__*/Decoration.widget({ widget: /*@__PURE__*/new class extends WidgetType { + toDOM() { + let span = document.createElement("span"); + span.className = "cm-snippetFieldPosition"; + return span; + } + ignoreEvent() { return false; } + } }); +let fieldRange = /*@__PURE__*/Decoration.mark({ class: "cm-snippetField" }); +class ActiveSnippet { + constructor(ranges, active) { + this.ranges = ranges; + this.active = active; + this.deco = Decoration.set(ranges.map(r => (r.from == r.to ? fieldMarker : fieldRange).range(r.from, r.to))); + } + map(changes) { + let ranges = []; + for (let r of this.ranges) { + let mapped = r.map(changes); + if (!mapped) + return null; + ranges.push(mapped); + } + return new ActiveSnippet(ranges, this.active); + } + selectionInsideField(sel) { + return sel.ranges.every(range => this.ranges.some(r => r.field == this.active && r.from <= range.from && r.to >= range.to)); + } +} +const setActive = /*@__PURE__*/StateEffect.define({ + map(value, changes) { return value && value.map(changes); } +}); +const moveToField = /*@__PURE__*/StateEffect.define(); +const snippetState = /*@__PURE__*/StateField.define({ + create() { return null; }, + update(value, tr) { + for (let effect of tr.effects) { + if (effect.is(setActive)) + return effect.value; + if (effect.is(moveToField) && value) + return new ActiveSnippet(value.ranges, effect.value); + } + if (value && tr.docChanged) + value = value.map(tr.changes); + if (value && tr.selection && !value.selectionInsideField(tr.selection)) + value = null; + return value; + }, + provide: f => EditorView.decorations.from(f, val => val ? val.deco : Decoration.none) +}); +function fieldSelection(ranges, field) { + return EditorSelection.create(ranges.filter(r => r.field == field).map(r => EditorSelection.range(r.from, r.to))); +} +/** +Convert a snippet template to a function that can +[apply](https://codemirror.net/6/docs/ref/#autocomplete.Completion.apply) it. Snippets are written +using syntax like this: + + "for (let ${index} = 0; ${index} < ${end}; ${index}++) {\n\t${}\n}" + +Each `${}` placeholder (you may also use `#{}`) indicates a field +that the user can fill in. Its name, if any, will be the default +content for the field. + +When the snippet is activated by calling the returned function, +the code is inserted at the given position. Newlines in the +template are indented by the indentation of the start line, plus +one [indent unit](https://codemirror.net/6/docs/ref/#language.indentUnit) per tab character after +the newline. + +On activation, (all instances of) the first field are selected. +The user can move between fields with Tab and Shift-Tab as long as +the fields are active. Moving to the last field or moving the +cursor out of the current field deactivates the fields. + +The order of fields defaults to textual order, but you can add +numbers to placeholders (`${1}` or `${1:defaultText}`) to provide +a custom order. + +To include a literal `{` or `}` in your template, put a backslash +in front of it. This will be removed and the brace will not be +interpreted as indicating a placeholder. +*/ +function snippet(template) { + let snippet = Snippet.parse(template); + return (editor, completion, from, to) => { + let { text, ranges } = snippet.instantiate(editor.state, from); + let spec = { + changes: { from, to, insert: Text.of(text) }, + scrollIntoView: true, + annotations: completion ? [pickedCompletion.of(completion), Transaction.userEvent.of("input.complete")] : undefined + }; + if (ranges.length) + spec.selection = fieldSelection(ranges, 0); + if (ranges.some(r => r.field > 0)) { + let active = new ActiveSnippet(ranges, 0); + let effects = spec.effects = [setActive.of(active)]; + if (editor.state.field(snippetState, false) === undefined) + effects.push(StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme$2])); + } + editor.dispatch(editor.state.update(spec)); + }; +} +function moveField(dir) { + return ({ state, dispatch }) => { + let active = state.field(snippetState, false); + if (!active || dir < 0 && active.active == 0) + return false; + let next = active.active + dir, last = dir > 0 && !active.ranges.some(r => r.field == next + dir); + dispatch(state.update({ + selection: fieldSelection(active.ranges, next), + effects: setActive.of(last ? null : new ActiveSnippet(active.ranges, next)), + scrollIntoView: true + })); + return true; + }; +} +/** +A command that clears the active snippet, if any. +*/ +const clearSnippet = ({ state, dispatch }) => { + let active = state.field(snippetState, false); + if (!active) + return false; + dispatch(state.update({ effects: setActive.of(null) })); + return true; +}; +/** +Move to the next snippet field, if available. +*/ +const nextSnippetField = /*@__PURE__*/moveField(1); +/** +Move to the previous snippet field, if available. +*/ +const prevSnippetField = /*@__PURE__*/moveField(-1); +/** +Check if there is an active snippet with a next field for +`nextSnippetField` to move to. +*/ +function hasNextSnippetField(state) { + let active = state.field(snippetState, false); + return !!(active && active.ranges.some(r => r.field == active.active + 1)); +} +/** +Returns true if there is an active snippet and a previous field +for `prevSnippetField` to move to. +*/ +function hasPrevSnippetField(state) { + let active = state.field(snippetState, false); + return !!(active && active.active > 0); +} +const defaultSnippetKeymap = [ + { key: "Tab", run: nextSnippetField, shift: prevSnippetField }, + { key: "Escape", run: clearSnippet } +]; +/** +A facet that can be used to configure the key bindings used by +snippets. The default binds Tab to +[`nextSnippetField`](https://codemirror.net/6/docs/ref/#autocomplete.nextSnippetField), Shift-Tab to +[`prevSnippetField`](https://codemirror.net/6/docs/ref/#autocomplete.prevSnippetField), and Escape +to [`clearSnippet`](https://codemirror.net/6/docs/ref/#autocomplete.clearSnippet). +*/ +const snippetKeymap = /*@__PURE__*/Facet.define({ + combine(maps) { return maps.length ? maps[0] : defaultSnippetKeymap; } +}); +const addSnippetKeymap = /*@__PURE__*/Prec.highest(/*@__PURE__*/keymap.compute([snippetKeymap], state => state.facet(snippetKeymap))); +/** +Create a completion from a snippet. Returns an object with the +properties from `completion`, plus an `apply` function that +applies the snippet. +*/ +function snippetCompletion(template, completion) { + return Object.assign(Object.assign({}, completion), { apply: snippet(template) }); +} +const snippetPointerHandler = /*@__PURE__*/EditorView.domEventHandlers({ + mousedown(event, view) { + let active = view.state.field(snippetState, false), pos; + if (!active || (pos = view.posAtCoords({ x: event.clientX, y: event.clientY })) == null) + return false; + let match = active.ranges.find(r => r.from <= pos && r.to >= pos); + if (!match || match.field == active.active) + return false; + view.dispatch({ + selection: fieldSelection(active.ranges, match.field), + effects: setActive.of(active.ranges.some(r => r.field > match.field) + ? new ActiveSnippet(active.ranges, match.field) : null), + scrollIntoView: true + }); + return true; + } +}); + +function wordRE(wordChars) { + let escaped = wordChars.replace(/[\]\-\\]/g, "\\$&"); + try { + return new RegExp(`[\\p{Alphabetic}\\p{Number}_${escaped}]+`, "ug"); + } + catch (_a) { + return new RegExp(`[\w${escaped}]`, "g"); + } +} +function mapRE(re, f) { + return new RegExp(f(re.source), re.unicode ? "u" : ""); +} +const wordCaches = /*@__PURE__*/Object.create(null); +function wordCache(wordChars) { + return wordCaches[wordChars] || (wordCaches[wordChars] = new WeakMap); +} +function storeWords(doc, wordRE, result, seen, ignoreAt) { + for (let lines = doc.iterLines(), pos = 0; !lines.next().done;) { + let { value } = lines, m; + wordRE.lastIndex = 0; + while (m = wordRE.exec(value)) { + if (!seen[m[0]] && pos + m.index != ignoreAt) { + result.push({ type: "text", label: m[0] }); + seen[m[0]] = true; + if (result.length >= 2000 /* C.MaxList */) + return; + } + } + pos += value.length + 1; + } +} +function collectWords(doc, cache, wordRE, to, ignoreAt) { + let big = doc.length >= 1000 /* C.MinCacheLen */; + let cached = big && cache.get(doc); + if (cached) + return cached; + let result = [], seen = Object.create(null); + if (doc.children) { + let pos = 0; + for (let ch of doc.children) { + if (ch.length >= 1000 /* C.MinCacheLen */) { + for (let c of collectWords(ch, cache, wordRE, to - pos, ignoreAt - pos)) { + if (!seen[c.label]) { + seen[c.label] = true; + result.push(c); + } + } + } + else { + storeWords(ch, wordRE, result, seen, ignoreAt - pos); + } + pos += ch.length + 1; + } + } + else { + storeWords(doc, wordRE, result, seen, ignoreAt); + } + if (big && result.length < 2000 /* C.MaxList */) + cache.set(doc, result); + return result; +} +/** +A completion source that will scan the document for words (using a +[character categorizer](https://codemirror.net/6/docs/ref/#state.EditorState.charCategorizer)), and +return those as completions. +*/ +const completeAnyWord = context => { + let wordChars = context.state.languageDataAt("wordChars", context.pos).join(""); + let re = wordRE(wordChars); + let token = context.matchBefore(mapRE(re, s => s + "$")); + if (!token && !context.explicit) + return null; + let from = token ? token.from : context.pos; + let options = collectWords(context.state.doc, wordCache(wordChars), re, 50000 /* C.Range */, from); + return { from, options, validFor: mapRE(re, s => "^" + s) }; +}; + +const defaults = { + brackets: ["(", "[", "{", "'", '"'], + before: ")]}:;>", + stringPrefixes: [] +}; +const closeBracketEffect = /*@__PURE__*/StateEffect.define({ + map(value, mapping) { + let mapped = mapping.mapPos(value, -1, exports.MapMode.TrackAfter); + return mapped == null ? undefined : mapped; + } +}); +const closedBracket = /*@__PURE__*/new class extends RangeValue { +}; +closedBracket.startSide = 1; +closedBracket.endSide = -1; +const bracketState = /*@__PURE__*/StateField.define({ + create() { return RangeSet.empty; }, + update(value, tr) { + value = value.map(tr.changes); + if (tr.selection) { + let line = tr.state.doc.lineAt(tr.selection.main.head); + value = value.update({ filter: from => from >= line.from && from <= line.to }); + } + for (let effect of tr.effects) + if (effect.is(closeBracketEffect)) + value = value.update({ add: [closedBracket.range(effect.value, effect.value + 1)] }); + return value; + } +}); +/** +Extension to enable bracket-closing behavior. When a closeable +bracket is typed, its closing bracket is immediately inserted +after the cursor. When closing a bracket directly in front of a +closing bracket inserted by the extension, the cursor moves over +that bracket. +*/ +function closeBrackets() { + return [inputHandler, bracketState]; +} +const definedClosing = "()[]{}<>"; +function closing(ch) { + for (let i = 0; i < definedClosing.length; i += 2) + if (definedClosing.charCodeAt(i) == ch) + return definedClosing.charAt(i + 1); + return fromCodePoint(ch < 128 ? ch : ch + 1); +} +function config(state, pos) { + return state.languageDataAt("closeBrackets", pos)[0] || defaults; +} +const android = typeof navigator == "object" && /*@__PURE__*//Android\b/.test(navigator.userAgent); +const inputHandler = /*@__PURE__*/EditorView.inputHandler.of((view, from, to, insert) => { + if ((android ? view.composing : view.compositionStarted) || view.state.readOnly) + return false; + let sel = view.state.selection.main; + if (insert.length > 2 || insert.length == 2 && codePointSize(codePointAt(insert, 0)) == 1 || + from != sel.from || to != sel.to) + return false; + let tr = insertBracket(view.state, insert); + if (!tr) + return false; + view.dispatch(tr); + return true; +}); +/** +Command that implements deleting a pair of matching brackets when +the cursor is between them. +*/ +const deleteBracketPair = ({ state, dispatch }) => { + if (state.readOnly) + return false; + let conf = config(state, state.selection.main.head); + let tokens = conf.brackets || defaults.brackets; + let dont = null, changes = state.changeByRange(range => { + if (range.empty) { + let before = prevChar(state.doc, range.head); + for (let token of tokens) { + if (token == before && nextChar(state.doc, range.head) == closing(codePointAt(token, 0))) + return { changes: { from: range.head - token.length, to: range.head + token.length }, + range: EditorSelection.cursor(range.head - token.length) }; + } + } + return { range: dont = range }; + }); + if (!dont) + dispatch(state.update(changes, { scrollIntoView: true, userEvent: "delete.backward" })); + return !dont; +}; +/** +Close-brackets related key bindings. Binds Backspace to +[`deleteBracketPair`](https://codemirror.net/6/docs/ref/#autocomplete.deleteBracketPair). +*/ +const closeBracketsKeymap = [ + { key: "Backspace", run: deleteBracketPair } +]; +/** +Implements the extension's behavior on text insertion. If the +given string counts as a bracket in the language around the +selection, and replacing the selection with it requires custom +behavior (inserting a closing version or skipping past a +previously-closed bracket), this function returns a transaction +representing that custom behavior. (You only need this if you want +to programmatically insert brackets—the +[`closeBrackets`](https://codemirror.net/6/docs/ref/#autocomplete.closeBrackets) extension will +take care of running this for user input.) +*/ +function insertBracket(state, bracket) { + let conf = config(state, state.selection.main.head); + let tokens = conf.brackets || defaults.brackets; + for (let tok of tokens) { + let closed = closing(codePointAt(tok, 0)); + if (bracket == tok) + return closed == tok ? handleSame(state, tok, tokens.indexOf(tok + tok + tok) > -1, conf) + : handleOpen(state, tok, closed, conf.before || defaults.before); + if (bracket == closed && closedBracketAt(state, state.selection.main.from)) + return handleClose(state, tok, closed); + } + return null; +} +function closedBracketAt(state, pos) { + let found = false; + state.field(bracketState).between(0, state.doc.length, from => { + if (from == pos) + found = true; + }); + return found; +} +function nextChar(doc, pos) { + let next = doc.sliceString(pos, pos + 2); + return next.slice(0, codePointSize(codePointAt(next, 0))); +} +function prevChar(doc, pos) { + let prev = doc.sliceString(pos - 2, pos); + return codePointSize(codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1); +} +function handleOpen(state, open, close, closeBefore) { + let dont = null, changes = state.changeByRange(range => { + if (!range.empty) + return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }], + effects: closeBracketEffect.of(range.to + open.length), + range: EditorSelection.range(range.anchor + open.length, range.head + open.length) }; + let next = nextChar(state.doc, range.head); + if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1) + return { changes: { insert: open + close, from: range.head }, + effects: closeBracketEffect.of(range.head + open.length), + range: EditorSelection.cursor(range.head + open.length) }; + return { range: dont = range }; + }); + return dont ? null : state.update(changes, { + scrollIntoView: true, + userEvent: "input.type" + }); +} +function handleClose(state, _open, close) { + let dont = null, changes = state.changeByRange(range => { + if (range.empty && nextChar(state.doc, range.head) == close) + return { changes: { from: range.head, to: range.head + close.length, insert: close }, + range: EditorSelection.cursor(range.head + close.length) }; + return dont = { range }; + }); + return dont ? null : state.update(changes, { + scrollIntoView: true, + userEvent: "input.type" + }); +} +// Handles cases where the open and close token are the same, and +// possibly triple quotes (as in `"""abc"""`-style quoting). +function handleSame(state, token, allowTriple, config) { + let stringPrefixes = config.stringPrefixes || defaults.stringPrefixes; + let dont = null, changes = state.changeByRange(range => { + if (!range.empty) + return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }], + effects: closeBracketEffect.of(range.to + token.length), + range: EditorSelection.range(range.anchor + token.length, range.head + token.length) }; + let pos = range.head, next = nextChar(state.doc, pos), start; + if (next == token) { + if (nodeStart(state, pos)) { + return { changes: { insert: token + token, from: pos }, + effects: closeBracketEffect.of(pos + token.length), + range: EditorSelection.cursor(pos + token.length) }; + } + else if (closedBracketAt(state, pos)) { + let isTriple = allowTriple && state.sliceDoc(pos, pos + token.length * 3) == token + token + token; + let content = isTriple ? token + token + token : token; + return { changes: { from: pos, to: pos + content.length, insert: content }, + range: EditorSelection.cursor(pos + content.length) }; + } + } + else if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token && + (start = canStartStringAt(state, pos - 2 * token.length, stringPrefixes)) > -1 && + nodeStart(state, start)) { + return { changes: { insert: token + token + token + token, from: pos }, + effects: closeBracketEffect.of(pos + token.length), + range: EditorSelection.cursor(pos + token.length) }; + } + else if (state.charCategorizer(pos)(next) != exports.CharCategory.Word) { + if (canStartStringAt(state, pos, stringPrefixes) > -1 && !probablyInString(state, pos, token, stringPrefixes)) + return { changes: { insert: token + token, from: pos }, + effects: closeBracketEffect.of(pos + token.length), + range: EditorSelection.cursor(pos + token.length) }; + } + return { range: dont = range }; + }); + return dont ? null : state.update(changes, { + scrollIntoView: true, + userEvent: "input.type" + }); +} +function nodeStart(state, pos) { + let tree = syntaxTree(state).resolveInner(pos + 1); + return tree.parent && tree.from == pos; +} +function probablyInString(state, pos, quoteToken, prefixes) { + let node = syntaxTree(state).resolveInner(pos, -1); + let maxPrefix = prefixes.reduce((m, p) => Math.max(m, p.length), 0); + for (let i = 0; i < 5; i++) { + let start = state.sliceDoc(node.from, Math.min(node.to, node.from + quoteToken.length + maxPrefix)); + let quotePos = start.indexOf(quoteToken); + if (!quotePos || quotePos > -1 && prefixes.indexOf(start.slice(0, quotePos)) > -1) { + let first = node.firstChild; + while (first && first.from == node.from && first.to - first.from > quoteToken.length + quotePos) { + if (state.sliceDoc(first.to - quoteToken.length, first.to) == quoteToken) + return false; + first = first.firstChild; + } + return true; + } + let parent = node.to == pos && node.parent; + if (!parent) + break; + node = parent; + } + return false; +} +function canStartStringAt(state, pos, prefixes) { + let charCat = state.charCategorizer(pos); + if (charCat(state.sliceDoc(pos - 1, pos)) != exports.CharCategory.Word) + return pos; + for (let prefix of prefixes) { + let start = pos - prefix.length; + if (state.sliceDoc(start, pos) == prefix && charCat(state.sliceDoc(start - 1, start)) != exports.CharCategory.Word) + return start; + } + return -1; +} + +/** +Returns an extension that enables autocompletion. +*/ +function autocompletion(config = {}) { + return [ + completionState, + completionConfig.of(config), + completionPlugin, + completionKeymapExt, + baseTheme$2 + ]; +} +/** +Basic keybindings for autocompletion. + + - Ctrl-Space: [`startCompletion`](https://codemirror.net/6/docs/ref/#autocomplete.startCompletion) + - Escape: [`closeCompletion`](https://codemirror.net/6/docs/ref/#autocomplete.closeCompletion) + - ArrowDown: [`moveCompletionSelection`](https://codemirror.net/6/docs/ref/#autocomplete.moveCompletionSelection)`(true)` + - ArrowUp: [`moveCompletionSelection`](https://codemirror.net/6/docs/ref/#autocomplete.moveCompletionSelection)`(false)` + - PageDown: [`moveCompletionSelection`](https://codemirror.net/6/docs/ref/#autocomplete.moveCompletionSelection)`(true, "page")` + - PageDown: [`moveCompletionSelection`](https://codemirror.net/6/docs/ref/#autocomplete.moveCompletionSelection)`(true, "page")` + - Enter: [`acceptCompletion`](https://codemirror.net/6/docs/ref/#autocomplete.acceptCompletion) +*/ +const completionKeymap = [ + { key: "Ctrl-Space", run: startCompletion }, + { key: "Escape", run: closeCompletion }, + { key: "ArrowDown", run: /*@__PURE__*/moveCompletionSelection(true) }, + { key: "ArrowUp", run: /*@__PURE__*/moveCompletionSelection(false) }, + { key: "PageDown", run: /*@__PURE__*/moveCompletionSelection(true, "page") }, + { key: "PageUp", run: /*@__PURE__*/moveCompletionSelection(false, "page") }, + { key: "Enter", run: acceptCompletion } +]; +const completionKeymapExt = /*@__PURE__*/Prec.highest(/*@__PURE__*/keymap.computeN([completionConfig], state => state.facet(completionConfig).defaultKeymap ? [completionKeymap] : [])); +/** +Get the current completion status. When completions are available, +this will return `"active"`. When completions are pending (in the +process of being queried), this returns `"pending"`. Otherwise, it +returns `null`. +*/ +function completionStatus(state) { + let cState = state.field(completionState, false); + return cState && cState.active.some(a => a.state == 1 /* State.Pending */) ? "pending" + : cState && cState.active.some(a => a.state != 0 /* State.Inactive */) ? "active" : null; +} +const completionArrayCache = /*@__PURE__*/new WeakMap; +/** +Returns the available completions as an array. +*/ +function currentCompletions(state) { + var _a; + let open = (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.open; + if (!open || open.disabled) + return []; + let completions = completionArrayCache.get(open.options); + if (!completions) + completionArrayCache.set(open.options, completions = open.options.map(o => o.completion)); + return completions; +} +/** +Return the currently selected completion, if any. +*/ +function selectedCompletion(state) { + var _a; + let open = (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.open; + return open && !open.disabled && open.selected >= 0 ? open.options[open.selected].completion : null; +} +/** +Returns the currently selected position in the active completion +list, or null if no completions are active. +*/ +function selectedCompletionIndex(state) { + var _a; + let open = (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.open; + return open && !open.disabled && open.selected >= 0 ? open.selected : null; +} +/** +Create an effect that can be attached to a transaction to change +the currently selected completion. +*/ +function setSelectedCompletion(index) { + return setSelectedEffect.of(index); +} + /** Comment or uncomment the current selection. Will use line comments if available, otherwise falling back to block comments. @@ -22619,6 +24593,7 @@ exports.BlockInfo = BlockInfo; exports.ChangeDesc = ChangeDesc; exports.ChangeSet = ChangeSet; exports.Compartment = Compartment; +exports.CompletionContext = CompletionContext; exports.Decoration = Decoration; exports.DocInput = DocInput; exports.EditorSelection = EditorSelection; @@ -22658,22 +24633,33 @@ exports.ViewPlugin = ViewPlugin; exports.ViewUpdate = ViewUpdate; exports.WidgetType = WidgetType; exports.__test = __test; +exports.acceptCompletion = acceptCompletion; +exports.autocompletion = autocompletion; exports.blockComment = blockComment; exports.blockUncomment = blockUncomment; exports.bracketMatching = bracketMatching; exports.bracketMatchingHandle = bracketMatchingHandle; exports.classHighlighter = classHighlighter; +exports.clearSnippet = clearSnippet; +exports.closeBrackets = closeBrackets; +exports.closeBracketsKeymap = closeBracketsKeymap; +exports.closeCompletion = closeCompletion; exports.closeHoverTooltips = closeHoverTooltips; exports.closeSearchPanel = closeSearchPanel; exports.codeFolding = codeFolding; exports.codePointAt = codePointAt; exports.codePointSize = codePointSize; exports.combineConfig = combineConfig; +exports.completeAnyWord = completeAnyWord; +exports.completeFromList = completeFromList; +exports.completionKeymap = completionKeymap; +exports.completionStatus = completionStatus; exports.continuedIndent = continuedIndent; exports.copyLineDown = copyLineDown; exports.copyLineUp = copyLineUp; exports.countColumn = countColumn; exports.crosshairCursor = crosshairCursor; +exports.currentCompletions = currentCompletions; exports.cursorCharBackward = cursorCharBackward; exports.cursorCharForward = cursorCharForward; exports.cursorCharLeft = cursorCharLeft; @@ -22702,6 +24688,7 @@ exports.cursorSyntaxRight = cursorSyntaxRight; exports.defaultHighlightStyle = defaultHighlightStyle; exports.defaultKeymap = defaultKeymap; exports.defineLanguageFacet = defineLanguageFacet; +exports.deleteBracketPair = deleteBracketPair; exports.deleteCharBackward = deleteCharBackward; exports.deleteCharForward = deleteCharForward; exports.deleteGroupBackward = deleteGroupBackward; @@ -22745,6 +24732,8 @@ exports.gutter = gutter; exports.gutterLineClass = gutterLineClass; exports.gutters = gutters; exports.hasHoverTooltips = hasHoverTooltips; +exports.hasNextSnippetField = hasNextSnippetField; +exports.hasPrevSnippetField = hasPrevSnippetField; exports.highlightActiveLine = highlightActiveLine; exports.highlightActiveLineGutter = highlightActiveLineGutter; exports.highlightCode = highlightCode; @@ -22758,6 +24747,8 @@ exports.history = history; exports.historyField = historyField; exports.historyKeymap = historyKeymap; exports.hoverTooltip = hoverTooltip; +exports.ifIn = ifIn; +exports.ifNotIn = ifNotIn; exports.indentLess = indentLess; exports.indentMore = indentMore; exports.indentNodeProp = indentNodeProp; @@ -22769,6 +24760,8 @@ exports.indentString = indentString; exports.indentUnit = indentUnit; exports.indentWithTab = indentWithTab; exports.insertBlankLine = insertBlankLine; +exports.insertBracket = insertBracket; +exports.insertCompletionText = insertCompletionText; exports.insertNewline = insertNewline; exports.insertNewlineAndIndent = insertNewlineAndIndent; exports.insertTab = insertTab; @@ -22784,11 +24777,15 @@ exports.lineNumbers = lineNumbers; exports.lineUncomment = lineUncomment; exports.logException = logException; exports.matchBrackets = matchBrackets; +exports.moveCompletionSelection = moveCompletionSelection; exports.moveLineDown = moveLineDown; exports.moveLineUp = moveLineUp; +exports.nextSnippetField = nextSnippetField; exports.openSearchPanel = openSearchPanel; exports.panels = panels; +exports.pickedCompletion = pickedCompletion; exports.placeholder = placeholder; +exports.prevSnippetField = prevSnippetField; exports.rectangularSelection = rectangularSelection; exports.redo = redo; exports.redoDepth = redoDepth; @@ -22832,12 +24829,19 @@ exports.selectSubwordBackward = selectSubwordBackward; exports.selectSubwordForward = selectSubwordForward; exports.selectSyntaxLeft = selectSyntaxLeft; exports.selectSyntaxRight = selectSyntaxRight; +exports.selectedCompletion = selectedCompletion; +exports.selectedCompletionIndex = selectedCompletionIndex; exports.setSearchQuery = setSearchQuery; +exports.setSelectedCompletion = setSelectedCompletion; exports.showPanel = showPanel; exports.showTooltip = showTooltip; exports.simplifySelection = simplifySelection; +exports.snippet = snippet; +exports.snippetCompletion = snippetCompletion; +exports.snippetKeymap = snippetKeymap; exports.splitLine = splitLine; exports.standardKeymap = standardKeymap; +exports.startCompletion = startCompletion; exports.styleTags = styleTags; exports.sublanguageProp = sublanguageProp; exports.syntaxHighlighting = syntaxHighlighting; diff --git a/tests/jest/codemirror.bidiIsolation.test.js b/tests/jest/codemirror.bidiIsolation.test.js index d1fda580..cc418296 100644 --- a/tests/jest/codemirror.bidiIsolation.test.js +++ b/tests/jest/codemirror.bidiIsolation.test.js @@ -22,7 +22,12 @@ document.body.appendChild( textarea ); const cm = new CodeMirror( textarea ); const mwLang = mediaWikiLang( { bidiIsolation: true }, - { tags: { ref: true } } + { + doubleUnderscore: [ {}, {} ], + functionSynonyms: [ {}, {} ], + tags: { ref: true }, + urlProtocols: 'http\\:\\/\\/' + } ); cm.initialize( [ ...cm.defaultExtensions, mwLang ] ); // Normally ran by mw.hook, but we don't mock the hook system in the Jest tests. diff --git a/tests/jest/codemirror.mediawiki.test.js b/tests/jest/codemirror.mediawiki.test.js index eb3c790c..7b2db787 100644 --- a/tests/jest/codemirror.mediawiki.test.js +++ b/tests/jest/codemirror.mediawiki.test.js @@ -184,7 +184,7 @@ const mwLang = mediaWikiLang( {}, { urlProtocols: 'ftp://|https://|news:', doubleUnderscore: [ { __notoc__: 'notoc' - } ], + }, {} ], functionSynonyms: [ {}, { '!': '!', 'מיון רגיל': 'defaultsort' diff --git a/tests/phpunit/DataScriptTest.php b/tests/phpunit/DataScriptTest.php index 54fe1852..a8c2e583 100644 --- a/tests/phpunit/DataScriptTest.php +++ b/tests/phpunit/DataScriptTest.php @@ -17,6 +17,7 @@ class DataScriptTest extends \MediaWikiIntegrationTestCase { $this->assertStringContainsString( '"extCodeMirrorConfig":', $script ); $this->assertStringContainsString( '"lineNumberingNamespaces":', $script ); $this->assertStringContainsString( '"templateFoldingNamespaces":', $script ); + $this->assertStringContainsString( '"autocompleteNamespaces":', $script ); $this->assertStringContainsString( '"pluginModules":', $script ); $this->assertStringContainsString( '"tagModes":', $script ); $this->assertStringContainsString( '"tags":', $script );