Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9f31987
chore: remove the unused @codemirror/fold 0.19 package
lightbulbman Sep 10, 2026
7a9e284
feat: replace the CodeMirror 5 code editor with CodeMirror 6
lightbulbman Sep 11, 2026
26363da
feat: add the most-used Sublime Text shortcuts to the code editor keymap
lightbulbman Sep 11, 2026
74fc82d
chore: add Playwright coverage for the code editor
lightbulbman Sep 11, 2026
94dc044
docs: describe the CodeMirror 6 code editor in the changelog and cont…
lightbulbman Sep 11, 2026
1f36c6e
chore: wait for keyboard focus rather than the focus class in the pre…
lightbulbman Sep 11, 2026
33a0b72
fix: keep code editor fold markers visible on themes that colour gutt…
lightbulbman Sep 11, 2026
b2bf660
feat: add PHP autocompletion with a WordPress and PHP function index …
lightbulbman Sep 11, 2026
04925ba
docs: note code editor autocompletion in the changelog
lightbulbman Sep 11, 2026
d4194ac
fix: remeasure the snippet preview editor once the preview modal has …
lightbulbman Sep 11, 2026
5cd5656
feat: add indentation guide and trailing space highlighting settings …
lightbulbman Sep 11, 2026
65762ab
feat: add WordPress code templates to PHP autocompletion
lightbulbman Sep 11, 2026
184248a
docs: note the new code editor settings and templates in the changelog
lightbulbman Sep 11, 2026
3f8b943
docs: move the code editor changelog entries to the 4.1.0 release
lightbulbman Sep 11, 2026
80dba73
fix: keep long unwrapped lines scrolling inside the code editor inste…
lightbulbman Sep 11, 2026
8679ebd
chore: escape completion labels fully when matching them in the code …
lightbulbman Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ plugins.
| Layer | Technology |
|---------------|----------------------------------------------------------------|
| Backend | PHP 7.4+, WordPress APIs, PSR-4 via Composer |
| Frontend | TypeScript, React 18, `@wordpress/components`, CodeMirror 5 |
| Frontend | TypeScript, React 18, `@wordpress/components`, CodeMirror 6 |
| Styling | SCSS (PostCSS, logical properties via `stylelint-use-logical`) |
| Build | Webpack 5, Babel, `ts-loader`, `sass-loader` |
| PHP linting | PHPCS + WPCS (`npm run lint:php`) |
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## [4.1.0] (UPCOMING)

### Added
* CSS and JavaScript snippets are checked for syntax errors as you type.
* Autocompletion for PHP in the code editor, covering keywords, the snippet's own variables and functions, and WordPress and PHP functions with their parameters, a summary and a link to their documentation.
* Browser globals such as `document` and `window` are suggested while typing JavaScript.
* Templates for common WordPress code, such as hooking into an action or filter, registering a shortcode and enqueueing a stylesheet, offered in PHP autocompletion.
* Indentation Guides editor setting, on by default, showing a vertical guide for each level of indentation.
* Highlight Trailing Spaces editor setting, off by default, marking spaces and tabs left at the end of a line.

### Changed
* The code editor now uses CodeMirror 6, bundled with the plugin rather than loaded from WordPress. Existing editor settings and themes carry over.
* The editor's find and replace panel is now a single panel at the top of the editor, and its labels can be translated.
* The keyboard shortcut list in the editor now reflects CodeMirror 6 shortcuts. Shift+Tab outdents the current line or selection.
* With Indent With Tabs enabled, each indentation level is one tab.
* The Sublime Text keymap now covers the most-used Sublime Text shortcuts, such as selecting, deleting, duplicating and moving lines, and selecting the next occurrence. Less common ones, such as joining lines and selecting between brackets, are no longer available.
* CSS snippets no longer show CSSLint style warnings; only syntax errors are reported.
* Code previews on the snippets list load the code editor only when a preview is opened.
* Custom editor themes added through the `code_snippets_codemirror_atts` filter keep their syntax colours, but rules for the editor background, gutters, cursor, selection and active line must use CodeMirror 6 class names such as `.cm-editor`, `.cm-gutters` and `.cm-activeLine`.
* The `code_snippets_codemirror_atts` filter only applies the editor settings' own options; options for the WordPress code editor, such as `mode`, `gutters` and `extraKeys`, are ignored. Plugin screens no longer create `wp.codeEditor` instances or `.CodeMirror` elements.

## [4.0.0] (UPCOMING)

### Added
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ npm run build

This will run the basic Webpack configuration, which will:

1. Copies required vendor files from the `node_modules/` directory, such as CodeMirror and PrismJS theme files.
1. Converts the CodeMirror 5 theme stylesheets in `node_modules/` into code editor themes.
2. Transforms the SCSS source files into browser-ready minified CSS code, including right-to-left files where
appropriate.
3. Transforms the TypeScript source files into browser-ready minified JavaScript code, after checking with a linter.
Expand Down
5 changes: 0 additions & 5 deletions config/webpack/modules/codemirror-keymaps.d.ts

This file was deleted.

26 changes: 26 additions & 0 deletions config/webpack/modules/replit-codemirror.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// These packages' export maps have no types condition, so their declarations
// are not found when they are resolved as CommonJS.
declare module '@replit/codemirror-emacs' {
import type { Extension } from '@codemirror/state'

export const emacs: () => Extension
}

declare module '@replit/codemirror-indentation-markers' {
import type { Extension } from '@codemirror/state'

export interface IndentationMarkerConfiguration {
hideFirstIndent?: boolean
markerType?: 'fullScope' | 'codeOnly'
thickness?: number
activeThickness?: number
colors?: {
light?: string
dark?: string
activeLight?: string
activeDark?: string
}
}

export const indentationMarkers: (config?: IndentationMarkerConfiguration) => Extension
}
97 changes: 97 additions & 0 deletions config/webpack/postcss-codemirror-theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { PluginCreator, Rule } from 'postcss'

/**
* Selectors for CodeMirror 5 editor structure, and their CodeMirror 6
* equivalents. Token classes such as `.cm-keyword` need no mapping, as the
* editor's highlight style emits them unchanged.
*/
const SELECTOR_MAP: readonly (readonly [RegExp, string])[] = [
[/\.CodeMirror-activeline-background(?![\w-])/g, '.cm-activeLine'],
[/\.CodeMirror-activeline-gutter(?![\w-])/g, '.cm-activeLineGutter'],
[/\.CodeMirror-gutters(?![\w-])/g, '.cm-gutters'],
[/\.CodeMirror-gutter-elt(?![\w-])/g, '.cm-gutterElement'],
[/\.CodeMirror-gutter(?![\w-])/g, '.cm-gutter'],
[/\.CodeMirror-linenumber(?![\w-])/g, '.cm-lineNumbers .cm-gutterElement'],
[/\.CodeMirror-guttermarker-subtle(?![\w-])/g, '.cm-foldGutter .cm-gutterElement'],
[/\.CodeMirror-foldmarker(?![\w-])/g, '.cm-foldPlaceholder'],
[/\.CodeMirror-cursor(?![\w-])/g, '.cm-cursor'],
[/\.CodeMirror-selected(?![\w-])/g, '.cm-selectionBackground'],
[/\.CodeMirror-matchingbracket(?![\w-])/g, '.cm-matchingBracket'],
[/\.CodeMirror-nonmatchingbracket(?![\w-])/g, '.cm-nonmatchingBracket'],
[/\.CodeMirror-focused(?![\w-])/g, '.cm-focused'],
[/\.CodeMirror-line(?![\w-])/g, '.cm-line'],
[/\.CodeMirror-(?:code|lines)(?![\w-])/g, '.cm-content'],
[/\.CodeMirror-scroll(?![\w-])/g, '.cm-scroller'],
[/\.CodeMirror(?![\w-])/g, '.cm-editor'],
[/\.cm-searching(?![\w-])/g, '.cm-searchMatch'],
[/\.cm-matchhighlight(?![\w-])/g, '.cm-selectionMatch']
]

/**
* Selectors with no CodeMirror 6 counterpart: native selection is hidden in
* favour of drawn selection, and the rest style addons that are not loaded.
* Fold markers were the subtle gutter markers; plain ones marked breakpoints
* and the like, which the editor does not have.
*/
const UNSUPPORTED_ADDON_CLASSES = [
'guttermarker(?![\\w-])', 'simplescroll', 'overlayscroll', 'hint', 'ruler', 'widget', 'secondarycursor', 'overwrite', 'cursors',
'matchingtag', 'selectedtext', 'foldgutter', 'gutter-text', 'activeline(?![\\w-])'
]

const UNSUPPORTED_SELECTOR = new RegExp(`::?-?(?:moz-)?selection|\\.CodeMirror-(?:${UNSUPPORTED_ADDON_CLASSES.join('|')})`)

const STRUCTURAL_CLASSES = [
'editor', 'gutters?', 'gutterElement', 'lineNumbers', 'foldGutter', 'foldPlaceholder', 'cursor',
'selectionBackground', 'matchingBracket', 'nonmatchingBracket', 'focused', 'line', 'content', 'scroller',
'activeLine', 'activeLineGutter', 'searchMatch', 'selectionMatch'
]

const STRUCTURAL_SELECTOR = new RegExp(`\\.cm-(?:${STRUCTURAL_CLASSES.join('|')})(?![\\w-])`)

const convertSelector = (selector: string): string | null => {
if (UNSUPPORTED_SELECTOR.test(selector)) {
return null
}

const converted = SELECTOR_MAP.reduce((result, [pattern, replacement]) =>
result.replace(pattern, replacement), selector)

return converted.includes('.CodeMirror') ? null : converted
}

/**
* Convert a CodeMirror 5 theme stylesheet to style a CodeMirror 6 editor.
*
* Declarations on editor structure are made important: CodeMirror 6 injects
* its base theme after the page's stylesheets, so it would otherwise win any
* rule of equal specificity.
*/
const postcssCodeMirrorTheme: PluginCreator<void> = () => ({
postcssPlugin: 'postcss-codemirror-theme',
Rule(rule: Rule) {
if ('atrule' === rule.parent?.type && 'keyframes' === (<{ name?: string }> rule.parent).name) {
return
}

const selectors = rule.selectors
.map(convertSelector)
.filter((selector): selector is string => null !== selector)

if (0 === selectors.length) {
rule.remove()
return
}

rule.selectors = selectors

if (selectors.some(selector => STRUCTURAL_SELECTOR.test(selector))) {
rule.walkDecls(declaration => {
declaration.important = true
})
}
}
})

postcssCodeMirrorTheme.postcss = true

export default postcssCodeMirrorTheme
10 changes: 7 additions & 3 deletions config/webpack/webpack-css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import rgbaCompat from 'postcss-hexrgba'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts'
import { glob } from 'glob'
import codeMirrorTheme from './postcss-codemirror-theme'
import hslCompat from './postcss-hsl-legacy'
import type { Configuration, EntryObject } from 'webpack'
import type { Config as PostCssConfig } from 'postcss-load-config'
Expand All @@ -21,6 +22,8 @@ const postcssOptions: PostCssConfig = {
]
}

const EDITOR_THEME_FILES = 'node_modules/codemirror/theme/*.css'

const entriesFromFiles = (patterns: string | string[], entry: (filename: string) => string): EntryObject =>
Object.fromEntries(
glob.sync(patterns)
Expand All @@ -34,9 +37,10 @@ export const cssWebpackConfig: Configuration = {
filename => `${path.parse(filename).name}-css`
),
...entriesFromFiles(
'node_modules/codemirror/theme/*.css',
EDITOR_THEME_FILES,
filename => `codemirror-theme-${path.parse(filename).name}`
)
),
'editor-themes-css': glob.sync(EDITOR_THEME_FILES).sort().map(filename => `./${filename}`)
},
module: {
rules: [
Expand Down Expand Up @@ -85,7 +89,7 @@ export const cssWebpackConfig: Configuration = {
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [cssnano()]
plugins: [codeMirrorTheme(), cssnano()]
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion config/webpack/webpack-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const jsWebpackConfig: Configuration = {
output: {
path: join(resolve(__dirname), '..', '..', DEST_DIR),
filename: '[name].js',
chunkFilename: 'chunks/[name].[contenthash:8].js',
clean: true
},
externalsType: 'window',
Expand All @@ -50,7 +51,6 @@ export const jsWebpackConfig: Configuration = {
'react/jsx-runtime': 'ReactJSXRuntime',
'jquery': 'jQuery',
'tinymce': 'tinymce',
'codemirror': ['wp', 'CodeMirror'],
...Object.fromEntries(
Object.keys(dependencies)
.filter(name => name.startsWith('@wordpress/'))
Expand All @@ -69,6 +69,11 @@ export const jsWebpackConfig: Configuration = {
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
// Dynamic imports are resolved as ECMAScript modules by TypeScript, which
// requires them to name a file extension.
resolve: {
extensionAlias: { '.js': ['.ts', '.tsx', '.js'] }
},
use: {
loader: 'babel-loader',
options: babelConfig
Expand Down
Loading
Loading