@synclineapi/editor
v4.0.3
Published
A zero-dependency, pixel-perfect, fully customisable browser-based code editor
Maintainers
Readme
Syncline Editor
A zero-dependency, pixel-perfect, fully customisable browser-based code editor built as a TypeScript library.
Ships as both an ES module and UMD bundle, runs entirely inside a Shadow DOM, and handles 100,000+ line files via virtual rendering.
Table of Contents
- Features
- Installation
- Quick Start
- Configuration — Full Reference
- Runtime API
- Syntax Highlighting
- Token Colors
- Themes
- Autocomplete
- Snippets
- Emmet
- Dynamic Completion Provider
- Events & Callbacks
- Advanced Features
- Behavioral Options
- Hover Documentation
- Keyboard Shortcuts
- Recipes
- TypeScript Types
- Project Structure
- Development
Features
| Feature | Detail |
|---|---|
| Zero dependencies | No external runtime libraries — one self-contained bundle |
| Virtual rendering | Only visible rows exist in the DOM; handles 100,000+ line files smoothly |
| Shadow DOM | Fully encapsulated styles — no CSS leakage in either direction |
| Dual build | ES module + UMD bundle, full TypeScript declarations |
| Syntax highlighting | TypeScript, JavaScript, CSS, JSON, Markdown — nine token classes |
| Token color overrides | Override individual token colours on top of any theme without replacing it |
| 6 built-in themes | VR Dark, VS Code Dark+, Monokai, Dracula, GitHub Light, Solarized Light |
| Custom themes | Full ThemeDefinition API — every CSS variable exposed |
| Unified autocomplete | One completions array for keywords, snippets, custom symbols, and DSL items |
| VS Code-style docs panel | Description panel beside the popup — shown for items with description |
| Dynamic provider | provideCompletions callback for context-aware, fully runtime-controlled completions |
| Built-in snippets | 17 snippets across TypeScript/JS, CSS, and HTML — Tab-expandable with full tab-stop session ($1 → $2 → …) |
| Emmet expansion | div.wrapper>ul>li*3 → Tab — with inline preview tooltip |
| Multi-cursor | Alt+Click to add cursors; Ctrl+D to select next occurrence |
| Hover documentation | Tooltip on pointer-rest over any identifier — built-in JS/TS/CSS docs + custom provideHover callback |
| Move line | Alt+Up / Alt+Down to move the current line or selected block up/down |
| Duplicate line | Shift+Alt+Down / Shift+Alt+Up to duplicate the current line or selected block — cursor follows the new copy |
| Go to Line | Ctrl+G / Cmd+G prompt to jump to any line number — opt-in via goToLine: true |
| Placeholder | Ghost hint text shown in the editor when the document is empty — set via placeholder option |
| Find & Replace | Literal and regex search, case-sensitive mode, replace one / all |
| Code folding | Collapse {} blocks via gutter toggle |
| Bracket matching | Highlights matching ()[]{} pairs at the cursor |
| Word highlight | All occurrences of the word under the cursor highlighted subtly |
| Active line highlight | Distinct background on the current line and its gutter cell |
| Minimap | Canvas-rendered overview with draggable viewport slider |
| Status bar | Language · line/col · selection · undo depth · word-wrap toggle · theme picker |
| Indent guides | Faint vertical lines at each indentation level |
| Whitespace rendering | Visible · / → for spaces and tabs (none / boundary / all) |
| Cursor styles | line, block, or underline; configurable blink rate |
| Read-only mode | All edits blocked; navigation, selection, and copy still work |
Installation
npm install syncline-editorQuick Start
ES Module
import { createEditor } from 'syncline-editor';
const editor = createEditor(document.getElementById('app')!, {
value: 'const greeting = "Hello, world!";',
language: 'typescript',
theme: 'dracula',
onChange: (value) => console.log('changed:', value.length, 'chars'),
});Give the container an explicit height — the editor fills 100% of its host element:
<div id="app" style="width: 100%; height: 600px;"></div>UMD / CDN
<script src="syncline-editor.umd.js"></script>
<script>
const editor = SynclineEditor.createEditor(document.getElementById('app'), {
value: '// start coding',
language: 'typescript',
theme: 'vscode-dark',
});
</script>Configuration — Full Reference
Pass any subset of EditorConfig to createEditor(). Every field is optional. All options can also be updated at runtime via editor.updateConfig(patch).
Document
| Option | Type | Default | Description |
|---|---|---|---|
| value | string \| string[] | '' | Initial document content. String or pre-split string[]. |
| language | Language | 'typescript' | Syntax highlighting and autocomplete language. |
Display
| Option | Type | Default | Description |
|---|---|---|---|
| showGutter | boolean | true | Show/hide the line-number gutter. |
| showMinimap | boolean | true | Show/hide the minimap panel. |
| showStatusBar | boolean | true | Show/hide the bottom status bar. |
| showIndentGuides | boolean | true | Faint vertical lines at each indentation level. |
| highlightActiveLine | boolean | true | Background tint on the active (cursor) line and gutter cell. |
| wordHighlight | boolean | true | Highlight all other occurrences of the word under the cursor. |
| renderWhitespace | 'none' \| 'boundary' \| 'all' | 'none' | Spaces render as ·, tabs as →. 'boundary' = leading/trailing only. |
Typography
| Option | Type | Default | Description |
|---|---|---|---|
| fontFamily | string | "'JetBrains Mono', monospace" | CSS font-family for all code text. |
| fontSize | number | 13 | Font size in pixels. |
| lineHeight | number | 22 | Row height in pixels — all scroll and minimap calculations derive from this. |
Cursor
| Option | Type | Default | Description |
|---|---|---|---|
| cursorStyle | 'line' \| 'block' \| 'underline' | 'line' | Visual cursor style. |
| cursorBlinkRate | number | 1050 | Blink period in ms. Set to 999999 to disable blinking. |
Layout
| Option | Type | Default | Description |
|---|---|---|---|
| gutterWidth | number | 60 | Gutter width in pixels. |
| minimapWidth | number | 120 | Minimap panel width in pixels. |
| wordWrap | boolean | false | Soft-wrap long lines. Toggle at runtime with Alt+Z. |
| wrapColumn | number | 80 | Column at which soft-wrap breaks when wordWrap is true. |
Editing
| Option | Type | Default | Description |
|---|---|---|---|
| tabSize | number | 2 | Spaces per Tab press. |
| insertSpaces | boolean | true | Insert spaces on Tab; false inserts a literal \t. |
| maxUndoHistory | number | 300 | Maximum undo snapshots retained. |
| undoBatchMs | number | 700 | Keystrokes within this window are grouped into one undo step. 0 = per-keystroke. |
| readOnly | boolean | false | Block all edits. Navigation, selection, and copy still work. |
| autoClosePairs | Record<string,string> | see below | Characters that auto-close. Pass {} to disable entirely. |
| lineCommentToken | string | '' | Prefix for Ctrl+/ toggle comment. Auto-detects from language when empty. |
| wordSeparators | string | '' | Extra characters treated as word boundaries for double-click and Ctrl+←/→. |
Default autoClosePairs:
{ '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' }Features Config
| Option | Type | Default | Description |
|---|---|---|---|
| bracketMatching | boolean | true | Highlight matching ()[]{} pair at the cursor. |
| codeFolding | boolean | true | Gutter fold button for collapsible blocks. |
| emmet | boolean | true | Emmet abbreviation expansion via Tab. |
| snippetExpansion | boolean | true | Tab-expand built-in and custom snippets. |
| autocomplete | boolean | true | Show the autocomplete popup while typing. |
| autocompletePrefixLength | number | 2 | Minimum characters typed before the popup appears. |
| multiCursor | boolean | true | Alt+Click and Ctrl+D multi-cursor. |
| find | boolean | true | Enable the find bar (Ctrl+F). |
| findReplace | boolean | true | Enable find-and-replace (Ctrl+H). |
| wordSelection | boolean | true | Double-click selects the word under the cursor. |
| 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. |
| goToLine | boolean | false | Enable the Go to Line bar (Ctrl+G / Cmd+G). Pressing the shortcut opens a centered prompt; Enter jumps the cursor, Escape dismisses. Off by default. |
| placeholder | string | '' | Ghost text rendered at the cursor position when the document is completely empty. Hidden when the document has any content or when the string is empty. |
Syntax & Autocomplete Customisation
| Option | Type | Default | Description |
|---|---|---|---|
| extraKeywords | string[] | [] | Words highlighted as keywords and added to autocomplete. |
| extraTypes | string[] | [] | Words highlighted as types and added to autocomplete. |
| completions | CompletionItem[] | [] | Unified completions array — symbols, snippets, DSL items, all in one place. |
| replaceBuiltins | boolean | false | When true, completions replaces the built-in language keywords/types entirely. |
| provideCompletions | (ctx: CompletionContext) => CompletionItem[] \| null | undefined | Dynamic callback — called on every popup open; return null to fall through to defaults. |
| 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. |
| maxCompletions | number | 14 | Maximum items shown in the autocomplete popup. |
Token Colors Config
| Option | Type | Default | Description |
|---|---|---|---|
| tokenColors | TokenColors | {} | Per-token colour overrides layered on top of the active theme. See Token Colors. |
Theme Config
| Option | Type | Default | Description |
|---|---|---|---|
| theme | string \| ThemeDefinition | '' (VR Dark) | Built-in theme ID or a full ThemeDefinition object. |
Callbacks
| Option | Signature | Description |
|---|---|---|
| onChange | (value: string) => void | Fired after every content change (keystroke, paste, undo, setValue). |
| onCursorChange | (pos: CursorPosition) => void | Fired when the cursor moves. |
| onSelectionChange | (sel: Selection \| null) => void | Fired when the selection changes or clears. |
| onFocus | () => void | Fired when the editor gains keyboard focus. |
| onBlur | () => void | Fired when the editor loses keyboard focus. |
| provideHover | (ctx: HoverContext) => HoverDoc \| null \| undefined | Provides custom hover documentation. Called after built-in lookup and completions search. |
Updating Config at Runtime
Any option can be changed after creation — no reload needed:
// Toggle features instantly
editor.updateConfig({ wordWrap: true, showMinimap: false });
// Switch language (rebuilds highlighting + completions)
editor.updateConfig({ language: 'css' });
// Change font
editor.updateConfig({
fontFamily: "'Fira Code', monospace",
fontSize: 14,
lineHeight: 24,
});
// Enter read-only mode
editor.updateConfig({ readOnly: true });
// Override token colours on top of current theme
editor.updateConfig({
tokenColors: { keyword: '#ff79c6', string: '#f1fa8c' },
});
// Restore all token colours to theme defaults
editor.updateConfig({ tokenColors: {} });
// Swap completions at runtime
editor.updateConfig({ completions: newCompletions });Runtime API
All methods on the EditorAPI object returned by createEditor().
Content
editor.getValue(): string // full document as a newline-joined string
editor.setValue(value: string): void // replace document (records undo snapshot)Cursor & Selection
editor.getCursor(): CursorPosition // { row, col } — zero-based
editor.setCursor(pos: CursorPosition): void // moves cursor, scrolls into view
editor.getSelection(): Selection | null // { ar, ac, fr, fc } or null
editor.setSelection(sel: Selection | null): void // null to deselect
editor.insertText(text: string): void // insert at cursor position; no-op when readOnlyHistory
editor.undo(): void
editor.redo(): voidCommands
editor.executeCommand(name: string): void| Command | Action | Blocked by readOnly |
|---|---|---|
| 'undo' | Undo | ✓ |
| 'redo' | Redo | ✓ |
| 'selectAll' | Select all | — |
| 'copy' | Copy selection to clipboard | — |
| 'cut' | Cut selection | ✓ |
| 'paste' | Paste from clipboard | ✓ |
| 'toggleComment' | Toggle line comment | ✓ |
| 'duplicateLine' | Duplicate current line | ✓ |
| 'deleteLine' | Delete current line | ✓ |
| 'toggleWordWrap' | Toggle word wrap | — |
| 'find' | Open find bar | — |
| 'findReplace' | Open find + replace bar | — |
| 'indentLine' | Indent selection / current line | ✓ |
| 'outdentLine' | Outdent selection / current line | ✓ |
Themes API
editor.setTheme(theme: string | ThemeDefinition): void // switch by ID or object
editor.getThemes(): string[] // all registered theme IDs
editor.registerTheme(theme: ThemeDefinition): void // register for later useConfig
editor.updateConfig(patch: Partial<EditorConfig>): voidLifecycle
editor.focus(): void // programmatically focus the editor
editor.destroy(): void // unmount all DOM nodes; do not use the instance afterwardsSyntax Highlighting
The editor uses a hand-written, zero-dependency tokeniser that produces nine token classes mapped to CSS custom properties.
Supported Languages
| language | Keywords | Types | Built-in completions |
|---|---|---|---|
| 'typescript' | 55 keywords (interface, type, readonly, enum, satisfies, …) | 25 types (Promise, Array, HTMLElement, …) | ~50 JS/TS functions |
| 'javascript' | 35 keywords (TypeScript-specific syntax excluded) | 17 types | Same ~50 JS functions |
| 'css' | 50 value keywords (flex, block, grid, @media, …) | — | 100+ CSS properties + CSS functions |
| 'json' | null, true, false | — | — |
| 'markdown' | — | — | — |
| 'text' | — | — | — |
Token Classes and CSS Variables
| Token class | CSS variable | What it colours | Examples |
|---|---|---|---|
| kw | --tok-kw | Keywords | const, return, if, flex, @media |
| str | --tok-str | Strings | "hello", 'world', `template` |
| cmt | --tok-cmt | Comments | // line, /* block */ |
| fn | --tok-fn | Functions | console.log(, fetch(, calc( |
| num | --tok-num | Numbers | 42, 3.14, 0xff, 1n |
| cls | --tok-cls | Classes | MyClass, EventEmitter, Promise |
| op | --tok-op | Operators | +, =>, ===, &&, ?., \| |
| typ | --tok-typ | Types | string, boolean, HTMLElement |
| dec | --tok-dec | Decorators | @Component, @Injectable |
Adding Extra Keywords and Types
extraKeywords and extraTypes affect both syntax highlighting and autocomplete:
createEditor(container, {
language: 'typescript',
extraKeywords: ['pipeline', 'stage', 'emit'],
extraTypes: ['Observable', 'Subject', 'BehaviorSubject'],
});Update at runtime (rebuilds the tokeniser cache immediately):
editor.updateConfig({
extraKeywords: ['pipeline', 'stage', 'emit', 'dispatch'],
});Token Colors
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.
Quick Overrides
import type { TokenColors } from 'syncline-editor';
// Override specific tokens — all others remain from the active theme
editor.updateConfig({
tokenColors: {
keyword: '#ff79c6', // pink keywords
string: '#f1fa8c', // yellow strings
comment: '#6272a4', // muted blue comments
function: '#50fa7b', // green functions
number: '#bd93f9', // purple numbers
class: '#8be9fd', // cyan class names
operator: '#f8f8f2', // near-white operators
type: '#8be9fd', // cyan types
decorator: '#ffb86c', // orange decorators
},
});
// Restore everything to the current theme's defaults
editor.updateConfig({ tokenColors: {} });
// Remove just one override and keep the rest
editor.updateConfig({
tokenColors: { ...currentOverrides, keyword: '' },
});TokenColors Fields
| Field | CSS variable | What it highlights |
|---|---|---|
| keyword | --tok-kw | if, const, class, interface, flex, @media |
| string | --tok-str | String and template literals |
| comment | --tok-cmt | Line and block comments |
| function | --tok-fn | Function names, CSS functions like calc() |
| number | --tok-num | Numeric literals |
| class | --tok-cls | Class names, constructor calls |
| operator | --tok-op | Operators and punctuation |
| type | --tok-typ | Type names, built-in types |
| decorator | --tok-dec | Decorator annotations (@Component, @Injectable) |
How Token Colour Overrides Work
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:
setTheme()— writes all--tok-*values from the theme definitionupdateConfig({ tokenColors })— overwrites specific properties on top
This means token color overrides survive theme switches — they are automatically re-applied each time the theme changes.
Tip: Prefer
tokenColorsfor quick colour customisation. Use a fullThemeDefinitiononly when you need to control backgrounds, borders, highlights, and the minimap too.
Themes
Built-in Themes
| ID | Style |
|---|---|
| '' (empty string) | VR Dark (default) |
| 'vscode-dark' | VS Code Dark+ |
| 'monokai' | Monokai |
| 'dracula' | Dracula |
| 'github-light' | GitHub Light |
| 'solarized-light' | Solarized Light |
Switching Themes
// By ID
editor.setTheme('dracula');
editor.setTheme('github-light');
// By object (register not required when passing directly)
editor.setTheme(myCustomTheme);
// Via updateConfig
editor.updateConfig({ theme: 'monokai' });
// Get all registered IDs
const ids = editor.getThemes();
// ['', 'vscode-dark', 'monokai', 'dracula', 'github-light', 'solarized-light', ...]Importing Built-in Theme Objects
import {
BUILT_IN_THEMES, // ThemeDefinition[] — all six
THEME_VR_DARK,
THEME_VSCODE_DARK,
THEME_MONOKAI,
THEME_DRACULA,
THEME_GITHUB_LIGHT,
THEME_SOLARIZED_LIGHT,
} from 'syncline-editor';Creating a Custom Theme
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.
import type { ThemeDefinition } from 'syncline-editor';
const myTheme: ThemeDefinition = {
id: 'my-purple',
name: 'My Purple',
description: 'Custom purple dark theme',
light: false,
tokens: {
// ── Backgrounds ────────────────────────────────────────────
bg0: '#0a0a14', bg1: '#0f0f1a', bg2: '#141420',
bg3: '#1a1a28', bg4: '#20203a',
// ── Borders ────────────────────────────────────────────────
border: 'rgba(255,255,255,.08)',
border2: 'rgba(255,255,255,.14)',
border3: 'rgba(255,255,255,.22)',
// ── Text ───────────────────────────────────────────────────
text: '#e0deff', text2: '#8080c0', text3: '#404060',
// ── Accent ─────────────────────────────────────────────────
accent: '#ff79c6', accent2: '#6644aa',
// ── Semantic colours ───────────────────────────────────────
green: '#50fa7b', orange: '#ffb86c',
purple: '#bd93f9', red: '#ff5555', yellow: '#f1fa8c',
// ── Cursor ─────────────────────────────────────────────────
cur: '#ff79c6', curGlow: 'rgba(255,121,198,.55)',
// ── Editor surface ─────────────────────────────────────────
curLineBg: 'rgba(255,121,198,.04)',
curLineGutter: '#161628',
gutterBg: '#0a0a14',
gutterHover: '#161628',
gutterBorder: 'rgba(255,255,255,.06)',
gutterNum: '#404060',
gutterNumAct: '#8080c0',
// ── Highlights ─────────────────────────────────────────────
selBg: 'rgba(189,147,249,.25)',
wordHlBg: 'rgba(255,121,198,.07)',
wordHlBorder: 'rgba(255,121,198,.25)',
bmBorder: 'rgba(255,121,198,.65)',
foldBg: 'rgba(255,121,198,.07)',
foldBorder: 'rgba(255,121,198,.30)',
// ── Find bar ───────────────────────────────────────────────
findBg: 'rgba(241,250,140,.14)',
findBorder: 'rgba(241,250,140,.52)',
findCurBg: '#f1fa8c',
findCurBorder: '#d4dc50',
findCurText: '#282a36',
// ── Sidebar ────────────────────────────────────────────────
fileActiveBg: 'rgba(255,121,198,.12)',
fileActiveText: '#ff79c6',
// ── Minimap ────────────────────────────────────────────────
mmBg: '#0a0a14',
mmSlider: 'rgba(255,255,255,.07)',
mmDim: 'rgba(0,0,0,.32)',
mmEdge: 'rgba(255,255,255,.20)',
// ── Misc ───────────────────────────────────────────────────
indentGuide: 'rgba(255,255,255,.07)',
// ── Syntax token colours ───────────────────────────────────
tokKw: '#ff79c6', tokStr: '#f1fa8c',
tokCmt: '#6272a4', tokFn: '#50fa7b',
tokNum: '#bd93f9', tokCls: '#8be9fd',
tokOp: '#f8f8f2', tokTyp: '#8be9fd',
tokDec: '#ffb86c',
},
};
// Register once, then switch by ID from anywhere
editor.registerTheme(myTheme);
editor.setTheme('my-purple');Extending a Built-in Theme
Spread an existing theme's tokens and override only the fields you need:
import { THEME_DRACULA } from 'syncline-editor';
editor.registerTheme({
id: 'dracula-tweaked',
name: 'Dracula (tweaked)',
description: 'Dracula with warmer keyword and string colours',
light: false,
tokens: {
...THEME_DRACULA.tokens, // inherit everything
tokKw: '#ffb86c', // orange keywords instead of pink
tokStr: '#f1fa8c', // yellow strings instead of green
},
});
editor.setTheme('dracula-tweaked');ThemeTokens Reference
Every key maps to a --<name> CSS custom property on the editor's Shadow DOM host.
| Group | Keys |
|---|---|
| Backgrounds | bg0 bg1 bg2 bg3 bg4 |
| Borders | border border2 border3 |
| Text | text text2 text3 |
| Accent | accent accent2 |
| Semantic | green orange purple red yellow |
| Cursor | cur curGlow |
| Editor surface | curLineBg curLineGutter gutterBg gutterHover gutterBorder gutterNum gutterNumAct |
| Highlights | selBg wordHlBg wordHlBorder bmBorder foldBg foldBorder |
| Find bar | findBg findBorder findCurBg findCurBorder findCurText |
| Sidebar | fileActiveBg fileActiveText |
| Minimap | mmBg mmSlider mmDim mmEdge |
| Misc | indentGuide |
| Syntax token colours | tokKw tokStr tokCmt tokFn tokNum tokCls tokOp tokTyp tokDec |
Autocomplete
The popup opens when the user has typed at least autocompletePrefixLength characters (default 2) and shows up to maxCompletions items (default 14).
Emmet abbreviations also surface in the same popup with an E badge whenever the current prefix matches a valid abbreviation.
The Unified completions Array
All custom completions — regular symbols, snippets, language-filtered items — go into a single completions array and are differentiated by kind:
import type { CompletionItem } from 'syncline-editor';
createEditor(container, {
language: 'typescript',
completions: [
// Regular function symbol — shows description on the right panel when selected
{
label: 'fetchUser',
kind: 'fn',
detail: '(id: string) => Promise<User>',
description: 'Fetches a user by ID from the REST API.\n\nReturns `null` if the user does not exist.',
},
// Type symbol
{
label: 'UserStatus',
kind: 'typ',
detail: 'enum',
description: 'Represents the current state of a user account.\n\n`active` | `suspended` | `pending`',
},
// Snippet — kind: "snip" + body template
{
label: 'mycomp',
kind: 'snip',
detail: 'React component scaffold',
description: 'Scaffolds a named React functional component with JSX return.',
body: 'export function $1Component() {\n return (\n <div>\n $2\n </div>\n );\n}',
language: ['typescript'], // only show in TypeScript files
},
],
});Update completions at runtime (takes effect on the next popup open):
editor.updateConfig({ completions: updatedItems });CompletionItem Reference
interface CompletionItem {
label: string; // text inserted on accept; used for prefix matching
kind: CompletionKind; // badge type — see table below
detail?: string; // short hint shown on the right of the popup row
description?: string; // full docs shown in the side panel when selected
body?: string; // snippet template; Tab/Enter expands it when set
language?: string | string[]; // restrict to specific language(s); omit = all
}| kind | Badge | Intended use |
|---|---|---|
| 'kw' | K | Language keyword |
| 'fn' | f | Function, method, CSS function |
| 'typ' | T | Type, interface, enum |
| 'cls' | C | Class name |
| 'var' | · | Variable, CSS property, in-file word |
| 'snip' | S | Snippet — set body and accepting it expands the template |
| 'emmet' | E | Emmet abbreviation (auto-generated, not user-defined) |
Description Panel
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.
{
label: 'useState',
kind: 'fn',
detail: '<S>(initialState: S) => [S, Dispatch<SetStateAction<S>>]',
description: 'Returns a stateful value and a setter function.\n\n' +
'During the initial render the state equals `initialState`.\n\n' +
'The setter function updates the state and triggers a re-render.',
}Multi-line descriptions use \n for line breaks. Markdown is not rendered — plain text only.
Replace Built-in Completions — replaceBuiltins
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:
// SQL editor — hides all JS/TS keywords, shows only SQL
createEditor(container, {
language: 'text',
replaceBuiltins: true,
completions: [
{ label: 'SELECT', kind: 'kw', detail: 'SQL', description: 'Retrieve rows from a table.' },
{ label: 'FROM', kind: 'kw', detail: 'SQL' },
{ label: 'WHERE', kind: 'kw', detail: 'SQL', description: 'Filter rows by a condition.' },
{ label: 'JOIN', kind: 'kw', detail: 'SQL' },
{ label: 'GROUP BY', kind: 'kw', detail: 'SQL' },
{ label: 'ORDER BY', kind: 'kw', detail: 'SQL' },
{ label: 'LIMIT', kind: 'kw', detail: 'SQL', description: 'Limit the number of rows returned.' },
],
});Popup Size
createEditor(container, {
maxCompletions: 20, // max items shown (default 14)
autocompletePrefixLength: 1, // trigger after 1 char (default 2)
});Priority Order
When multiple sources are configured, the final suggestion list is built in this order:
provideCompletionscallback — if it returns a non-null array, it wins entirely and all other sources are skippedcompletionswithreplaceBuiltins: true— your items replace language defaultscompletions(default) — merged on top of language defaults- Language built-ins — keywords, types, and functions for the active language
- In-file words — identifiers extracted from the current document, always appended last
Hover Documentation
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.
How the lookup works (priority order)
- Built-in docs — ~75 entries covering common JS/TS APIs (
console.log,Math.floor,Promise.all,fetch,async,const, utility types likeRecord,Partial, …) completionsarray — any item in yourcompletionsconfig that has adescription(and/ordetail) is automatically available as a hover doc. No extra config needed.provideHovercallback — a runtime function you supply for anything not covered above.
Hover from completions — automatic
If you already define completions with descriptions, hover just works:
const editor = createEditor(container, {
completions: [
{
label: 'myQuery',
kind: 'fn',
detail: '(id: string) => Promise<User>', // → shown as type signature
description: 'Fetches a user by their ID.\n\nReturns null if the user does not exist.', // → shown as body
},
{
label: 'MyEntity',
kind: 'cls',
detail: 'class MyEntity',
description: 'The core domain model. Includes all persistence and validation logic.',
},
],
});
// Hovering over "myQuery" or "MyEntity" in the editor now shows a tooltip automatically.provideHover — fully custom docs
Use provideHover for symbols that live in an external symbol table, API schema, or documentation database:
const editor = createEditor(container, {
provideHover: (ctx) => {
// ctx = { word, row, col, line, language, doc }
// Example: look up from a GraphQL schema
const field = mySchema.getField(ctx.word);
if (!field) return null; // return null to show nothing
return {
title: field.name,
type: field.type, // e.g. "String!"
body: field.description, // e.g. "The unique identifier of this node."
};
},
});HoverDoc reference
interface HoverDoc {
title: string; // Bold name at the top of the tooltip
type?: string; // Type signature in monospace (optional)
body: string; // Description text
}HoverContext reference
interface HoverContext {
word: string; // The identifier under the pointer (may include dot prefix, e.g. "console.log")
row: number; // Zero-based document line
col: number; // Zero-based character column
line: string; // Full text of the hovered line
language: string; // Active language (e.g. "typescript")
doc: readonly string[]; // Full document as array of lines
}Disable hover entirely
editor.updateConfig({ hover: false });Snippets
Snippets let you type a short trigger word and press Tab to expand a full multi-line block. After expansion a tab-stop session begins: the cursor lands at $1, and each subsequent Tab advances to $2, $3, and so on. Shift+Tab moves backward. Upcoming tab stops are shown as dim ghost cursor markers so you always know where you'll land next. The session ends when you reach the last stop, press Escape, or click elsewhere.
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.
Built-in Snippets by Language
TypeScript / JavaScript
| Trigger | Expands to |
|---|---|
| fn | function name(params) { } |
| afn | const name = (params) => { }; |
| asyncfn | async function name(params): Promise<T> { } |
| cl | class Name { constructor(params) { } } |
| forof | for (const item of iterable) { } |
| forin | for (const key in object) { } |
| trycatch | try { } catch (error) { } |
| promise | new Promise<T>((resolve, reject) => { }) |
| imp | import { name } from 'module'; |
| iface | interface Name { field: Type; } (TypeScript only) |
| ife | Immediately-invoked function expression |
| sw | switch (expr) { case val: … default: … } |
CSS
| Trigger | Expands to |
|---|---|
| flex | Flexbox container (display: flex; align-items; justify-content) |
| grid | CSS grid (display: grid; grid-template-columns; gap) |
| media | @media (max-width: 768px) { } |
| anim | @keyframes name { from { } to { } } |
| var | --name: value; |
HTML / General (available in all languages)
| Trigger | Expands to |
|---|---|
| accordion | <details><summary>…</summary><div>…</div></details> |
| card | Card with header, body, and footer divs |
| navbar | <nav> with anchor links |
| modal | Modal dialog with header, body, and footer |
| table | <table> with <thead> and <tbody> |
Custom Snippets
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:
import type { CompletionItem } from 'syncline-editor';
createEditor(container, {
language: 'typescript',
completions: [
{
label: 'rcomp',
kind: 'snip',
detail: 'React component',
description: 'Scaffolds a typed React functional component.',
body: 'export function $1({ $2 }: $1Props) {\n return (\n <div>\n $3\n </div>\n );\n}',
language: ['typescript'],
},
{
label: 'clog',
kind: 'snip',
detail: 'console.log with label',
body: "console.log('$1:', $2);",
},
{
label: 'todo',
kind: 'snip',
detail: 'TODO comment',
body: '// TODO($1): $2',
},
],
});
// Add more at runtime
editor.updateConfig({ completions: [...existing, newItem] });Disable all snippet expansion (built-in and custom):
createEditor(container, { snippetExpansion: false });Snippet Body Syntax
| Placeholder | Meaning |
|---|---|
| $1 | First tab stop — cursor lands here immediately after expansion |
| $2, $3, … | Additional tab stops — press Tab to jump through them in order |
| $0 | Final cursor position (VS Code convention) — session ends here |
| \n | Line break — continuation lines inherit the trigger line's indentation |
Tab stops are rendered as dim ghost cursor markers at their positions in the document. As you type at the current stop the ghost markers shift to stay accurate. Pressing Shift+Tab moves back to the previous stop. The session exits when you reach $0 (or the last stop), press Escape, click elsewhere, or press Enter / Delete.
Priority: Emmet is tried first on Tab. If both Emmet and a snippet match the same word, Emmet wins.
Emmet
Type an abbreviation and press Tab to expand. A preview tooltip appears above the cursor while a valid abbreviation is detected.
ul>li*5 → <ul><li></li> × 5</ul>
div.container → <div class="container"></div>
input[type=text] → <input type="text">
a:href → <a href=""></a>
section>h2+p → <section><h2></h2><p></p></section>Emmet abbreviations also appear in the autocomplete popup with an E badge — select and press Tab or Enter to expand.
createEditor(container, { emmet: false }); // disableDynamic Completion Provider
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.
provideCompletions
import type { CompletionContext, CompletionItem } from 'syncline-editor';
createEditor(container, {
provideCompletions: (ctx: CompletionContext): CompletionItem[] | null => {
// ctx.prefix — characters typed before the cursor on the current line
// ctx.language — active language string ('typescript', 'css', etc.)
// ctx.line — zero-based cursor row
// ctx.col — zero-based cursor column
// ctx.doc — full document as string[] (one element per line)
// Example: variable completions after "$"
if (ctx.prefix.startsWith('$')) {
return myVariableTable.map(v => ({
label: v.name,
kind: 'var' as const,
detail: v.type,
description: v.docs,
}));
}
// Example: import suggestions inside import statements
const importLine = ctx.doc[ctx.line];
if (importLine.startsWith('import ') && ctx.prefix.length >= 1) {
return myModuleList.map(m => ({
label: m.name,
kind: 'cls' as const,
detail: m.version,
}));
}
// Return null to fall through to built-in completions
return null;
},
});Update the provider at runtime:
editor.updateConfig({
provideCompletions: (ctx) => newProvider(ctx),
});CompletionContext
interface CompletionContext {
prefix: string; // characters typed before the cursor (the match prefix)
language: string; // active language ID
line: number; // cursor row, zero-based
col: number; // cursor column, zero-based
doc: string[]; // full document split into lines
}Events & Callbacks
All callbacks are defined in EditorConfig and can be updated via updateConfig at any time:
const editor = createEditor(container, {
onChange: (value) => {
// Fires after every edit — keystroke, paste, undo, setValue(), …
// value: full document as a newline-joined string
autoSave(value);
},
onCursorChange: (pos) => {
// pos.row and pos.col are zero-based
statusEl.textContent = `Ln ${pos.row + 1}, Col ${pos.col + 1}`;
},
onSelectionChange: (sel) => {
// sel is null when the selection is cleared
if (sel) {
const rows = Math.abs(sel.fr - sel.ar) + 1;
console.log(`${rows} line(s) selected`);
}
},
onFocus: () => container.classList.add('editor-focused'),
onBlur: () => container.classList.remove('editor-focused'),
});
// Replace a callback at runtime — no re-creation needed
editor.updateConfig({
onChange: (value) => newAutoSave(value),
});Advanced Features
Multi-cursor
| Action | Shortcut |
|---|---|
| Add cursor at click position | Alt / Option + Click |
| Select next occurrence of word | Ctrl / Cmd + D |
| Clear all extra cursors | Escape |
Every cursor has its own independent selection. All cursors type, delete, and move in sync.
createEditor(container, { multiCursor: false }); // disableFind & Replace
Open programmatically:
editor.executeCommand('find'); // opens find bar
editor.executeCommand('findReplace'); // opens with replace row visibleThe find bar supports:
- Match case —
Aatoggle - Regex mode —
.*toggle - Navigate —
↑/↓buttons orEnter/Shift+Enter - Replace one — replaces the current highlighted match
- Replace all — replaces every match in the document
Configuration options:
// Disable both find and replace
createEditor(container, { find: false });
// Find only — no replace row
createEditor(container, { find: true, findReplace: false });Code Folding
Click the ▾ / ▸ gutter toggle next to any foldable block. Folded sections show a dashed bottom border and a … indicator.
createEditor(container, { codeFolding: false }); // disableBracket Matching
When the cursor is adjacent to (, ), [, ], {, or }, both the opening and closing characters are highlighted with a border.
createEditor(container, { bracketMatching: false }); // disableColour controlled by ThemeTokens.bmBorder.
Word Highlight
When the cursor rests on a word, all other occurrences in the document are subtly boxed. Automatically disabled while a selection is active.
createEditor(container, { wordHighlight: false }); // disableColours: ThemeTokens.wordHlBg (fill) and ThemeTokens.wordHlBorder (outline).
Whitespace Rendering
createEditor(container, { renderWhitespace: 'none' }); // default — invisible
createEditor(container, { renderWhitespace: 'boundary' }); // leading/trailing only
createEditor(container, { renderWhitespace: 'all' }); // every space and tabSpaces render as · and tabs as →.
Cursor Styles
createEditor(container, { cursorStyle: 'line' }); // thin vertical beam (default)
createEditor(container, { cursorStyle: 'block' }); // filled block behind character
createEditor(container, { cursorStyle: 'underline' }); // horizontal bar below character
createEditor(container, { cursorBlinkRate: 2000 }); // slower blink
createEditor(container, { cursorBlinkRate: 999999 }); // no blinkMinimap
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.
createEditor(container, {
showMinimap: true,
minimapWidth: 100, // pixels (default 120)
});Colours: ThemeTokens.mmBg, mmSlider, mmDim, mmEdge.
Word Wrap
createEditor(container, {
wordWrap: true,
wrapColumn: 100, // default 80
});
// Toggle at runtime — also available via Alt+Z keyboard shortcut
editor.executeCommand('toggleWordWrap');
editor.updateConfig({ wordWrap: !currentWrap });Indent Guides
Faint vertical lines connecting matching indentation levels:
createEditor(container, { showIndentGuides: false }); // disableColour controlled by ThemeTokens.indentGuide.
Active Line Highlight
The row containing the cursor gets a distinct background and gutter colour:
createEditor(container, { highlightActiveLine: false }); // disableColours: ThemeTokens.curLineBg (row background) and ThemeTokens.curLineGutter (gutter cell).
Read-Only Mode
const viewer = createEditor(container, {
value: sourceCode,
readOnly: true,
});
// These are all silently no-ops in read-only mode:
viewer.insertText('test');
viewer.executeCommand('cut');
viewer.executeCommand('deleteLine');
// These still work normally:
viewer.getCursor();
viewer.getSelection();
viewer.executeCommand('copy');
viewer.executeCommand('find');
// Toggle at runtime:
editor.updateConfig({ readOnly: false }); // re-enable editing
editor.updateConfig({ readOnly: true }); // lock againBehavioral Options
Auto-Close Pairs
// Default — close all bracket and quote types
createEditor(container, {
autoClosePairs: { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' },
});
// Only parentheses and curly braces
createEditor(container, {
autoClosePairs: { '(': ')', '{': '}' },
});
// Disable auto-closing entirely
createEditor(container, { autoClosePairs: {} });Typing the opening character inserts the closing character and places the cursor between them. Typing the closing character again skips over it.
Line Comment Token
Controls the Ctrl+/ toggle-comment prefix:
// Auto-detect from language (default — // for TS/JS/CSS, nothing for JSON/Markdown)
createEditor(container, { lineCommentToken: '' });
// Python / Ruby / YAML / shell
createEditor(container, { lineCommentToken: '#' });
// SQL / Lua
createEditor(container, { lineCommentToken: '--' });
// LaTeX
createEditor(container, { lineCommentToken: '%' });Word Separators
Characters treated as word boundaries for double-click selection, Ctrl+Left/Right, and Ctrl+D:
// Default — use built-in \w word boundary
createEditor(container, { wordSeparators: '' });
// Include hyphens (good for CSS, kebab-case identifiers)
createEditor(container, {
wordSeparators: '`~!@#%^&*()-=+[{}]\\|;:\'",.<>/?',
});Undo Batch Window
Controls how keystrokes are grouped into undo steps:
// Default — group keystrokes within 700 ms into one undo step
createEditor(container, { undoBatchMs: 700 });
// Per-keystroke undo — every character is its own step
createEditor(container, { undoBatchMs: 0 });
// One undo per ~2 seconds of continuous typing
createEditor(container, { undoBatchMs: 2000 });Feature Flags
Disable individual features at creation or toggle them at runtime:
createEditor(container, {
bracketMatching: false, // no bracket-pair highlight
codeFolding: false, // no fold buttons in gutter
emmet: false, // no Emmet expansion
snippetExpansion: false, // no snippet Tab-expansion
autocomplete: false, // no completion popup
multiCursor: false, // no Alt+Click / Ctrl+D
find: false, // no find bar
findReplace: false, // no replace row
wordSelection: false, // double-click places cursor only
wordHighlight: false, // no occurrence boxes
highlightActiveLine: false, // no active-line background
showIndentGuides: false, // no indent guide lines
showGutter: false, // hide line numbers
showMinimap: false, // hide minimap panel
showStatusBar: false, // hide status bar
goToLine: true, // enable Ctrl+G / Cmd+G Go to Line
placeholder: 'Start typing…', // shown when document is empty
});Keyboard Shortcuts
| Shortcut | Action | Controlled by |
|---|---|---|
| Ctrl / Cmd + Z | Undo | undoBatchMs, maxUndoHistory |
| Ctrl / Cmd + Y or Ctrl + Shift + Z | Redo | — |
| Ctrl / Cmd + A | Select all | — |
| Ctrl / Cmd + C | Copy | — |
| Ctrl / Cmd + X | Cut | readOnly |
| Ctrl / Cmd + V | Paste | readOnly |
| Ctrl / Cmd + F | Open find bar | find |
| Ctrl / Cmd + H | Open find + replace | findReplace |
| Ctrl / Cmd + G | Open Go to Line prompt | goToLine |
| Ctrl / Cmd + D | Select next occurrence | multiCursor |
| Ctrl / Cmd + Shift + D | Duplicate line | readOnly |
| Ctrl / Cmd + K | Delete line | readOnly |
| Ctrl / Cmd + / | Toggle line comment | lineCommentToken, readOnly |
| Alt / Option + Z | Toggle word wrap | — |
| Alt / Option + ↑ | Move current line (or selected block) up | readOnly |
| Alt / Option + ↓ | Move current line (or selected block) down | readOnly |
| Shift + Alt / Option + ↓ | Duplicate current line (or selected block) down — cursor moves to new copy | readOnly |
| Shift + Alt / Option + ↑ | Duplicate current line (or selected block) up — cursor stays on new copy | readOnly |
| Tab | Advance to next snippet tab stop · expand Emmet · expand snippet · indent | snippetExpansion, emmet, tabSize |
| Shift + Tab | Go to previous snippet tab stop · outdent | snippetExpansion, tabSize |
| Alt / Option + → (Mac) | Word skip right (groups by character class: word / punctuation) | wordSeparators |
| Alt / Option + ← (Mac) | Word skip left (groups by character class: word / punctuation) | wordSeparators |
| Alt / Option + Click | Add extra cursor | multiCursor |
| Double-click | Select word | wordSelection, wordSeparators |
| Triple-click | Select entire line | — |
| Escape | Clear selection · exit snippet session · close popup · close find · remove extra cursors | — |
| ↑ ↓ ← → | Move cursor | — |
| Cmd + ← → (Mac) | Start / end of line | — |
| Cmd + ↑ ↓ (Mac) | Start / end of document | — |
| Ctrl + ← → (Win) | Word skip left / right | wordSeparators |
| Ctrl + Home / End (Win) | Start / end of document | — |
| Option + ← → (Mac) | Word skip left / right | wordSeparators |
| Shift + arrow keys | Extend selection | — |
| Shift + Cmd/Ctrl + arrow | Extend selection to boundary | — |
| Home | First non-whitespace (press again = column 0) | — |
| End | End of line | — |
| PageUp / PageDown | Scroll one viewport | — |
| Backspace | Delete character left | readOnly |
| Delete | Delete character right | readOnly |
All mutating shortcuts are silently blocked when readOnly: true. Navigation, selection, and Ctrl+C always work.
Recipes
Real-world patterns you can copy and adapt.
Monaco-style Editor Embed
A feature-complete editor inside a fixed-height container, closely resembling a VS Code embed:
import { createEditor } from 'syncline-editor';
const editor = createEditor(document.getElementById('editor')!, {
value: initialCode,
language: 'typescript',
theme: 'vscode-dark',
fontSize: 13,
lineHeight: 20,
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
tabSize: 2,
insertSpaces: true,
wordWrap: false,
showMinimap: true,
showGutter: true,
showStatusBar: true,
showIndentGuides: true,
bracketMatching: true,
codeFolding: true,
emmet: true,
autocomplete: true,
multiCursor: true,
find: true,
findReplace: true,
wordHighlight: true,
highlightActiveLine: true,
renderWhitespace: 'boundary',
onChange: (v) => localStorage.setItem('draft', v),
onFocus: () => console.log('editor focused'),
});#editor { width: 100%; height: 100vh; }DSL / SQL Editor
Replace all built-in completions with domain-specific keywords — zero JS/TS noise:
import { createEditor } from 'syncline-editor';
import type { CompletionItem } from 'syncline-editor';
const sqlCompletions: CompletionItem[] = [
{ label: 'SELECT', kind: 'kw', detail: 'SQL', description: 'Retrieve rows from a table or view.' },
{ label: 'FROM', kind: 'kw', detail: 'SQL', description: 'Specify the source table.' },
{ label: 'WHERE', kind: 'kw', detail: 'SQL', description: 'Filter rows using a predicate.' },
{ label: 'JOIN', kind: 'kw', detail: 'SQL' },
{ label: 'LEFT JOIN', kind: 'kw', detail: 'SQL' },
{ label: 'INNER JOIN', kind: 'kw', detail: 'SQL' },
{ label: 'GROUP BY', kind: 'kw', detail: 'SQL' },
{ label: 'ORDER BY', kind: 'kw', detail: 'SQL' },
{ label: 'HAVING', kind: 'kw', detail: 'SQL' },
{ label: 'LIMIT', kind: 'kw', detail: 'SQL' },
{ label: 'OFFSET', kind: 'kw', detail: 'SQL' },
{ label: 'INSERT INTO', kind: 'kw', detail: 'SQL' },
{ label: 'UPDATE', kind: 'kw', detail: 'SQL' },
{ label: 'DELETE FROM', kind: 'kw', detail: 'SQL' },
{ label: 'COUNT', kind: 'fn', detail: 'aggregate', description: 'COUNT(*) — number of rows.' },
{ label: 'SUM', kind: 'fn', detail: 'aggregate' },
{ label: 'AVG', kind: 'fn', detail: 'aggregate' },
{ label: 'MAX', kind: 'fn', detail: 'aggregate' },
{ label: 'MIN', kind: 'fn', detail: 'aggregate' },
];
createEditor(container, {
language: 'text',
replaceBuiltins: true,
completions: sqlCompletions,
lineCommentToken: '--',
theme: 'vscode-dark',
});Read-Only Code Viewer
A zero-interaction code display with syntax highlighting, no editing, no cursors:
createEditor(container, {
value: sourceCode,
language: 'typescript',
theme: 'github-light',
readOnly: true,
autocomplete: false,
emmet: false,
snippetExpansion: false,
find: false,
findReplace: false,
multiCursor: false,
codeFolding: false,
showStatusBar: false,
showMinimap: false,
highlightActiveLine: false,
wordHighlight: false,
bracketMatching: false,
cursorBlinkRate: 999999,
});Custom Theme from Scratch (Recipe)
Minimal theme extending Dracula with a brand accent colour:
import { THEME_DRACULA } from 'syncline-editor';
const brandTheme = {
id: 'brand-dark',
name: 'Brand Dark',
description: 'Company brand colour scheme',
light: false,
tokens: {
...THEME_DRACULA.tokens,
accent: '#00C2FF', // brand blue accent
accent2: '#005F7A',
cur: '#00C2FF',
curGlow: 'rgba(0,194,255,.45)',
selBg: 'rgba(0,194,255,.20)',
wordHlBorder: 'rgba(0,194,255,.35)',
tokKw: '#00C2FF', // brand-coloured keywords
},
};
editor.registerTheme(brandTheme);
editor.setTheme('brand-dark');Framework-aware Autocomplete
Use provideCompletions to serve different items based on context — React hooks inside .tsx, lifecycle methods inside class components, etc.:
import type { CompletionContext, CompletionItem } from 'syncline-editor';
const reactHooks: CompletionItem[] = [
{ label: 'useState', kind: 'fn', detail: 'React hook', description: 'Adds local state to a functional component.' },
{ label: 'useEffect', kind: 'fn', detail: 'React hook', description: 'Run side-effects after render.' },
{ label: 'useCallback', kind: 'fn', detail: 'React hook' },
{ label: 'useMemo', kind: 'fn', detail: 'React hook' },
{ label: 'useRef', kind: 'fn', detail: 'React hook' },
{ label: 'useContext', kind: 'fn', detail: 'React hook' },
];
createEditor(container, {
language: 'typescript',
provideCompletions: (ctx: CompletionContext): CompletionItem[] | null => {
// Serve React hooks when prefix starts with "use"
if (ctx.prefix.startsWith('use')) {
return reactHooks.filter(h => h.label.startsWith(ctx.prefix));
}
return null; // fall through to built-in completions
},
});Auto-Save with Debounce
Debounce onChange so the backend isn't hammered on every keystroke:
function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T {
let timer: ReturnType<typeof setTimeout>;
return ((...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
}) as T;
}
const save = debounce(async (value: string) => {
await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: value }),
});
}, 1000);
createEditor(container, {
onChange: save,
});TypeScript Types
All types are exported from the package root:
import type {
// Core
EditorConfig,
EditorAPI,
Language,
// Positions
CursorPosition,
Selection,
FindMatch,
ExtraCursor,
// Autocomplete
CompletionItem,
CompletionKind,
CompletionContext,
// Token colours
TokenColors,
// Themes
ThemeDefinition,
ThemeTokens,
} from 'syncline-editor';Language
type Language =
| 'typescript'
| 'javascript'
| 'css'
| 'json'
| 'markdown'
| 'text';CursorPosition
interface CursorPosition {
row: number; // zero-based line index
col: number; // zero-based character offset
}Selection
interface Selection {
ar: number; // anchor row — where selection started
ac: number; // anchor col
fr: number; // focus row — where the caret currently sits
fc: number; // focus col
}Either end can come before the other — backward selections are valid.
CompletionItem
interface CompletionItem {
label: string; // text inserted on accept; used for prefix matching
kind: CompletionKind; // 'kw' | 'fn' | 'typ' | 'cls' | 'var' | 'snip' | 'emmet'
detail?: string; // short hint shown on the right of the popup row
description?: string; // full docs shown in the side panel when this item is selected
body?: string; // snippet template — Tab/Enter expands when set
language?: string | string[]; // restrict to specific language(s); omit = all languages
}CompletionKind
type CompletionKind = 'kw' | 'fn' | 'typ' | 'cls' | 'var' | 'snip' | 'emmet';CompletionContext
interface CompletionContext {
prefix: string; // characters before the cursor (the match prefix)
language: string; // active language ID
line: number; // cursor row (zero-based)
col: number; // cursor column (zero-based)
doc: string[]; // full document split into lines
}TokenColors
interface TokenColors {
keyword?: string; // --tok-kw — if, const, class, interface, @media
string?: string; // --tok-str — string and template literals
comment?: string; // --tok-cmt — line and block comments
function?: string; // --tok-fn — function names, CSS functions
number?: string; // --tok-num — numeric literals
class?: string; // --tok-cls — class names, constructor calls
operator?: string; // --tok-op — +, =>, ===, &&, ?.
type?: string; // --tok-typ — type names, built-in types
decorator?: string; // --tok-dec — @Component, @Injectable
}Pass an empty string '' for any field to restore that token to the current theme's default.
ThemeDefinition
interface ThemeDefinition {
id: string; // unique identifier — used with setTheme() and getThemes()
name: string; // human-readable display name
description: string; // short description
light: boolean; // true for light themes (affects status-bar icon tinting)
tokens: ThemeTokens; // complete set of CSS variable values
}Project Structure
syncline-editor/
├── src/
│ ├── core/
│ │ ├── constants.ts # Per-language keyword/type sets + layout constants
│ │ ├── document.ts # Document model, undo/redo, selection helpers
│ │ ├── tokeniser.ts # Language-aware syntax tokeniser (zero deps)
│ │ └── wrap-map.ts # Soft-wrap virtual line map
│ ├── features/
│ │ ├── autocomplete.ts # Completion engine — ranking, filtering, snippet items
│ │ ├── bracket-matcher.ts # Bracket-pair finder
│ │ ├── code-folding.ts # Block folding logic
│ │ ├── emmet.ts # Emmet abbreviation expander
│ │ ├── find-replace.ts # Find / replace engine
│ │ ├── multi-cursor.ts # Multi-cursor state manager
│ │ ├── snippets.ts # Built-in snippet library (TS/JS, CSS, HTML)
│ │ └── word-highlight.ts # Same-word occurrence finder
│ ├── renderer/
│ │ ├── minimap-renderer.ts # Canvas minimap + scroll calculations
│ │ └── row-renderer.ts # Virtual row → HTML string
│ ├── themes/
│ │ ├── built-in-themes.ts # 6 built-in ThemeDefinition objects
│ │ └── theme-manager.ts # Theme registry + CSS variable injection
│ ├── types/
│ │ └── index.ts # All public TypeScript interfaces and types
│ ├── ui/
│ │ └── styles.ts # All CSS injected into Shadow DOM
│ ├── utils/
│ │ ├── dom.ts # DOM helpers
│ │ ├── string.ts # String utilities (word boundaries, escaping, …)
│ │ └── validation.ts # Config validation helpers
│ ├── SynclineEditor.ts # Main editor class — implements EditorAPI
│ └── index.ts # Public API barrel export
├── playground/
│ └── index.html # Interactive playground (every option exposed)
├── tests/ # Vitest test suite
├── package.json
├── tsconfig.json
├── vite.config.ts # Playground dev server + build
├── vite.lib.config.ts # Library build (ES module + UMD + .d.ts)
└── README.mdDevelopment
# Install dependencies
npm install
# Start the interactive playground (Vite dev server, hot reload)
npm run dev
# Type-check only (no emit)
npm run typecheck
# Build the playground into dist/
npm run build
# Build the distributable library (ES + UMD + .d.ts)
npm run build:lib
# Preview the built playground
npm run preview
# Run the test suite
npm run testPlayground
The playground at playground/index.html is a fully self-contained interactive demo with every option wired to live controls:
- Theme switcher — all 6 built-in themes as clickable chips
- Language selector — TypeScript · JavaScript · CSS · JSON · Markdown · plain text
- Token Colors — 9 colour pickers with live preview, enable/disable toggle, and reset
- Features — checkbox for every boolean option
- Typography — font family, size, line height, cursor style, whitespace rendering
- Layout — gutter width, minimap width, wrap column
- Autocomplete —
provideCompletionscode editor, extra keywords/types, unifiedcompletionsJSON editor with live validation,replaceBuiltinstoggle - Behavior — auto-close pairs, line comment token, word separators, undo batch window
- Actions — buttons for every
executeCommandandgetValue/setValue - Event log — live feed of all
onChange/onCursorChange/onSelectionChange/onFocus/onBlurevents
License
MIT
