@synclineapi/editor 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1858 @@
1
+ # Syncline Editor
2
+
3
+ > A zero-dependency, pixel-perfect, fully customisable browser-based code editor built as a TypeScript library.
4
+
5
+ Ships as both an **ES module** and **UMD bundle**, runs entirely inside a **Shadow DOM**, and handles **100,000+ line files** via virtual rendering.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [ES Module](#es-module)
15
+ - [UMD / CDN](#umd--cdn)
16
+ - [Configuration — Full Reference](#configuration--full-reference)
17
+ - [Document](#document)
18
+ - [Display](#display)
19
+ - [Typography](#typography)
20
+ - [Cursor](#cursor)
21
+ - [Layout](#layout)
22
+ - [Editing](#editing)
23
+ - [Features](#features-config)
24
+ - [Syntax & Autocomplete](#syntax--autocomplete-customisation)
25
+ - [Token Colors](#token-colors-config)
26
+ - [Theme](#theme-config)
27
+ - [Callbacks](#callbacks)
28
+ - [Updating at Runtime](#updating-config-at-runtime)
29
+ - [Runtime API](#runtime-api)
30
+ - [Content](#content)
31
+ - [Cursor & Selection](#cursor--selection)
32
+ - [Commands](#commands)
33
+ - [Themes](#themes-api)
34
+ - [Lifecycle](#lifecycle)
35
+ - [Syntax Highlighting](#syntax-highlighting)
36
+ - [Supported Languages](#supported-languages)
37
+ - [Token Classes](#token-classes-and-css-variables)
38
+ - [Extra Keywords & Types](#adding-extra-keywords-and-types)
39
+ - [Token Colors](#token-colors)
40
+ - [Quick Overrides](#quick-overrides)
41
+ - [Field Reference](#tokencolors-fields)
42
+ - [How Overrides Work](#how-token-colour-overrides-work)
43
+ - [Themes](#themes)
44
+ - [Built-in Themes](#built-in-themes)
45
+ - [Switching Themes](#switching-themes)
46
+ - [Creating a Custom Theme](#creating-a-custom-theme)
47
+ - [Extending a Built-in Theme](#extending-a-built-in-theme)
48
+ - [ThemeTokens Reference](#themetokens-reference)
49
+ - [Autocomplete](#autocomplete)
50
+ - [Unified Completions Array](#the-unified-completions-array)
51
+ - [CompletionItem Reference](#completionitem-reference)
52
+ - [Description Panel](#description-panel)
53
+ - [Replace Built-ins](#replace-built-in-completions--replacebuiltins)
54
+ - [Popup Size](#popup-size)
55
+ - [Priority Order](#priority-order)
56
+ - [Snippets](#snippets)
57
+ - [Built-in Snippets](#built-in-snippets-by-language)
58
+ - [Custom Snippets](#custom-snippets)
59
+ - [Snippet Body Syntax](#snippet-body-syntax)
60
+ - [Emmet](#emmet)
61
+ - [Dynamic Completion Provider](#dynamic-completion-provider)
62
+ - [provideCompletions](#providecompletions)
63
+ - [CompletionContext](#completioncontext)
64
+ - [Events & Callbacks](#events--callbacks)
65
+ - [Advanced Features](#advanced-features)
66
+ - [Multi-cursor](#multi-cursor)
67
+ - [Find & Replace](#find--replace)
68
+ - [Code Folding](#code-folding)
69
+ - [Bracket Matching](#bracket-matching)
70
+ - [Word Highlight](#word-highlight)
71
+ - [Whitespace Rendering](#whitespace-rendering)
72
+ - [Cursor Styles](#cursor-styles)
73
+ - [Minimap](#minimap)
74
+ - [Word Wrap](#word-wrap)
75
+ - [Indent Guides](#indent-guides)
76
+ - [Active Line Highlight](#active-line-highlight)
77
+ - [Read-Only Mode](#read-only-mode)
78
+ - [Behavioral Options](#behavioral-options)
79
+ - [Auto-Close Pairs](#auto-close-pairs)
80
+ - [Line Comment Token](#line-comment-token)
81
+ - [Word Separators](#word-separators)
82
+ - [Undo Batch Window](#undo-batch-window)
83
+ - [Feature Flags](#feature-flags)
84
+ - [Hover Documentation](#hover-documentation)
85
+ - [Hover from completions](#hover-from-completions--automatic)
86
+ - [provideHover callback](#providehover--fully-custom-docs)
87
+ - [HoverDoc reference](#hoverdoc-reference)
88
+ - [HoverContext reference](#hovercontext-reference)
89
+ - [Keyboard Shortcuts](#keyboard-shortcuts)
90
+ - [Recipes](#recipes)
91
+ - [Monaco-style Editor Embed](#monaco-style-editor-embed)
92
+ - [DSL / SQL Editor](#dsl--sql-editor)
93
+ - [Read-Only Code Viewer](#read-only-code-viewer)
94
+ - [Custom Theme from Scratch](#custom-theme-from-scratch-recipe)
95
+ - [Framework-aware Autocomplete](#framework-aware-autocomplete)
96
+ - [Auto-Save with Debounce](#auto-save-with-debounce)
97
+ - [TypeScript Types](#typescript-types)
98
+ - [Project Structure](#project-structure)
99
+ - [Development](#development)
100
+
101
+ ---
102
+
103
+ ## Features
104
+
105
+ | Feature | Detail |
106
+ |---|---|
107
+ | **Zero dependencies** | No external runtime libraries — one self-contained bundle |
108
+ | **Virtual rendering** | Only visible rows exist in the DOM; handles 100,000+ line files smoothly |
109
+ | **Shadow DOM** | Fully encapsulated styles — no CSS leakage in either direction |
110
+ | **Dual build** | ES module + UMD bundle, full TypeScript declarations |
111
+ | **Syntax highlighting** | TypeScript, JavaScript, CSS, JSON, Markdown — nine token classes |
112
+ | **Token color overrides** | Override individual token colours on top of any theme without replacing it |
113
+ | **6 built-in themes** | VR Dark, VS Code Dark+, Monokai, Dracula, GitHub Light, Solarized Light |
114
+ | **Custom themes** | Full `ThemeDefinition` API — every CSS variable exposed |
115
+ | **Unified autocomplete** | One `completions` array for keywords, snippets, custom symbols, and DSL items |
116
+ | **VS Code-style docs panel** | Description panel beside the popup — shown for items with `description` |
117
+ | **Dynamic provider** | `provideCompletions` callback for context-aware, fully runtime-controlled completions |
118
+ | **Built-in snippets** | 17 snippets across TypeScript/JS, CSS, and HTML — Tab-expandable |
119
+ | **Emmet expansion** | `div.wrapper>ul>li*3` → Tab — with inline preview tooltip |
120
+ | **Multi-cursor** | `Alt+Click` to add cursors; `Ctrl+D` to select next occurrence |
121
+ | **Hover documentation** | Tooltip on pointer-rest over any identifier — built-in JS/TS/CSS docs + custom `provideHover` callback |
122
+ | **Move line** | `Alt+Up` / `Alt+Down` to move the current line or selected block up/down |
123
+ | **Find & Replace** | Literal and regex search, case-sensitive mode, replace one / all |
124
+ | **Code folding** | Collapse `{}` blocks via gutter toggle |
125
+ | **Bracket matching** | Highlights matching `()[]{}` pairs at the cursor |
126
+ | **Word highlight** | All occurrences of the word under the cursor highlighted subtly |
127
+ | **Active line highlight** | Distinct background on the current line and its gutter cell |
128
+ | **Minimap** | Canvas-rendered overview with draggable viewport slider |
129
+ | **Status bar** | Language · line/col · selection · undo depth · word-wrap toggle · theme picker |
130
+ | **Indent guides** | Faint vertical lines at each indentation level |
131
+ | **Whitespace rendering** | Visible `·` / `→` for spaces and tabs (`none` / `boundary` / `all`) |
132
+ | **Cursor styles** | `line`, `block`, or `underline`; configurable blink rate |
133
+ | **Read-only mode** | All edits blocked; navigation, selection, and copy still work |
134
+
135
+ ---
136
+
137
+ ## Installation
138
+
139
+ ```bash
140
+ npm install syncline-editor
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Quick Start
146
+
147
+ ### ES Module
148
+
149
+ ```ts
150
+ import { createEditor } from 'syncline-editor';
151
+
152
+ const editor = createEditor(document.getElementById('app')!, {
153
+ value: 'const greeting = "Hello, world!";',
154
+ language: 'typescript',
155
+ theme: 'dracula',
156
+ onChange: (value) => console.log('changed:', value.length, 'chars'),
157
+ });
158
+ ```
159
+
160
+ Give the container an explicit height — the editor fills 100% of its host element:
161
+
162
+ ```html
163
+ <div id="app" style="width: 100%; height: 600px;"></div>
164
+ ```
165
+
166
+ ### UMD / CDN
167
+
168
+ ```html
169
+ <script src="syncline-editor.umd.js"></script>
170
+ <script>
171
+ const editor = SynclineEditor.createEditor(document.getElementById('app'), {
172
+ value: '// start coding',
173
+ language: 'typescript',
174
+ theme: 'vscode-dark',
175
+ });
176
+ </script>
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Configuration — Full Reference
182
+
183
+ Pass any subset of `EditorConfig` to `createEditor()`. Every field is optional. All options can also be updated at runtime via `editor.updateConfig(patch)`.
184
+
185
+ ### Document
186
+
187
+ | Option | Type | Default | Description |
188
+ |---|---|---|---|
189
+ | `value` | `string \| string[]` | `''` | Initial document content. String or pre-split `string[]`. |
190
+ | `language` | `Language` | `'typescript'` | Syntax highlighting and autocomplete language. |
191
+
192
+ ### Display
193
+
194
+ | Option | Type | Default | Description |
195
+ |---|---|---|---|
196
+ | `showGutter` | `boolean` | `true` | Show/hide the line-number gutter. |
197
+ | `showMinimap` | `boolean` | `true` | Show/hide the minimap panel. |
198
+ | `showStatusBar` | `boolean` | `true` | Show/hide the bottom status bar. |
199
+ | `showIndentGuides` | `boolean` | `true` | Faint vertical lines at each indentation level. |
200
+ | `highlightActiveLine` | `boolean` | `true` | Background tint on the active (cursor) line and gutter cell. |
201
+ | `wordHighlight` | `boolean` | `true` | Highlight all other occurrences of the word under the cursor. |
202
+ | `renderWhitespace` | `'none' \| 'boundary' \| 'all'` | `'none'` | Spaces render as `·`, tabs as `→`. `'boundary'` = leading/trailing only. |
203
+
204
+ ### Typography
205
+
206
+ | Option | Type | Default | Description |
207
+ |---|---|---|---|
208
+ | `fontFamily` | `string` | `"'JetBrains Mono', monospace"` | CSS `font-family` for all code text. |
209
+ | `fontSize` | `number` | `13` | Font size in pixels. |
210
+ | `lineHeight` | `number` | `22` | Row height in pixels — all scroll and minimap calculations derive from this. |
211
+
212
+ ### Cursor
213
+
214
+ | Option | Type | Default | Description |
215
+ |---|---|---|---|
216
+ | `cursorStyle` | `'line' \| 'block' \| 'underline'` | `'line'` | Visual cursor style. |
217
+ | `cursorBlinkRate` | `number` | `1050` | Blink period in ms. Set to `999999` to disable blinking. |
218
+
219
+ ### Layout
220
+
221
+ | Option | Type | Default | Description |
222
+ |---|---|---|---|
223
+ | `gutterWidth` | `number` | `60` | Gutter width in pixels. |
224
+ | `minimapWidth` | `number` | `120` | Minimap panel width in pixels. |
225
+ | `wordWrap` | `boolean` | `false` | Soft-wrap long lines. Toggle at runtime with `Alt+Z`. |
226
+ | `wrapColumn` | `number` | `80` | Column at which soft-wrap breaks when `wordWrap` is `true`. |
227
+
228
+ ### Editing
229
+
230
+ | Option | Type | Default | Description |
231
+ |---|---|---|---|
232
+ | `tabSize` | `number` | `2` | Spaces per Tab press. |
233
+ | `insertSpaces` | `boolean` | `true` | Insert spaces on Tab; `false` inserts a literal `\t`. |
234
+ | `maxUndoHistory` | `number` | `300` | Maximum undo snapshots retained. |
235
+ | `undoBatchMs` | `number` | `700` | Keystrokes within this window are grouped into one undo step. `0` = per-keystroke. |
236
+ | `readOnly` | `boolean` | `false` | Block all edits. Navigation, selection, and copy still work. |
237
+ | `autoClosePairs` | `Record<string,string>` | see below | Characters that auto-close. Pass `{}` to disable entirely. |
238
+ | `lineCommentToken` | `string` | `''` | Prefix for `Ctrl+/` toggle comment. Auto-detects from language when empty. |
239
+ | `wordSeparators` | `string` | `''` | Extra characters treated as word boundaries for double-click and `Ctrl+←/→`. |
240
+
241
+ Default `autoClosePairs`:
242
+ ```ts
243
+ { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' }
244
+ ```
245
+
246
+ ### Features Config
247
+
248
+ | Option | Type | Default | Description |
249
+ |---|---|---|---|
250
+ | `bracketMatching` | `boolean` | `true` | Highlight matching `()[]{}` pair at the cursor. |
251
+ | `codeFolding` | `boolean` | `true` | Gutter fold button for collapsible blocks. |
252
+ | `emmet` | `boolean` | `true` | Emmet abbreviation expansion via Tab. |
253
+ | `snippetExpansion` | `boolean` | `true` | Tab-expand built-in and custom snippets. |
254
+ | `autocomplete` | `boolean` | `true` | Show the autocomplete popup while typing. |
255
+ | `autocompletePrefixLength` | `number` | `2` | Minimum characters typed before the popup appears. |
256
+ | `multiCursor` | `boolean` | `true` | `Alt+Click` and `Ctrl+D` multi-cursor. |
257
+ | `find` | `boolean` | `true` | Enable the find bar (`Ctrl+F`). |
258
+ | `findReplace` | `boolean` | `true` | Enable find-and-replace (`Ctrl+H`). |
259
+ | `wordSelection` | `boolean` | `true` | Double-click selects the word under the cursor. |
260
+ | `hover` | `boolean` | `true` | Show a documentation tooltip when the pointer rests on a known identifier for ~500 ms. Covers built-in JS/TS symbols and any symbol in `completions` with a `description`, plus the `provideHover` callback. |
261
+
262
+ ### Syntax & Autocomplete Customisation
263
+
264
+ | Option | Type | Default | Description |
265
+ |---|---|---|---|
266
+ | `extraKeywords` | `string[]` | `[]` | Words highlighted as keywords **and** added to autocomplete. |
267
+ | `extraTypes` | `string[]` | `[]` | Words highlighted as types **and** added to autocomplete. |
268
+ | `completions` | `CompletionItem[]` | `[]` | Unified completions array — symbols, snippets, DSL items, all in one place. |
269
+ | `replaceBuiltins` | `boolean` | `false` | When `true`, `completions` replaces the built-in language keywords/types entirely. |
270
+ | `provideCompletions` | `(ctx: CompletionContext) => CompletionItem[] \| null` | `undefined` | Dynamic callback — called on every popup open; return `null` to fall through to defaults. |
271
+ | `provideHover` | `(ctx: HoverContext) => HoverDoc \| null` | `undefined` | Dynamic hover callback — return a `HoverDoc` to show a tooltip for any word not covered by built-in docs or the `completions` array. |
272
+ | `maxCompletions` | `number` | `14` | Maximum items shown in the autocomplete popup. |
273
+
274
+ ### Token Colors Config
275
+
276
+ | Option | Type | Default | Description |
277
+ |---|---|---|---|
278
+ | `tokenColors` | `TokenColors` | `{}` | Per-token colour overrides layered on top of the active theme. See [Token Colors](#token-colors). |
279
+
280
+ ### Theme Config
281
+
282
+ | Option | Type | Default | Description |
283
+ |---|---|---|---|
284
+ | `theme` | `string \| ThemeDefinition` | `''` (VR Dark) | Built-in theme ID or a full `ThemeDefinition` object. |
285
+
286
+ ### Callbacks
287
+
288
+ | Option | Signature | Description |
289
+ |---|---|---|
290
+ | `onChange` | `(value: string) => void` | Fired after every content change (keystroke, paste, undo, `setValue`). |
291
+ | `onCursorChange` | `(pos: CursorPosition) => void` | Fired when the cursor moves. |
292
+ | `onSelectionChange` | `(sel: Selection \| null) => void` | Fired when the selection changes or clears. |
293
+ | `onFocus` | `() => void` | Fired when the editor gains keyboard focus. |
294
+ | `onBlur` | `() => void` | Fired when the editor loses keyboard focus. |
295
+ | `provideHover` | `(ctx: HoverContext) => HoverDoc \| null \| undefined` | Provides custom hover documentation. Called after built-in lookup and `completions` search. |
296
+
297
+ ### Updating Config at Runtime
298
+
299
+ Any option can be changed after creation — no reload needed:
300
+
301
+ ```ts
302
+ // Toggle features instantly
303
+ editor.updateConfig({ wordWrap: true, showMinimap: false });
304
+
305
+ // Switch language (rebuilds highlighting + completions)
306
+ editor.updateConfig({ language: 'css' });
307
+
308
+ // Change font
309
+ editor.updateConfig({
310
+ fontFamily: "'Fira Code', monospace",
311
+ fontSize: 14,
312
+ lineHeight: 24,
313
+ });
314
+
315
+ // Enter read-only mode
316
+ editor.updateConfig({ readOnly: true });
317
+
318
+ // Override token colours on top of current theme
319
+ editor.updateConfig({
320
+ tokenColors: { keyword: '#ff79c6', string: '#f1fa8c' },
321
+ });
322
+
323
+ // Restore all token colours to theme defaults
324
+ editor.updateConfig({ tokenColors: {} });
325
+
326
+ // Swap completions at runtime
327
+ editor.updateConfig({ completions: newCompletions });
328
+ ```
329
+
330
+ ---
331
+
332
+ ## Runtime API
333
+
334
+ All methods on the `EditorAPI` object returned by `createEditor()`.
335
+
336
+ ### Content
337
+
338
+ ```ts
339
+ editor.getValue(): string // full document as a newline-joined string
340
+ editor.setValue(value: string): void // replace document (records undo snapshot)
341
+ ```
342
+
343
+ ### Cursor & Selection
344
+
345
+ ```ts
346
+ editor.getCursor(): CursorPosition // { row, col } — zero-based
347
+ editor.setCursor(pos: CursorPosition): void // moves cursor, scrolls into view
348
+
349
+ editor.getSelection(): Selection | null // { ar, ac, fr, fc } or null
350
+ editor.setSelection(sel: Selection | null): void // null to deselect
351
+
352
+ editor.insertText(text: string): void // insert at cursor position; no-op when readOnly
353
+ ```
354
+
355
+ ### History
356
+
357
+ ```ts
358
+ editor.undo(): void
359
+ editor.redo(): void
360
+ ```
361
+
362
+ ### Commands
363
+
364
+ ```ts
365
+ editor.executeCommand(name: string): void
366
+ ```
367
+
368
+ | Command | Action | Blocked by `readOnly` |
369
+ |---|---|---|
370
+ | `'undo'` | Undo | ✓ |
371
+ | `'redo'` | Redo | ✓ |
372
+ | `'selectAll'` | Select all | — |
373
+ | `'copy'` | Copy selection to clipboard | — |
374
+ | `'cut'` | Cut selection | ✓ |
375
+ | `'paste'` | Paste from clipboard | ✓ |
376
+ | `'toggleComment'` | Toggle line comment | ✓ |
377
+ | `'duplicateLine'` | Duplicate current line | ✓ |
378
+ | `'deleteLine'` | Delete current line | ✓ |
379
+ | `'toggleWordWrap'` | Toggle word wrap | — |
380
+ | `'find'` | Open find bar | — |
381
+ | `'findReplace'` | Open find + replace bar | — |
382
+ | `'indentLine'` | Indent selection / current line | ✓ |
383
+ | `'outdentLine'` | Outdent selection / current line | ✓ |
384
+
385
+ ### Themes API
386
+
387
+ ```ts
388
+ editor.setTheme(theme: string | ThemeDefinition): void // switch by ID or object
389
+ editor.getThemes(): string[] // all registered theme IDs
390
+ editor.registerTheme(theme: ThemeDefinition): void // register for later use
391
+ ```
392
+
393
+ ### Config
394
+
395
+ ```ts
396
+ editor.updateConfig(patch: Partial<EditorConfig>): void
397
+ ```
398
+
399
+ ### Lifecycle
400
+
401
+ ```ts
402
+ editor.focus(): void // programmatically focus the editor
403
+ editor.destroy(): void // unmount all DOM nodes; do not use the instance afterwards
404
+ ```
405
+
406
+ ---
407
+
408
+ ## Syntax Highlighting
409
+
410
+ The editor uses a hand-written, zero-dependency tokeniser that produces nine token classes mapped to CSS custom properties.
411
+
412
+ ### Supported Languages
413
+
414
+ | `language` | Keywords | Types | Built-in completions |
415
+ |---|---|---|---|
416
+ | `'typescript'` | 55 keywords (`interface`, `type`, `readonly`, `enum`, `satisfies`, …) | 25 types (`Promise`, `Array`, `HTMLElement`, …) | ~50 JS/TS functions |
417
+ | `'javascript'` | 35 keywords (TypeScript-specific syntax excluded) | 17 types | Same ~50 JS functions |
418
+ | `'css'` | 50 value keywords (`flex`, `block`, `grid`, `@media`, …) | — | 100+ CSS properties + CSS functions |
419
+ | `'json'` | `null`, `true`, `false` | — | — |
420
+ | `'markdown'` | — | — | — |
421
+ | `'text'` | — | — | — |
422
+
423
+ ### Token Classes and CSS Variables
424
+
425
+ | Token class | CSS variable | What it colours | Examples |
426
+ |---|---|---|---|
427
+ | `kw` | `--tok-kw` | Keywords | `const`, `return`, `if`, `flex`, `@media` |
428
+ | `str` | `--tok-str` | Strings | `"hello"`, `'world'`, `` `template` `` |
429
+ | `cmt` | `--tok-cmt` | Comments | `// line`, `/* block */` |
430
+ | `fn` | `--tok-fn` | Functions | `console.log(`, `fetch(`, `calc(` |
431
+ | `num` | `--tok-num` | Numbers | `42`, `3.14`, `0xff`, `1n` |
432
+ | `cls` | `--tok-cls` | Classes | `MyClass`, `EventEmitter`, `Promise` |
433
+ | `op` | `--tok-op` | Operators | `+`, `=>`, `===`, `&&`, `?.`, `\|` |
434
+ | `typ` | `--tok-typ` | Types | `string`, `boolean`, `HTMLElement` |
435
+ | `dec` | `--tok-dec` | Decorators | `@Component`, `@Injectable` |
436
+
437
+ ### Adding Extra Keywords and Types
438
+
439
+ `extraKeywords` and `extraTypes` affect **both** syntax highlighting and autocomplete:
440
+
441
+ ```ts
442
+ createEditor(container, {
443
+ language: 'typescript',
444
+ extraKeywords: ['pipeline', 'stage', 'emit'],
445
+ extraTypes: ['Observable', 'Subject', 'BehaviorSubject'],
446
+ });
447
+ ```
448
+
449
+ Update at runtime (rebuilds the tokeniser cache immediately):
450
+
451
+ ```ts
452
+ editor.updateConfig({
453
+ extraKeywords: ['pipeline', 'stage', 'emit', 'dispatch'],
454
+ });
455
+ ```
456
+
457
+ ---
458
+
459
+ ## Token Colors
460
+
461
+ Override individual syntax token colours on top of any active theme without replacing the whole theme. Pass only the fields you want to change — omitted fields keep the theme default.
462
+
463
+ ### Quick Overrides
464
+
465
+ ```ts
466
+ import type { TokenColors } from 'syncline-editor';
467
+
468
+ // Override specific tokens — all others remain from the active theme
469
+ editor.updateConfig({
470
+ tokenColors: {
471
+ keyword: '#ff79c6', // pink keywords
472
+ string: '#f1fa8c', // yellow strings
473
+ comment: '#6272a4', // muted blue comments
474
+ function: '#50fa7b', // green functions
475
+ number: '#bd93f9', // purple numbers
476
+ class: '#8be9fd', // cyan class names
477
+ operator: '#f8f8f2', // near-white operators
478
+ type: '#8be9fd', // cyan types
479
+ decorator: '#ffb86c', // orange decorators
480
+ },
481
+ });
482
+
483
+ // Restore everything to the current theme's defaults
484
+ editor.updateConfig({ tokenColors: {} });
485
+
486
+ // Remove just one override and keep the rest
487
+ editor.updateConfig({
488
+ tokenColors: { ...currentOverrides, keyword: '' },
489
+ });
490
+ ```
491
+
492
+ ### TokenColors Fields
493
+
494
+ | Field | CSS variable | What it highlights |
495
+ |---|---|---|
496
+ | `keyword` | `--tok-kw` | `if`, `const`, `class`, `interface`, `flex`, `@media` |
497
+ | `string` | `--tok-str` | String and template literals |
498
+ | `comment` | `--tok-cmt` | Line and block comments |
499
+ | `function` | `--tok-fn` | Function names, CSS functions like `calc()` |
500
+ | `number` | `--tok-num` | Numeric literals |
501
+ | `class` | `--tok-cls` | Class names, constructor calls |
502
+ | `operator` | `--tok-op` | Operators and punctuation |
503
+ | `type` | `--tok-typ` | Type names, built-in types |
504
+ | `decorator` | `--tok-dec` | Decorator annotations (`@Component`, `@Injectable`) |
505
+
506
+ ### How Token Colour Overrides Work
507
+
508
+ Token colours are CSS custom properties (`--tok-kw`, `--tok-str`, …) written as **inline styles** on the editor's host element. Both the theme engine and `tokenColors` write to the same properties:
509
+
510
+ 1. `setTheme()` — writes all `--tok-*` values from the theme definition
511
+ 2. `updateConfig({ tokenColors })` — overwrites specific properties on top
512
+
513
+ This means **token color overrides survive theme switches** — they are automatically re-applied each time the theme changes.
514
+
515
+ > **Tip:** Prefer `tokenColors` for quick colour customisation. Use a full `ThemeDefinition` only when you need to control backgrounds, borders, highlights, and the minimap too.
516
+
517
+ ---
518
+
519
+ ## Themes
520
+
521
+ ### Built-in Themes
522
+
523
+ | ID | Style |
524
+ |---|---|
525
+ | `''` (empty string) | VR Dark *(default)* |
526
+ | `'vscode-dark'` | VS Code Dark+ |
527
+ | `'monokai'` | Monokai |
528
+ | `'dracula'` | Dracula |
529
+ | `'github-light'` | GitHub Light |
530
+ | `'solarized-light'` | Solarized Light |
531
+
532
+ ### Switching Themes
533
+
534
+ ```ts
535
+ // By ID
536
+ editor.setTheme('dracula');
537
+ editor.setTheme('github-light');
538
+
539
+ // By object (register not required when passing directly)
540
+ editor.setTheme(myCustomTheme);
541
+
542
+ // Via updateConfig
543
+ editor.updateConfig({ theme: 'monokai' });
544
+
545
+ // Get all registered IDs
546
+ const ids = editor.getThemes();
547
+ // ['', 'vscode-dark', 'monokai', 'dracula', 'github-light', 'solarized-light', ...]
548
+ ```
549
+
550
+ ### Importing Built-in Theme Objects
551
+
552
+ ```ts
553
+ import {
554
+ BUILT_IN_THEMES, // ThemeDefinition[] — all six
555
+ THEME_VR_DARK,
556
+ THEME_VSCODE_DARK,
557
+ THEME_MONOKAI,
558
+ THEME_DRACULA,
559
+ THEME_GITHUB_LIGHT,
560
+ THEME_SOLARIZED_LIGHT,
561
+ } from 'syncline-editor';
562
+ ```
563
+
564
+ ### Creating a Custom Theme
565
+
566
+ A `ThemeDefinition` requires an `id`, `name`, `description`, a `light` flag, and a complete `tokens` object. Provide all keys to ensure full coverage across every UI surface.
567
+
568
+ ```ts
569
+ import type { ThemeDefinition } from 'syncline-editor';
570
+
571
+ const myTheme: ThemeDefinition = {
572
+ id: 'my-purple',
573
+ name: 'My Purple',
574
+ description: 'Custom purple dark theme',
575
+ light: false,
576
+ tokens: {
577
+ // ── Backgrounds ────────────────────────────────────────────
578
+ bg0: '#0a0a14', bg1: '#0f0f1a', bg2: '#141420',
579
+ bg3: '#1a1a28', bg4: '#20203a',
580
+
581
+ // ── Borders ────────────────────────────────────────────────
582
+ border: 'rgba(255,255,255,.08)',
583
+ border2: 'rgba(255,255,255,.14)',
584
+ border3: 'rgba(255,255,255,.22)',
585
+
586
+ // ── Text ───────────────────────────────────────────────────
587
+ text: '#e0deff', text2: '#8080c0', text3: '#404060',
588
+
589
+ // ── Accent ─────────────────────────────────────────────────
590
+ accent: '#ff79c6', accent2: '#6644aa',
591
+
592
+ // ── Semantic colours ───────────────────────────────────────
593
+ green: '#50fa7b', orange: '#ffb86c',
594
+ purple: '#bd93f9', red: '#ff5555', yellow: '#f1fa8c',
595
+
596
+ // ── Cursor ─────────────────────────────────────────────────
597
+ cur: '#ff79c6', curGlow: 'rgba(255,121,198,.55)',
598
+
599
+ // ── Editor surface ─────────────────────────────────────────
600
+ curLineBg: 'rgba(255,121,198,.04)',
601
+ curLineGutter: '#161628',
602
+ gutterBg: '#0a0a14',
603
+ gutterHover: '#161628',
604
+ gutterBorder: 'rgba(255,255,255,.06)',
605
+ gutterNum: '#404060',
606
+ gutterNumAct: '#8080c0',
607
+
608
+ // ── Highlights ─────────────────────────────────────────────
609
+ selBg: 'rgba(189,147,249,.25)',
610
+ wordHlBg: 'rgba(255,121,198,.07)',
611
+ wordHlBorder: 'rgba(255,121,198,.25)',
612
+ bmBorder: 'rgba(255,121,198,.65)',
613
+ foldBg: 'rgba(255,121,198,.07)',
614
+ foldBorder: 'rgba(255,121,198,.30)',
615
+
616
+ // ── Find bar ───────────────────────────────────────────────
617
+ findBg: 'rgba(241,250,140,.14)',
618
+ findBorder: 'rgba(241,250,140,.52)',
619
+ findCurBg: '#f1fa8c',
620
+ findCurBorder: '#d4dc50',
621
+ findCurText: '#282a36',
622
+
623
+ // ── Sidebar ────────────────────────────────────────────────
624
+ fileActiveBg: 'rgba(255,121,198,.12)',
625
+ fileActiveText: '#ff79c6',
626
+
627
+ // ── Minimap ────────────────────────────────────────────────
628
+ mmBg: '#0a0a14',
629
+ mmSlider: 'rgba(255,255,255,.07)',
630
+ mmDim: 'rgba(0,0,0,.32)',
631
+ mmEdge: 'rgba(255,255,255,.20)',
632
+
633
+ // ── Misc ───────────────────────────────────────────────────
634
+ indentGuide: 'rgba(255,255,255,.07)',
635
+
636
+ // ── Syntax token colours ───────────────────────────────────
637
+ tokKw: '#ff79c6', tokStr: '#f1fa8c',
638
+ tokCmt: '#6272a4', tokFn: '#50fa7b',
639
+ tokNum: '#bd93f9', tokCls: '#8be9fd',
640
+ tokOp: '#f8f8f2', tokTyp: '#8be9fd',
641
+ tokDec: '#ffb86c',
642
+ },
643
+ };
644
+
645
+ // Register once, then switch by ID from anywhere
646
+ editor.registerTheme(myTheme);
647
+ editor.setTheme('my-purple');
648
+ ```
649
+
650
+ ### Extending a Built-in Theme
651
+
652
+ Spread an existing theme's tokens and override only the fields you need:
653
+
654
+ ```ts
655
+ import { THEME_DRACULA } from 'syncline-editor';
656
+
657
+ editor.registerTheme({
658
+ id: 'dracula-tweaked',
659
+ name: 'Dracula (tweaked)',
660
+ description: 'Dracula with warmer keyword and string colours',
661
+ light: false,
662
+ tokens: {
663
+ ...THEME_DRACULA.tokens, // inherit everything
664
+ tokKw: '#ffb86c', // orange keywords instead of pink
665
+ tokStr: '#f1fa8c', // yellow strings instead of green
666
+ },
667
+ });
668
+
669
+ editor.setTheme('dracula-tweaked');
670
+ ```
671
+
672
+ ### ThemeTokens Reference
673
+
674
+ Every key maps to a `--<name>` CSS custom property on the editor's Shadow DOM host.
675
+
676
+ | Group | Keys |
677
+ |---|---|
678
+ | **Backgrounds** | `bg0` `bg1` `bg2` `bg3` `bg4` |
679
+ | **Borders** | `border` `border2` `border3` |
680
+ | **Text** | `text` `text2` `text3` |
681
+ | **Accent** | `accent` `accent2` |
682
+ | **Semantic** | `green` `orange` `purple` `red` `yellow` |
683
+ | **Cursor** | `cur` `curGlow` |
684
+ | **Editor surface** | `curLineBg` `curLineGutter` `gutterBg` `gutterHover` `gutterBorder` `gutterNum` `gutterNumAct` |
685
+ | **Highlights** | `selBg` `wordHlBg` `wordHlBorder` `bmBorder` `foldBg` `foldBorder` |
686
+ | **Find bar** | `findBg` `findBorder` `findCurBg` `findCurBorder` `findCurText` |
687
+ | **Sidebar** | `fileActiveBg` `fileActiveText` |
688
+ | **Minimap** | `mmBg` `mmSlider` `mmDim` `mmEdge` |
689
+ | **Misc** | `indentGuide` |
690
+ | **Syntax token colours** | `tokKw` `tokStr` `tokCmt` `tokFn` `tokNum` `tokCls` `tokOp` `tokTyp` `tokDec` |
691
+
692
+ ---
693
+
694
+ ## Autocomplete
695
+
696
+ The popup opens when the user has typed at least `autocompletePrefixLength` characters (default `2`) and shows up to `maxCompletions` items (default `14`).
697
+
698
+ > Emmet abbreviations also surface in the same popup with an **E** badge whenever the current prefix matches a valid abbreviation.
699
+
700
+ ### The Unified `completions` Array
701
+
702
+ All custom completions — regular symbols, snippets, language-filtered items — go into a single `completions` array and are differentiated by `kind`:
703
+
704
+ ```ts
705
+ import type { CompletionItem } from 'syncline-editor';
706
+
707
+ createEditor(container, {
708
+ language: 'typescript',
709
+ completions: [
710
+ // Regular function symbol — shows description on the right panel when selected
711
+ {
712
+ label: 'fetchUser',
713
+ kind: 'fn',
714
+ detail: '(id: string) => Promise<User>',
715
+ description: 'Fetches a user by ID from the REST API.\n\nReturns `null` if the user does not exist.',
716
+ },
717
+
718
+ // Type symbol
719
+ {
720
+ label: 'UserStatus',
721
+ kind: 'typ',
722
+ detail: 'enum',
723
+ description: 'Represents the current state of a user account.\n\n`active` | `suspended` | `pending`',
724
+ },
725
+
726
+ // Snippet — kind: "snip" + body template
727
+ {
728
+ label: 'mycomp',
729
+ kind: 'snip',
730
+ detail: 'React component scaffold',
731
+ description: 'Scaffolds a named React functional component with JSX return.',
732
+ body: 'export function $1Component() {\n return (\n <div>\n $2\n </div>\n );\n}',
733
+ language: ['typescript'], // only show in TypeScript files
734
+ },
735
+ ],
736
+ });
737
+ ```
738
+
739
+ Update completions at runtime (takes effect on the next popup open):
740
+
741
+ ```ts
742
+ editor.updateConfig({ completions: updatedItems });
743
+ ```
744
+
745
+ ### CompletionItem Reference
746
+
747
+ ```ts
748
+ interface CompletionItem {
749
+ label: string; // text inserted on accept; used for prefix matching
750
+ kind: CompletionKind; // badge type — see table below
751
+ detail?: string; // short hint shown on the right of the popup row
752
+ description?: string; // full docs shown in the side panel when selected
753
+ body?: string; // snippet template; Tab/Enter expands it when set
754
+ language?: string | string[]; // restrict to specific language(s); omit = all
755
+ }
756
+ ```
757
+
758
+ | `kind` | Badge | Intended use |
759
+ |---|---|---|
760
+ | `'kw'` | **K** | Language keyword |
761
+ | `'fn'` | **f** | Function, method, CSS function |
762
+ | `'typ'` | **T** | Type, interface, enum |
763
+ | `'cls'` | **C** | Class name |
764
+ | `'var'` | **·** | Variable, CSS property, in-file word |
765
+ | `'snip'` | **S** | Snippet — set `body` and accepting it expands the template |
766
+ | `'emmet'` | **E** | Emmet abbreviation (auto-generated, not user-defined) |
767
+
768
+ ### Description Panel
769
+
770
+ When a selected item has a `description`, a VS Code-style documentation panel opens **to the right** of the suggestion list. For `'snip'` items without an explicit `description`, the body template is shown as a preview automatically.
771
+
772
+ ```ts
773
+ {
774
+ label: 'useState',
775
+ kind: 'fn',
776
+ detail: '<S>(initialState: S) => [S, Dispatch<SetStateAction<S>>]',
777
+ description: 'Returns a stateful value and a setter function.\n\n' +
778
+ 'During the initial render the state equals `initialState`.\n\n' +
779
+ 'The setter function updates the state and triggers a re-render.',
780
+ }
781
+ ```
782
+
783
+ Multi-line descriptions use `\n` for line breaks. Markdown is not rendered — plain text only.
784
+
785
+ ### Replace Built-in Completions — `replaceBuiltins`
786
+
787
+ Set `replaceBuiltins: true` to suppress all built-in language keywords and show **only** your `completions`. Perfect for DSL editors where language noise would be confusing:
788
+
789
+ ```ts
790
+ // SQL editor — hides all JS/TS keywords, shows only SQL
791
+ createEditor(container, {
792
+ language: 'text',
793
+ replaceBuiltins: true,
794
+ completions: [
795
+ { label: 'SELECT', kind: 'kw', detail: 'SQL', description: 'Retrieve rows from a table.' },
796
+ { label: 'FROM', kind: 'kw', detail: 'SQL' },
797
+ { label: 'WHERE', kind: 'kw', detail: 'SQL', description: 'Filter rows by a condition.' },
798
+ { label: 'JOIN', kind: 'kw', detail: 'SQL' },
799
+ { label: 'GROUP BY', kind: 'kw', detail: 'SQL' },
800
+ { label: 'ORDER BY', kind: 'kw', detail: 'SQL' },
801
+ { label: 'LIMIT', kind: 'kw', detail: 'SQL', description: 'Limit the number of rows returned.' },
802
+ ],
803
+ });
804
+ ```
805
+
806
+ ### Popup Size
807
+
808
+ ```ts
809
+ createEditor(container, {
810
+ maxCompletions: 20, // max items shown (default 14)
811
+ autocompletePrefixLength: 1, // trigger after 1 char (default 2)
812
+ });
813
+ ```
814
+
815
+ ### Priority Order
816
+
817
+ When multiple sources are configured, the final suggestion list is built in this order:
818
+
819
+ 1. `provideCompletions` callback — if it returns a non-null array, it **wins entirely** and all other sources are skipped
820
+ 2. `completions` with `replaceBuiltins: true` — your items replace language defaults
821
+ 3. `completions` (default) — merged on top of language defaults
822
+ 4. Language built-ins — keywords, types, and functions for the active language
823
+ 5. In-file words — identifiers extracted from the current document, always appended last
824
+
825
+ ---
826
+
827
+ ## Hover Documentation
828
+
829
+ When `hover: true` (the default), resting the pointer over a recognised identifier for ~500 ms shows a floating tooltip with a **title**, **type signature**, and **description** — exactly like VS Code's hover.
830
+
831
+ ### How the lookup works (priority order)
832
+
833
+ 1. **Built-in docs** — ~75 entries covering common JS/TS APIs (`console.log`, `Math.floor`, `Promise.all`, `fetch`, `async`, `const`, utility types like `Record`, `Partial`, …)
834
+ 2. **`completions` array** — any item in your `completions` config that has a `description` (and/or `detail`) is automatically available as a hover doc. No extra config needed.
835
+ 3. **`provideHover` callback** — a runtime function you supply for anything not covered above.
836
+
837
+ ### Hover from `completions` — automatic
838
+
839
+ If you already define completions with descriptions, hover just works:
840
+
841
+ ```ts
842
+ const editor = createEditor(container, {
843
+ completions: [
844
+ {
845
+ label: 'myQuery',
846
+ kind: 'fn',
847
+ detail: '(id: string) => Promise<User>', // → shown as type signature
848
+ description: 'Fetches a user by their ID.\n\nReturns null if the user does not exist.', // → shown as body
849
+ },
850
+ {
851
+ label: 'MyEntity',
852
+ kind: 'cls',
853
+ detail: 'class MyEntity',
854
+ description: 'The core domain model. Includes all persistence and validation logic.',
855
+ },
856
+ ],
857
+ });
858
+ // Hovering over "myQuery" or "MyEntity" in the editor now shows a tooltip automatically.
859
+ ```
860
+
861
+ ### `provideHover` — fully custom docs
862
+
863
+ Use `provideHover` for symbols that live in an external symbol table, API schema, or documentation database:
864
+
865
+ ```ts
866
+ const editor = createEditor(container, {
867
+ provideHover: (ctx) => {
868
+ // ctx = { word, row, col, line, language, doc }
869
+
870
+ // Example: look up from a GraphQL schema
871
+ const field = mySchema.getField(ctx.word);
872
+ if (!field) return null; // return null to show nothing
873
+
874
+ return {
875
+ title: field.name,
876
+ type: field.type, // e.g. "String!"
877
+ body: field.description, // e.g. "The unique identifier of this node."
878
+ };
879
+ },
880
+ });
881
+ ```
882
+
883
+ ### `HoverDoc` reference
884
+
885
+ ```ts
886
+ interface HoverDoc {
887
+ title: string; // Bold name at the top of the tooltip
888
+ type?: string; // Type signature in monospace (optional)
889
+ body: string; // Description text
890
+ }
891
+ ```
892
+
893
+ ### `HoverContext` reference
894
+
895
+ ```ts
896
+ interface HoverContext {
897
+ word: string; // The identifier under the pointer (may include dot prefix, e.g. "console.log")
898
+ row: number; // Zero-based document line
899
+ col: number; // Zero-based character column
900
+ line: string; // Full text of the hovered line
901
+ language: string; // Active language (e.g. "typescript")
902
+ doc: readonly string[]; // Full document as array of lines
903
+ }
904
+ ```
905
+
906
+ ### Disable hover entirely
907
+
908
+ ```ts
909
+ editor.updateConfig({ hover: false });
910
+ ```
911
+
912
+ ---
913
+
914
+ ## Snippets
915
+
916
+ Snippets let you type a short trigger word and press `Tab` to expand a full multi-line block. The cursor lands at the first `$1` tab stop after expansion.
917
+
918
+ Snippets appear in the unified autocomplete popup with an **S** badge alongside keywords, functions, and Emmet abbreviations. When a snippet item is selected in the popup, its body template is previewed in the description panel on the right.
919
+
920
+ ### Built-in Snippets by Language
921
+
922
+ **TypeScript / JavaScript**
923
+
924
+ | Trigger | Expands to |
925
+ |---|---|
926
+ | `fn` | `function name(params) { }` |
927
+ | `afn` | `const name = (params) => { };` |
928
+ | `asyncfn` | `async function name(params): Promise<T> { }` |
929
+ | `cl` | `class Name { constructor(params) { } }` |
930
+ | `forof` | `for (const item of iterable) { }` |
931
+ | `forin` | `for (const key in object) { }` |
932
+ | `trycatch` | `try { } catch (error) { }` |
933
+ | `promise` | `new Promise<T>((resolve, reject) => { })` |
934
+ | `imp` | `import { name } from 'module';` |
935
+ | `iface` | `interface Name { field: Type; }` *(TypeScript only)* |
936
+ | `ife` | Immediately-invoked function expression |
937
+ | `sw` | `switch (expr) { case val: … default: … }` |
938
+
939
+ **CSS**
940
+
941
+ | Trigger | Expands to |
942
+ |---|---|
943
+ | `flex` | Flexbox container (`display: flex; align-items; justify-content`) |
944
+ | `grid` | CSS grid (`display: grid; grid-template-columns; gap`) |
945
+ | `media` | `@media (max-width: 768px) { }` |
946
+ | `anim` | `@keyframes name { from { } to { } }` |
947
+ | `var` | `--name: value;` |
948
+
949
+ **HTML / General** *(available in all languages)*
950
+
951
+ | Trigger | Expands to |
952
+ |---|---|
953
+ | `accordion` | `<details><summary>…</summary><div>…</div></details>` |
954
+ | `card` | Card with header, body, and footer divs |
955
+ | `navbar` | `<nav>` with anchor links |
956
+ | `modal` | Modal dialog with header, body, and footer |
957
+ | `table` | `<table>` with `<thead>` and `<tbody>` |
958
+
959
+ ### Custom Snippets
960
+
961
+ Add your own snippets to the unified `completions` array with `kind: 'snip'` and a `body` template. They appear in the popup with an **S** badge and also expand on Tab when the label is typed exactly:
962
+
963
+ ```ts
964
+ import type { CompletionItem } from 'syncline-editor';
965
+
966
+ createEditor(container, {
967
+ language: 'typescript',
968
+ completions: [
969
+ {
970
+ label: 'rcomp',
971
+ kind: 'snip',
972
+ detail: 'React component',
973
+ description: 'Scaffolds a typed React functional component.',
974
+ body: 'export function $1({ $2 }: $1Props) {\n return (\n <div>\n $3\n </div>\n );\n}',
975
+ language: ['typescript'],
976
+ },
977
+ {
978
+ label: 'clog',
979
+ kind: 'snip',
980
+ detail: 'console.log with label',
981
+ body: "console.log('$1:', $2);",
982
+ },
983
+ {
984
+ label: 'todo',
985
+ kind: 'snip',
986
+ detail: 'TODO comment',
987
+ body: '// TODO($1): $2',
988
+ },
989
+ ],
990
+ });
991
+
992
+ // Add more at runtime
993
+ editor.updateConfig({ completions: [...existing, newItem] });
994
+ ```
995
+
996
+ Disable all snippet expansion (built-in and custom):
997
+
998
+ ```ts
999
+ createEditor(container, { snippetExpansion: false });
1000
+ ```
1001
+
1002
+ ### Snippet Body Syntax
1003
+
1004
+ | Placeholder | Meaning |
1005
+ |---|---|
1006
+ | `$1` | First cursor stop — where the caret lands immediately after expansion |
1007
+ | `$2`, `$3`, … | Additional tab stops (rendered as visual hints; no cycling yet) |
1008
+ | `\n` | Line break — continuation lines inherit the trigger line's indentation |
1009
+
1010
+ > **Priority:** Emmet is tried first on Tab. If both Emmet and a snippet match the same word, Emmet wins.
1011
+
1012
+ ---
1013
+
1014
+ ## Emmet
1015
+
1016
+ Type an abbreviation and press `Tab` to expand. A preview tooltip appears above the cursor while a valid abbreviation is detected.
1017
+
1018
+ ```
1019
+ ul>li*5 → <ul><li></li> × 5</ul>
1020
+ div.container → <div class="container"></div>
1021
+ input[type=text] → <input type="text">
1022
+ a:href → <a href=""></a>
1023
+ section>h2+p → <section><h2></h2><p></p></section>
1024
+ ```
1025
+
1026
+ Emmet abbreviations also appear in the autocomplete popup with an **E** badge — select and press `Tab` or `Enter` to expand.
1027
+
1028
+ ```ts
1029
+ createEditor(container, { emmet: false }); // disable
1030
+ ```
1031
+
1032
+ ---
1033
+
1034
+ ## Dynamic Completion Provider
1035
+
1036
+ Use `provideCompletions` for fully context-aware completions that change based on the cursor position, current prefix, or document content. When this callback returns a non-null array, it **completely overrides** all other completion sources.
1037
+
1038
+ ### provideCompletions
1039
+
1040
+ ```ts
1041
+ import type { CompletionContext, CompletionItem } from 'syncline-editor';
1042
+
1043
+ createEditor(container, {
1044
+ provideCompletions: (ctx: CompletionContext): CompletionItem[] | null => {
1045
+ // ctx.prefix — characters typed before the cursor on the current line
1046
+ // ctx.language — active language string ('typescript', 'css', etc.)
1047
+ // ctx.line — zero-based cursor row
1048
+ // ctx.col — zero-based cursor column
1049
+ // ctx.doc — full document as string[] (one element per line)
1050
+
1051
+ // Example: variable completions after "$"
1052
+ if (ctx.prefix.startsWith('$')) {
1053
+ return myVariableTable.map(v => ({
1054
+ label: v.name,
1055
+ kind: 'var' as const,
1056
+ detail: v.type,
1057
+ description: v.docs,
1058
+ }));
1059
+ }
1060
+
1061
+ // Example: import suggestions inside import statements
1062
+ const importLine = ctx.doc[ctx.line];
1063
+ if (importLine.startsWith('import ') && ctx.prefix.length >= 1) {
1064
+ return myModuleList.map(m => ({
1065
+ label: m.name,
1066
+ kind: 'cls' as const,
1067
+ detail: m.version,
1068
+ }));
1069
+ }
1070
+
1071
+ // Return null to fall through to built-in completions
1072
+ return null;
1073
+ },
1074
+ });
1075
+ ```
1076
+
1077
+ Update the provider at runtime:
1078
+
1079
+ ```ts
1080
+ editor.updateConfig({
1081
+ provideCompletions: (ctx) => newProvider(ctx),
1082
+ });
1083
+ ```
1084
+
1085
+ ### CompletionContext
1086
+
1087
+ ```ts
1088
+ interface CompletionContext {
1089
+ prefix: string; // characters typed before the cursor (the match prefix)
1090
+ language: string; // active language ID
1091
+ line: number; // cursor row, zero-based
1092
+ col: number; // cursor column, zero-based
1093
+ doc: string[]; // full document split into lines
1094
+ }
1095
+ ```
1096
+
1097
+ ---
1098
+
1099
+ ## Events & Callbacks
1100
+
1101
+ All callbacks are defined in `EditorConfig` and can be updated via `updateConfig` at any time:
1102
+
1103
+ ```ts
1104
+ const editor = createEditor(container, {
1105
+ onChange: (value) => {
1106
+ // Fires after every edit — keystroke, paste, undo, setValue(), …
1107
+ // value: full document as a newline-joined string
1108
+ autoSave(value);
1109
+ },
1110
+
1111
+ onCursorChange: (pos) => {
1112
+ // pos.row and pos.col are zero-based
1113
+ statusEl.textContent = `Ln ${pos.row + 1}, Col ${pos.col + 1}`;
1114
+ },
1115
+
1116
+ onSelectionChange: (sel) => {
1117
+ // sel is null when the selection is cleared
1118
+ if (sel) {
1119
+ const rows = Math.abs(sel.fr - sel.ar) + 1;
1120
+ console.log(`${rows} line(s) selected`);
1121
+ }
1122
+ },
1123
+
1124
+ onFocus: () => container.classList.add('editor-focused'),
1125
+ onBlur: () => container.classList.remove('editor-focused'),
1126
+ });
1127
+
1128
+ // Replace a callback at runtime — no re-creation needed
1129
+ editor.updateConfig({
1130
+ onChange: (value) => newAutoSave(value),
1131
+ });
1132
+ ```
1133
+
1134
+ ---
1135
+
1136
+ ## Advanced Features
1137
+
1138
+ ### Multi-cursor
1139
+
1140
+ | Action | Shortcut |
1141
+ |---|---|
1142
+ | Add cursor at click position | `Alt / Option + Click` |
1143
+ | Select next occurrence of word | `Ctrl / Cmd + D` |
1144
+ | Clear all extra cursors | `Escape` |
1145
+
1146
+ Every cursor has its own independent selection. All cursors type, delete, and move in sync.
1147
+
1148
+ ```ts
1149
+ createEditor(container, { multiCursor: false }); // disable
1150
+ ```
1151
+
1152
+ ### Find & Replace
1153
+
1154
+ Open programmatically:
1155
+
1156
+ ```ts
1157
+ editor.executeCommand('find'); // opens find bar
1158
+ editor.executeCommand('findReplace'); // opens with replace row visible
1159
+ ```
1160
+
1161
+ The find bar supports:
1162
+ - **Match case** — `Aa` toggle
1163
+ - **Regex mode** — `.*` toggle
1164
+ - **Navigate** — `↑` / `↓` buttons or `Enter` / `Shift+Enter`
1165
+ - **Replace one** — replaces the current highlighted match
1166
+ - **Replace all** — replaces every match in the document
1167
+
1168
+ Configuration options:
1169
+
1170
+ ```ts
1171
+ // Disable both find and replace
1172
+ createEditor(container, { find: false });
1173
+
1174
+ // Find only — no replace row
1175
+ createEditor(container, { find: true, findReplace: false });
1176
+ ```
1177
+
1178
+ ### Code Folding
1179
+
1180
+ Click the `▾` / `▸` gutter toggle next to any foldable block. Folded sections show a dashed bottom border and a `…` indicator.
1181
+
1182
+ ```ts
1183
+ createEditor(container, { codeFolding: false }); // disable
1184
+ ```
1185
+
1186
+ ### Bracket Matching
1187
+
1188
+ When the cursor is adjacent to `(`, `)`, `[`, `]`, `{`, or `}`, both the opening and closing characters are highlighted with a border.
1189
+
1190
+ ```ts
1191
+ createEditor(container, { bracketMatching: false }); // disable
1192
+ ```
1193
+
1194
+ Colour controlled by `ThemeTokens.bmBorder`.
1195
+
1196
+ ### Word Highlight
1197
+
1198
+ When the cursor rests on a word, all other occurrences in the document are subtly boxed. Automatically disabled while a selection is active.
1199
+
1200
+ ```ts
1201
+ createEditor(container, { wordHighlight: false }); // disable
1202
+ ```
1203
+
1204
+ Colours: `ThemeTokens.wordHlBg` (fill) and `ThemeTokens.wordHlBorder` (outline).
1205
+
1206
+ ### Whitespace Rendering
1207
+
1208
+ ```ts
1209
+ createEditor(container, { renderWhitespace: 'none' }); // default — invisible
1210
+ createEditor(container, { renderWhitespace: 'boundary' }); // leading/trailing only
1211
+ createEditor(container, { renderWhitespace: 'all' }); // every space and tab
1212
+ ```
1213
+
1214
+ Spaces render as `·` and tabs as `→`.
1215
+
1216
+ ### Cursor Styles
1217
+
1218
+ ```ts
1219
+ createEditor(container, { cursorStyle: 'line' }); // thin vertical beam (default)
1220
+ createEditor(container, { cursorStyle: 'block' }); // filled block behind character
1221
+ createEditor(container, { cursorStyle: 'underline' }); // horizontal bar below character
1222
+
1223
+ createEditor(container, { cursorBlinkRate: 2000 }); // slower blink
1224
+ createEditor(container, { cursorBlinkRate: 999999 }); // no blink
1225
+ ```
1226
+
1227
+ ### Minimap
1228
+
1229
+ Canvas-rendered pixel-accurate overview of the full document with a draggable viewport slider. Scroll, drag the slider, or click anywhere on the minimap to jump.
1230
+
1231
+ ```ts
1232
+ createEditor(container, {
1233
+ showMinimap: true,
1234
+ minimapWidth: 100, // pixels (default 120)
1235
+ });
1236
+ ```
1237
+
1238
+ Colours: `ThemeTokens.mmBg`, `mmSlider`, `mmDim`, `mmEdge`.
1239
+
1240
+ ### Word Wrap
1241
+
1242
+ ```ts
1243
+ createEditor(container, {
1244
+ wordWrap: true,
1245
+ wrapColumn: 100, // default 80
1246
+ });
1247
+
1248
+ // Toggle at runtime — also available via Alt+Z keyboard shortcut
1249
+ editor.executeCommand('toggleWordWrap');
1250
+ editor.updateConfig({ wordWrap: !currentWrap });
1251
+ ```
1252
+
1253
+ ### Indent Guides
1254
+
1255
+ Faint vertical lines connecting matching indentation levels:
1256
+
1257
+ ```ts
1258
+ createEditor(container, { showIndentGuides: false }); // disable
1259
+ ```
1260
+
1261
+ Colour controlled by `ThemeTokens.indentGuide`.
1262
+
1263
+ ### Active Line Highlight
1264
+
1265
+ The row containing the cursor gets a distinct background and gutter colour:
1266
+
1267
+ ```ts
1268
+ createEditor(container, { highlightActiveLine: false }); // disable
1269
+ ```
1270
+
1271
+ Colours: `ThemeTokens.curLineBg` (row background) and `ThemeTokens.curLineGutter` (gutter cell).
1272
+
1273
+ ### Read-Only Mode
1274
+
1275
+ ```ts
1276
+ const viewer = createEditor(container, {
1277
+ value: sourceCode,
1278
+ readOnly: true,
1279
+ });
1280
+
1281
+ // These are all silently no-ops in read-only mode:
1282
+ viewer.insertText('test');
1283
+ viewer.executeCommand('cut');
1284
+ viewer.executeCommand('deleteLine');
1285
+
1286
+ // These still work normally:
1287
+ viewer.getCursor();
1288
+ viewer.getSelection();
1289
+ viewer.executeCommand('copy');
1290
+ viewer.executeCommand('find');
1291
+
1292
+ // Toggle at runtime:
1293
+ editor.updateConfig({ readOnly: false }); // re-enable editing
1294
+ editor.updateConfig({ readOnly: true }); // lock again
1295
+ ```
1296
+
1297
+ ---
1298
+
1299
+ ## Behavioral Options
1300
+
1301
+ ### Auto-Close Pairs
1302
+
1303
+ ```ts
1304
+ // Default — close all bracket and quote types
1305
+ createEditor(container, {
1306
+ autoClosePairs: { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' },
1307
+ });
1308
+
1309
+ // Only parentheses and curly braces
1310
+ createEditor(container, {
1311
+ autoClosePairs: { '(': ')', '{': '}' },
1312
+ });
1313
+
1314
+ // Disable auto-closing entirely
1315
+ createEditor(container, { autoClosePairs: {} });
1316
+ ```
1317
+
1318
+ Typing the opening character inserts the closing character and places the cursor between them. Typing the closing character again skips over it.
1319
+
1320
+ ### Line Comment Token
1321
+
1322
+ Controls the `Ctrl+/` toggle-comment prefix:
1323
+
1324
+ ```ts
1325
+ // Auto-detect from language (default — // for TS/JS/CSS, nothing for JSON/Markdown)
1326
+ createEditor(container, { lineCommentToken: '' });
1327
+
1328
+ // Python / Ruby / YAML / shell
1329
+ createEditor(container, { lineCommentToken: '#' });
1330
+
1331
+ // SQL / Lua
1332
+ createEditor(container, { lineCommentToken: '--' });
1333
+
1334
+ // LaTeX
1335
+ createEditor(container, { lineCommentToken: '%' });
1336
+ ```
1337
+
1338
+ ### Word Separators
1339
+
1340
+ Characters treated as word boundaries for double-click selection, `Ctrl+Left/Right`, and `Ctrl+D`:
1341
+
1342
+ ```ts
1343
+ // Default — use built-in \w word boundary
1344
+ createEditor(container, { wordSeparators: '' });
1345
+
1346
+ // Include hyphens (good for CSS, kebab-case identifiers)
1347
+ createEditor(container, {
1348
+ wordSeparators: '`~!@#%^&*()-=+[{}]\\|;:\'",.<>/?',
1349
+ });
1350
+ ```
1351
+
1352
+ ### Undo Batch Window
1353
+
1354
+ Controls how keystrokes are grouped into undo steps:
1355
+
1356
+ ```ts
1357
+ // Default — group keystrokes within 700 ms into one undo step
1358
+ createEditor(container, { undoBatchMs: 700 });
1359
+
1360
+ // Per-keystroke undo — every character is its own step
1361
+ createEditor(container, { undoBatchMs: 0 });
1362
+
1363
+ // One undo per ~2 seconds of continuous typing
1364
+ createEditor(container, { undoBatchMs: 2000 });
1365
+ ```
1366
+
1367
+ ### Feature Flags
1368
+
1369
+ Disable individual features at creation or toggle them at runtime:
1370
+
1371
+ ```ts
1372
+ createEditor(container, {
1373
+ bracketMatching: false, // no bracket-pair highlight
1374
+ codeFolding: false, // no fold buttons in gutter
1375
+ emmet: false, // no Emmet expansion
1376
+ snippetExpansion: false, // no snippet Tab-expansion
1377
+ autocomplete: false, // no completion popup
1378
+ multiCursor: false, // no Alt+Click / Ctrl+D
1379
+ find: false, // no find bar
1380
+ findReplace: false, // no replace row
1381
+ wordSelection: false, // double-click places cursor only
1382
+ wordHighlight: false, // no occurrence boxes
1383
+ highlightActiveLine: false, // no active-line background
1384
+ showIndentGuides: false, // no indent guide lines
1385
+ showGutter: false, // hide line numbers
1386
+ showMinimap: false, // hide minimap panel
1387
+ showStatusBar: false, // hide status bar
1388
+ });
1389
+ ```
1390
+
1391
+ ---
1392
+
1393
+ ## Keyboard Shortcuts
1394
+
1395
+ | Shortcut | Action | Controlled by |
1396
+ |---|---|---|
1397
+ | `Ctrl / Cmd + Z` | Undo | `undoBatchMs`, `maxUndoHistory` |
1398
+ | `Ctrl / Cmd + Y` or `Ctrl + Shift + Z` | Redo | — |
1399
+ | `Ctrl / Cmd + A` | Select all | — |
1400
+ | `Ctrl / Cmd + C` | Copy | — |
1401
+ | `Ctrl / Cmd + X` | Cut | `readOnly` |
1402
+ | `Ctrl / Cmd + V` | Paste | `readOnly` |
1403
+ | `Ctrl / Cmd + F` | Open find bar | `find` |
1404
+ | `Ctrl / Cmd + H` | Open find + replace | `findReplace` |
1405
+ | `Ctrl / Cmd + D` | Select next occurrence | `multiCursor` |
1406
+ | `Ctrl / Cmd + Shift + D` | Duplicate line | `readOnly` |
1407
+ | `Ctrl / Cmd + K` | Delete line | `readOnly` |
1408
+ | `Ctrl / Cmd + /` | Toggle line comment | `lineCommentToken`, `readOnly` |
1409
+ | `Alt / Option + Z` | Toggle word wrap | — |
1410
+ | `Alt / Option + ↑` | Move current line (or selected block) up | `readOnly` |
1411
+ | `Alt / Option + ↓` | Move current line (or selected block) down | `readOnly` |
1412
+ | `Tab` | Indent / expand Emmet / expand snippet | `emmet`, `snippetExpansion`, `tabSize` |
1413
+ | `Shift + Tab` | Outdent | `tabSize` |
1414
+ | `Alt / Option + Click` | Add extra cursor | `multiCursor` |
1415
+ | `Double-click` | Select word | `wordSelection`, `wordSeparators` |
1416
+ | `Triple-click` | Select entire line | — |
1417
+ | `Escape` | Clear selection · close popup · close find · remove extra cursors | — |
1418
+ | `↑ ↓ ← →` | Move cursor | — |
1419
+ | `Cmd + ← →` (Mac) | Start / end of line | — |
1420
+ | `Cmd + ↑ ↓` (Mac) | Start / end of document | — |
1421
+ | `Ctrl + ← →` (Win) | Word skip left / right | `wordSeparators` |
1422
+ | `Ctrl + Home / End` (Win) | Start / end of document | — |
1423
+ | `Option + ← →` (Mac) | Word skip left / right | `wordSeparators` |
1424
+ | `Shift + arrow keys` | Extend selection | — |
1425
+ | `Shift + Cmd/Ctrl + arrow` | Extend selection to boundary | — |
1426
+ | `Home` | First non-whitespace (press again = column 0) | — |
1427
+ | `End` | End of line | — |
1428
+ | `PageUp / PageDown` | Scroll one viewport | — |
1429
+ | `Backspace` | Delete character left | `readOnly` |
1430
+ | `Delete` | Delete character right | `readOnly` |
1431
+
1432
+ All mutating shortcuts are silently blocked when `readOnly: true`. Navigation, selection, and `Ctrl+C` always work.
1433
+
1434
+ ---
1435
+
1436
+ ## Recipes
1437
+
1438
+ Real-world patterns you can copy and adapt.
1439
+
1440
+ ### Monaco-style Editor Embed
1441
+
1442
+ A feature-complete editor inside a fixed-height container, closely resembling a VS Code embed:
1443
+
1444
+ ```ts
1445
+ import { createEditor } from 'syncline-editor';
1446
+
1447
+ const editor = createEditor(document.getElementById('editor')!, {
1448
+ value: initialCode,
1449
+ language: 'typescript',
1450
+ theme: 'vscode-dark',
1451
+ fontSize: 13,
1452
+ lineHeight: 20,
1453
+ fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
1454
+ tabSize: 2,
1455
+ insertSpaces: true,
1456
+ wordWrap: false,
1457
+ showMinimap: true,
1458
+ showGutter: true,
1459
+ showStatusBar: true,
1460
+ showIndentGuides: true,
1461
+ bracketMatching: true,
1462
+ codeFolding: true,
1463
+ emmet: true,
1464
+ autocomplete: true,
1465
+ multiCursor: true,
1466
+ find: true,
1467
+ findReplace: true,
1468
+ wordHighlight: true,
1469
+ highlightActiveLine: true,
1470
+ renderWhitespace: 'boundary',
1471
+ onChange: (v) => localStorage.setItem('draft', v),
1472
+ onFocus: () => console.log('editor focused'),
1473
+ });
1474
+ ```
1475
+
1476
+ ```css
1477
+ #editor { width: 100%; height: 100vh; }
1478
+ ```
1479
+
1480
+ ---
1481
+
1482
+ ### DSL / SQL Editor
1483
+
1484
+ Replace all built-in completions with domain-specific keywords — zero JS/TS noise:
1485
+
1486
+ ```ts
1487
+ import { createEditor } from 'syncline-editor';
1488
+ import type { CompletionItem } from 'syncline-editor';
1489
+
1490
+ const sqlCompletions: CompletionItem[] = [
1491
+ { label: 'SELECT', kind: 'kw', detail: 'SQL', description: 'Retrieve rows from a table or view.' },
1492
+ { label: 'FROM', kind: 'kw', detail: 'SQL', description: 'Specify the source table.' },
1493
+ { label: 'WHERE', kind: 'kw', detail: 'SQL', description: 'Filter rows using a predicate.' },
1494
+ { label: 'JOIN', kind: 'kw', detail: 'SQL' },
1495
+ { label: 'LEFT JOIN', kind: 'kw', detail: 'SQL' },
1496
+ { label: 'INNER JOIN', kind: 'kw', detail: 'SQL' },
1497
+ { label: 'GROUP BY', kind: 'kw', detail: 'SQL' },
1498
+ { label: 'ORDER BY', kind: 'kw', detail: 'SQL' },
1499
+ { label: 'HAVING', kind: 'kw', detail: 'SQL' },
1500
+ { label: 'LIMIT', kind: 'kw', detail: 'SQL' },
1501
+ { label: 'OFFSET', kind: 'kw', detail: 'SQL' },
1502
+ { label: 'INSERT INTO', kind: 'kw', detail: 'SQL' },
1503
+ { label: 'UPDATE', kind: 'kw', detail: 'SQL' },
1504
+ { label: 'DELETE FROM', kind: 'kw', detail: 'SQL' },
1505
+ { label: 'COUNT', kind: 'fn', detail: 'aggregate', description: 'COUNT(*) — number of rows.' },
1506
+ { label: 'SUM', kind: 'fn', detail: 'aggregate' },
1507
+ { label: 'AVG', kind: 'fn', detail: 'aggregate' },
1508
+ { label: 'MAX', kind: 'fn', detail: 'aggregate' },
1509
+ { label: 'MIN', kind: 'fn', detail: 'aggregate' },
1510
+ ];
1511
+
1512
+ createEditor(container, {
1513
+ language: 'text',
1514
+ replaceBuiltins: true,
1515
+ completions: sqlCompletions,
1516
+ lineCommentToken: '--',
1517
+ theme: 'vscode-dark',
1518
+ });
1519
+ ```
1520
+
1521
+ ---
1522
+
1523
+ ### Read-Only Code Viewer
1524
+
1525
+ A zero-interaction code display with syntax highlighting, no editing, no cursors:
1526
+
1527
+ ```ts
1528
+ createEditor(container, {
1529
+ value: sourceCode,
1530
+ language: 'typescript',
1531
+ theme: 'github-light',
1532
+ readOnly: true,
1533
+ autocomplete: false,
1534
+ emmet: false,
1535
+ snippetExpansion: false,
1536
+ find: false,
1537
+ findReplace: false,
1538
+ multiCursor: false,
1539
+ codeFolding: false,
1540
+ showStatusBar: false,
1541
+ showMinimap: false,
1542
+ highlightActiveLine: false,
1543
+ wordHighlight: false,
1544
+ bracketMatching: false,
1545
+ cursorBlinkRate: 999999,
1546
+ });
1547
+ ```
1548
+
1549
+ ---
1550
+
1551
+ ### Custom Theme from Scratch (Recipe)
1552
+
1553
+ Minimal theme extending Dracula with a brand accent colour:
1554
+
1555
+ ```ts
1556
+ import { THEME_DRACULA } from 'syncline-editor';
1557
+
1558
+ const brandTheme = {
1559
+ id: 'brand-dark',
1560
+ name: 'Brand Dark',
1561
+ description: 'Company brand colour scheme',
1562
+ light: false,
1563
+ tokens: {
1564
+ ...THEME_DRACULA.tokens,
1565
+ accent: '#00C2FF', // brand blue accent
1566
+ accent2: '#005F7A',
1567
+ cur: '#00C2FF',
1568
+ curGlow: 'rgba(0,194,255,.45)',
1569
+ selBg: 'rgba(0,194,255,.20)',
1570
+ wordHlBorder: 'rgba(0,194,255,.35)',
1571
+ tokKw: '#00C2FF', // brand-coloured keywords
1572
+ },
1573
+ };
1574
+
1575
+ editor.registerTheme(brandTheme);
1576
+ editor.setTheme('brand-dark');
1577
+ ```
1578
+
1579
+ ---
1580
+
1581
+ ### Framework-aware Autocomplete
1582
+
1583
+ Use `provideCompletions` to serve different items based on context — React hooks inside `.tsx`, lifecycle methods inside class components, etc.:
1584
+
1585
+ ```ts
1586
+ import type { CompletionContext, CompletionItem } from 'syncline-editor';
1587
+
1588
+ const reactHooks: CompletionItem[] = [
1589
+ { label: 'useState', kind: 'fn', detail: 'React hook', description: 'Adds local state to a functional component.' },
1590
+ { label: 'useEffect', kind: 'fn', detail: 'React hook', description: 'Run side-effects after render.' },
1591
+ { label: 'useCallback', kind: 'fn', detail: 'React hook' },
1592
+ { label: 'useMemo', kind: 'fn', detail: 'React hook' },
1593
+ { label: 'useRef', kind: 'fn', detail: 'React hook' },
1594
+ { label: 'useContext', kind: 'fn', detail: 'React hook' },
1595
+ ];
1596
+
1597
+ createEditor(container, {
1598
+ language: 'typescript',
1599
+ provideCompletions: (ctx: CompletionContext): CompletionItem[] | null => {
1600
+ // Serve React hooks when prefix starts with "use"
1601
+ if (ctx.prefix.startsWith('use')) {
1602
+ return reactHooks.filter(h => h.label.startsWith(ctx.prefix));
1603
+ }
1604
+ return null; // fall through to built-in completions
1605
+ },
1606
+ });
1607
+ ```
1608
+
1609
+ ---
1610
+
1611
+ ### Auto-Save with Debounce
1612
+
1613
+ Debounce `onChange` so the backend isn't hammered on every keystroke:
1614
+
1615
+ ```ts
1616
+ function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T {
1617
+ let timer: ReturnType<typeof setTimeout>;
1618
+ return ((...args) => {
1619
+ clearTimeout(timer);
1620
+ timer = setTimeout(() => fn(...args), ms);
1621
+ }) as T;
1622
+ }
1623
+
1624
+ const save = debounce(async (value: string) => {
1625
+ await fetch('/api/save', {
1626
+ method: 'POST',
1627
+ headers: { 'Content-Type': 'application/json' },
1628
+ body: JSON.stringify({ content: value }),
1629
+ });
1630
+ }, 1000);
1631
+
1632
+ createEditor(container, {
1633
+ onChange: save,
1634
+ });
1635
+ ```
1636
+
1637
+ ---
1638
+
1639
+ ## TypeScript Types
1640
+
1641
+ All types are exported from the package root:
1642
+
1643
+ ```ts
1644
+ import type {
1645
+ // Core
1646
+ EditorConfig,
1647
+ EditorAPI,
1648
+ Language,
1649
+
1650
+ // Positions
1651
+ CursorPosition,
1652
+ Selection,
1653
+ FindMatch,
1654
+ ExtraCursor,
1655
+
1656
+ // Autocomplete
1657
+ CompletionItem,
1658
+ CompletionKind,
1659
+ CompletionContext,
1660
+
1661
+ // Token colours
1662
+ TokenColors,
1663
+
1664
+ // Themes
1665
+ ThemeDefinition,
1666
+ ThemeTokens,
1667
+ } from 'syncline-editor';
1668
+ ```
1669
+
1670
+ ### `Language`
1671
+
1672
+ ```ts
1673
+ type Language =
1674
+ | 'typescript'
1675
+ | 'javascript'
1676
+ | 'css'
1677
+ | 'json'
1678
+ | 'markdown'
1679
+ | 'text';
1680
+ ```
1681
+
1682
+ ### `CursorPosition`
1683
+
1684
+ ```ts
1685
+ interface CursorPosition {
1686
+ row: number; // zero-based line index
1687
+ col: number; // zero-based character offset
1688
+ }
1689
+ ```
1690
+
1691
+ ### `Selection`
1692
+
1693
+ ```ts
1694
+ interface Selection {
1695
+ ar: number; // anchor row — where selection started
1696
+ ac: number; // anchor col
1697
+ fr: number; // focus row — where the caret currently sits
1698
+ fc: number; // focus col
1699
+ }
1700
+ ```
1701
+
1702
+ Either end can come before the other — backward selections are valid.
1703
+
1704
+ ### `CompletionItem`
1705
+
1706
+ ```ts
1707
+ interface CompletionItem {
1708
+ label: string; // text inserted on accept; used for prefix matching
1709
+ kind: CompletionKind; // 'kw' | 'fn' | 'typ' | 'cls' | 'var' | 'snip' | 'emmet'
1710
+ detail?: string; // short hint shown on the right of the popup row
1711
+ description?: string; // full docs shown in the side panel when this item is selected
1712
+ body?: string; // snippet template — Tab/Enter expands when set
1713
+ language?: string | string[]; // restrict to specific language(s); omit = all languages
1714
+ }
1715
+ ```
1716
+
1717
+ ### `CompletionKind`
1718
+
1719
+ ```ts
1720
+ type CompletionKind = 'kw' | 'fn' | 'typ' | 'cls' | 'var' | 'snip' | 'emmet';
1721
+ ```
1722
+
1723
+ ### `CompletionContext`
1724
+
1725
+ ```ts
1726
+ interface CompletionContext {
1727
+ prefix: string; // characters before the cursor (the match prefix)
1728
+ language: string; // active language ID
1729
+ line: number; // cursor row (zero-based)
1730
+ col: number; // cursor column (zero-based)
1731
+ doc: string[]; // full document split into lines
1732
+ }
1733
+ ```
1734
+
1735
+ ### `TokenColors`
1736
+
1737
+ ```ts
1738
+ interface TokenColors {
1739
+ keyword?: string; // --tok-kw — if, const, class, interface, @media
1740
+ string?: string; // --tok-str — string and template literals
1741
+ comment?: string; // --tok-cmt — line and block comments
1742
+ function?: string; // --tok-fn — function names, CSS functions
1743
+ number?: string; // --tok-num — numeric literals
1744
+ class?: string; // --tok-cls — class names, constructor calls
1745
+ operator?: string; // --tok-op — +, =>, ===, &&, ?.
1746
+ type?: string; // --tok-typ — type names, built-in types
1747
+ decorator?: string; // --tok-dec — @Component, @Injectable
1748
+ }
1749
+ ```
1750
+
1751
+ Pass an empty string `''` for any field to restore that token to the current theme's default.
1752
+
1753
+ ### `ThemeDefinition`
1754
+
1755
+ ```ts
1756
+ interface ThemeDefinition {
1757
+ id: string; // unique identifier — used with setTheme() and getThemes()
1758
+ name: string; // human-readable display name
1759
+ description: string; // short description
1760
+ light: boolean; // true for light themes (affects status-bar icon tinting)
1761
+ tokens: ThemeTokens; // complete set of CSS variable values
1762
+ }
1763
+ ```
1764
+
1765
+ ---
1766
+
1767
+ ## Project Structure
1768
+
1769
+ ```
1770
+ syncline-editor/
1771
+ ├── src/
1772
+ │ ├── core/
1773
+ │ │ ├── constants.ts # Per-language keyword/type sets + layout constants
1774
+ │ │ ├── document.ts # Document model, undo/redo, selection helpers
1775
+ │ │ ├── tokeniser.ts # Language-aware syntax tokeniser (zero deps)
1776
+ │ │ └── wrap-map.ts # Soft-wrap virtual line map
1777
+ │ ├── features/
1778
+ │ │ ├── autocomplete.ts # Completion engine — ranking, filtering, snippet items
1779
+ │ │ ├── bracket-matcher.ts # Bracket-pair finder
1780
+ │ │ ├── code-folding.ts # Block folding logic
1781
+ │ │ ├── emmet.ts # Emmet abbreviation expander
1782
+ │ │ ├── find-replace.ts # Find / replace engine
1783
+ │ │ ├── multi-cursor.ts # Multi-cursor state manager
1784
+ │ │ ├── snippets.ts # Built-in snippet library (TS/JS, CSS, HTML)
1785
+ │ │ └── word-highlight.ts # Same-word occurrence finder
1786
+ │ ├── renderer/
1787
+ │ │ ├── minimap-renderer.ts # Canvas minimap + scroll calculations
1788
+ │ │ └── row-renderer.ts # Virtual row → HTML string
1789
+ │ ├── themes/
1790
+ │ │ ├── built-in-themes.ts # 6 built-in ThemeDefinition objects
1791
+ │ │ └── theme-manager.ts # Theme registry + CSS variable injection
1792
+ │ ├── types/
1793
+ │ │ └── index.ts # All public TypeScript interfaces and types
1794
+ │ ├── ui/
1795
+ │ │ └── styles.ts # All CSS injected into Shadow DOM
1796
+ │ ├── utils/
1797
+ │ │ ├── dom.ts # DOM helpers
1798
+ │ │ ├── string.ts # String utilities (word boundaries, escaping, …)
1799
+ │ │ └── validation.ts # Config validation helpers
1800
+ │ ├── SynclineEditor.ts # Main editor class — implements EditorAPI
1801
+ │ └── index.ts # Public API barrel export
1802
+ ├── playground/
1803
+ │ └── index.html # Interactive playground (every option exposed)
1804
+ ├── tests/ # Vitest test suite
1805
+ ├── package.json
1806
+ ├── tsconfig.json
1807
+ ├── vite.config.ts # Playground dev server + build
1808
+ ├── vite.lib.config.ts # Library build (ES module + UMD + .d.ts)
1809
+ └── README.md
1810
+ ```
1811
+
1812
+ ---
1813
+
1814
+ ## Development
1815
+
1816
+ ```bash
1817
+ # Install dependencies
1818
+ npm install
1819
+
1820
+ # Start the interactive playground (Vite dev server, hot reload)
1821
+ npm run dev
1822
+
1823
+ # Type-check only (no emit)
1824
+ npm run typecheck
1825
+
1826
+ # Build the playground into dist/
1827
+ npm run build
1828
+
1829
+ # Build the distributable library (ES + UMD + .d.ts)
1830
+ npm run build:lib
1831
+
1832
+ # Preview the built playground
1833
+ npm run preview
1834
+
1835
+ # Run the test suite
1836
+ npm run test
1837
+ ```
1838
+
1839
+ ### Playground
1840
+
1841
+ The playground at `playground/index.html` is a fully self-contained interactive demo with every option wired to live controls:
1842
+
1843
+ - **Theme switcher** — all 6 built-in themes as clickable chips
1844
+ - **Language selector** — TypeScript · JavaScript · CSS · JSON · Markdown · plain text
1845
+ - **Token Colors** — 9 colour pickers with live preview, enable/disable toggle, and reset
1846
+ - **Features** — checkbox for every boolean option
1847
+ - **Typography** — font family, size, line height, cursor style, whitespace rendering
1848
+ - **Layout** — gutter width, minimap width, wrap column
1849
+ - **Autocomplete** — `provideCompletions` code editor, extra keywords/types, unified `completions` JSON editor with live validation, `replaceBuiltins` toggle
1850
+ - **Behavior** — auto-close pairs, line comment token, word separators, undo batch window
1851
+ - **Actions** — buttons for every `executeCommand` and `getValue` / `setValue`
1852
+ - **Event log** — live feed of all `onChange` / `onCursorChange` / `onSelectionChange` / `onFocus` / `onBlur` events
1853
+
1854
+ ---
1855
+
1856
+ ## License
1857
+
1858
+ MIT