Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
587 lines
18 KiB
Markdown
587 lines
18 KiB
Markdown
# Monaco Code Editor for Winter CMS
|
|
|
|
This is the Monaco Editor integration for Winter CMS Backend, replacing the legacy Ace Editor with Microsoft's Monaco Editor (the same editor that powers VS Code).
|
|
|
|
## Overview
|
|
|
|
**Monaco Editor** provides a rich, modern code editing experience with:
|
|
- IntelliSense (code completion)
|
|
- Syntax highlighting for 15+ languages
|
|
- Advanced find/replace with regex support
|
|
- Multi-cursor editing
|
|
- Code folding
|
|
- Bracket matching and colorization
|
|
- Minimap overview
|
|
- Color picker for CSS colors
|
|
- And many more VS Code features
|
|
|
|
## Features
|
|
|
|
### 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
|
|
|
|
### Monaco Features (20+)
|
|
|
|
Enabled features include:
|
|
- Anchor select
|
|
- Bracket matching
|
|
- Caret operations
|
|
- Clipboard operations
|
|
- Code lens
|
|
- Color picker
|
|
- Comment toggling
|
|
- Context menu
|
|
- Cursor undo/redo
|
|
- Find and replace
|
|
- Code folding
|
|
- Go to symbol
|
|
- Hover information
|
|
- In-place replace
|
|
- Indentation
|
|
- Inline hints
|
|
- Links
|
|
- Multi-cursor editing
|
|
- Parameter hints
|
|
- Rename symbol
|
|
- Smart select
|
|
- Snippets
|
|
- Suggest (autocomplete)
|
|
- Word highlighter
|
|
- Word operations
|
|
|
|
### Themes (35+)
|
|
|
|
Includes legacy tmTheme themes plus modern JSON themes
|
|
|
|
### User Preferences
|
|
|
|
All editor preferences are configurable from **Backend → Preferences → Code editor**:
|
|
|
|
**Appearance:**
|
|
- Font size (default: 12px)
|
|
- Theme selection
|
|
- Show/hide line numbers (gutter)
|
|
- Show/hide invisibles (whitespace)
|
|
- Highlight active line
|
|
- Show minimap
|
|
- Bracket colorization
|
|
- Color picker for CSS
|
|
|
|
**Behavior:**
|
|
- Tab size (default: 4 spaces)
|
|
- Use soft tabs (spaces) vs hard tabs
|
|
- Word wrap
|
|
- Auto-closing brackets/quotes
|
|
- Code folding
|
|
- Indent guides
|
|
- Print margin
|
|
|
|
All preferences persist across sessions and are stored per-user.
|
|
|
|
## Editor Architecture
|
|
|
|
Winter CMS uses a **dual-editor architecture** to optimize for different use cases:
|
|
|
|
### Monaco Editor (this FormWidget)
|
|
**Used by:** CodeEditor FormWidget
|
|
**Location:** `/modules/backend/formwidgets/codeeditor/`
|
|
**Purpose:** Advanced code editing with IntelliSense, syntax highlighting, and modern IDE features
|
|
**Bundle Size:** ~15 MB gzipped (main bundle + workers)
|
|
**Best for:** Writing PHP, JavaScript, CSS, YAML, and other code files
|
|
|
|
### Ace Editor (preserved)
|
|
**Used by:** RichEditor and MarkdownEditor FormWidgets
|
|
**Location:** `/modules/backend/assets/vendor/ace-codeeditor/`
|
|
**Purpose:** HTML source code editing within WYSIWYG editors
|
|
**Bundle Size:** ~500 KB (significantly lighter)
|
|
**Best for:** Viewing/editing raw HTML in rich text contexts
|
|
|
|
### Why Both?
|
|
|
|
**Monaco for CodeEditor:**
|
|
- Full IntelliSense and code completion
|
|
- Advanced refactoring tools
|
|
- Multi-cursor editing
|
|
- Rich language support
|
|
- Worth the bundle size for dedicated code editing
|
|
|
|
**Ace for RichEditor/MarkdownEditor:**
|
|
- Users rarely need advanced IDE features for HTML source view
|
|
- Lighter bundle improves page load performance
|
|
- Sufficient for basic HTML editing needs
|
|
- Reduces total application bundle by keeping WYSIWYG tools lean
|
|
|
|
This architecture balances modern features where they matter most (code editing) with performance optimization for general-purpose rich text editing.
|
|
|
|
## Technical Details
|
|
|
|
### Architecture
|
|
|
|
```text
|
|
modules/backend/formwidgets/codeeditor/
|
|
├── assets/
|
|
│ ├── css/
|
|
│ │ └── codeeditor.css - Compiled styles
|
|
│ ├── fonts/
|
|
│ │ └── codicon.ttf - Monaco icons font
|
|
│ ├── js/
|
|
│ │ ├── codeeditor.js - Main Monaco integration
|
|
│ │ └── build/
|
|
│ │ ├── codeeditor.bundle.js - Main bundle (19 MB)
|
|
│ │ ├── css.worker.js - CSS language worker
|
|
│ │ ├── editor.worker.js - Base editor worker
|
|
│ │ ├── html.worker.js - HTML language worker
|
|
│ │ ├── json.worker.js - JSON language worker
|
|
│ │ ├── ts.worker.js - TypeScript worker
|
|
│ │ └── [language-chunks] - 15 language modules
|
|
│ ├── less/
|
|
│ │ └── codeeditor.less - Source styles
|
|
│ ├── themes/
|
|
│ │ ├── [34 .tmTheme files] - Legacy TextMate themes
|
|
│ │ ├── one-dark-pro.json - Modern JSON theme
|
|
│ │ └── winter.json - Modern JSON theme
|
|
│ ├── winter.mix.js - Laravel Mix build configuration
|
|
│ └── package.json - NPM dependencies (in parent)
|
|
├── partials/
|
|
│ └── codeeditor.htm - Widget template
|
|
└── CodeEditor.php - FormWidget class
|
|
```
|
|
|
|
### Build System
|
|
|
|
**Current:** Laravel Mix 6 with Webpack 5
|
|
|
|
#### Build Command
|
|
```bash
|
|
php artisan mix:compile --package=module-backend.formwidgets.codeeditor -f
|
|
```
|
|
|
|
#### Build Configuration
|
|
|
|
See `assets/winter.mix.js`:
|
|
- Uses `monaco-editor-webpack-plugin` for proper worker splitting
|
|
- Polyfills for browser compatibility (> 0.5%, last 2 versions, Firefox ESR)
|
|
- Removes inline codicon font CSS (post-build hook)
|
|
- Minification and terser optimization
|
|
|
|
### Web Workers
|
|
|
|
Monaco Editor uses Web Workers for language services:
|
|
|
|
| Worker | Size | Purpose |
|
|
|--------|------|---------|
|
|
| editor.worker.js | 1.6 MB | Base editor operations |
|
|
| ts.worker.js | 22 MB | TypeScript/JavaScript IntelliSense |
|
|
| css.worker.js | 4.7 MB | CSS validation and completion |
|
|
| html.worker.js | 3.3 MB | HTML validation |
|
|
| json.worker.js | 2.2 MB | JSON schema validation |
|
|
|
|
Workers are loaded asynchronously and run in separate threads for better performance.
|
|
|
|
### Theme System
|
|
|
|
Themes are loaded directly as static assets via HTTP fetch (no PHP handler required). Theme preference values include the file extension (e.g., `twilight.tmTheme`, `one-dark-pro.json`).
|
|
|
|
#### Supported Formats
|
|
|
|
**1. TextMate Themes (.tmTheme)**
|
|
Legacy XML-based themes. Converted to Monaco format at runtime using `fast-plist` library.
|
|
|
|
**2. JSON Themes (.json)**
|
|
Modern VS Code theme format. Parsed and mapped to Monaco's theme structure.
|
|
|
|
```javascript
|
|
// codeeditor.js - Themes loaded via static fetch
|
|
async fetchTheme(themeName) {
|
|
// Theme name includes extension (e.g., "twilight.tmTheme", "one-dark-pro.json")
|
|
// Legacy values without extension default to .tmTheme
|
|
const basePath = window.Snowboard.url().asset('/modules/backend/formwidgets/codeeditor/assets/themes/');
|
|
const response = await fetch(`${basePath}${themeName}`);
|
|
// Format determined from file extension
|
|
}
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Basic Usage
|
|
|
|
```yaml
|
|
# fields.yaml
|
|
code:
|
|
type: codeeditor
|
|
size: giant
|
|
language: php
|
|
```
|
|
|
|
### Available Options
|
|
|
|
```yaml
|
|
code:
|
|
type: codeeditor
|
|
# Editor size
|
|
size: tiny|small|large|huge|giant # Default: large
|
|
|
|
# Programming language
|
|
language: php|javascript|css|html|twig|yaml|etc # Default: php
|
|
|
|
# Theme (overrides user preference)
|
|
theme: twilight|monokai|github|one-dark-pro|etc
|
|
|
|
# Line numbers
|
|
showGutter: true|false # Default: true
|
|
|
|
# Word wrapping
|
|
wordWrap: true|false # Default: true
|
|
|
|
# Code folding
|
|
codeFolding: true|false # Default: true
|
|
|
|
# Auto-closing brackets
|
|
autoClosing: true|false # Default: true
|
|
|
|
# Soft tabs (spaces)
|
|
useSoftTabs: true|false # Default: true
|
|
tabSize: 2|4|8 # Default: 4
|
|
|
|
# Font size (px)
|
|
fontSize: 10|12|14|16|18 # Default: 12
|
|
|
|
# Read-only mode
|
|
readOnly: true|false # Default: false
|
|
disabled: true|false # Sets readOnly
|
|
|
|
# Display options
|
|
showInvisibles: true|false # Default: false
|
|
highlightActiveLine: true|false # Default: true
|
|
displayIndentGuides: true|false # Default: true
|
|
showPrintMargin: true|false # Default: false
|
|
showMinimap: true|false # Default: true
|
|
bracketColors: true|false # Default: false
|
|
showColors: true|false # Default: true (CSS color picker)
|
|
```
|
|
|
|
### JavaScript API
|
|
|
|
```javascript
|
|
// Get editor instance
|
|
const $editor = $('#my-editor');
|
|
const wrapper = $editor.data('oc.codeeditor');
|
|
|
|
// Access Monaco instance directly
|
|
const monacoEditor = wrapper.editor;
|
|
|
|
// Get/set content (via wrapper)
|
|
const code = wrapper.getValue();
|
|
wrapper.setValue('function test() {}');
|
|
|
|
// Get/set language
|
|
wrapper.setLanguage('javascript');
|
|
|
|
// Change theme
|
|
wrapper.setTheme('one-dark-pro');
|
|
|
|
// Insert at cursor
|
|
wrapper.insert('code here');
|
|
|
|
// Get cursor position
|
|
const position = wrapper.getPosition(); // { lineNumber: 1, column: 1 }
|
|
|
|
// Fullscreen
|
|
wrapper.enterFullscreen();
|
|
wrapper.exitFullscreen();
|
|
```
|
|
|
|
### Migrating from ACE to Monaco API
|
|
|
|
Winter CMS has migrated from ACE Editor to Monaco Editor. While backward compatibility is maintained for accessing the editor instance via jQuery `.data('oc.codeEditor')`, direct ACE API calls need to be updated.
|
|
|
|
#### Breaking Changes
|
|
|
|
**ACE's Session API is Removed:**
|
|
- `editor.getSession()` → No longer available
|
|
- ACE used a separate "session" object for document operations
|
|
- Monaco combines session and model into a single API
|
|
|
|
**Position Indexing Changed:**
|
|
- ACE uses **0-indexed** positions (rows and columns start at 0)
|
|
- Monaco uses **1-indexed** positions (lines and columns start at 1)
|
|
- Example: ACE row 5 = Monaco line 6, ACE column 0 = Monaco column 1
|
|
|
|
**Annotations Replaced with Markers:**
|
|
- ACE's `setAnnotations()` → Monaco's `monaco.editor.setModelMarkers()`
|
|
- Different data structure and API
|
|
|
|
#### Quick Migration Guide
|
|
|
|
**Getting/Setting Editor Value:**
|
|
|
|
```javascript
|
|
// ❌ OLD (ACE API - Deprecated)
|
|
const editor = $('[data-control=codeeditor]').data('oc.codeEditor').editor;
|
|
const value = editor.getSession().getValue();
|
|
editor.getSession().setValue('new value');
|
|
|
|
// ✅ NEW (Recommended - Use Wrapper)
|
|
const wrapper = $('[data-control=codeeditor]').data('oc.codeEditor');
|
|
const value = wrapper.getValue();
|
|
wrapper.setValue('new value');
|
|
|
|
// ✅ ALTERNATIVE (Direct Monaco API)
|
|
const monacoEditor = wrapper.editor;
|
|
const value = monacoEditor.getModel().getValue();
|
|
monacoEditor.getModel().setValue('new value');
|
|
```
|
|
|
|
**Inserting Text at Cursor:**
|
|
|
|
```javascript
|
|
// ❌ OLD (ACE API)
|
|
editor.insert('text');
|
|
|
|
// ✅ NEW (Wrapper provides this method)
|
|
wrapper.insert('text');
|
|
```
|
|
|
|
**Working with Annotations/Markers:**
|
|
|
|
```javascript
|
|
// ❌ OLD (ACE Annotations)
|
|
editor.getSession().setAnnotations([
|
|
{ row: 5, column: 0, text: 'Warning message', type: 'warning' }
|
|
]);
|
|
|
|
// Clear annotations
|
|
editor.getSession().setAnnotations([]);
|
|
|
|
// ✅ NEW (Monaco Wrapper Method - Recommended)
|
|
wrapper.setMarkers('sourceId', [
|
|
{
|
|
startLineNumber: 6, // ACE row 5 = Monaco line 6 (1-indexed!)
|
|
startColumn: 1, // ACE column 0 = Monaco column 1
|
|
endLineNumber: 6,
|
|
endColumn: Number.MAX_VALUE, // End of line
|
|
message: 'Warning message',
|
|
severity: wrapper.monaco.MarkerSeverity.Warning // Info, Warning, or Error
|
|
}
|
|
]);
|
|
|
|
// Clear markers
|
|
wrapper.setMarkers('sourceId', []);
|
|
```
|
|
|
|
**Getting Cursor Position:**
|
|
|
|
```javascript
|
|
// ❌ OLD (ACE API)
|
|
const cursor = editor.getCursorPosition(); // { row: 5, column: 10 } (0-indexed)
|
|
|
|
// ✅ NEW (Wrapper)
|
|
const position = wrapper.getPosition(); // { lineNumber: 6, column: 11 } (1-indexed)
|
|
|
|
// ✅ ALTERNATIVE (Direct Monaco)
|
|
const position = wrapper.editor.getPosition();
|
|
```
|
|
|
|
**Getting Selection:**
|
|
|
|
```javascript
|
|
// ❌ OLD (ACE API)
|
|
const range = editor.getSelection().getRange();
|
|
|
|
// ✅ NEW (Wrapper)
|
|
const selection = wrapper.getSelection();
|
|
|
|
// ✅ ALTERNATIVE (Direct Monaco)
|
|
const selection = wrapper.editor.getSelection();
|
|
```
|
|
|
|
#### API Comparison Table
|
|
|
|
| Operation | ACE API (Deprecated) | Monaco Wrapper (Recommended) | Direct Monaco API |
|
|
|-----------|---------------------|------------------------------|-------------------|
|
|
| Get value | `editor.getSession().getValue()` | `wrapper.getValue()` | `editor.getModel().getValue()` |
|
|
| Set value | `editor.getSession().setValue(v)` | `wrapper.setValue(v)` | `editor.getModel().setValue(v)` |
|
|
| Insert text | `editor.insert(text)` | `wrapper.insert(text)` | Complex - use wrapper |
|
|
| Get position | `editor.getCursorPosition()` | `wrapper.getPosition()` | `editor.getPosition()` |
|
|
| Get selection | `editor.getSelection()` | `wrapper.getSelection()` | `editor.getSelection()` |
|
|
| Set annotations | `editor.getSession().setAnnotations()` | `wrapper.setMarkers(id, markers)` | `monaco.editor.setModelMarkers()` |
|
|
| Focus editor | `editor.focus()` | `wrapper.focus()` | `editor.focus()` |
|
|
| Set language | N/A | `wrapper.setLanguage(lang)` | Complex - use wrapper |
|
|
|
|
#### Migration Checklist for Plugin Developers
|
|
|
|
If your plugin interacts with the CodeEditor widget, follow these steps:
|
|
|
|
1. **Update Editor Instance Access:**
|
|
- ✅ Keep: `.data('oc.codeEditor')` - Returns the wrapper
|
|
- ⚠️ Avoid: `.data('oc.codeEditor').editor` - Returns raw Monaco (advanced use only)
|
|
|
|
2. **Replace ACE Session Methods:**
|
|
- ❌ Remove all: `getSession().getValue()` → ✅ Use: `getValue()`
|
|
- ❌ Remove all: `getSession().setValue()` → ✅ Use: `setValue()`
|
|
|
|
3. **Update Annotations:**
|
|
- ❌ Remove: `getSession().setAnnotations(annotations)`
|
|
- ✅ Add: `wrapper.setMarkers(sourceId, markers)`
|
|
- ⚠️ Remember: Convert 0-indexed row/column to 1-indexed line/column
|
|
- Use `wrapper.monaco.MarkerSeverity` for severity constants
|
|
|
|
4. **Test Thoroughly:**
|
|
- Verify all editor interactions work
|
|
- Check that cursor operations use correct indexing
|
|
- Ensure markers/warnings display correctly
|
|
|
|
#### Available Wrapper Methods
|
|
|
|
The Monaco Snowboard editor wrapper provides these convenience methods:
|
|
|
|
```javascript
|
|
const wrapper = $('[data-control=codeeditor]').data('oc.codeEditor');
|
|
|
|
// Content
|
|
wrapper.getValue() // Get editor content
|
|
wrapper.setValue(value) // Set editor content
|
|
wrapper.insert(text) // Insert at cursor position
|
|
|
|
// Position & Selection
|
|
wrapper.getPosition() // Get cursor position (1-indexed)
|
|
wrapper.getSelection() // Get selection range
|
|
|
|
// Markers (Errors/Warnings/Info with squiggly underlines)
|
|
wrapper.setMarkers(sourceId, markers) // Set error/warning markers in editor
|
|
// Example: wrapper.setMarkers('myPlugin', [{ startLineNumber: 5, startColumn: 1,
|
|
// endLineNumber: 5, endColumn: Number.MAX_VALUE, message: 'Warning',
|
|
// severity: wrapper.monaco.MarkerSeverity.Warning }])
|
|
|
|
// Decorations (Visual highlights WITHOUT error semantics)
|
|
wrapper.setDecorations(sourceId, decorations) // Set visual highlights (no squiggles)
|
|
// Example: wrapper.setDecorations('myHighlight', [{ range: new monaco.Range(5, 1, 5, Number.MAX_VALUE),
|
|
// options: { isWholeLine: true, className: 'myHighlightClass',
|
|
// linesDecorationsClassName: 'myGutterClass' } }])
|
|
|
|
// Configuration
|
|
wrapper.setLanguage(lang) // Change syntax highlighting language
|
|
wrapper.setTheme(theme) // Change color theme
|
|
wrapper.focus() // Focus the editor
|
|
|
|
// View
|
|
wrapper.enterFullscreen() // Enter fullscreen mode
|
|
wrapper.exitFullscreen() // Exit fullscreen mode
|
|
wrapper.refresh() // Refresh editor (re-create instance)
|
|
|
|
// Direct Access (Advanced)
|
|
wrapper.editor // Access Monaco editor instance
|
|
wrapper.getEditor() // Same as wrapper.editor
|
|
wrapper.getModel() // Get Monaco model
|
|
wrapper.monaco // Access Monaco namespace (for constants like MarkerSeverity)
|
|
```
|
|
|
|
#### Example: Winter.Builder Plugin Migration
|
|
|
|
The Winter.Builder plugin was migrated to use Monaco API. Here's a real example:
|
|
|
|
**Before (ACE):**
|
|
```javascript
|
|
Localization.prototype.copyStringsFromDone = function(data) {
|
|
var codeEditor = this.getCodeEditor($masterTabPane);
|
|
|
|
// Set value using ACE Session API
|
|
codeEditor.getSession().setValue(responseData.strings);
|
|
|
|
// Set annotations using ACE
|
|
var annotations = [];
|
|
for (var i = 0; i < updatedLines.length; i++) {
|
|
annotations.push({
|
|
row: updatedLines[i], // 0-indexed
|
|
column: 0,
|
|
text: 'New String',
|
|
type: 'warning'
|
|
});
|
|
}
|
|
codeEditor.getSession().setAnnotations(annotations);
|
|
}
|
|
```
|
|
|
|
**After (Monaco):**
|
|
```javascript
|
|
Localization.prototype.copyStringsFromDone = function(data) {
|
|
var wrapper = this.getCodeEditor($masterTabPane);
|
|
|
|
// Set value using wrapper method
|
|
wrapper.setValue(responseData.strings);
|
|
|
|
// Convert to Monaco decorations (visual highlights without error semantics)
|
|
var decorations = [];
|
|
for (var i = 0; i < updatedLines.length; i++) {
|
|
decorations.push({
|
|
range: new wrapper.monaco.Range(
|
|
updatedLines[i] + 1, // Convert to 1-indexed!
|
|
1, // Start column
|
|
updatedLines[i] + 1, // End line (same line)
|
|
Number.MAX_VALUE // End column (end of line)
|
|
),
|
|
options: {
|
|
isWholeLine: true,
|
|
className: 'builder-new-translation-line', // Background highlight
|
|
linesDecorationsClassName: 'builder-new-translation-gutter', // Gutter indicator
|
|
hoverMessage: { value: 'New string or section' } // Tooltip on hover
|
|
}
|
|
});
|
|
}
|
|
wrapper.setDecorations('builderLocalization', decorations);
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Playwright E2E Tests
|
|
|
|
Comprehensive test suite with 55+ tests:
|
|
|
|
```bash
|
|
# Run all tests
|
|
npm run test:e2e
|
|
|
|
# Run with UI
|
|
npm run test:e2e:ui
|
|
|
|
# Run specific test file
|
|
npx playwright test fullscreen.spec.js
|
|
|
|
# Debug tests
|
|
npm run test:e2e:debug
|
|
```
|
|
|
|
### Test Coverage
|
|
|
|
- **Fullscreen functionality** (6 tests)
|
|
- **Theme loading and switching** (9 tests)
|
|
- **Language support** (14 tests for all 15 languages)
|
|
- **Monaco features** (14 tests: find, replace, folding, multi-cursor, etc.)
|
|
- **Preferences persistence** (12 tests)
|
|
|
|
See `tests/e2e/README-TESTING.md` for full testing documentation.
|
|
|
|
## Resources
|
|
|
|
- **Monaco Editor Documentation:** https://microsoft.github.io/monaco-editor/
|
|
- **Monaco Editor GitHub:** https://github.com/microsoft/monaco-editor
|
|
- **VS Code Themes:** https://marketplace.visualstudio.com/search?target=VSCode&category=Themes
|
|
- **Winter CMS Docs:** https://wintercms.com/docs
|
|
- **PR #801:** https://github.com/wintercms/winter/pull/801
|
|
- **Issue #431:** https://github.com/wintercms/winter/issues/431
|