mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 13:56:44 +00:00
CodeMirrorModeMediaWiki: autocompletion
Autocomplete magic words, tag names and url protocols. This patch also enables block comment using `<!-- -->`. Bug: T95100 Change-Id: If37da956ac1eb945b96753e6728c0247b1a68b66
This commit is contained in:
parent
6cfde8a849
commit
197b5649ff
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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 ),
|
||||
|
|
20
package-lock.json
generated
20
package-lock.json
generated
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
20
resources/codemirror.mediawiki.autocomplete.js
Normal file
20
resources/codemirror.mediawiki.autocomplete.js
Normal file
|
@ -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;
|
|
@ -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<Tag>}
|
||||
* @private
|
||||
*/
|
||||
tokenTable: this.tokenTable
|
||||
tokenTable: this.tokenTable,
|
||||
|
||||
/**
|
||||
* @see https://codemirror.net/docs/ref/#language.StreamParser.languageData
|
||||
* @return {Object<any>}
|
||||
* @private
|
||||
*/
|
||||
languageData: {
|
||||
// TODO: Rewrite the comment command using jQuery.textSelection
|
||||
commentTokens: { block: { open: '<!--', close: '-->' } },
|
||||
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 );
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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' ) );
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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.
|
||||
|
|
|
@ -184,7 +184,7 @@ const mwLang = mediaWikiLang( {}, {
|
|||
urlProtocols: 'ftp://|https://|news:',
|
||||
doubleUnderscore: [ {
|
||||
__notoc__: 'notoc'
|
||||
} ],
|
||||
}, {} ],
|
||||
functionSynonyms: [ {}, {
|
||||
'!': '!',
|
||||
'מיון רגיל': 'defaultsort'
|
||||
|
|
|
@ -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 );
|
||||
|
|
Loading…
Reference in a new issue