@nocturnium/svelte-ide 1.0.1 → 1.0.3

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.
Files changed (74) hide show
  1. package/README.md +5 -3
  2. package/dist/components/ai/AIMessageContent.svelte +24 -14
  3. package/dist/components/ai/AIPanel.svelte +22 -0
  4. package/dist/components/editor/CollaborativeEditor.svelte +68 -5
  5. package/dist/components/editor/CollaborativeEditor.svelte.d.ts +14 -0
  6. package/dist/components/editor/CustomEditor.svelte +52 -33
  7. package/dist/components/editor/CustomEditor.svelte.d.ts +2 -2
  8. package/dist/components/editor/Editor.svelte +17 -0
  9. package/dist/components/editor/Editor.svelte.d.ts +9 -0
  10. package/dist/components/editor/EditorPane.svelte +18 -1
  11. package/dist/components/editor/EditorPane.svelte.d.ts +5 -0
  12. package/dist/components/editor/EditorSelections.svelte +27 -11
  13. package/dist/components/editor/EditorSelections.svelte.d.ts +1 -0
  14. package/dist/components/editor/core/folding.d.ts +11 -0
  15. package/dist/components/editor/core/folding.js +41 -0
  16. package/dist/components/editor/core/index.d.ts +0 -5
  17. package/dist/components/editor/core/index.js +4 -5
  18. package/dist/components/editor/core/state.d.ts +5 -0
  19. package/dist/components/editor/core/state.js +131 -12
  20. package/dist/components/editor/editor-find.d.ts +1 -0
  21. package/dist/components/editor/editor-find.js +6 -5
  22. package/dist/components/editor/editor-input.d.ts +1 -0
  23. package/dist/components/editor/editor-input.js +4 -1
  24. package/dist/components/editor/editor-scroll.d.ts +1 -0
  25. package/dist/components/editor/editor-scroll.js +2 -1
  26. package/dist/components/editor/index.d.ts +19 -3
  27. package/dist/components/editor/index.js +18 -4
  28. package/dist/components/editor/tokenizer/base.d.ts +1 -25
  29. package/dist/components/editor/tokenizer/base.js +0 -172
  30. package/dist/components/editor/tokenizer/index.d.ts +4 -0
  31. package/dist/components/editor/tokenizer/index.js +1 -1
  32. package/dist/components/editor/tokenizer/languages/html.d.ts +3 -2
  33. package/dist/components/editor/tokenizer/languages/html.js +64 -6
  34. package/dist/components/editor/tokenizer/languages/javascript.d.ts +13 -5
  35. package/dist/components/editor/tokenizer/languages/javascript.js +69 -57
  36. package/dist/components/editor/tokenizer/languages/svelte.d.ts +1 -1
  37. package/dist/components/editor/tokenizer/languages/svelte.js +6 -1
  38. package/dist/components/editor/tokenizer/types.d.ts +0 -28
  39. package/dist/crdt/awareness.d.ts +8 -2
  40. package/dist/crdt/awareness.js +11 -4
  41. package/dist/crdt/document.d.ts +10 -1
  42. package/dist/crdt/document.js +15 -7
  43. package/dist/crdt/index.d.ts +8 -2
  44. package/dist/crdt/index.js +5 -2
  45. package/dist/crdt/undo.d.ts +2 -7
  46. package/dist/crdt/undo.js +1 -8
  47. package/dist/index.d.ts +7 -9
  48. package/dist/index.js +7 -9
  49. package/dist/services/error-handling.d.ts +2 -11
  50. package/dist/services/error-handling.js +15 -4
  51. package/dist/services/lsp-client.d.ts +3 -0
  52. package/dist/services/lsp-client.js +55 -10
  53. package/dist/services/optimistic.d.ts +8 -5
  54. package/dist/services/optimistic.js +36 -10
  55. package/dist/services/vfs-client.js +11 -3
  56. package/dist/stores/agents.svelte.js +3 -2
  57. package/dist/stores/ai-persistence.svelte.js +7 -2
  58. package/dist/stores/ai.svelte.js +2 -1
  59. package/dist/stores/collaboration.svelte.d.ts +1 -1
  60. package/dist/stores/collaboration.svelte.js +3 -2
  61. package/dist/stores/editor.svelte.js +29 -5
  62. package/dist/stores/layout.svelte.js +3 -0
  63. package/dist/stores/plugin.svelte.js +9 -3
  64. package/dist/stores/vfs.svelte.js +26 -9
  65. package/dist/styles/theme.css +43 -0
  66. package/dist/types/vfs.d.ts +15 -1
  67. package/dist/types/vfs.js +9 -0
  68. package/dist/utils/language.d.ts +4 -3
  69. package/dist/utils/language.js +8 -18
  70. package/package.json +1 -1
  71. package/dist/components/editor/MinimalEditor.svelte +0 -75
  72. package/dist/components/editor/MinimalEditor.svelte.d.ts +0 -6
  73. package/dist/components/editor/MinimalEditor2.svelte +0 -84
  74. package/dist/components/editor/MinimalEditor2.svelte.d.ts +0 -6
@@ -2,6 +2,7 @@
2
2
  import Editor from './Editor.svelte';
3
3
  import EditorTabs from './EditorTabs.svelte';
4
4
  import type { EditorPreferences } from '../../types';
5
+ import type { AIAwareness } from './core/ai-awareness';
5
6
  import {
6
7
  getTabs,
7
8
  getActiveTab,
@@ -15,11 +16,23 @@
15
16
 
16
17
  interface Props {
17
18
  preferences?: Partial<EditorPreferences>;
19
+ folding?: boolean;
20
+ multiCursor?: boolean;
21
+ maxCursors?: number;
22
+ aiAgents?: AIAwareness[];
18
23
  onSave?: (path: string, content: string) => Promise<void>;
19
24
  class?: string;
20
25
  }
21
26
 
22
- let { preferences = {}, onSave, class: className = '' }: Props = $props();
27
+ let {
28
+ preferences = {},
29
+ folding = true,
30
+ multiCursor = true,
31
+ maxCursors = 100,
32
+ aiAgents = [],
33
+ onSave,
34
+ class: className = ''
35
+ }: Props = $props();
23
36
 
24
37
  // Use getter functions for reactive access
25
38
  let tabs = $derived(getTabs());
@@ -70,6 +83,10 @@
70
83
  language={activeTab.language}
71
84
  readonly={activeTab.aiEditing}
72
85
  {preferences}
86
+ {folding}
87
+ {multiCursor}
88
+ {maxCursors}
89
+ {aiAgents}
73
90
  onChange={handleChange}
74
91
  onCursorChange={handleCursorChange}
75
92
  onSave={handleSave}
@@ -1,6 +1,11 @@
1
1
  import type { EditorPreferences } from '../../types';
2
+ import type { AIAwareness } from './core/ai-awareness';
2
3
  interface Props {
3
4
  preferences?: Partial<EditorPreferences>;
5
+ folding?: boolean;
6
+ multiCursor?: boolean;
7
+ maxCursors?: number;
8
+ aiAgents?: AIAwareness[];
4
9
  onSave?: (path: string, content: string) => Promise<void>;
5
10
  class?: string;
6
11
  }
@@ -26,6 +26,7 @@
26
26
  viewportHeight: number;
27
27
  getLine: (n: number) => { text: string } | undefined;
28
28
  lineCount: number;
29
+ lineToVisualRow?: (line: number) => number;
29
30
  }
30
31
 
31
32
  let {
@@ -39,7 +40,8 @@
39
40
  scrollTop,
40
41
  viewportHeight,
41
42
  getLine,
42
- lineCount
43
+ lineCount,
44
+ lineToVisualRow = (line) => line
43
45
  }: Props = $props();
44
46
 
45
47
  // Selection rects memoization
@@ -70,17 +72,17 @@
70
72
 
71
73
  // Calculate visible line range (with 1-line buffer for smooth scrolling)
72
74
  const vh = viewportHeight || FALLBACK_VIEWPORT_HEIGHT;
73
- const firstVisibleLine = Math.max(0, Math.floor(scrollTop / lineHeight) - 1);
74
- const lastVisibleLine = Math.min(lineCount - 1, Math.ceil((scrollTop + vh) / lineHeight) + 1);
75
+ const firstVisibleRow = Math.max(0, Math.floor(scrollTop / lineHeight) - 1);
76
+ const lastVisibleRow = Math.max(0, Math.ceil((scrollTop + vh) / lineHeight) + 1);
75
77
 
76
78
  // Create cache key from all cursors, scroll position, and measurement values
77
79
  const cursorKeys = cursors
78
80
  .map(
79
81
  (c) =>
80
- `${c.id}:${c.selection.anchor.line}:${c.selection.anchor.column}-${c.selection.head.line}:${c.selection.head.column}`
82
+ `${c.id}:${c.selection.anchor.line}/${lineToVisualRow(c.selection.anchor.line)}:${c.selection.anchor.column}-${c.selection.head.line}/${lineToVisualRow(c.selection.head.line)}:${c.selection.head.column}`
81
83
  )
82
84
  .join('|');
83
- const key = `${cursorKeys}@${firstVisibleLine}-${lastVisibleLine}:${charWidth}:${lineHeight}:${gutterWidth}`;
85
+ const key = `${cursorKeys}@${firstVisibleRow}-${lastVisibleRow}:${charWidth}:${lineHeight}:${gutterWidth}`;
84
86
  if (key === cachedSelectionKey) {
85
87
  return cachedSelectionRects;
86
88
  }
@@ -100,13 +102,14 @@
100
102
  const start = getSelectionStart(cursor.selection);
101
103
  const end = getSelectionEnd(cursor.selection);
102
104
 
103
- // Only iterate over lines that are both selected AND visible
104
- const renderStart = Math.max(start.line, firstVisibleLine);
105
- const renderEnd = Math.min(end.line, lastVisibleLine);
105
+ const renderStart = Math.max(start.line, 0);
106
+ const renderEnd = Math.min(end.line, lineCount - 1);
106
107
 
107
108
  for (let line = renderStart; line <= renderEnd; line++) {
108
109
  const lineContent = getLine(line);
109
110
  if (!lineContent) continue;
111
+ const visualRow = lineToVisualRow(line);
112
+ if (visualRow < firstVisibleRow || visualRow > lastVisibleRow) continue;
110
113
 
111
114
  let startCol = 0;
112
115
  let endCol = lineContent.text.length;
@@ -122,7 +125,7 @@
122
125
  const width = Math.max((endCol - startCol) * charWidth, 4);
123
126
 
124
127
  rects.push({
125
- top: line * lineHeight,
128
+ top: visualRow * lineHeight,
126
129
  left: gutterWidth + contentPadding + startCol * charWidth,
127
130
  width,
128
131
  height: lineHeight,
@@ -139,7 +142,7 @@
139
142
  // Get cursor style for a specific cursor position
140
143
  function getCursorStyleForPosition(pos: Position): string {
141
144
  const left = gutterWidth + contentPadding + pos.column * charWidth;
142
- const top = pos.line * lineHeight;
145
+ const top = lineToVisualRow(pos.line) * lineHeight;
143
146
  return `left: ${left}px; top: ${top}px; height: ${lineHeight}px;`;
144
147
  }
145
148
  </script>
@@ -156,11 +159,12 @@
156
159
  </div>
157
160
 
158
161
  <!-- Cursors (all cursors rendered with primary/secondary distinction) -->
159
- {#if cursorVisible && !isReadonly}
162
+ {#if !isReadonly}
160
163
  {#each cursors as cursor (cursor.id)}
161
164
  <div
162
165
  class="custom-editor__cursor"
163
166
  class:custom-editor__cursor--secondary={!cursor.isPrimary}
167
+ class:custom-editor__cursor--hidden={!cursorVisible}
164
168
  style={getCursorStyleForPosition(cursor.selection.head)}
165
169
  ></div>
166
170
  {/each}
@@ -193,6 +197,7 @@
193
197
  background: var(--color-nocturnium-aurora-blue);
194
198
  z-index: 10;
195
199
  pointer-events: none;
200
+ opacity: 1;
196
201
  transition: opacity 80ms;
197
202
  }
198
203
 
@@ -201,4 +206,15 @@
201
206
  background: var(--ide-interactive-muted);
202
207
  opacity: 0.85;
203
208
  }
209
+
210
+ .custom-editor__cursor--hidden {
211
+ opacity: 0;
212
+ }
213
+
214
+ @media (prefers-reduced-motion: reduce) {
215
+ .custom-editor__cursor {
216
+ opacity: 1;
217
+ transition: none;
218
+ }
219
+ }
204
220
  </style>
@@ -19,6 +19,7 @@ interface Props {
19
19
  text: string;
20
20
  } | undefined;
21
21
  lineCount: number;
22
+ lineToVisualRow?: (line: number) => number;
22
23
  }
23
24
  declare const EditorSelections: import("svelte").Component<Props, {}, "">;
24
25
  type EditorSelections = ReturnType<typeof EditorSelections>;
@@ -109,6 +109,17 @@ export declare class FoldManager {
109
109
  * Get visible lines (line numbers that should be rendered)
110
110
  */
111
111
  getVisibleLines(totalLines: number): number[];
112
+ /**
113
+ * Map a raw document line to its compacted visual row.
114
+ *
115
+ * Visible lines map to their exact row in getVisibleLines(totalLines). Hidden
116
+ * lines map to the row of the nearest visible fold header at or above them.
117
+ */
118
+ lineToVisualRow(line: number, totalLines: number): number;
119
+ /**
120
+ * Map a compacted visual row back to the raw document line rendered there.
121
+ */
122
+ visualRowToLine(visualRow: number, totalLines: number): number;
112
123
  /**
113
124
  * Get the number of hidden lines after a fold start
114
125
  */
@@ -641,6 +641,47 @@ export class FoldManager {
641
641
  }
642
642
  return visible;
643
643
  }
644
+ /**
645
+ * Map a raw document line to its compacted visual row.
646
+ *
647
+ * Visible lines map to their exact row in getVisibleLines(totalLines). Hidden
648
+ * lines map to the row of the nearest visible fold header at or above them.
649
+ */
650
+ lineToVisualRow(line, totalLines) {
651
+ if (totalLines <= 0)
652
+ return 0;
653
+ const clampedLine = Math.max(0, Math.min(Math.floor(line), totalLines - 1));
654
+ const visible = this.getVisibleLines(totalLines);
655
+ if (visible.length === 0)
656
+ return 0;
657
+ let low = 0;
658
+ let high = visible.length;
659
+ while (low < high) {
660
+ const mid = Math.floor((low + high) / 2);
661
+ if (visible[mid] < clampedLine) {
662
+ low = mid + 1;
663
+ }
664
+ else {
665
+ high = mid;
666
+ }
667
+ }
668
+ if (visible[low] === clampedLine) {
669
+ return low;
670
+ }
671
+ return Math.max(0, low - 1);
672
+ }
673
+ /**
674
+ * Map a compacted visual row back to the raw document line rendered there.
675
+ */
676
+ visualRowToLine(visualRow, totalLines) {
677
+ if (totalLines <= 0)
678
+ return 0;
679
+ const visible = this.getVisibleLines(totalLines);
680
+ if (visible.length === 0)
681
+ return 0;
682
+ const clampedRow = Math.max(0, Math.min(Math.floor(visualRow), visible.length - 1));
683
+ return visible[clampedRow];
684
+ }
644
685
  /**
645
686
  * Get the number of hidden lines after a fold start
646
687
  */
@@ -4,18 +4,13 @@
4
4
  export * from './state';
5
5
  export * from './navigation';
6
6
  export * from './keybindings';
7
- export * from './crdt-binding';
8
7
  export * from './search';
9
8
  export * from './folding';
10
9
  export * from './multi-cursor';
11
10
  export * from './complexity-analyzer';
12
11
  export * from './ai-awareness';
13
- export * from './ghost-pair';
14
12
  export * from './semantic-analyzer';
15
13
  export * from './commands';
16
- export * from './timeline';
17
- export * from './conflict-predictor';
18
- export * from './echo-cursor';
19
14
  export * from './bracket-healer';
20
15
  export * from './git-blame';
21
16
  export * from './snippet-manager';
@@ -4,18 +4,17 @@
4
4
  export * from './state';
5
5
  export * from './navigation';
6
6
  export * from './keybindings';
7
- export * from './crdt-binding';
7
+ // NOTE: './crdt-binding' is intentionally NOT re-exported here. It imports yjs
8
+ // (an optional peer dependency), so leaking it through this barrel would force
9
+ // yjs onto every consumer of the non-collaborative editor. The CRDT binding is
10
+ // available through the dedicated `@nocturnium/svelte-ide/crdt` entry instead.
8
11
  export * from './search';
9
12
  export * from './folding';
10
13
  export * from './multi-cursor';
11
14
  export * from './complexity-analyzer';
12
15
  export * from './ai-awareness';
13
- export * from './ghost-pair';
14
16
  export * from './semantic-analyzer';
15
17
  export * from './commands';
16
- export * from './timeline';
17
- export * from './conflict-predictor';
18
- export * from './echo-cursor';
19
18
  export * from './bracket-healer';
20
19
  export * from './git-blame';
21
20
  export * from './snippet-manager';
@@ -305,6 +305,10 @@ export declare class EditorState {
305
305
  * Deep copy a selection to prevent reference issues in history
306
306
  */
307
307
  private deepCopySelection;
308
+ private transformCursorUpdatesForInsert;
309
+ private transformPositionForInsert;
310
+ private transformCursorUpdatesForDelete;
311
+ private transformPositionForDelete;
308
312
  /**
309
313
  * Check if undo is available
310
314
  */
@@ -317,6 +321,7 @@ export declare class EditorState {
317
321
  * Compare two tokenizer states for equality
318
322
  */
319
323
  private tokenizerStatesEqual;
324
+ private tokenizerStateValuesEqual;
320
325
  /**
321
326
  * Re-tokenize a range of lines with early-exit optimization
322
327
  */
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { getTokenizer } from '../tokenizer';
9
9
  import { HISTORY_GROUP_TIMEOUT_MS, MAX_HISTORY_SIZE } from '../constants';
10
- import { CursorManager, createCursorManager, getSelectionStart, getSelectionEnd, isSelectionEmpty } from './multi-cursor';
10
+ import { CursorManager, createCursorManager, comparePositions, getSelectionStart, getSelectionEnd, isSelectionEmpty } from './multi-cursor';
11
11
  /**
12
12
  * Core editor state class
13
13
  */
@@ -361,10 +361,12 @@ export class EditorState {
361
361
  const start = getSelectionStart(cursor.selection);
362
362
  const end = getSelectionEnd(cursor.selection);
363
363
  this.deleteRangeInternal(start, end);
364
+ this.transformCursorUpdatesForDelete(updates, start, end);
364
365
  currentPos = start;
365
366
  }
366
367
  // Insert text
367
368
  const newPos = this.insertAtInternal(currentPos, text);
369
+ this.transformCursorUpdatesForInsert(updates, currentPos, text);
368
370
  updates.push({ id: cursor.id, position: newPos });
369
371
  }
370
372
  // Apply all cursor updates using batch update to avoid multiple merge/emit calls
@@ -453,6 +455,7 @@ export class EditorState {
453
455
  const start = getSelectionStart(cursor.selection);
454
456
  const end = getSelectionEnd(cursor.selection);
455
457
  this.deleteRangeInternal(start, end);
458
+ this.transformCursorUpdatesForDelete(updates, start, end);
456
459
  updates.push({ id: cursor.id, position: start });
457
460
  }
458
461
  }
@@ -519,25 +522,32 @@ export class EditorState {
519
522
  for (const cursor of cursors) {
520
523
  const { line, column } = cursor.selection.head;
521
524
  if (column > 0) {
525
+ const from = { line, column: column - 1 };
526
+ const to = { line, column };
522
527
  // Delete within line
523
528
  const currentLine = this._lines[line];
524
- currentLine.text = currentLine.text.slice(0, column - 1) + currentLine.text.slice(column);
529
+ currentLine.text =
530
+ currentLine.text.slice(0, from.column) + currentLine.text.slice(to.column);
525
531
  // Re-tokenize to end of document so deleting a multi-line construct
526
532
  // delimiter re-propagates state to lines below.
527
533
  this.retokenize(line, this._lines.length);
528
- updates.push({ id: cursor.id, position: { line, column: column - 1 } });
534
+ this.transformCursorUpdatesForDelete(updates, from, to);
535
+ updates.push({ id: cursor.id, position: from });
529
536
  }
530
537
  else if (line > 0) {
531
538
  // Join with previous line
532
539
  const prevLine = this._lines[line - 1];
533
540
  const currentLine = this._lines[line];
534
541
  const newColumn = prevLine.text.length;
542
+ const from = { line: line - 1, column: newColumn };
543
+ const to = { line, column: 0 };
535
544
  prevLine.text += currentLine.text;
536
545
  this._lines.splice(line, 1);
537
546
  this._tokenizerStates.splice(line, 1);
538
547
  this.renumberLines(line);
539
548
  this.retokenize(line - 1, this._lines.length);
540
- updates.push({ id: cursor.id, position: { line: line - 1, column: newColumn } });
549
+ this.transformCursorUpdatesForDelete(updates, from, to);
550
+ updates.push({ id: cursor.id, position: from });
541
551
  }
542
552
  }
543
553
  // Apply all cursor updates using batch update
@@ -564,28 +574,41 @@ export class EditorState {
564
574
  return;
565
575
  }
566
576
  this.saveHistory('delete');
577
+ const updates = [];
567
578
  for (const cursor of cursors) {
568
579
  const { line, column } = cursor.selection.head;
569
580
  const currentLine = this._lines[line];
570
581
  if (column < currentLine.text.length) {
582
+ const from = { line, column };
583
+ const to = { line, column: column + 1 };
571
584
  // Delete within line
572
- currentLine.text = currentLine.text.slice(0, column) + currentLine.text.slice(column + 1);
585
+ currentLine.text =
586
+ currentLine.text.slice(0, from.column) + currentLine.text.slice(to.column);
573
587
  // Re-tokenize to end of document so deleting a multi-line construct
574
588
  // delimiter re-propagates state to lines below.
575
589
  this.retokenize(line, this._lines.length);
590
+ this.transformCursorUpdatesForDelete(updates, from, to);
591
+ updates.push({ id: cursor.id, position: from });
576
592
  }
577
593
  else if (line < this._lines.length - 1) {
578
594
  // Join with next line
579
595
  const nextLine = this._lines[line + 1];
596
+ const from = { line, column };
597
+ const to = { line: line + 1, column: 0 };
580
598
  currentLine.text += nextLine.text;
581
599
  this._lines.splice(line + 1, 1);
582
600
  this._tokenizerStates.splice(line + 1, 1);
583
601
  this.renumberLines(line + 1);
584
602
  this.retokenize(line, this._lines.length);
603
+ this.transformCursorUpdatesForDelete(updates, from, to);
604
+ updates.push({ id: cursor.id, position: from });
585
605
  }
586
606
  }
607
+ // Apply all cursor updates using batch update
608
+ this._cursorManager.batchUpdateCursors(updates);
587
609
  this.emitChange({ type: 'delete', from: this.cursor, to: this.cursor });
588
610
  this.emitSelectionChange();
611
+ this.emitCursorChange();
589
612
  }
590
613
  /**
591
614
  * Insert a new line (enter key)
@@ -624,8 +647,8 @@ export class EditorState {
624
647
  timestamp: now
625
648
  };
626
649
  if (shouldMerge) {
627
- // Update the last entry instead of creating a new one
628
- this._undoStack[this._undoStack.length - 1] = entry;
650
+ // Keep the original restore snapshot; only extend the group's recency.
651
+ this._undoStack[this._undoStack.length - 1].timestamp = now;
629
652
  }
630
653
  else {
631
654
  this._undoStack.push(entry);
@@ -715,6 +738,57 @@ export class EditorState {
715
738
  head: { line: sel.head.line, column: sel.head.column }
716
739
  };
717
740
  }
741
+ transformCursorUpdatesForInsert(updates, at, text) {
742
+ for (const update of updates) {
743
+ update.position = this.transformPositionForInsert(update.position, at, text);
744
+ }
745
+ }
746
+ transformPositionForInsert(position, at, text) {
747
+ if (comparePositions(position, at) < 0) {
748
+ return position;
749
+ }
750
+ const textLines = text.split('\n');
751
+ const insertedLineCount = textLines.length - 1;
752
+ if (insertedLineCount === 0) {
753
+ if (position.line !== at.line) {
754
+ return position;
755
+ }
756
+ return { line: position.line, column: position.column + text.length };
757
+ }
758
+ if (position.line === at.line) {
759
+ return {
760
+ line: position.line + insertedLineCount,
761
+ column: textLines[textLines.length - 1].length + position.column - at.column
762
+ };
763
+ }
764
+ return { line: position.line + insertedLineCount, column: position.column };
765
+ }
766
+ transformCursorUpdatesForDelete(updates, from, to) {
767
+ for (const update of updates) {
768
+ update.position = this.transformPositionForDelete(update.position, from, to);
769
+ }
770
+ }
771
+ transformPositionForDelete(position, from, to) {
772
+ if (comparePositions(position, from) <= 0) {
773
+ return position;
774
+ }
775
+ if (comparePositions(position, to) <= 0) {
776
+ return from;
777
+ }
778
+ if (from.line === to.line) {
779
+ if (position.line !== from.line) {
780
+ return position;
781
+ }
782
+ return { line: position.line, column: position.column - (to.column - from.column) };
783
+ }
784
+ if (position.line === to.line) {
785
+ return {
786
+ line: from.line,
787
+ column: from.column + position.column - to.column
788
+ };
789
+ }
790
+ return { line: position.line - (to.line - from.line), column: position.column };
791
+ }
718
792
  /**
719
793
  * Check if undo is available
720
794
  */
@@ -738,11 +812,56 @@ export class EditorState {
738
812
  return true;
739
813
  if (!a || !b)
740
814
  return a === b;
741
- return (a.inBlockComment === b.inBlockComment &&
742
- a.inTemplateLiteral === b.inTemplateLiteral &&
743
- a.inMultilineString === b.inMultilineString &&
744
- a.stringDelimiter === b.stringDelimiter &&
745
- a.templateDepth === b.templateDepth);
815
+ const aKeys = Object.keys(a)
816
+ .filter((key) => a[key] !== undefined)
817
+ .sort();
818
+ const bKeys = Object.keys(b)
819
+ .filter((key) => b[key] !== undefined)
820
+ .sort();
821
+ if (aKeys.length !== bKeys.length)
822
+ return false;
823
+ for (let i = 0; i < aKeys.length; i++) {
824
+ const key = aKeys[i];
825
+ if (key !== bKeys[i])
826
+ return false;
827
+ if (!this.tokenizerStateValuesEqual(a[key], b[key])) {
828
+ return false;
829
+ }
830
+ }
831
+ return true;
832
+ }
833
+ tokenizerStateValuesEqual(a, b) {
834
+ if (a === b)
835
+ return true;
836
+ if (typeof a !== typeof b)
837
+ return false;
838
+ if (a === null || b === null)
839
+ return a === b;
840
+ if (typeof a !== 'object' || typeof b !== 'object')
841
+ return false;
842
+ if (Array.isArray(a) || Array.isArray(b)) {
843
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
844
+ return false;
845
+ return a.every((value, index) => this.tokenizerStateValuesEqual(value, b[index]));
846
+ }
847
+ const aRecord = a;
848
+ const bRecord = b;
849
+ const aKeys = Object.keys(aRecord)
850
+ .filter((key) => aRecord[key] !== undefined)
851
+ .sort();
852
+ const bKeys = Object.keys(bRecord)
853
+ .filter((key) => bRecord[key] !== undefined)
854
+ .sort();
855
+ if (aKeys.length !== bKeys.length)
856
+ return false;
857
+ for (let i = 0; i < aKeys.length; i++) {
858
+ const key = aKeys[i];
859
+ if (key !== bKeys[i])
860
+ return false;
861
+ if (!this.tokenizerStateValuesEqual(aRecord[key], bRecord[key]))
862
+ return false;
863
+ }
864
+ return true;
746
865
  }
747
866
  /**
748
867
  * Re-tokenize a range of lines with early-exit optimization
@@ -17,6 +17,7 @@ export interface EditorFindDeps {
17
17
  scrollCursorIntoView: () => void;
18
18
  focusEditor: () => void;
19
19
  isReadonly: () => boolean;
20
+ lineToVisualRow?: (line: number) => number;
20
21
  }
21
22
  export interface MatchRect {
22
23
  top: number;
@@ -92,19 +92,20 @@ export function createEditorFind(deps) {
92
92
  return [];
93
93
  const { scrollTop, viewportHeight } = viewport;
94
94
  const { lineHeight, charWidth, gutterWidth, contentPadding } = measurements;
95
- const firstVisibleLine = Math.max(0, Math.floor(scrollTop / lineHeight) - 1);
96
- const lineCount = deps.getLineCount();
97
- const lastVisibleLine = Math.min(lineCount - 1, Math.ceil((scrollTop + viewportHeight) / lineHeight) + 1);
95
+ const lineToVisualRow = deps.lineToVisualRow ?? ((line) => line);
96
+ const firstVisibleRow = Math.max(0, Math.floor(scrollTop / lineHeight) - 1);
97
+ const lastVisibleRow = Math.max(0, Math.ceil((scrollTop + viewportHeight) / lineHeight) + 1);
98
98
  const rects = [];
99
99
  for (let i = 0; i < matches.length; i++) {
100
100
  const match = matches[i];
101
- if (match.line < firstVisibleLine || match.line > lastVisibleLine)
101
+ const visualRow = lineToVisualRow(match.line);
102
+ if (visualRow < firstVisibleRow || visualRow > lastVisibleRow)
102
103
  continue;
103
104
  const width = (match.endColumn - match.startColumn) * charWidth;
104
105
  if (width <= 0)
105
106
  continue;
106
107
  rects.push({
107
- top: match.line * lineHeight,
108
+ top: visualRow * lineHeight,
108
109
  left: gutterWidth + contentPadding + match.startColumn * charWidth,
109
110
  width,
110
111
  height: lineHeight,
@@ -30,6 +30,7 @@ export interface EditorInputDeps {
30
30
  lineHeight: number;
31
31
  gutterWidth: number;
32
32
  };
33
+ visualRowToLine?: (visualRow: number) => number;
33
34
  /**
34
35
  * Editor tab size (columns per tab stop). Optional for backwards
35
36
  * compatibility; when omitted, DEFAULT_TAB_SIZE is used. Needed so that
@@ -82,7 +82,10 @@ export function createEditorInput(deps) {
82
82
  const x = e.clientX - rect.left - gutterWidth - CONTENT_PADDING;
83
83
  const y = e.clientY - rect.top;
84
84
  const editorState = deps.getEditorState();
85
- const line = Math.max(0, Math.min(Math.floor((y + scrollTop) / lineHeight), editorState.lineCount - 1));
85
+ const visualRow = Math.max(0, Math.min(Math.floor((y + scrollTop) / lineHeight), editorState.lineCount - 1));
86
+ const line = deps.visualRowToLine
87
+ ? deps.visualRowToLine(visualRow)
88
+ : Math.max(0, Math.min(visualRow, editorState.lineCount - 1));
86
89
  const lineContent = editorState.getLine(line);
87
90
  const lineText = lineContent?.text ?? '';
88
91
  const tabSize = deps.getTabSize?.() ?? DEFAULT_TAB_SIZE;
@@ -2,6 +2,7 @@ import type { Selection } from './core';
2
2
  export interface ScrollConfig {
3
3
  getEditorContent: () => HTMLDivElement | null;
4
4
  getSelection: () => Selection;
5
+ lineToVisualRow?: (line: number) => number;
5
6
  getMeasurements: () => {
6
7
  lineHeight: number;
7
8
  charWidth: number;
@@ -8,7 +8,8 @@ export function createEditorScroll(config) {
8
8
  return;
9
9
  const selection = config.getSelection();
10
10
  const { lineHeight, charWidth, gutterWidth } = config.getMeasurements();
11
- const cursorTop = selection.head.line * lineHeight;
11
+ const lineToVisualRow = config.lineToVisualRow ?? ((line) => line);
12
+ const cursorTop = lineToVisualRow(selection.head.line) * lineHeight;
12
13
  const cursorLeft = gutterWidth + CONTENT_PADDING + selection.head.column * charWidth;
13
14
  const viewportHeight = editorContent.clientHeight;
14
15
  const viewportWidth = editorContent.clientWidth;
@@ -3,13 +3,29 @@
3
3
  */
4
4
  export { default as Editor } from './Editor.svelte';
5
5
  export { default as CustomEditor } from './CustomEditor.svelte';
6
- export { default as CollaborativeEditor } from './CollaborativeEditor.svelte';
7
6
  export { default as EditorTabs } from './EditorTabs.svelte';
8
7
  export { default as EditorPane } from './EditorPane.svelte';
9
8
  export { default as FileIcon } from './FileIcon.svelte';
10
9
  export { default as FileExplorer } from './FileExplorer.svelte';
11
- export * from './core';
10
+ export * from './core/state';
11
+ export * from './core/navigation';
12
+ export * from './core/keybindings';
13
+ export * from './core/search';
14
+ export * from './core/folding';
15
+ export * from './core/multi-cursor';
16
+ export * from './core/complexity-analyzer';
17
+ export * from './core/ai-awareness';
18
+ export * from './core/semantic-analyzer';
19
+ export * from './core/commands';
20
+ export * from './core/bracket-healer';
21
+ export * from './core/git-blame';
22
+ export * from './core/snippet-manager';
23
+ export * from './core/quick-actions';
24
+ export * from './core/diagnostics';
25
+ export * from './core/breakpoints';
26
+ export type { Position } from './core/state';
27
+ export type { Diagnostic, Range } from './core/quick-actions';
12
28
  export * from './theme';
13
29
  export { getLanguageExtension, getLanguageConfig, getLanguageFromExtension, getLanguageFromFilename, getLanguageFromMimeType, getAllLanguageConfigs, resolveLanguage, type LanguageConfig } from './languages';
14
30
  export { getSupportedLanguages, isLanguageSupported } from './languages';
15
- export { getTokenizer, tokenize, getTokenClass, tokensToHTML, PlaintextTokenizer, SimpleTokenizer, GrammarTokenizer, createToken, type Token, type TokenizedLine, type TokenizerState, type TokenType, type LanguageTokenizer, type TokenRule, type LanguageGrammar } from './tokenizer';
31
+ export { getTokenizer, tokenize, getTokenClass, tokensToHTML, PlaintextTokenizer, createToken, type Token, type TokenizedLine, type TokenizerState, type TokenType, type LanguageTokenizer } from './tokenizer';