Skip to content

Conversation

@bennothommo
Copy link
Member

@bennothommo bennothommo commented Dec 13, 2022

Fixes #431. Related: wintercms/wn-builder-plugin#73
Screenshot 2026-01-02 at 1 02 11 AM
Screenshot 2026-01-02 at 1 02 40 AM
Screenshot 2026-01-02 at 1 03 25 AM

This PR introduces the Monaco editor in replacement of the Ace Editor. At this stage for the PR, we are aiming for parity with the functionality previously provided by Ace, with the intention of increasing the functionality after this PR is merged.

Still to do:

  • Support all prior configuration options.
  • Re-implement all previous themes, or find suitable alternatives (see https://github.com/brijeshb42/monaco-themes)
  • Ensure code editor works in all current core locations (settings areas, CMS editor, Builder plugin)
  • Try and figure out how to remove the automated loading of the Codicon font, as it's being implemented in the widget's CSS assets.

Changes:

  • Auto-completing tags preference is no longer available, as Monaco doesn't have this as an option.
  • Autocompletion and code snippets preferences are no longer available, as these are more contextual and will be implemented based on what is being edited in the editor.
  • Code folding is now a simple boolean.
  • The Crimson Editor theme has been dropped - I couldn't find a complete replica of this theme, and it looks too similar to other themes in the list.

Summary by CodeRabbit

  • New Features

    • Replaced legacy editor with a modern Monaco-based editor: minimap, bracket colorization, inline color previews, richer language services, and many new themes.
  • Improvements

    • Consolidated editor preferences into a unified plugin for smoother settings.
    • Modernized editor styling and toolbar; adjusted tab collapse icon colors and layout.
  • Bug Fixes

    • Addressed duplicate render handling that could cause double render invocation.
  • Documentation

    • Added a README describing the new editor integration.

✏️ Tip: You can customize this high-level summary in your review settings.

@bennothommo bennothommo added enhancement PRs that implement a new feature or substantial change Status: In Progress labels Dec 13, 2022
@bennothommo bennothommo added this to the v1.2.2 milestone Dec 13, 2022
@what-the-diff

This comment was marked as outdated.

Markdown editor needs some more work, we might look at using TipTap or Milkdown / Crepe in the future.
Interacting with the PackageManager triggers a need to use the database connection to get the active theme, the database connection is not available until the boot process.
…educe issues caused by timing issues with webpack loading
For anyone else trying to track down self being clobbered, this is very handy:

(function () {
  try {
    const desc = Object.getOwnPropertyDescriptor(window, 'self');
    console.log('window.self descriptor:', desc);

    let _selfValue = window;
    Object.defineProperty(window, 'self', {
      configurable: true,
      get() { return _selfValue; },
      set(v) {
        console.error('window.self set to:', v);
        console.trace('stack:');     // <- prints the exact script/line
        _selfValue = v;
      },
    });
  } catch (e) {
    console.warn('Failed to trap window.self:', e);
  }
})();
@LukeTowers LukeTowers marked this pull request as ready for review January 2, 2026 07:14
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Also added build cleanup step to build process to avoid leaving behind artifacts from previous builds.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In
@modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/612.js:
- Around line 1-8: The bundle at variables R and A is an old Monaco SQL language
build (v0.34.1); update the monaco-editor dependency to the current stable
(e.g., 0.54.0), regenerate the editor language bundle (so R and A are rebuilt
with the latest tokenizer/keywords/operators/builtinFunctions), replace the
generated codeeditor.bundle/612.js artifact with the newly built output, and
commit the regenerated file (ensure package.json and lockfile reflect the
monaco-editor version change and run the same build script used to produce the
original bundle).

In @modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js:
- Around line 644-649: The findAll method incorrectly passes a hard limit of 1
to this.model.findMatches, so it only returns a single match; update findAll
(method name: findAll) to call this.model.findMatches without the
limitResultCount parameter (or pass null/undefined) so it returns all matches
for searchString (constructed from search) with the existing flags (isRegex,
matchCase, etc.).
- Around line 1515-1526: The resolveCodeLens implementation in addCodeLens calls
resolver(model, codeLens, token) even when resolver is null, causing a
TypeError; change it to only invoke resolver when it's provided (e.g. use
optional chaining resolver?.(model, codeLens, token) ?? codeLens or check typeof
resolver === 'function' and call it, otherwise return codeLens) while keeping
provideCodeLenses unchanged; update the resolveCodeLens lambda in addCodeLens
accordingly.
- Around line 679-689: The alignment call uses search.startColumn which is
undefined when search is a String/RegExp; update the call in the
pushEditOperations block to pass a valid start column (use
found.range.startColumn as fallback or compute const startCol = search &&
search.startColumn ? search.startColumn : found.range.startColumn) so
alignText(replace, startCol) always receives a number; modify the invocation
around this.model.pushEditOperations / alignText to use that startCol.
- Around line 196-212: The dispose() method currently removes the window resize
listener but not the document click listener, causing accumulated click
handlers; update attachListeners() to store the click handler reference (e.g.,
assign the function to this.clickListener or register it in
this.callbacks.click) when calling document.addEventListener('click', ...), and
then update dispose() to call document.removeEventListener('click',
this.clickListener) (and clear this.clickListener) so the click listener is
properly removed during disposal; ensure the same function reference is used for
add/remove and that attachListeners and dispose both reference the same symbol
(attachListeners(), dispose(), this.clickListener or this.callbacks.click).
- Around line 1437-1466: normalizeKeyBinding currently only recognizes a few
hard-coded modifier orderings leaving key null for plain keys and many modifier
combos; change the string parsing to split the keyCode on '+' (e.g., parts =
keyCode.split('+')), iterate parts to set ctrl/alt/shift flags when part matches
those modifiers, and set keyBinding.key to the remaining non-modifier part
(joined or last non-empty piece) so plain keys like "Escape", single modifiers
like "Shift+X", and any order combos like "Ctrl+Alt+Shift+K" are all handled;
keep the existing branch that merges when keyCode is already an object.
- Around line 1248-1273: The fullscreen click handler in enableStatusBarActions
adds a new listener on every editor recreate; change it to register a single
reusable handler: create/store a named function (e.g.
this.callbacks.fullscreenClick) and before adding call
fullscreen.removeEventListener('click', this.callbacks.fullscreenClick) to clear
any prior listener, then assign and add the listener; ensure you reference the
same stored handler when removing so duplicated listeners are not accumulated
(also keep existing use of this.callbacks.fullScreenChange and the
editor.layout() call inside the stored handler).
🧹 Nitpick comments (3)
modules/backend/formwidgets/codeeditor/assets/css/codeeditor.css (1)

1-1: Consider using CSS custom properties for theme colors.

Hardcoded color values (#d1d6d9, rgba(0,0,0,.8), etc.) are scattered throughout. For better theming consistency—especially with Monaco's dark/light theme support—consider using CSS variables:

:root {
  --codeeditor-border-color: #d1d6d9;
  --codeeditor-toolbar-bg: rgba(0, 0, 0, 0.8);
}

This would align with the PR's theme-related changes and simplify future theme adjustments.

modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js (2)

1047-1057: Redundant regex match after validation.

The color parsing now correctly returns early on invalid format (line 1052), but line 1054 redundantly calls color.match() again instead of reusing the validation result. Minor inefficiency.

♻️ Suggested optimization
-            if (!color.match(/^#(..)(..)(..)(..)$/)) {
+            const rgbaMatch = color.match(/^#(..)(..)(..)(..)$/);
+            if (!rgbaMatch) {
                 // Invalid color format - return original color
                 return color;
             }
-            const rgba = color.match(/^#(..)(..)(..)(..)$/).slice(1).map((c) => parseInt(c, 16));
+            const rgba = rgbaMatch.slice(1).map((c) => parseInt(c, 16));
             rgba[3] = (rgba[3] / 0xFF).toPrecision(2);
             return `rgba(${rgba.join(', ')})`;

788-791: Silent theme loading failure hides errors.

The catch block at lines 788-790 silently swallows errors, which could make debugging difficult when theme files are missing or malformed.

♻️ Suggested improvement
                 }).catch(() => {
-                    // Theme loading failed - silently fall back to default theme
+                    // Theme loading failed - log in development, fall back silently in production
+                    if (process.env.NODE_ENV !== 'production') {
+                        console.warn(`CodeEditor: Failed to load theme "${newTheme}", falling back to default`);
+                    }
                 });

Alternatively, consider emitting an event so consuming code can react:

this.events.fire('themeLoadError', newTheme);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4294755 and 4f73c68.

📒 Files selected for processing (28)
  • modules/backend/formwidgets/CodeEditor.php
  • modules/backend/formwidgets/codeeditor/assets/css/codeeditor.css
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/129.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/237.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/309.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/504.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/566.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/612.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/641.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/683.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/74.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/744.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/754.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/758.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/847.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/897.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/919.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/952.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/99.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/css.worker.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/editor.worker.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/html.worker.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/json.worker.js
  • modules/backend/formwidgets/codeeditor/assets/js/build/ts.worker.js
  • modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js
  • modules/backend/formwidgets/codeeditor/assets/winter.mix.js
  • modules/backend/models/Preference.php
🧰 Additional context used
🪛 Biome (2.1.2)
modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/683.js

[error] 7-7: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 7-7: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 7-7: This case is falling through to the next case.

Add a break or return statement to the end of this case to prevent fallthrough.

(lint/suspicious/noFallthroughSwitchClause)


[error] 7-7: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/309.js

[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/74.js

[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)


[error] 7-7: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: ubuntu-latest / PHP 8.3
  • GitHub Check: windows-latest / PHP 8.4
  • GitHub Check: windows-latest / PHP 8.2
  • GitHub Check: windows-latest / PHP 8.1
  • GitHub Check: ubuntu-latest / PHP 8.1
  • GitHub Check: ubuntu-latest / PHP 8.4
  • GitHub Check: windows-latest / PHP 8.3
  • GitHub Check: ubuntu-latest / PHP 8.2
  • GitHub Check: windows-latest / JavaScript
🔇 Additional comments (23)
modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/566.js (1)

1-8: Build artifact with proper license attribution.

This is a minified webpack chunk containing Monaco Editor's Markdown language definition. The MIT license header for Monaco Editor v0.34.1 is correctly preserved. As a generated bundle, any modifications should be made to the source files rather than this output.

modules/backend/formwidgets/codeeditor/assets/css/codeeditor.css (1)

1-1: Flex-based layout structure is sound.

The migration from absolute positioning to flexbox (display:flex;flex-direction:column) with proper flex-grow/flex-shrink on .editor-container and fixed sizing on .editor-toolbar is a solid approach for responsive editor layouts. The z-index stacking (9→10→11) for toolbar layering is also correctly structured.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/744.js (1)

1-8: Vendored Monaco language bundle — no concerns.

This is a minified Monaco Editor language definition bundle for INI files, generated from the Monaco build process (v0.34.1, MIT licensed). No application-specific review needed.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/952.js (1)

1-8: Vendored Monaco language bundle — no concerns.

This is a minified Monaco Editor language definition bundle for LESS stylesheets (v0.34.1, MIT licensed). Consistent with other language bundles in this PR.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/847.js (1)

1-8: Vendored Monaco language bundle — no concerns.

This is a minified Monaco Editor language definition bundle for Twig templates (v0.34.1, MIT licensed). Essential for Winter CMS's Twig-based templating.

modules/backend/formwidgets/CodeEditor.php (3)

100-113: New Monaco-specific properties look good.

The new properties showMinimap, bracketColors, and showColors have sensible defaults that match typical Monaco Editor behavior. Documentation is clear.


202-206: Asset path updated for Monaco bundle.

The change from build-min.js to codeeditor.bundle.js aligns with the new Monaco-based asset structure.


219-231: Verify that the preference property names are defined in the Preference model.

The fallback logic for codeFolding (line 219) provides good backward compatibility with the previous Ace configuration. Ensure all referenced properties (editor_show_minimap, editor_bracket_colors, editor_show_colors, editor_enable_folding, editor_auto_closing, editor_tab_size, editor_theme, editor_show_invisibles, editor_highlight_active_line, editor_use_hard_tabs, editor_show_gutter, editor_display_indent_guides, editor_show_print_margin) are defined as attributes or accessors in the Preference model.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/754.js (1)

1-7: Vendored Monaco TypeScript/JavaScript language service bundle — no concerns.

This is a minified Monaco Editor language service bundle (v0.34.1, MIT licensed) providing IntelliSense, diagnostics, and other language features for TypeScript/JavaScript. The static analysis warnings are false positives from minification artifacts in vendor code.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/683.js (1)

1-7: Vendored Monaco JSON language service bundle — no concerns.

This is a minified Monaco Editor JSON language service bundle (v0.34.1, MIT licensed) providing schema validation, completion, and formatting. The static analysis warnings are false positives from minification artifacts in vendor code.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/309.js (1)

1-7: Vendored Monaco HTML language service bundle — no concerns.

This is a minified Monaco Editor HTML language service bundle (v0.34.1, MIT licensed) providing completion, hover, links, and formatting features. The static analysis warnings are false positives from minification artifacts in vendor code.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/237.js (1)

1-8: LGTM — Standard Monaco CSS language bundle.

Generated build artifact providing CSS language support with proper tokenizer configuration for selectors, properties, values, comments, and strings.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/897.js (1)

1-7: LGTM — Standard Monaco HTML language bundle.

Generated build artifact with comprehensive HTML tokenizer including proper handling for void elements, embedded <script> and <style> blocks, comments, and attributes.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/504.js (1)

1-8: LGTM — Standard Monaco SCSS language bundle.

Generated build artifact with SCSS-specific tokenizer including variable declarations, mixins, control flow statements, and nested property support.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/641.js (1)

1-8: LGTM — Standard Monaco PHP language bundle.

Generated build artifact with comprehensive PHP tokenizer including HTML integration, PHP tag handling (<?php, <?=), predefined variables, keywords, and embedded script/style support with PHP interpolation.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/129.js (1)

1-7: LGTM — Standard Monaco XML language bundle.

Generated build artifact with XML tokenizer including qualified name handling, CDATA sections, comments, and proper onEnterRules for indentation.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/758.js (1)

1-7: LGTM — Standard Monaco TypeScript language bundle.

Generated build artifact with comprehensive TypeScript tokenizer including TypeScript-specific keywords (interface, type, namespace, readonly, etc.), regex literals, template string interpolation with bracket counting, and JSDoc comment support.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/919.js (1)

1-2: Generated Monaco language bundle - LGTM.

This is a standard webpack-bundled YAML language definition from Monaco Editor v0.34.1. The export structure (conf, language) matches the established pattern across other language bundles in this PR.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/99.js (1)

1-12: Generated Monaco language bundle - LGTM.

This webpack chunk bundles JavaScript and TypeScript language definitions from Monaco Editor v0.34.1. The dual-module structure (1099 for JS, 6758 for TS) with shared configuration is the expected pattern for these related languages.

modules/backend/formwidgets/codeeditor/assets/js/build/codeeditor.bundle/74.js (1)

1-7: Generated Monaco adapter bundle - static analysis hints are false positives.

The Biome errors about "variable used before declaration" are false positives. This is minified webpack output where:

  1. Variable hoisting and module scoping work correctly at runtime
  2. Static analyzers cannot properly trace webpack's module system (self.webpackChunk patterns)
  3. This is unmodified code from monaco-editor v0.34.1's CSS language service

The bundle correctly exports the WorkerManager and language service adapters (completion, diagnostics, hover, etc.) needed for Monaco's CSS/SCSS/LESS language features.

modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js (3)

1-44: Well-implemented cross-origin worker support.

The MonacoEnvironment configuration correctly handles both same-origin and cross-origin (CDN/Lambda) scenarios. The blob URL wrapper pattern at lines 31-34 is the standard approach for loading workers from different origins. Setting __webpack_public_path__ before imports ensures code-split chunks load from the correct asset URL.


46-99: Clean class initialization.

The constructor properly initializes all instance properties, sets up callback references for event handlers (enabling proper cleanup), and uses the IntersectionObserver pattern for visibility-based editor lifecycle management.


1529-1531: Plugin registration looks correct.

The Snowboard plugin registration and widget handler wiring follow the established pattern for Winter CMS form widgets.

Comment on lines 644 to 649
findAll(search, matchCase) {
const searchString = (search instanceof RegExp) ? search.source : search;
const matches = this.model.findMatches(searchString, true, (search instanceof RegExp), matchCase || false, null, true, 1);

// CODEEDITOR NO CONFLICT
// =================
return matches;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

findAll() limited to one match.

The findMatches() call has limitResultCount set to 1, contradicting the method's purpose. Remove the limit parameter to return all matches.

🔧 Proposed fix
         findAll(search, matchCase) {
             const searchString = (search instanceof RegExp) ? search.source : search;
-            const matches = this.model.findMatches(searchString, true, (search instanceof RegExp), matchCase || false, null, true, 1);
+            const matches = this.model.findMatches(searchString, true, (search instanceof RegExp), matchCase || false, null, true);

             return matches;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
findAll(search, matchCase) {
const searchString = (search instanceof RegExp) ? search.source : search;
const matches = this.model.findMatches(searchString, true, (search instanceof RegExp), matchCase || false, null, true, 1);
// CODEEDITOR NO CONFLICT
// =================
return matches;
}
findAll(search, matchCase) {
const searchString = (search instanceof RegExp) ? search.source : search;
const matches = this.model.findMatches(searchString, true, (search instanceof RegExp), matchCase || false, null, true);
return matches;
}
🤖 Prompt for AI Agents
In @modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js around lines
644 - 649, The findAll method incorrectly passes a hard limit of 1 to
this.model.findMatches, so it only returns a single match; update findAll
(method name: findAll) to call this.model.findMatches without the
limitResultCount parameter (or pass null/undefined) so it returns all matches
for searchString (constructed from search) with the existing flags (isRegex,
matchCase, etc.).

Comment on lines 679 to 689
return this.model.pushEditOperations(
this.editor.getSelections(),
[
{
forceMoveMarkers: false,
range: new monaco.Range(found.range.startLineNumber, found.range.startColumn, found.range.endLineNumber, found.range.endColumn),
text: (align) ? this.alignText(replace, search.startColumn) : replace,
},
],
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Incorrect alignment column when search is String/RegExp.

At line 685, search.startColumn is used for alignment, but when search is a String or RegExp (not a Range), this property is undefined, causing silent alignment failure.

🔧 Proposed fix
             return this.model.pushEditOperations(
                 this.editor.getSelections(),
                 [
                     {
                         forceMoveMarkers: false,
                         range: new monaco.Range(found.range.startLineNumber, found.range.startColumn, found.range.endLineNumber, found.range.endColumn),
-                        text: (align) ? this.alignText(replace, search.startColumn) : replace,
+                        text: (align) ? this.alignText(replace, found.range.startColumn) : replace,
                     },
                 ],
             );
🤖 Prompt for AI Agents
In @modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js around lines
679 - 689, The alignment call uses search.startColumn which is undefined when
search is a String/RegExp; update the call in the pushEditOperations block to
pass a valid start column (use found.range.startColumn as fallback or compute
const startCol = search && search.startColumn ? search.startColumn :
found.range.startColumn) so alignText(replace, startCol) always receives a
number; modify the invocation around this.model.pushEditOperations / alignText
to use that startCol.

Comment on lines 1248 to 1273
enableStatusBarActions() {
if (!this.statusBar) {
return;
}

const fullscreen = this.statusBar.querySelector('[data-full-screen]');
fullscreen.addEventListener('click', () => {
if (!this.fullscreen) {
this.element.requestFullscreen({
navigationUI: 'hide',
}).then(() => {
this.fullscreen = true;
fullscreen.classList.add('active');
this.element.addEventListener('fullscreenchange', this.callbacks.fullScreenChange);
// Use Monaco's layout() instead of refresh() to avoid disposing the editor
if (this.editor) {
window.requestAnimationFrame(() => {
this.editor.layout();
});
}
});
} else {
document.exitFullscreen();
}
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fullscreen button click listener accumulates on editor recreation.

Each call to enableStatusBarActions() (via createEditor()) adds a new click listener to the fullscreen button without removing previous ones.

🔧 Proposed fix

Track and clean up the listener:

+        // Add to construct():
+        this.fullscreenClickHandler = null;

         enableStatusBarActions() {
             if (!this.statusBar) {
                 return;
             }

             const fullscreen = this.statusBar.querySelector('[data-full-screen]');
+
+            // Remove previous listener if exists
+            if (this.fullscreenClickHandler) {
+                fullscreen.removeEventListener('click', this.fullscreenClickHandler);
+            }
+
+            this.fullscreenClickHandler = () => {
-            fullscreen.addEventListener('click', () => {
                 if (!this.fullscreen) {
                     // ... existing code
                 } else {
                     document.exitFullscreen();
                 }
-            });
+            };
+
+            fullscreen.addEventListener('click', this.fullscreenClickHandler);
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js around lines
1248 - 1273, The fullscreen click handler in enableStatusBarActions adds a new
listener on every editor recreate; change it to register a single reusable
handler: create/store a named function (e.g. this.callbacks.fullscreenClick) and
before adding call fullscreen.removeEventListener('click',
this.callbacks.fullscreenClick) to clear any prior listener, then assign and add
the listener; ensure you reference the same stored handler when removing so
duplicated listeners are not accumulated (also keep existing use of
this.callbacks.fullScreenChange and the editor.layout() call inside the stored
handler).

Comment on lines 1437 to 1466
normalizeKeyBinding(keyCode) {
let keyBinding = {
key: null,
ctrl: false,
alt: false,
shift: false,
};

if (typeof keyCode === 'string') {
if (keyCode.startsWith('Shift+Ctrl+')) {
keyBinding.key = keyCode.replace('Shift+Ctrl+', '');
keyBinding.shift = true;
keyBinding.ctrl = true;
} else if (keyCode.startsWith('Shift+Alt+')) {
keyBinding.key = keyCode.replace('Shift+Alt+', '');
keyBinding.shift = true;
keyBinding.alt = true;
} else if (keyCode.startsWith('Ctrl+')) {
keyBinding.key = keyCode.replace('Ctrl+', '');
keyBinding.ctrl = true;
} else if (keyCode.startsWith('Alt+')) {
keyBinding.key = keyCode.replace('Alt+', '');
keyBinding.alt = true;
}
} else {
keyBinding = { ...keyBinding, ...keyCode };
}

return keyBinding;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incomplete keybinding string parsing.

The parser misses several valid combinations:

  • Plain keys without modifiers (e.g., "Escape") — key remains null
  • Shift+ alone without Ctrl/Alt
  • Ctrl+Alt+ combinations
  • Ctrl+Shift+ ordering
🔧 Proposed fix using split-based parsing
         normalizeKeyBinding(keyCode) {
             let keyBinding = {
                 key: null,
                 ctrl: false,
                 alt: false,
                 shift: false,
             };

             if (typeof keyCode === 'string') {
-                if (keyCode.startsWith('Shift+Ctrl+')) {
-                    keyBinding.key = keyCode.replace('Shift+Ctrl+', '');
-                    keyBinding.shift = true;
-                    keyBinding.ctrl = true;
-                } else if (keyCode.startsWith('Shift+Alt+')) {
-                    // ... etc
-                }
+                const parts = keyCode.split('+');
+                keyBinding.key = parts.pop(); // Last part is the key
+                parts.forEach((mod) => {
+                    const modifier = mod.toLowerCase();
+                    if (modifier === 'ctrl' || modifier === 'cmd') keyBinding.ctrl = true;
+                    else if (modifier === 'alt') keyBinding.alt = true;
+                    else if (modifier === 'shift') keyBinding.shift = true;
+                });
             } else {
                 keyBinding = { ...keyBinding, ...keyCode };
             }

             return keyBinding;
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js around lines
1437 - 1466, normalizeKeyBinding currently only recognizes a few hard-coded
modifier orderings leaving key null for plain keys and many modifier combos;
change the string parsing to split the keyCode on '+' (e.g., parts =
keyCode.split('+')), iterate parts to set ctrl/alt/shift flags when part matches
those modifiers, and set keyBinding.key to the remaining non-modifier part
(joined or last non-empty piece) so plain keys like "Escape", single modifiers
like "Shift+X", and any order combos like "Ctrl+Alt+Shift+K" are all handled;
keep the existing branch that merges when keyCode is already an object.

Comment on lines 1515 to 1526
addCodeLens(language, provider, resolver = null) {
this.disposables.push(
monaco.languages.registerCodeLensProvider(language, {
provideCodeLenses: (model, token) => {
return provider(model, token);
},
resolveCodeLens: (model, codeLens, token) => {
return resolver(model, codeLens, token) ?? codeLens;
},
})
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Null pointer dereference when resolver is not provided.

When addCodeLens is called with default resolver = null, line 1522 throws TypeError because resolver(...) is invoked before the nullish coalescing operator evaluates.

🔧 Proposed fix
         addCodeLens(language, provider, resolver = null) {
             this.disposables.push(
                 monaco.languages.registerCodeLensProvider(language, {
                     provideCodeLenses: (model, token) => {
                         return provider(model, token);
                     },
                     resolveCodeLens: (model, codeLens, token) => {
-                        return resolver(model, codeLens, token) ?? codeLens;
+                        return resolver ? resolver(model, codeLens, token) : codeLens;
                     },
                 })
             );
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
addCodeLens(language, provider, resolver = null) {
this.disposables.push(
monaco.languages.registerCodeLensProvider(language, {
provideCodeLenses: (model, token) => {
return provider(model, token);
},
resolveCodeLens: (model, codeLens, token) => {
return resolver(model, codeLens, token) ?? codeLens;
},
})
);
}
addCodeLens(language, provider, resolver = null) {
this.disposables.push(
monaco.languages.registerCodeLensProvider(language, {
provideCodeLenses: (model, token) => {
return provider(model, token);
},
resolveCodeLens: (model, codeLens, token) => {
return resolver ? resolver(model, codeLens, token) : codeLens;
},
})
);
}
🤖 Prompt for AI Agents
In @modules/backend/formwidgets/codeeditor/assets/js/codeeditor.js around lines
1515 - 1526, The resolveCodeLens implementation in addCodeLens calls
resolver(model, codeLens, token) even when resolver is null, causing a
TypeError; change it to only invoke resolver when it's provided (e.g. use
optional chaining resolver?.(model, codeLens, token) ?? codeLens or check typeof
resolver === 'function' and call it, otherwise return codeLens) while keeping
provideCodeLenses unchanged; update the resolveCodeLens lambda in addCodeLens
accordingly.

coderabbitai[bot]

This comment was marked as resolved.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@modules/backend/formwidgets/codeeditor/README.md`:
- Around line 20-36: The README header "Supported Languages (15)" is incorrect
because the enumerated list contains 14 languages and the webpack language
registration in the project’s build config also registers 14 languages; update
either the header to "Supported Languages (14)" or add the missing 15th language
to both the README list and the build config language registration so they
match; locate the header text "Supported Languages (15)" in README.md and the
language registration block in the build/webpack config, then make the count and
list consistent.
♻️ Duplicate comments (1)
modules/backend/formwidgets/codeeditor/README.md (1)

581-586: Bare URLs need Markdown link formatting.

This issue was previously flagged and remains unresolved. The URLs should be wrapped in proper Markdown link syntax for correct rendering.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f97b894 and 24e6c92.

📒 Files selected for processing (8)
  • modules/backend/formwidgets/codeeditor/README.md
  • modules/cms/assets/js/winter.cmspage.js
  • modules/cms/classes/Asset.php
  • modules/cms/classes/CmsObject.php
  • modules/cms/classes/Theme.php
  • modules/cms/controllers/Index.php
  • modules/system/assets/js/framework-min.js
  • modules/system/assets/js/framework.combined-min.js
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
modules/backend/formwidgets/codeeditor/README.md

581-581: Bare URL used

(MD034, no-bare-urls)


582-582: Bare URL used

(MD034, no-bare-urls)


583-583: Bare URL used

(MD034, no-bare-urls)


584-584: Bare URL used

(MD034, no-bare-urls)


585-585: Bare URL used

(MD034, no-bare-urls)


586-586: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: windows-latest / PHP 8.2
  • GitHub Check: windows-latest / PHP 8.1
  • GitHub Check: ubuntu-latest / PHP 8.1
  • GitHub Check: ubuntu-latest / PHP 8.4
  • GitHub Check: windows-latest / PHP 8.3
  • GitHub Check: ubuntu-latest / PHP 8.3
  • GitHub Check: windows-latest / PHP 8.4
  • GitHub Check: ubuntu-latest / PHP 8.2
  • GitHub Check: windows-latest / JavaScript
🔇 Additional comments (1)
modules/backend/formwidgets/codeeditor/README.md (1)

1-586: Excellent comprehensive documentation!

This README provides thorough documentation for the Monaco Editor integration, including:

  • Clear architecture rationale for the dual-editor approach
  • Detailed migration guide with before/after examples
  • Complete API reference and usage examples
  • Testing coverage information

The documentation will significantly help developers understand the transition from Ace to Monaco and how to update their code accordingly.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 20 to 36
### Supported Languages (15)

1. **TypeScript** - Full TypeScript support with type checking
2. **JavaScript** - Modern ES6+ support
3. **CSS** - Including CSS3 properties
4. **JSON** - With schema validation
5. **HTML** - HTML5 support
6. **INI** - Configuration files
7. **LESS** - CSS preprocessor
8. **Markdown** - Rich markdown editing
9. **MySQL** - SQL syntax highlighting
10. **PHP** - Full PHP support
11. **SCSS** - Sass CSS preprocessor
12. **Twig** - Template engine syntax
13. **XML** - Markup language support
14. **YAML** - Configuration file support

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find Monaco language registrations
rg -n -A2 -B2 "monaco\.languages\.register|registerLanguage|languages:\s*\[" modules/backend/formwidgets/codeeditor/

Repository: wintercms/winter

Length of output: 50374


🏁 Script executed:

# Check the exact content at line 20 and around line 573
sed -n '18,25p' modules/backend/formwidgets/codeeditor/README.md
echo "---"
sed -n '570,580p' modules/backend/formwidgets/codeeditor/README.md

Repository: wintercms/winter

Length of output: 626


🏁 Script executed:

# Check for language definitions in JavaScript/configuration files
fd -e "js" -e "ts" -e "json" modules/backend/formwidgets/codeeditor/ -x grep -l "languages\|monaco"

Repository: wintercms/winter

Length of output: 521


🏁 Script executed:

# Get the complete languages array from winter.mix.js
sed -n '44,65p' modules/backend/formwidgets/codeeditor/assets/winter.mix.js

Repository: wintercms/winter

Length of output: 719


🏁 Script executed:

# Search for language definitions across the codebase
rg "languages:\s*\[|'language'|registerLanguage" -A 20 modules/backend/formwidgets/codeeditor/ -t js -t ts --max-count=5

Repository: wintercms/winter

Length of output: 50374


🏁 Script executed:

# Check if there are any other language references in the README itself
grep -n "language" modules/backend/formwidgets/codeeditor/README.md | head -20

Repository: wintercms/winter

Length of output: 823


Correct the language count to match the actual configuration.

The documentation claims "Supported Languages (15)" at line 20, but the enumerated list contains only 14 languages. The webpack configuration in assets/winter.mix.js also only registers 14 languages. Either reduce the count to 14 or add the missing 15th language to the configuration and list.

🤖 Prompt for AI Agents
In `@modules/backend/formwidgets/codeeditor/README.md` around lines 20 - 36, The
README header "Supported Languages (15)" is incorrect because the enumerated
list contains 14 languages and the webpack language registration in the
project’s build config also registers 14 languages; update either the header to
"Supported Languages (14)" or add the missing 15th language to both the README
list and the build config language registration so they match; locate the header
text "Supported Languages (15)" in README.md and the language registration block
in the build/webpack config, then make the count and list consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement PRs that implement a new feature or substantial change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Switch code editor to Monaco

4 participants