From a0cfad909312bdfa509fb1b6d98f11455d4ff99c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Mon, 2 Feb 2026 13:02:14 -0800 Subject: [PATCH 01/13] https://core.trac.wordpress.org/attachment/ticket/17133/17133.diff --- src/js/_enqueues/wp/theme-plugin-editor.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index 1e3ac0d904c77..81eab4adc9566 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -399,6 +399,15 @@ wp.themePluginEditor = (function( $ ) { editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings ); editor.codemirror.on( 'change', component.onChange ); + editor.codemirror.setOption( 'extraKeys', { + 'Ctrl-S': function () { + component.form.submit(); + }, + 'Cmd-S': function () { + component.form.submit(); + } + }); + // Improve the editor accessibility. $( editor.codemirror.display.lineDiv ) .attr({ From 03f49508beb73535a436339c1995505204d66513 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 13:03:09 -0800 Subject: [PATCH 02/13] Update whitespace for code style --- src/js/_enqueues/wp/theme-plugin-editor.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index 81eab4adc9566..763927bb2ae9d 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -400,13 +400,13 @@ wp.themePluginEditor = (function( $ ) { editor.codemirror.on( 'change', component.onChange ); editor.codemirror.setOption( 'extraKeys', { - 'Ctrl-S': function () { - component.form.submit(); - }, - 'Cmd-S': function () { - component.form.submit(); - } - }); + 'Ctrl-S': function () { + component.form.submit(); + }, + 'Cmd-S': function () { + component.form.submit(); + }, + } ); // Improve the editor accessibility. $( editor.codemirror.display.lineDiv ) From f322df5ca69acc81c637695f39682c62b2aa2768 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 15:59:17 -0800 Subject: [PATCH 03/13] Remove EOL whitespace --- src/js/_enqueues/wp/code-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index 4266f392929a4..c92797fbef865 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -82,7 +82,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { } /* - * Note that rules must be sent in the "deprecated" lint.options property + * Note that rules must be sent in the "deprecated" lint.options property * to prevent linter from complaining about unrecognized options. * See . */ From c2da37e68d6979c563d4591a8c39a289a152b6c3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 16:24:13 -0800 Subject: [PATCH 04/13] Display lint errors when saving via shortcut. Expose the `updateErrorNotice` method on the `wp.codeEditor` instance to allow forcing the display of linting errors. In the Theme and Plugin editors, this method is now called during the submission process. This ensures that errors are visible when using the save shortcuts (`Ctrl+S` or `Cmd+S`) while the editor is focused, preventing a silent failure when the save is blocked by validation errors. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/wp/code-editor.js | 12 ++++++++---- src/js/_enqueues/wp/theme-plugin-editor.js | 16 ++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index c92797fbef865..7fcadaa3d9ce5 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -46,7 +46,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { * @param {Function} settings.onChangeLintingErrors - Callback for when there are changes to linting errors. * @param {Function} settings.onUpdateErrorNotice - Callback to update error notice. * - * @return {void} + * @return {Function} Update error notice function. */ function configureLinting( editor, settings ) { // eslint-disable-line complexity var currentErrorAnnotations = [], previouslyShownErrorAnnotations = []; @@ -209,6 +209,8 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { updateErrorNotice(); } }); + + return updateErrorNotice; } /** @@ -261,6 +263,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { * @typedef {object} wp.codeEditor~CodeEditorInstance * @property {object} settings - The code editor settings. * @property {CodeMirror} codemirror - The CodeMirror instance. + * @property {Function} updateErrorNotice - Force update the error notice. */ /** @@ -282,7 +285,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { * @return {CodeEditorInstance} Instance. */ wp.codeEditor.initialize = function initialize( textarea, settings ) { - var $textarea, codemirror, instanceSettings, instance; + var $textarea, codemirror, instanceSettings, instance, updateErrorNotice; if ( 'string' === typeof textarea ) { $textarea = $( '#' + textarea ); } else { @@ -294,11 +297,12 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { codemirror = wp.CodeMirror.fromTextArea( $textarea[0], instanceSettings.codemirror ); - configureLinting( codemirror, instanceSettings ); + updateErrorNotice = configureLinting( codemirror, instanceSettings ); instance = { settings: instanceSettings, - codemirror: codemirror + codemirror: codemirror, + updateErrorNotice: updateErrorNotice }; if ( codemirror.showHint ) { diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index 763927bb2ae9d..46bda8b74338a 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -191,6 +191,10 @@ wp.themePluginEditor = (function( $ ) { return; } + if ( component.instance && component.instance.updateErrorNotice ) { + component.instance.updateErrorNotice(); + } + // Scroll to the line that has the error. if ( component.lintErrors.length ) { component.instance.codemirror.setCursor( component.lintErrors[0].from.line ); @@ -399,13 +403,13 @@ wp.themePluginEditor = (function( $ ) { editor = wp.codeEditor.initialize( $( '#newcontent' ), codeEditorSettings ); editor.codemirror.on( 'change', component.onChange ); + function onSaveShortcut() { + component.form.submit(); + } + editor.codemirror.setOption( 'extraKeys', { - 'Ctrl-S': function () { - component.form.submit(); - }, - 'Cmd-S': function () { - component.form.submit(); - }, + 'Ctrl-S': onSaveShortcut, + 'Cmd-S': onSaveShortcut, } ); // Improve the editor accessibility. From 18898a393dd3935ee2e220bbe9996b23e5d238d8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 16:36:50 -0800 Subject: [PATCH 05/13] Enable save keyboard shortcut when editor is not focused or syntax highlighting (CodeMirror) is disabled Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/wp/theme-plugin-editor.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index 46bda8b74338a..abd67f784f914 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -81,6 +81,14 @@ wp.themePluginEditor = (function( $ ) { component.docsLookUpButton.prop( 'disabled', false ); } } ); + + // Initiate saving the file when not focused in CodeMirror or when the user has syntax highlighting turned off. + $( window ).on( 'keydown', function( event ) { + if ( ( event.ctrlKey || event.metaKey ) && ( 's' === event.key.toLowerCase() ) ) { + event.preventDefault(); + component.submit( event ); + } + } ); }; /** From b5f6e853cf2217500a820b0a192f6a65ea0100ef Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 20:36:24 -0800 Subject: [PATCH 06/13] Prevent autocompletion when read-only --- src/js/_enqueues/wp/code-editor.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index 7fcadaa3d9ce5..55531a5159552 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -312,6 +312,11 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { return; } + // Prevent autocompletion when read-only (e.g. while saving). + if ( codemirror.getOption( 'readOnly' ) ) { + return; + } + // Prevent autocompletion in string literals or comments. token = codemirror.getTokenAt( codemirror.getCursor() ); if ( 'string' === token.type || 'comment' === token.type ) { From d2437dd3a01c3454547abb008b7f95efdd1d5ff7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 21:08:26 -0800 Subject: [PATCH 07/13] Improve autocomplete reliability by using `inputRead`. Updates the Code Editor to use the `inputRead` event instead of `keyup` for triggering autocompletion. `inputRead` is more reliable than `keyup` for triggering autocompletion because it fires only when the document content has actually changed due to user input. 1. **Modifier Keys Don't Trigger It:** `keyup` fires whenever *any* key is released. This includes `Shift`, `Ctrl`, `Cmd`, `Alt`, etc. This was causing a bug where releasing `Cmd` after `Cmd+S` triggered the `keyup` listener. `inputRead` ignores these entirely because pressing `Cmd` doesn't insert text into the editor. 2. **No Race Conditions:** With `keyup`, one has to guess if the keystroke was part of a combo (like `Cmd+S`) by checking modifier states. However, if the user releases the modifier *before* the character key, the `keyup` event for the character key sees `metaKey: false`, looking exactly like a normal typed character. `inputRead` avoids this race condition completely because the save command (Cmd+S) doesn't insert text, so `inputRead` never fires. 3. **Handles Non-Keyboard Input:** `inputRead` also handles cases like dragging and dropping text or using an Input Method Editor (IME), where `keyup` events can be complex. 4. **Simpler Logic:** There is no need to manually filter out navigation keys (Arrows, Home, End) or function keys. `inputRead` only indicates "text was added", which is exactly when we want to consider showing a hint. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/wp/code-editor.js | 35 ++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index 55531a5159552..fee00b40af235 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -306,14 +306,25 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { }; if ( codemirror.showHint ) { - codemirror.on( 'keyup', function( editor, event ) { // eslint-disable-line complexity - var shouldAutocomplete, isAlphaKey = /^[a-zA-Z]$/.test( event.key ), lineBeforeCursor, innerMode, token; - if ( codemirror.state.completionActive && isAlphaKey ) { + codemirror.on( 'inputRead', function( editor, change ) { + var shouldAutocomplete, isAlphaKey, lineBeforeCursor, innerMode, token, char; + + // Skip autocompletion when pasting as it could result in overwhelming hints. + if ( 'paste' === change.origin ) { return; } - // Prevent autocompletion when read-only (e.g. while saving). - if ( codemirror.getOption( 'readOnly' ) ) { + // Only trigger autocompletion for single-character inputs. + // The text property is an array of strings, one for each line. + // We check that there is only one line and that line has only one character. + if ( 1 !== change.text.length || 1 !== change.text[0].length ) { + return; + } + + char = change.text[0]; + isAlphaKey = /^[a-zA-Z]$/.test( char ); + + if ( codemirror.state.completionActive && isAlphaKey ) { return; } @@ -327,11 +338,11 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { lineBeforeCursor = codemirror.doc.getLine( codemirror.doc.getCursor().line ).substr( 0, codemirror.doc.getCursor().ch ); if ( 'html' === innerMode || 'xml' === innerMode ) { shouldAutocomplete = ( - '<' === event.key || - ( '/' === event.key && 'tag' === token.type ) || + '<' === char || + ( '/' === char && 'tag' === token.type ) || ( isAlphaKey && 'tag' === token.type ) || ( isAlphaKey && 'attribute' === token.type ) || - ( '=' === event.key && ( + ( '=' === char && ( token.state.htmlState?.tagName || token.state.curState?.htmlState?.tagName ) ) @@ -339,17 +350,17 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { } else if ( 'css' === innerMode ) { shouldAutocomplete = isAlphaKey || - ':' === event.key || - ( ' ' === event.key && /:\s+$/.test( lineBeforeCursor ) ); + ':' === char || + ( ' ' === char && /:\s+$/.test( lineBeforeCursor ) ); } else if ( 'javascript' === innerMode ) { - shouldAutocomplete = isAlphaKey || '.' === event.key; + shouldAutocomplete = isAlphaKey || '.' === char; } else if ( 'clike' === innerMode && 'php' === codemirror.options.mode ) { shouldAutocomplete = isAlphaKey && ( 'keyword' === token.type || 'variable' === token.type ); } if ( shouldAutocomplete ) { codemirror.showHint( { completeSingle: false } ); } - }); + } ); } // Facilitate tabbing out of the editor. From 0d8c2df94dad68c20516c556e66881c528656394 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 21:12:01 -0800 Subject: [PATCH 08/13] Use `.trigger( 'submit' )` instead of `.submit()` shorthand. Fixes a `JQMIGRATE: jQuery.fn.submit() event shorthand is deprecated` warning by using the explicit `.trigger()` method to initiate form submission. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/wp/theme-plugin-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index abd67f784f914..e413dc2bac750 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -412,7 +412,7 @@ wp.themePluginEditor = (function( $ ) { editor.codemirror.on( 'change', component.onChange ); function onSaveShortcut() { - component.form.submit(); + component.form.trigger( 'submit' ); } editor.codemirror.setOption( 'extraKeys', { From 9001dc92d63493a13a333eab09041c5e6ee59b33 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 21:21:18 -0800 Subject: [PATCH 09/13] Use `.trigger( 'submit' )` for consistency in global shortcut handler. Update the global keyboard listener for save shortcuts to use the same trigger mechanism as the CodeMirror shortcut handler. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/wp/theme-plugin-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index e413dc2bac750..142affc98b0ba 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -86,7 +86,7 @@ wp.themePluginEditor = (function( $ ) { $( window ).on( 'keydown', function( event ) { if ( ( event.ctrlKey || event.metaKey ) && ( 's' === event.key.toLowerCase() ) ) { event.preventDefault(); - component.submit( event ); + component.form.trigger( 'submit' ); } } ); }; From bb7fac9370c4390813d73f5941e17ed6ffeab961 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 2 Feb 2026 21:28:40 -0800 Subject: [PATCH 10/13] Refine `inputRead` origin check for autocompletion. Improves the robustness of the autocompletion trigger by specifically allowing only `+input` and `*compose` origins. These origins are part of CodeMirror 5's internal system for categorizing document changes: - `+input`: Represents direct character input from keyboard typing. - `*compose`: Represents Input Method Editor (IME) composition, used for languages like Japanese, Chinese, or Korean. By specifically targeting these origins, the editor effectively ignores changes that should not trigger autocompletion, such as `undo`, `redo`, `drag`, or programmatic updates. This also replaces the previous less-specific check for `paste`. Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/js/_enqueues/wp/code-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index fee00b40af235..7c91c7e078032 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -309,8 +309,8 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { codemirror.on( 'inputRead', function( editor, change ) { var shouldAutocomplete, isAlphaKey, lineBeforeCursor, innerMode, token, char; - // Skip autocompletion when pasting as it could result in overwhelming hints. - if ( 'paste' === change.origin ) { + // Only trigger autocompletion for typed input or IME composition. + if ( '+input' !== change.origin && ! change.origin.startsWith( '*compose' ) ) { return; } From e2cb6eb8f5c194bdceb3dd871428c7515b49f693 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 21:39:41 -0800 Subject: [PATCH 11/13] Prevent duplicate submit trigger when using save shortcut outside editor Co-authored-by: Mukesh Panchal --- src/js/_enqueues/wp/theme-plugin-editor.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index 142affc98b0ba..3686019af8a80 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -84,7 +84,11 @@ wp.themePluginEditor = (function( $ ) { // Initiate saving the file when not focused in CodeMirror or when the user has syntax highlighting turned off. $( window ).on( 'keydown', function( event ) { - if ( ( event.ctrlKey || event.metaKey ) && ( 's' === event.key.toLowerCase() ) ) { + if ( + ( event.ctrlKey || event.metaKey ) && + ( 's' === event.key.toLowerCase() ) && + ( ! component.instance || ! component.instance.codemirror.hasFocus() ) + ) { event.preventDefault(); component.form.trigger( 'submit' ); } From 9cb5f59bb61876b3785cc425db8d3fd8a45fd9f3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 21:56:42 -0800 Subject: [PATCH 12/13] Improve construction of instance object --- src/js/_enqueues/wp/code-editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index 7c91c7e078032..bc570263858a8 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -301,8 +301,8 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { instance = { settings: instanceSettings, - codemirror: codemirror, - updateErrorNotice: updateErrorNotice + codemirror, + updateErrorNotice, }; if ( codemirror.showHint ) { From 2d71516a8dbcd9558fd7f1c20879ed0e28025e44 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 3 Feb 2026 22:07:06 -0800 Subject: [PATCH 13/13] Prevent overriding existing extraKeys Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/js/_enqueues/wp/theme-plugin-editor.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/_enqueues/wp/theme-plugin-editor.js b/src/js/_enqueues/wp/theme-plugin-editor.js index 3686019af8a80..7bcff376e9ff2 100644 --- a/src/js/_enqueues/wp/theme-plugin-editor.js +++ b/src/js/_enqueues/wp/theme-plugin-editor.js @@ -2,7 +2,9 @@ * @output wp-admin/js/theme-plugin-editor.js */ -/* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1] }] */ +/* eslint-env es2020 */ + +/* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1, 9, 1000] }] */ if ( ! window.wp ) { window.wp = {}; @@ -420,6 +422,7 @@ wp.themePluginEditor = (function( $ ) { } editor.codemirror.setOption( 'extraKeys', { + ...( editor.codemirror.getOption( 'extraKeys' ) || {} ), 'Ctrl-S': onSaveShortcut, 'Cmd-S': onSaveShortcut, } );