# 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
