mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-12-24 19:43:16 +00:00
46b7208d13
Move WikiEditor-specific code to ext.CodeMirror.WikiEditor, leaivng only CodeMirror-specific things in ext.CodeMirror, including the logUsage method which was duplicated in the VE plugin and now refactored. Add .env to .gitignore so that selenium tests can be ran more easily This patch leaves the other non-mediawiki modes still using the 'scripts' system instead of 'packageFiles'. These are not used in MediaWiki directly but by some extensions (i.e. PhpTags) and using packageFiles will break that integration. Bug: T272035 Change-Id: I3bafef196c1f713443d7b8e9cb7dc2891b379f5d
283 lines
8.5 KiB
JavaScript
283 lines
8.5 KiB
JavaScript
/*!
|
|
* VisualEditor UserInterface CodeMirrorAction class.
|
|
*
|
|
* @copyright 2011-2017 VisualEditor Team and others; see http://ve.mit-license.org
|
|
*/
|
|
|
|
/**
|
|
* CodeMirror action
|
|
*
|
|
* @class
|
|
* @extends ve.ui.Action
|
|
* @constructor
|
|
* @param {ve.ui.Surface} surface Surface to act on
|
|
*/
|
|
ve.ui.CodeMirrorAction = function VeUiCodeMirrorAction() {
|
|
// Parent constructor
|
|
ve.ui.CodeMirrorAction.super.apply( this, arguments );
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.inheritClass( ve.ui.CodeMirrorAction, ve.ui.Action );
|
|
|
|
/* Static Properties */
|
|
|
|
ve.ui.CodeMirrorAction.static.name = 'codeMirror';
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.CodeMirrorAction.static.methods = [ 'toggle' ];
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
ve.ui.CodeMirrorAction.static.isLineNumbering = function () {
|
|
// T285660: Backspace related bug on Android browsers as of 2021
|
|
if ( /Android\b/.test( navigator.userAgent ) ) {
|
|
return false;
|
|
}
|
|
|
|
var namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
|
|
// Set to [] to disable everywhere, or null to enable everywhere
|
|
return !namespaces ||
|
|
namespaces.indexOf( mw.config.get( 'wgNamespaceNumber' ) ) !== -1;
|
|
};
|
|
|
|
/**
|
|
* @method
|
|
* @param {boolean} [enable] State to force toggle to, inverts current state if undefined
|
|
* @return {boolean} Action was executed
|
|
*/
|
|
ve.ui.CodeMirrorAction.prototype.toggle = function ( enable ) {
|
|
var action = this,
|
|
surface = this.surface,
|
|
surfaceView = surface.getView(),
|
|
doc = surface.getModel().getDocument();
|
|
|
|
if ( !surface.mirror && enable !== false ) {
|
|
surface.mirror = true;
|
|
mw.loader.using( [
|
|
'ext.CodeMirror.lib',
|
|
'ext.CodeMirror.mode.mediawiki',
|
|
'jquery.client'
|
|
] ).then( function () {
|
|
var config = mw.config.get( 'extCodeMirrorConfig' );
|
|
|
|
if ( !surface.mirror ) {
|
|
// Action was toggled to false since promise started
|
|
return;
|
|
}
|
|
mw.loader.using( config.pluginModules, function () {
|
|
if ( !surface.mirror ) {
|
|
// Action was toggled to false since promise started
|
|
return;
|
|
}
|
|
var tabSizeValue = surfaceView.documentView.documentNode.$element.css( 'tab-size' );
|
|
var cmOptions = {
|
|
value: surface.getDom(),
|
|
mwConfig: config,
|
|
readOnly: 'nocursor',
|
|
lineWrapping: true,
|
|
scrollbarStyle: 'null',
|
|
specialChars: /^$/,
|
|
viewportMargin: 5,
|
|
tabSize: tabSizeValue ? +tabSizeValue : 8,
|
|
// select mediawiki as text input mode
|
|
mode: 'text/mediawiki',
|
|
extraKeys: {
|
|
Tab: false,
|
|
'Shift-Tab': false
|
|
}
|
|
};
|
|
|
|
cmOptions.matchBrackets = {
|
|
highlightNonMatching: false,
|
|
maxHighlightLineLength: 10000
|
|
};
|
|
|
|
if ( ve.ui.CodeMirrorAction.static.isLineNumbering() ) {
|
|
$.extend( cmOptions, {
|
|
// Set up a special "padding" gutter to create space between the line numbers
|
|
// and page content. The first column name is a magic constant which causes
|
|
// the built-in line number gutter to appear in the desired, leftmost position.
|
|
gutters: [
|
|
'CodeMirror-linenumbers',
|
|
'CodeMirror-linenumber-padding'
|
|
],
|
|
lineNumbers: true
|
|
} );
|
|
}
|
|
|
|
surface.mirror = CodeMirror( surfaceView.$element[ 0 ], cmOptions );
|
|
|
|
// The VE/CM overlay technique only works with monospace fonts (as we use width-changing bold as a highlight)
|
|
// so revert any editfont user preference
|
|
surfaceView.$element.removeClass( 'mw-editfont-sans-serif mw-editfont-serif' ).addClass( 'mw-editfont-monospace' );
|
|
|
|
if ( mw.user.options.get( 'usecodemirror-colorblind' ) ) {
|
|
surfaceView.$element.addClass( 'cm-mw-colorblind-colors' );
|
|
}
|
|
|
|
var profile = $.client.profile();
|
|
var supportsTransparentText = 'WebkitTextFillColor' in document.body.style &&
|
|
// Disable on Firefox+OSX (T175223)
|
|
!( profile.layout === 'gecko' && profile.platform === 'mac' );
|
|
|
|
surfaceView.$documentNode.addClass(
|
|
supportsTransparentText ?
|
|
've-ce-documentNode-codeEditor-webkit-hide' :
|
|
've-ce-documentNode-codeEditor-hide'
|
|
);
|
|
|
|
if ( cmOptions.lineNumbers ) {
|
|
// Transfer gutter width to VE overlay.
|
|
var updateGutter = function ( cmDisplay ) {
|
|
surfaceView.$documentNode.css( 'margin-left', cmDisplay.gutters.offsetWidth );
|
|
};
|
|
CodeMirror.on( surface.mirror.display, 'updateGutter', updateGutter );
|
|
updateGutter( surface.mirror.display );
|
|
}
|
|
|
|
/* Events */
|
|
|
|
// As the action is regenerated each time, we need to store bound listeners
|
|
// in the mirror for later disconnection.
|
|
surface.mirror.veTransactionListener = action.onDocumentPrecommit.bind( action );
|
|
surface.mirror.veLangChangeListener = action.onLangChange.bind( action );
|
|
surface.mirror.veSelectListener = action.onSelect.bind( action );
|
|
|
|
doc.on( 'precommit', surface.mirror.veTransactionListener );
|
|
surfaceView.getDocument().on( 'langChange', surface.mirror.veLangChangeListener );
|
|
surface.getModel().on( 'select', surface.mirror.veSelectListener );
|
|
|
|
action.onLangChange();
|
|
|
|
ve.init.target.once( 'surfaceReady', function () {
|
|
if ( surface.mirror ) {
|
|
surface.mirror.refresh();
|
|
}
|
|
} );
|
|
} );
|
|
} );
|
|
} else if ( surface.mirror && enable !== true ) {
|
|
if ( surface.mirror !== true ) {
|
|
doc.off( 'precommit', surface.mirror.veTransactionListener );
|
|
surfaceView.getDocument().off( 'langChange', surface.mirror.veLangChangeListener );
|
|
surface.getModel().off( 'select', surface.mirror.veSelectListener );
|
|
|
|
// Restore edit-font
|
|
// eslint-disable-next-line mediawiki/class-doc
|
|
surfaceView.$element.removeClass( 'mw-editfont-monospace' ).addClass( 'mw-editfont-' + mw.user.options.get( 'editfont' ) );
|
|
|
|
surfaceView.$documentNode.removeClass(
|
|
've-ce-documentNode-codeEditor-webkit-hide ve-ce-documentNode-codeEditor-hide'
|
|
);
|
|
// Reset gutter.
|
|
surfaceView.$documentNode.css( 'margin-left', '' );
|
|
|
|
var mirrorElement = surface.mirror.getWrapperElement();
|
|
mirrorElement.parentNode.removeChild( mirrorElement );
|
|
}
|
|
|
|
surface.mirror = null;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Handle select events from the surface model
|
|
*
|
|
* @param {ve.dm.Selection} selection
|
|
*/
|
|
ve.ui.CodeMirrorAction.prototype.onSelect = function ( selection ) {
|
|
var range = selection.getCoveringRange();
|
|
|
|
// Do not re-trigger bracket matching as long as something is selected
|
|
if ( !range || !range.isCollapsed() ) {
|
|
return;
|
|
}
|
|
|
|
this.surface.mirror.setSelection( this.getPosFromOffset( range.from ) );
|
|
};
|
|
|
|
/**
|
|
* Handle langChange events from the document view
|
|
*/
|
|
ve.ui.CodeMirrorAction.prototype.onLangChange = function () {
|
|
var surface = this.surface,
|
|
doc = surface.getView().getDocument(),
|
|
dir = doc.getDir(), lang = doc.getLang();
|
|
|
|
surface.mirror.setOption( 'direction', dir );
|
|
|
|
// Set the wrapper to the appropriate language (T341342)
|
|
surface.mirror.getWrapperElement().setAttribute( 'lang', lang );
|
|
};
|
|
|
|
/**
|
|
* Handle precommit events from the document.
|
|
*
|
|
* The document is still in it's 'old' state before the transaction
|
|
* has been applied at this point.
|
|
*
|
|
* @param {ve.dm.Transaction} tx
|
|
*/
|
|
ve.ui.CodeMirrorAction.prototype.onDocumentPrecommit = function ( tx ) {
|
|
var offset = 0,
|
|
replacements = [],
|
|
action = this,
|
|
store = this.surface.getModel().getDocument().getStore(),
|
|
mirror = this.surface.mirror;
|
|
|
|
tx.operations.forEach( function ( op ) {
|
|
if ( op.type === 'retain' ) {
|
|
offset += op.length;
|
|
} else if ( op.type === 'replace' ) {
|
|
replacements.push( {
|
|
start: action.getPosFromOffset( offset ),
|
|
// Don't bother recalculating end offset if not a removal, replaceRange works with just one arg
|
|
end: op.remove.length ? action.getPosFromOffset( offset + op.remove.length ) : undefined,
|
|
data: new ve.dm.ElementLinearData( store, op.insert ).getSourceText()
|
|
} );
|
|
offset += op.remove.length;
|
|
}
|
|
} );
|
|
|
|
// Apply replacements in reverse to avoid having to shift offsets
|
|
for ( var i = replacements.length - 1; i >= 0; i-- ) {
|
|
mirror.replaceRange(
|
|
replacements[ i ].data,
|
|
replacements[ i ].start,
|
|
replacements[ i ].end
|
|
);
|
|
}
|
|
|
|
// HACK: The absolutely positioned CodeMirror doesn't calculate the viewport
|
|
// correctly when expanding from less than the viewport height. (T185184)
|
|
if ( mirror.display.sizer.style.minHeight !== this.lastHeight ) {
|
|
mirror.refresh();
|
|
this.lastHeight = mirror.display.sizer.style.minHeight;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Convert a VE offset to a 2D CodeMirror position
|
|
*
|
|
* @param {number} veOffset VE linear model offset
|
|
* @return {Object} Code mirror position, containing 'line' and 'ch' numbers
|
|
*/
|
|
ve.ui.CodeMirrorAction.prototype.getPosFromOffset = function ( veOffset ) {
|
|
return this.surface.mirror.posFromIndex(
|
|
this.surface.getModel().getSourceOffsetFromOffset( veOffset )
|
|
);
|
|
};
|
|
|
|
/* Registration */
|
|
|
|
ve.ui.actionFactory.register( ve.ui.CodeMirrorAction );
|