Merge "CodeMirrorSearch: add num results and current selection; improve tabbing"

This commit is contained in:
jenkins-bot 2024-11-01 05:52:50 +00:00 committed by Gerrit Code Review
commit 140ac0cdb0
6 changed files with 160 additions and 10 deletions

View file

@ -217,6 +217,7 @@
"codemirror-replace", "codemirror-replace",
"codemirror-replace-all", "codemirror-replace-all",
"codemirror-replace-placeholder", "codemirror-replace-placeholder",
"codemirror-find-results",
"codemirror-special-char-backspace", "codemirror-special-char-backspace",
"codemirror-special-char-bell", "codemirror-special-char-bell",
"codemirror-special-char-carriage-return", "codemirror-special-char-carriage-return",

View file

@ -25,6 +25,7 @@
"codemirror-replace-placeholder": "Replace", "codemirror-replace-placeholder": "Replace",
"codemirror-replace-all": "Replace all", "codemirror-replace-all": "Replace all",
"codemirror-done": "Done", "codemirror-done": "Done",
"codemirror-find-results": "$1 of $2",
"codemirror-goto-line": "Go to line", "codemirror-goto-line": "Go to line",
"codemirror-goto-line-go": "Go", "codemirror-goto-line-go": "Go",
"codemirror-control-character": "Control character $1", "codemirror-control-character": "Control character $1",

View file

@ -30,6 +30,7 @@
"codemirror-replace-placeholder": "Placeholder text for the 'Replace' input in the CodeMirror search panel.", "codemirror-replace-placeholder": "Placeholder text for the 'Replace' input in the CodeMirror search panel.",
"codemirror-replace-all": "Label for the 'Replace all' button in the CodeMirror search panel.", "codemirror-replace-all": "Label for the 'Replace all' button in the CodeMirror search panel.",
"codemirror-done": "Label for the 'Done' button in CodeMirror panels.\n{{Identical|Done}}", "codemirror-done": "Label for the 'Done' button in CodeMirror panels.\n{{Identical|Done}}",
"codemirror-find-results": "Label for find results showing how many results were found ($2), and which one is currently highlighted ($1).\n{{Identical|Of}}",
"codemirror-goto-line": "Label for the 'Go to line' input field.", "codemirror-goto-line": "Label for the 'Go to line' input field.",
"codemirror-goto-line-go": "Label for the 'Go to line' submit button.\n{{Identical|Go}}", "codemirror-goto-line-go": "Label for the 'Go to line' submit button.\n{{Identical|Go}}",
"codemirror-control-character": "Tooltip text shown when hovering over special characters. $1 is the Unicode value of the special character.", "codemirror-control-character": "Tooltip text shown when hovering over special characters. $1 is the Unicode value of the special character.",

View file

@ -138,6 +138,14 @@
} }
} }
.cm-mw-find-results {
color: @color-placeholder;
position: absolute;
right: 9px;
top: 50%;
transform: translateY( -50% );
}
.cm-mw-icon--match-case { .cm-mw-icon--match-case {
background-color: @color-base; background-color: @color-base;
.cdx-mixin-css-icon( @cdx-icon-search-case-sensitive, @color-base, @size-icon-medium, true ); .cdx-mixin-css-icon( @cdx-icon-search-case-sensitive, @color-base, @size-icon-medium, true );

View file

@ -4,13 +4,16 @@ const {
closeSearchPanel, closeSearchPanel,
findNext, findNext,
findPrevious, findPrevious,
getSearchQuery,
keymap, keymap,
openSearchPanel,
replaceAll, replaceAll,
replaceNext, replaceNext,
runScopeHandlers, runScopeHandlers,
search, search,
searchKeymap,
selectMatches, selectMatches,
selectNextOccurrence,
selectSelectionMatches,
setSearchQuery setSearchQuery
} = require( 'ext.CodeMirror.v6.lib' ); } = require( 'ext.CodeMirror.v6.lib' );
const CodeMirrorPanel = require( './codemirror.panel.js' ); const CodeMirrorPanel = require( './codemirror.panel.js' );
@ -70,6 +73,14 @@ class CodeMirrorSearch extends CodeMirrorPanel {
* @type {HTMLButtonElement} * @type {HTMLButtonElement}
*/ */
this.replaceAllButton = undefined; this.replaceAllButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.doneButton = undefined;
/**
* @type {HTMLSpanElement}
*/
this.findResultsText = undefined;
} }
/** /**
@ -83,7 +94,14 @@ class CodeMirrorSearch extends CodeMirrorPanel {
return this.panel; return this.panel;
} }
} ), } ),
keymap.of( searchKeymap ) keymap.of( [
{ key: 'Mod-f', run: openSearchPanel, scope: 'editor search-panel' },
{ key: 'F3', run: this.findNext.bind( this ), shift: this.findPrevious.bind( this ), scope: 'editor search-panel', preventDefault: true },
{ key: 'Mod-g', run: this.findNext.bind( this ), shift: this.findPrevious.bind( this ), scope: 'editor search-panel', preventDefault: true },
{ key: 'Escape', run: closeSearchPanel, scope: 'editor search-panel' },
{ key: 'Mod-Shift-l', run: selectSelectionMatches },
{ key: 'Mod-d', run: selectNextOccurrence, preventDefault: true }
] )
]; ];
} }
@ -102,11 +120,17 @@ class CodeMirrorSearch extends CodeMirrorPanel {
// Search input. // Search input.
const [ searchInputWrapper, searchInput ] = this.getTextInput( const [ searchInputWrapper, searchInput ] = this.getTextInput(
'search', 'search',
this.searchQuery.search || '', this.searchQuery.search || this.view.state.sliceDoc(
this.view.state.selection.main.from,
this.view.state.selection.main.to
),
'codemirror-find' 'codemirror-find'
); );
this.searchInput = searchInput; this.searchInput = searchInput;
this.searchInput.setAttribute( 'main-field', 'true' ); this.searchInput.setAttribute( 'main-field', 'true' );
this.findResultsText = document.createElement( 'span' );
this.findResultsText.className = 'cm-mw-find-results';
searchInputWrapper.appendChild( this.findResultsText );
firstRow.appendChild( searchInputWrapper ); firstRow.appendChild( searchInputWrapper );
this.appendPrevAndNextButtons( firstRow ); this.appendPrevAndNextButtons( firstRow );
@ -146,7 +170,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
buttonGroup.appendChild( this.prevButton ); buttonGroup.appendChild( this.prevButton );
this.prevButton.addEventListener( 'click', ( e ) => { this.prevButton.addEventListener( 'click', ( e ) => {
e.preventDefault(); e.preventDefault();
findPrevious( this.view ); this.findPrevious();
} ); } );
// "Next" button. // "Next" button.
@ -154,7 +178,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
buttonGroup.appendChild( this.nextButton ); buttonGroup.appendChild( this.nextButton );
this.nextButton.addEventListener( 'click', ( e ) => { this.nextButton.addEventListener( 'click', ( e ) => {
e.preventDefault(); e.preventDefault();
findNext( this.view ); this.findNext();
} ); } );
firstRow.appendChild( buttonGroup ); firstRow.appendChild( buttonGroup );
@ -225,6 +249,7 @@ class CodeMirrorSearch extends CodeMirrorPanel {
this.replaceButton.addEventListener( 'click', ( e ) => { this.replaceButton.addEventListener( 'click', ( e ) => {
e.preventDefault(); e.preventDefault();
replaceNext( this.view ); replaceNext( this.view );
this.updateNumMatchesText();
} ); } );
// "Replace all" button. // "Replace all" button.
@ -234,12 +259,13 @@ class CodeMirrorSearch extends CodeMirrorPanel {
this.replaceAllButton.addEventListener( 'click', ( e ) => { this.replaceAllButton.addEventListener( 'click', ( e ) => {
e.preventDefault(); e.preventDefault();
replaceAll( this.view ); replaceAll( this.view );
this.updateNumMatchesText();
} ); } );
// "Done" button. // "Done" button.
const doneButton = this.getButton( 'codemirror-done' ); this.doneButton = this.getButton( 'codemirror-done' );
row.appendChild( doneButton ); row.appendChild( this.doneButton );
doneButton.addEventListener( 'click', ( e ) => { this.doneButton.addEventListener( 'click', ( e ) => {
e.preventDefault(); e.preventDefault();
closeSearchPanel( this.view ); closeSearchPanel( this.view );
this.view.focus(); this.view.focus();
@ -254,12 +280,46 @@ class CodeMirrorSearch extends CodeMirrorPanel {
onKeydown( event ) { onKeydown( event ) {
if ( runScopeHandlers( this.view, event, 'search-panel' ) ) { if ( runScopeHandlers( this.view, event, 'search-panel' ) ) {
event.preventDefault(); event.preventDefault();
} else if ( event.key === 'Enter' && event.target === this.searchInput ) { return;
}
if ( this.view.state.readOnly ) {
// Use normal tab behaviour if the editor is read-only.
return;
}
if ( event.key === 'Enter' && event.target === this.searchInput ) {
event.preventDefault(); event.preventDefault();
( event.shiftKey ? findPrevious : findNext )( this.view ); ( event.shiftKey ? this.findPrevious : this.findNext ).call( this );
} else if ( event.key === 'Enter' && event.target === this.replaceInput ) { } else if ( event.key === 'Enter' && event.target === this.replaceInput ) {
event.preventDefault(); event.preventDefault();
replaceNext( this.view ); replaceNext( this.view );
this.updateNumMatchesText();
} else if ( event.key === 'Tab' ) {
if ( !event.shiftKey && event.target === this.searchInput ) {
// Tabbing from the search input should focus the replaceInput.
event.preventDefault();
this.replaceInput.focus();
} else if ( event.shiftKey && event.target === this.replaceInput ) {
// Shift+Tabbing from the replaceInput should focus the searchInput.
event.preventDefault();
this.searchInput.focus();
} else if ( !event.shiftKey && event.target === this.doneButton ) {
// Tabbing from the "Done" button should focus the prevButton.
event.preventDefault();
this.prevButton.focus();
} else if ( !event.shiftKey && event.target === this.wholeWordButton ) {
// Tabbing from the "Whole word" button should focus the editor,
// or the next focusable panel if there is one.
event.preventDefault();
const el = this.view.dom.querySelector( '.cm-mw-panel--search-panel' );
if ( el && el.nextElementSibling && el.nextElementSibling.classList.contains( 'cm-panel' ) ) {
const input = el.nextElementSibling.querySelector( 'input' );
( input || el.nextElementSibling ).focus();
} else {
this.view.focus();
}
}
} }
} }
@ -282,6 +342,55 @@ class CodeMirrorSearch extends CodeMirrorPanel {
effects: setSearchQuery.of( query ) effects: setSearchQuery.of( query )
} ); } );
} }
this.updateNumMatchesText( query );
}
/**
* Find the previous match.
*
* @return {boolean} Whether a match was found.
*/
findPrevious() {
const ret = findPrevious( this.view );
this.updateNumMatchesText();
return ret;
}
/**
* Find the next match.
*
* @return {boolean} Whether a match was found.
*/
findNext() {
const ret = findNext( this.view );
this.updateNumMatchesText();
return ret;
}
/**
* Show the number of matches for the given {@link SearchQuery}
* and the index of the current match in the find input.
*
* @param {SearchQuery} [query]
*/
updateNumMatchesText( query ) {
const cursor = query ?
query.getCursor( this.view.state ) :
getSearchQuery( this.view.state ).getCursor( this.view.state );
let count = 0,
current = 1;
const { from, to } = this.view.state.selection.main;
let item = cursor.next();
while ( !item.done ) {
if ( item.value.from === from && item.value.to === to ) {
current = count + 1;
}
item = cursor.next();
count++;
}
this.findResultsText.textContent = count ?
mw.msg( 'codemirror-find-results', current, count ) :
'';
} }
/** /**

View file

@ -0,0 +1,30 @@
/* eslint-disable-next-line n/no-missing-require */
const { EditorView, EditorState } = require( 'ext.CodeMirror.v6.lib' );
const CodeMirrorSearch = require( '../../resources/codemirror.search.js' );
describe( 'CodeMirrorSearch', () => {
it( 'should provide an Extension getter and a Panel getter', () => {
const cmSearch = new CodeMirrorSearch();
cmSearch.view = new EditorView();
expect( cmSearch.extension ).toBeInstanceOf( Array );
expect( cmSearch.extension[ 0 ][ 0 ].constructor.name ).toStrictEqual( 'FacetProvider' );
expect( cmSearch.panel ).toHaveProperty( 'dom' );
} );
it( 'should disable replacement fields if the textarea is read-only', () => {
const cmSearch = new CodeMirrorSearch();
cmSearch.view = new EditorView();
// eslint-disable-next-line no-unused-expressions
cmSearch.panel;
cmSearch.view = new EditorView( {
state: EditorState.create( {
doc: '',
extensions: [ EditorState.readOnly.of( true ) ]
} )
} );
// eslint-disable-next-line no-unused-expressions
cmSearch.panel;
expect( cmSearch.replaceInput.disabled ).toBe( true );
} );
} );