mediawiki-extensions-CodeMi.../resources/codemirror.mediawiki.templateFolding.js

336 lines
8.9 KiB
JavaScript
Raw Normal View History

const {
EditorState,
Extension,
KeyBinding,
StateField,
SyntaxNode,
Tree,
Tooltip,
codeFolding,
ensureSyntaxTree,
foldEffect,
foldedRanges,
keymap,
showTooltip,
syntaxTree,
unfoldAll,
unfoldEffect
} = require( 'ext.CodeMirror.v6.lib' );
const mwModeConfig = require( './codemirror.mediawiki.config.js' );
/**
* Check if a SyntaxNode is a template bracket (`{{` or `}}`)
*
* @param {SyntaxNode} node The SyntaxNode to check
* @return {boolean}
* @private
*/
const isBracket = ( node ) => node.name.split( '_' ).includes( mwModeConfig.tags.templateBracket ),
/**
* Check if a SyntaxNode is a template delimiter (`|`)
*
* @param {SyntaxNode} node The SyntaxNode to check
* @return {boolean}
* @private
*/
isDelimiter = ( node ) => node.name.split( '_' ).includes( mwModeConfig.tags.templateDelimiter ),
/**
* Check if a SyntaxNode is part of a template, except for the brackets
*
* @param {SyntaxNode} node The SyntaxNode to check
* @return {boolean}
* @private
*/
isTemplate = ( node ) => /-template[a-z\d-]+ground/.test( node.name ) && !isBracket( node ),
/**
* Update the stack of opening (+) or closing (-) brackets
*
* @param {EditorState} state EditorState instance
* @param {SyntaxNode} node The SyntaxNode of the bracket
* @return {number}
* @private
*/
stackUpdate = ( state, node ) => state.sliceDoc( node.from, node.from + 1 ) === '{' ? 1 : -1;
/**
* If the node is a template, find the range of the template parameters
*
* @param {EditorState} state EditorState instance
* @param {number|SyntaxNode} posOrNode Position or node
* @param {Tree|null} [tree] Syntax tree
* @return {{from: number, to: number}|null}
* @private
*/
const foldable = ( state, posOrNode, tree ) => {
if ( typeof posOrNode === 'number' ) {
tree = ensureSyntaxTree( state, posOrNode );
}
if ( !tree ) {
return null;
}
/** @type {SyntaxNode} */
let node;
if ( typeof posOrNode === 'number' ) {
// Find the initial template node on both sides of the position
node = tree.resolve( posOrNode, -1 );
if ( !isTemplate( node ) ) {
node = tree.resolve( posOrNode, 1 );
}
} else {
node = posOrNode;
}
if ( !isTemplate( node ) ) {
// Not a template
return null;
}
let { prevSibling, nextSibling } = node,
/** The stack of opening (+) or closing (-) brackets */
stack = 1,
/** The first delimiter */
delimiter = isDelimiter( node ) ? node : null;
while ( nextSibling ) {
if ( isBracket( nextSibling ) ) {
stack += stackUpdate( state, nextSibling );
if ( stack === 0 ) {
// The closing bracket of the current template
break;
}
} else if ( !delimiter && stack === 1 && isDelimiter( nextSibling ) ) {
// The first delimiter of the current template so far
delimiter = nextSibling;
}
( { nextSibling } = nextSibling );
}
if ( !nextSibling ) {
// The closing bracket of the current template is missing
return null;
}
stack = -1;
while ( prevSibling ) {
if ( isBracket( prevSibling ) ) {
stack += stackUpdate( state, prevSibling );
if ( stack === 0 ) {
// The opening bracket of the current template
break;
}
} else if ( stack === -1 && isDelimiter( prevSibling ) ) {
// The first delimiter of the current template so far
delimiter = prevSibling;
}
( { prevSibling } = prevSibling );
}
/** The end of the first delimiter */
const from = delimiter && delimiter.to,
/** The start of the closing bracket */
to = nextSibling.from;
if ( from && from < to ) {
return { from, to };
}
return null;
};
/**
* Create a tooltip for folding a template
*
* @param {EditorState} state EditorState instance
* @return {Tooltip|null}
* @private
*/
const create = ( state ) => {
const { selection: { main: { head } } } = state,
range = foldable( state, head );
if ( range ) {
const { from, to } = range;
let folded = false;
// Check if the range is already folded
foldedRanges( state ).between( from, to, ( i, j ) => {
if ( i === from && j === to ) {
folded = true;
}
} );
return folded ?
null :
{
pos: head,
above: true,
create( view ) {
const dom = document.createElement( 'div' );
dom.className = 'cm-tooltip-fold';
dom.textContent = '\uff0d';
dom.title = mw.msg( 'codemirror-fold-template' );
dom.onclick = () => {
view.dispatch( {
effects: foldEffect.of( { from, to } ),
selection: { anchor: to }
} );
dom.remove();
};
return { dom };
}
};
}
return null;
};
/**
* @type {KeyBinding[]}
* @private
*/
const foldKeymap = [
{
// Fold the template at the selection/cursor
key: 'Ctrl-Shift-[',
mac: 'Cmd-Alt-[',
run( view ) {
const { state } = view,
tree = ensureSyntaxTree( state, view.viewport.to );
if ( !tree ) {
return false;
}
const effects = [],
{ selection: { ranges } } = state;
/** The rightmost position of all selections, to be updated with folding */
let anchor = Math.max( ...ranges.map( ( { to } ) => to ) );
for ( const { from, to } of ranges ) {
let node;
if ( from === to ) {
// No selection, try both sides of the cursor position
node = tree.resolve( from, -1 );
}
if ( !node || !isTemplate( node ) ) {
node = tree.resolve( from, 1 );
}
while ( node && node.from <= to ) {
const range = foldable( state, node, tree );
if ( range ) {
effects.push( foldEffect.of( range ) );
node = tree.resolve( range.to, 1 );
// Update the anchor with the end of the last folded range
anchor = Math.max( anchor, range.to );
continue;
}
node = node.nextSibling;
}
}
if ( effects.length > 0 ) {
const dom = view.dom.querySelector( '.cm-tooltip-fold' );
if ( dom ) {
dom.remove();
}
// Fold the template(s) and update the cursor position
view.dispatch( { effects, selection: { anchor } } );
return true;
}
return false;
}
},
{
// Unfold the template at the selection/cursor
key: 'Ctrl-Shift-]',
mac: 'Cmd-Alt-]',
run( view ) {
const { state } = view,
{ selection } = state,
effects = [],
folded = foldedRanges( state );
for ( const { from, to } of selection.ranges ) {
// Unfold any folded range at the selection
folded.between( from, to, ( i, j ) => {
effects.push( unfoldEffect.of( { from: i, to: j } ) );
} );
}
if ( effects.length > 0 ) {
// Unfold the template(s) and redraw the selections
view.dispatch( { effects, selection } );
return true;
}
return false;
}
},
{
// Fold all templates in the document
key: 'Ctrl-Alt-[',
run( view ) {
const { state } = view,
tree = syntaxTree( state ),
effects = [];
/** The rightmost position of all selections, to be updated with folding */
let anchor = Math.max( ...state.selection.ranges.map( ( { to } ) => to ) ),
node = tree.topNode.firstChild;
while ( node ) {
const range = foldable( state, node, tree );
if ( range ) {
effects.push( foldEffect.of( range ) );
const { from, to } = range;
node = tree.resolve( to, 1 );
if ( from <= anchor && to > anchor ) {
// Update the anchor with the end of the last folded range
anchor = to;
}
continue;
}
node = node.nextSibling;
}
if ( effects.length > 0 ) {
const dom = view.dom.querySelector( '.cm-tooltip-fold' );
if ( dom ) {
dom.remove();
}
// Fold the template(s) and update the cursor position
view.dispatch( { effects, selection: { anchor } } );
return true;
}
return false;
}
},
{ key: 'Ctrl-Alt-]', run: unfoldAll }
];
/**
* CodeMirror extension providing
* [template folding](https://www.mediawiki.org/wiki/Help:Extension:CodeMirror#Template_folding)
* for the MediaWiki mode. This automatically applied when using {@link CodeMirrorModeMediaWiki}.
*
* @module CodeMirrorTemplateFolding
* @type {Extension}
*/
const templateFoldingExtension = [
codeFolding( {
placeholderDOM( view ) {
const element = document.createElement( 'span' );
element.textContent = '…';
element.setAttribute( 'aria-label', mw.msg( 'codemirror-folded-code' ) );
element.title = mw.msg( 'codemirror-unfold' );
element.className = 'cm-foldPlaceholder';
element.onclick = ( { target } ) => {
const pos = view.posAtDOM( target ),
{ state } = view,
{ selection } = state;
foldedRanges( state ).between( pos, pos, ( from, to ) => {
if ( from === pos ) {
// Unfold the template and redraw the selections
view.dispatch( { effects: unfoldEffect.of( { from, to } ), selection } );
}
} );
};
return element;
}
} ),
/** @see https://codemirror.net/examples/tooltip/ */
StateField.define( {
create,
update( tooltip, { state, docChanged, selection } ) {
if ( docChanged ) {
return null;
}
return selection ? create( state ) : tooltip;
},
provide( f ) {
return showTooltip.from( f );
}
} ),
keymap.of( foldKeymap )
];
module.exports = templateFoldingExtension;