Merge "CodeMirror: fix implementation of jQuery.textSelection encapsulate"

This commit is contained in:
jenkins-bot 2024-03-13 00:28:26 +00:00 committed by Gerrit Code Review
commit c63e81539a
6 changed files with 239 additions and 76 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,6 @@
import { EditorSelection, EditorState, Extension } from '@codemirror/state';
import { EditorState, Extension } from '@codemirror/state';
import { EditorView, lineNumbers, highlightSpecialChars } from '@codemirror/view';
import CodemirrorTextSelection from './codemirror.textSelection';
import bidiIsolationExtension from './codemirror.bidiIsolation';
/**
@ -8,6 +9,7 @@ import bidiIsolationExtension from './codemirror.bidiIsolation';
* @property {EditorView} view
* @property {EditorState} state
* @property {boolean} readOnly
* @property {CodemirrorTextSelection} textSelection
*/
export default class CodeMirror {
/**
@ -19,6 +21,7 @@ export default class CodeMirror {
this.view = null;
this.state = null;
this.readOnly = this.$textarea.prop( 'readonly' );
this.textSelection = null;
}
/**
@ -270,77 +273,18 @@ export default class CodeMirror {
* @return {Object}
*/
get cmTextSelection() {
const $cmDom = $( this.view.dom );
if ( !this.textSelection ) {
this.textSelection = new CodemirrorTextSelection( this.view );
}
return {
getContents: () => this.view.state.doc.toString(),
setContents: ( content ) => {
this.view.dispatch( {
changes: {
from: 0,
to: this.view.state.doc.length,
insert: content
}
} );
return $cmDom;
},
getSelection: () => {
return this.view.state.sliceDoc(
this.view.state.selection.main.from,
this.view.state.selection.main.to
);
},
setSelection: ( options = { start: 0, end: 0 } ) => {
this.view.dispatch( {
selection: { anchor: options.start, head: ( options.end || options.start ) }
} );
this.view.focus();
return $cmDom;
},
encapsulateSelection: ( options ) => {
// First set the selection, if applicable.
if ( options.selectionStart || options.selectionEnd ) {
this.view.dispatch( {
selection: {
anchor: options.selectionStart,
head: ( options.selectionEnd || options.selectionStart )
}
} );
}
// Do the actual replacements.
this.view.dispatch( this.view.state.changeByRange( ( range ) => ( {
changes: [
{ from: range.from, insert: options.pre },
{ from: range.to, insert: options.post }
],
range: EditorSelection.range(
range.from,
range.to + options.pre.length + options.post.length
)
} ) ) );
this.view.focus();
return $cmDom;
},
replaceSelection: ( value ) => {
this.view.dispatch(
this.view.state.replaceSelection( value )
);
return $cmDom;
},
getCaretPosition: ( options ) => {
if ( !options.startAndEnd ) {
return this.view.state.selection.main.head;
}
return [
this.view.state.selection.main.from,
this.view.state.selection.main.to
];
},
scrollToCaretPosition: () => {
this.view.dispatch( {
effects: EditorView.scrollIntoView( this.view.state.selection.main.head )
} );
return $cmDom;
}
getContents: () => this.textSelection.getContents(),
setContents: ( content ) => this.textSelection.setContents( content ),
getCaretPosition: ( options ) => this.textSelection.getCaretPosition( options ),
scrollToCaretPosition: () => this.textSelection.scrollToCaretPosition(),
getSelection: () => this.textSelection.getSelection(),
setSelection: ( options ) => this.textSelection.setSelection( options ),
replaceSelection: ( value ) => this.textSelection.replaceSelection( value ),
encapsulateSelection: ( options ) => this.textSelection.encapsulateSelection( options )
};
}
}

View file

@ -0,0 +1,202 @@
import { EditorView } from '@codemirror/view';
import { EditorSelection } from '@codemirror/state';
/**
* jQuery.textSelection implementation for CodeMirror.
*
* @see jQuery.fn.textSelection
* @class CodemirrorTextSelection
* @property {EditorView} view
* @property {jQuery} $cmDom
*/
export default class CodemirrorTextSelection {
/**
* @constructor
* @param {EditorView} view
*/
constructor( view ) {
this.view = view;
this.$cmDom = $( view.dom );
}
/**
* Get the contents of the editor.
*
* @return {string}
*/
getContents() {
return this.view.state.doc.toString();
}
/**
* Set the contents of the editor.
*
* @param {string} content
* @return {jQuery}
*/
setContents( content ) {
this.view.dispatch( {
changes: {
from: 0,
to: this.view.state.doc.length,
insert: content
}
} );
return this.$cmDom;
}
/**
* Get the current caret position.
*
* @param {Object} options
* @return {number[]|number}
*/
getCaretPosition( options ) {
if ( !options.startAndEnd ) {
return this.view.state.selection.main.head;
}
return [
this.view.state.selection.main.from,
this.view.state.selection.main.to
];
}
/**
* Scroll the editor to the current caret position.
*
* @return {jQuery}
*/
scrollToCaretPosition() {
const scrollEffect = EditorView.scrollIntoView( this.view.state.selection.main.head );
scrollEffect.value.isSnapshot = true;
this.view.dispatch( {
effects: scrollEffect
} );
return this.$cmDom;
}
/**
* Get the selected text.
*
* @return {string}
*/
getSelection() {
return this.view.state.sliceDoc(
this.view.state.selection.main.from,
this.view.state.selection.main.to
);
}
/**
* Set the selected text.
*
* @param {Object} options
* @return {jQuery}
*/
setSelection( options ) {
this.view.dispatch( {
selection: { anchor: options.start, head: ( options.end || options.start ) }
} );
this.view.focus();
return this.$cmDom;
}
/**
* Replace the selected text with the given value.
*
* @param {string} value
* @return {jQuery}
*/
replaceSelection( value ) {
this.view.dispatch(
this.view.state.replaceSelection( value )
);
return this.$cmDom;
}
/**
* Encapsulate the selected text with the given values.
*
* This is intentionally a near-identical implementation to jQuery.textSelection,
* except it uses CodeMirror's EditorState.changeByRange when there are multiple selections.
*
* @see jQuery.fn.textSelection.encapsulateSelection
* @todo Add support for 'ownline', 'selectPeri' and 'splitlines' options.
*
* @param {Object} options
* @return {jQuery}
*/
encapsulateSelection( options ) {
let selectedText,
isSample = false;
const checkSelectedText = () => {
if ( !selectedText ) {
selectedText = options.peri;
isSample = true;
} else if ( options.replace ) {
selectedText = options.peri;
} else {
while ( selectedText.charAt( selectedText.length - 1 ) === ' ' ) {
// Exclude ending space char
selectedText = selectedText.slice( 0, -1 );
options.post += ' ';
}
while ( selectedText.charAt( 0 ) === ' ' ) {
// Exclude prepending space char
selectedText = selectedText.slice( 1 );
options.pre = ' ' + options.pre;
}
}
};
this.view.focus();
// Set the selection, if applicable.
if ( options.selectionStart !== undefined ) {
this.setSelection( {
start: options.selectionStart,
end: options.selectionEnd || options.selectionStart
} );
}
selectedText = this.getSelection();
const [ startPos ] = this.getCaretPosition( { startAndEnd: true } );
checkSelectedText();
const insertText = options.pre + selectedText + options.post;
/**
* Use CodeMirror's API when there are multiple selections.
*
* @see https://codemirror.net/examples/change/
*/
if ( this.view.state.selection.ranges.length > 1 ) {
this.view.dispatch( this.view.state.changeByRange( ( range ) => ( {
changes: [
{ from: range.from, insert: options.pre },
{ from: range.to, insert: options.post }
],
range: EditorSelection.range(
range.to + options.pre.length + options.post.length,
range.to + options.pre.length + options.post.length
)
} ) ) );
return this.$cmDom;
}
this.replaceSelection( insertText );
if ( isSample && options.selectPeri ) {
this.setSelection( {
start: startPos + options.pre.length,
end: startPos + options.pre.length + selectedText.length
} );
} else {
this.setSelection( {
start: startPos + insertText.length
} );
}
return this.$cmDom;
}
}

View file

@ -83,7 +83,24 @@ describe( 'CodeMirror textSelection for the wikitext 2010 editor', () => {
);
} );
// Content is now "<div>foobaz</div>"
it( "correctly inserts the 'peri' option when using encapsulateSelection", async function () {
await browser.execute( () => {
$( '.cm-editor' ).textSelection( 'setContents', 'foobaz' )
.textSelection( 'encapsulateSelection', {
selectionStart: 0,
selectionEnd: 6,
pre: '<div>',
post: '</div>',
peri: 'Soundgarden',
replace: true
} );
} );
assert.strictEqual(
await browser.execute( () => $( '.cm-editor' ).textSelection( 'getContents' ) ),
'<div>Soundgarden</div>'
);
} );
it( 'scrolls to the correct place when using scrollToCaretPosition', async () => {
await browser.execute( () => {
const $cmEditor = $( '.cm-editor' );

View file

@ -89,8 +89,8 @@ module.exports = ( env, argv ) => ( {
// Minified uncompressed size limits for chunks / assets and entrypoints. Keep these numbers
// up-to-date and rounded to the nearest 10th of a kibibyte so that code sizing costs are
// well understood. Related to bundlesize minified, gzipped compressed file size tests.
maxAssetSize: 350.0 * 1024,
maxEntrypointSize: 350.0 * 1024,
maxAssetSize: 351.0 * 1024,
maxEntrypointSize: 351.0 * 1024,
// The default filter excludes map files, but we rename ours.
assetFilter: ( filename ) => !filename.endsWith( srcMapExt )