@synclineapi/editor 2.0.0 → 3.0.1

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.
@@ -1,3 +1,69 @@
1
+ export declare class AwarenessManager {
2
+ private _local;
3
+ private _peers;
4
+ private _transport;
5
+ private _unsub;
6
+ private _heartbeat;
7
+ private _gcTimer;
8
+ private _onChange;
9
+ constructor(transport: CRDTTransport, name: string);
10
+ /** Called by the binding when the local cursor moves */
11
+ updateCursor(row: number, col: number): void;
12
+ updateSelection(from: {
13
+ row: number;
14
+ col: number;
15
+ }, to: {
16
+ row: number;
17
+ col: number;
18
+ }): void;
19
+ clearSelection(): void;
20
+ get localState(): AwarenessState;
21
+ get peers(): Map<string, AwarenessState>;
22
+ onChange(cb: (peers: Map<string, AwarenessState>) => void): void;
23
+ /** Expose broadcast for external use (e.g. initial announcement) */
24
+ broadcast(): void;
25
+ private _broadcast;
26
+ destroy(): void;
27
+ }
28
+
29
+ export declare interface AwarenessState {
30
+ readonly siteId: string;
31
+ readonly name: string;
32
+ readonly color: string;
33
+ cursor: {
34
+ row: number;
35
+ col: number;
36
+ } | null;
37
+ selection: {
38
+ from: {
39
+ row: number;
40
+ col: number;
41
+ };
42
+ to: {
43
+ row: number;
44
+ col: number;
45
+ };
46
+ } | null;
47
+ readonly updatedAt: number;
48
+ }
49
+
50
+ export declare interface BindingOptions {
51
+ /** Display name shown to remote peers (default: 'Peer') */
52
+ name?: string;
53
+ /** Called whenever the set of remote peers changes */
54
+ onPeersChange?: (peers: Map<string, AwarenessState>) => void;
55
+ }
56
+
57
+ export declare class BroadcastChannelTransport implements CRDTTransport {
58
+ readonly siteId: string;
59
+ private readonly _channel;
60
+ private readonly _cbs;
61
+ constructor(channelName: string, siteId: string);
62
+ send(msg: ChannelMessage): void;
63
+ onMessage(cb: (msg: ChannelMessage) => void): () => void;
64
+ close(): void;
65
+ }
66
+
1
67
  export declare const BUILT_IN_THEMES: ThemeDefinition[];
2
68
 
3
69
  /**
@@ -15,6 +81,56 @@ export declare interface CacheMetrics {
15
81
  size: number;
16
82
  }
17
83
 
84
+ export declare type ChannelMessage = {
85
+ type: 'ops';
86
+ fromSite: string;
87
+ ops: CRDTOp[];
88
+ sv: Record<string, number>;
89
+ } | {
90
+ type: 'awareness';
91
+ state: AwarenessState;
92
+ } | {
93
+ type: 'sync-request';
94
+ fromSite: string;
95
+ sv: Record<string, number>;
96
+ } | {
97
+ type: 'sync-reply';
98
+ fromSite: string;
99
+ ops: CRDTOp[];
100
+ };
101
+
102
+ /** Unique identifier for a character node in the CRDT sequence */
103
+ export declare interface CharId {
104
+ readonly site: string;
105
+ readonly clock: number;
106
+ }
107
+
108
+ export declare function charIdEq(a: CharId, b: CharId): boolean;
109
+
110
+ export declare function charIdToStr(id: CharId): string;
111
+
112
+ /**
113
+ * Collaboration configuration passed to `EditorConfig.collab`.
114
+ * When set, the editor automatically creates a CRDT binding and renders
115
+ * remote peer cursors, selections, and presence indicators.
116
+ */
117
+ export declare interface CollabConfig {
118
+ /** The transport layer to use (LocalTransport, BroadcastChannelTransport, or WebSocketTransport). */
119
+ transport: CRDTTransport;
120
+ /** Display name shown to remote peers. Default: `'Peer'`. */
121
+ name?: string;
122
+ /**
123
+ * Called whenever the set of connected peers changes.
124
+ * The map is keyed by siteId and contains each peer's latest awareness state.
125
+ */
126
+ onPeersChange?: (peers: Map<string, AwarenessState>) => void;
127
+ /**
128
+ * Called when the WebSocket connection status changes.
129
+ * Only relevant when using `WebSocketTransport`.
130
+ */
131
+ onStatus?: (status: 'connecting' | 'connected' | 'disconnected' | 'syncing') => void;
132
+ }
133
+
18
134
  /**
19
135
  * Context passed to a `provideCompletions` callback.
20
136
  * Describes the current cursor position and document state at the moment
@@ -104,6 +220,74 @@ export declare interface CompletionItem {
104
220
  */
105
221
  export declare type CompletionKind = 'kw' | 'fn' | 'typ' | 'cls' | 'var' | 'snip' | 'emmet';
106
222
 
223
+ export declare class CRDTBinding {
224
+ readonly doc: RGADocument;
225
+ readonly awareness: AwarenessManager;
226
+ private readonly _editor;
227
+ private readonly _transport;
228
+ private readonly _unsub;
229
+ private _lastText;
230
+ private _suppress;
231
+ constructor(editor: EditorLike, doc: RGADocument, transport: CRDTTransport, opts?: BindingOptions);
232
+ private _onEditorChange;
233
+ private _onMessage;
234
+ private _applyRemoteOps;
235
+ destroy(): void;
236
+ }
237
+
238
+ /** A node in the CRDT linked sequence */
239
+ export declare interface CRDTChar {
240
+ readonly id: CharId;
241
+ readonly after: CharId;
242
+ readonly char: string;
243
+ deleted: boolean;
244
+ }
245
+
246
+ export declare type CRDTOp = InsertOp | DeleteOp;
247
+
248
+ export declare interface CRDTTransport {
249
+ readonly siteId: string;
250
+ send(msg: ChannelMessage): void;
251
+ onMessage(cb: (msg: ChannelMessage) => void): () => void;
252
+ close(): void;
253
+ }
254
+
255
+ /**
256
+ * Convenience factory: create a fully wired collaborative editor session.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const session = createCollabSession(editor, { room: 'my-doc', name: 'Alice' });
261
+ * // Later:
262
+ * session.destroy();
263
+ * ```
264
+ */
265
+ export declare function createCollabSession(editor: {
266
+ getValue(): string;
267
+ setValue(v: string): void;
268
+ updateConfig(c: Record<string, unknown>): void;
269
+ }, options?: {
270
+ /** Room / channel name shared by all collaborating peers */
271
+ room?: string;
272
+ /** Stable unique ID for this peer. Defaults to crypto.randomUUID(). */
273
+ siteId?: string;
274
+ /** Display name shown to remote peers */
275
+ name?: string;
276
+ /**
277
+ * Transport type:
278
+ * - 'local' (default) — in-process event bus, for same-page demos
279
+ * - 'broadcastchannel' — cross-tab on same origin
280
+ * - A CRDTTransport constructor (e.g. WebSocketTransport)
281
+ */
282
+ transport?: 'local' | 'broadcastchannel' | (new (room: string, siteId: string) => CRDTTransport);
283
+ /** Called whenever the set of remote peers changes */
284
+ onPeersChange?: (peers: Map<string, AwarenessState>) => void;
285
+ }): {
286
+ binding: CRDTBinding;
287
+ doc: RGADocument;
288
+ destroy(): void;
289
+ };
290
+
107
291
  /** Convenience type alias for the `createEditor` function signature. */
108
292
  export declare type CreateEditor = typeof createEditor;
109
293
 
@@ -153,6 +337,29 @@ export declare interface CursorPosition {
153
337
  col: number;
154
338
  }
155
339
 
340
+ export declare interface DeleteOp {
341
+ readonly type: 'delete';
342
+ readonly id: CharId;
343
+ }
344
+
345
+ export declare interface DiffEdit {
346
+ readonly type: 'insert' | 'delete';
347
+ readonly offset: number;
348
+ readonly chars: string;
349
+ }
350
+
351
+ /**
352
+ * Compute a minimal edit script from `oldText` to `newText`.
353
+ *
354
+ * Strategy: trim common prefix and suffix, then the middle is
355
+ * either a pure delete, a pure insert, or a replace (delete at
356
+ * position P + insert at position P). Offsets are in old-text
357
+ * coordinates for deletes and new-text coordinates for inserts.
358
+ *
359
+ * This is O(n) and correct for all practical editor operations.
360
+ */
361
+ export declare function diffText(a: string, b: string): DiffEdit[];
362
+
156
363
  /**
157
364
  * Public API surface returned by `SynclineEditor.create()`.
158
365
  * All methods are safe to call at any time after construction.
@@ -689,6 +896,54 @@ export declare interface EditorConfig {
689
896
  * ```
690
897
  */
691
898
  provideHover?: (context: HoverContext) => HoverDoc | null | undefined;
899
+ /**
900
+ * Ghost text shown inside the editor when the document is completely empty.
901
+ * Only rendered when this string is non-empty.
902
+ * @default ''
903
+ */
904
+ placeholder?: string;
905
+ /**
906
+ * Enable the Go to Line bar (Ctrl+G / Cmd+G).
907
+ * When `false` the shortcut does nothing and the bar is never shown.
908
+ * @default false
909
+ */
910
+ goToLine?: boolean;
911
+ /**
912
+ * Enable real-time collaborative editing via CRDT.
913
+ *
914
+ * When set, the editor automatically:
915
+ * - Creates an RGA CRDT document and binds it to this editor instance.
916
+ * - Broadcasts local edits and cursor/selection state to remote peers.
917
+ * - Renders remote peer cursors (coloured beams + name labels) and
918
+ * remote selections (semi-transparent coloured highlights) live.
919
+ * - Reconciles state on reconnect — works offline and re-syncs automatically.
920
+ *
921
+ * To enable collaboration just pass a transport:
922
+ * ```ts
923
+ * import { WebSocketTransport } from '@synclineapi/editor';
924
+ *
925
+ * const editor = SynclineEditor.create(container, {
926
+ * collab: {
927
+ * transport: new WebSocketTransport('wss://relay.example.com', 'my-room', crypto.randomUUID()),
928
+ * name: 'Alice',
929
+ * onPeersChange: (peers) => console.log('peers:', [...peers.values()].map(p => p.name)),
930
+ * },
931
+ * });
932
+ * ```
933
+ *
934
+ * To change or remove collab at runtime call `updateConfig({ collab: ... })`.
935
+ * Pass `undefined` to tear down the existing binding.
936
+ *
937
+ * @default undefined
938
+ */
939
+ collab?: CollabConfig;
940
+ }
941
+
942
+ /** Minimal interface required from the editor */
943
+ declare interface EditorLike {
944
+ getValue(): string;
945
+ setValue(v: string): void;
946
+ updateConfig(c: Record<string, unknown>): void;
692
947
  }
693
948
 
694
949
  /**
@@ -759,12 +1014,30 @@ declare interface HoverDoc {
759
1014
  language?: string | string[];
760
1015
  }
761
1016
 
1017
+ export declare interface InsertOp {
1018
+ readonly type: 'insert';
1019
+ readonly id: CharId;
1020
+ readonly after: CharId;
1021
+ readonly char: string;
1022
+ }
1023
+
762
1024
  /**
763
1025
  * Supported language identifiers for syntax highlighting and autocomplete.
764
1026
  * Pass as the `language` option in `EditorConfig`.
765
1027
  */
766
1028
  export declare type Language = 'typescript' | 'javascript' | 'css' | 'json' | 'markdown' | 'text';
767
1029
 
1030
+ export declare class LocalTransport implements CRDTTransport {
1031
+ readonly siteId: string;
1032
+ private readonly _room;
1033
+ private readonly _cbs;
1034
+ constructor(room: string, siteId: string);
1035
+ private readonly _receive;
1036
+ send(msg: ChannelMessage): void;
1037
+ onMessage(cb: (msg: ChannelMessage) => void): () => void;
1038
+ close(): void;
1039
+ }
1040
+
768
1041
  /**
769
1042
  * Colour palette used when painting the minimap canvas.
770
1043
  * Each value is a CSS colour string resolved from the active theme.
@@ -796,6 +1069,72 @@ export declare interface MinimapColors {
796
1069
  [key: string]: string;
797
1070
  }
798
1071
 
1072
+ export declare class RGADocument {
1073
+ /** All nodes, keyed by their canonical string ID */
1074
+ private readonly _chars;
1075
+ /** The full ordered sequence of IDs (visible + tombstones) */
1076
+ private _seq;
1077
+ /** Reverse index: idStr → index in _seq (invalidated on splice, rebuilt lazily) */
1078
+ private _idxDirty;
1079
+ private _idxMap;
1080
+ readonly siteId: string;
1081
+ private _clock;
1082
+ readonly stateVector: StateVector;
1083
+ /** Full operation log — used to answer sync-requests from peers */
1084
+ private readonly _opLog;
1085
+ constructor(siteId: string);
1086
+ get clock(): number;
1087
+ /** Return the visible document text */
1088
+ getText(): string;
1089
+ /** Number of visible characters */
1090
+ get length(): number;
1091
+ /**
1092
+ * Insert `text` at visible offset `offset`.
1093
+ * Returns the generated ops (apply them locally AND send to peers).
1094
+ */
1095
+ localInsert(offset: number, text: string): InsertOp[];
1096
+ /**
1097
+ * Delete `length` visible characters starting at `offset`.
1098
+ * Returns the generated ops.
1099
+ */
1100
+ localDelete(offset: number, length: number): DeleteOp[];
1101
+ /**
1102
+ * Apply an op from a remote peer.
1103
+ * Returns true if the op was new (applied), false if already seen.
1104
+ */
1105
+ apply(op: CRDTOp): boolean;
1106
+ /** Return all ops not yet seen by `sv` */
1107
+ getOpsSince(sv: StateVector): CRDTOp[];
1108
+ getStateVector(): StateVector;
1109
+ serialize(): {
1110
+ siteId: string;
1111
+ clock: number;
1112
+ ops: CRDTOp[];
1113
+ };
1114
+ static deserialize(data: {
1115
+ siteId: string;
1116
+ clock: number;
1117
+ ops: CRDTOp[];
1118
+ }): RGADocument;
1119
+ private _genId;
1120
+ private _updateSV;
1121
+ private _applyInsert;
1122
+ private _applyDelete;
1123
+ /**
1124
+ * YATA comparison: returns >0 if `a` has priority (goes first / to the left).
1125
+ * Higher clock wins; equal clock → lower siteId wins.
1126
+ */
1127
+ private _cmp;
1128
+ private _rebuildIdx;
1129
+ /** Return the ID of the n-th visible character (0-indexed). Returns ROOT_ID for n<0. */
1130
+ private _visibleIdAt;
1131
+ /** Return IDs of `len` visible characters starting at `offset` */
1132
+ private _visibleIdsInRange;
1133
+ }
1134
+
1135
+ /** Sentinel root node — logical start-of-document anchor */
1136
+ export declare const ROOT_ID: CharId;
1137
+
799
1138
  /**
800
1139
  * A selection range defined by an anchor (where selection started)
801
1140
  * and a focus (where the caret currently sits). Either end can be
@@ -813,6 +1152,15 @@ declare interface Selection_2 {
813
1152
  }
814
1153
  export { Selection_2 as Selection }
815
1154
 
1155
+ export declare function siteColor(siteId: string): string;
1156
+
1157
+ /** Maps siteId → highest clock value seen from that site */
1158
+ export declare type StateVector = Map<string, number>;
1159
+
1160
+ export declare function svFromRecord(r: Record<string, number>): StateVector;
1161
+
1162
+ export declare function svToRecord(sv: StateVector): Record<string, number>;
1163
+
816
1164
  export declare class SynclineEditor implements EditorAPI {
817
1165
  private readonly _host;
818
1166
  private readonly _shadow;
@@ -835,6 +1183,10 @@ export declare class SynclineEditor implements EditorAPI {
835
1183
  private readonly _hoverTip;
836
1184
  private readonly _themeOverlay;
837
1185
  private readonly _themePanel;
1186
+ private readonly _placeholderEl;
1187
+ private readonly _goToLineBar;
1188
+ private readonly _goToLineInput;
1189
+ private readonly _goToLineHint;
838
1190
  private _config;
839
1191
  private _tab;
840
1192
  private _wm;
@@ -867,6 +1219,10 @@ export declare class SynclineEditor implements EditorAPI {
867
1219
  private _dynamicStyleEl;
868
1220
  private _emmetExpanded;
869
1221
  private _snippetExpanded;
1222
+ /** Active snippet tab-stop session. Cleared on click, Escape, or structural edit. */
1223
+ private _snippetSession;
1224
+ private _collabBinding;
1225
+ private _remotePeers;
870
1226
  /** Pending debounce timer ID for autocomplete re-computation. */
871
1227
  private _acDebounceTimer;
872
1228
  /** Pending requestAnimationFrame ID for coalesced renders. */
@@ -893,8 +1249,15 @@ export declare class SynclineEditor implements EditorAPI {
893
1249
  undo(): void;
894
1250
  redo(): void;
895
1251
  executeCommand(command: string): void;
1252
+ private _initCollab;
1253
+ private _destroyCollab;
1254
+ /** Notify remote peers of the current local cursor / selection position. */
1255
+ private _broadcastCursorToCollab;
896
1256
  destroy(): void;
897
1257
  private _buildFindBar;
1258
+ private _buildGoToLineBar;
1259
+ private _openGoToLine;
1260
+ private _closeGoToLine;
898
1261
  private _buildStatusBar;
899
1262
  private _applyDynamicStyles;
900
1263
  /**
@@ -974,6 +1337,7 @@ export declare class SynclineEditor implements EditorAPI {
974
1337
  private _isWordChar;
975
1338
  private _wordSkipRight;
976
1339
  private _wordSkipLeft;
1340
+ private _duplicateLines;
977
1341
  private _moveLines;
978
1342
  private _scheduleHover;
979
1343
  private _doHover;
@@ -1180,4 +1544,53 @@ export declare interface TokenColors {
1180
1544
  decorator?: string;
1181
1545
  }
1182
1546
 
1547
+ export declare class WebSocketTransport implements CRDTTransport {
1548
+ readonly siteId: string;
1549
+ private _ws;
1550
+ private readonly _baseUrl;
1551
+ private readonly _cbs;
1552
+ private _reconnectTimer;
1553
+ private _pingTimer;
1554
+ private _closed;
1555
+ private _attempt;
1556
+ private _queue;
1557
+ private readonly _opts;
1558
+ /**
1559
+ * @param serverUrl Base WebSocket URL **without** trailing slash.
1560
+ * Room and siteId are appended automatically.
1561
+ * Example: `"ws://localhost:8080"` → connects to
1562
+ * `ws://localhost:8080/<room>?site=<siteId>`
1563
+ * @param room Document / collaboration room identifier.
1564
+ * @param siteId This peer's unique ID (UUID).
1565
+ * @param opts Optional behaviour hooks and tuning knobs.
1566
+ */
1567
+ constructor(serverUrl: string, room: string, siteId: string, opts?: WebSocketTransportOptions);
1568
+ private _connect;
1569
+ /** Exponential back-off: 250 ms × 2^attempt, capped at maxBackoffMs. */
1570
+ private _scheduleReconnect;
1571
+ private _startPing;
1572
+ private _stopPing;
1573
+ send(msg: ChannelMessage): void;
1574
+ private _rawSend;
1575
+ onMessage(cb: (msg: ChannelMessage) => void): () => void;
1576
+ close(): void;
1577
+ /** Current WebSocket ready-state as a human-readable string. */
1578
+ get connectionState(): 'connecting' | 'open' | 'closing' | 'closed';
1579
+ /** Number of messages currently buffered waiting for reconnection. */
1580
+ get queuedMessageCount(): number;
1581
+ }
1582
+
1583
+ declare interface WebSocketTransportOptions {
1584
+ /** Called each time the connection is established (including re-connects). */
1585
+ onConnect?: () => void;
1586
+ /** Called when the connection drops. `willReconnect` is false on explicit close(). */
1587
+ onDisconnect?: (willReconnect: boolean) => void;
1588
+ /** Called on unrecoverable or unexpected errors. */
1589
+ onError?: (err: Event) => void;
1590
+ /** Ping interval in ms. Default 25 000. Set to 0 to disable. */
1591
+ pingIntervalMs?: number;
1592
+ /** Maximum reconnection delay in ms. Default 30 000. */
1593
+ maxBackoffMs?: number;
1594
+ }
1595
+
1183
1596
  export { }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.0.0",
6
+ "version": "3.0.1",
7
7
  "description": "A zero-dependency, pixel-perfect, fully customisable browser-based code editor",
8
8
  "type": "module",
9
9
  "main": "./dist/syncline-editor.umd.js",
@@ -22,6 +22,7 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "dev": "vite --config vite.config.ts",
25
+ "dev:app": "vite --config vite.config.app.ts",
25
26
  "build": "tsc --noEmit && vite build --config vite.lib.config.ts",
26
27
  "build:app": "vite build --config vite.config.app.ts",
27
28
  "preview": "vite preview --config vite.config.ts",