@nocturnium/svelte-ide 1.0.2 → 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.
- package/README.md +5 -3
- package/dist/components/ai/AIMessageContent.svelte +24 -14
- package/dist/components/ai/AIPanel.svelte +22 -0
- package/dist/components/editor/CollaborativeEditor.svelte +68 -5
- package/dist/components/editor/CollaborativeEditor.svelte.d.ts +14 -0
- package/dist/components/editor/CustomEditor.svelte +52 -33
- package/dist/components/editor/CustomEditor.svelte.d.ts +2 -2
- package/dist/components/editor/Editor.svelte +17 -0
- package/dist/components/editor/Editor.svelte.d.ts +9 -0
- package/dist/components/editor/EditorPane.svelte +18 -1
- package/dist/components/editor/EditorPane.svelte.d.ts +5 -0
- package/dist/components/editor/EditorSelections.svelte +27 -11
- package/dist/components/editor/EditorSelections.svelte.d.ts +1 -0
- package/dist/components/editor/core/folding.d.ts +11 -0
- package/dist/components/editor/core/folding.js +41 -0
- package/dist/components/editor/core/index.d.ts +0 -5
- package/dist/components/editor/core/index.js +4 -5
- package/dist/components/editor/core/state.d.ts +5 -0
- package/dist/components/editor/core/state.js +131 -12
- package/dist/components/editor/editor-find.d.ts +1 -0
- package/dist/components/editor/editor-find.js +6 -5
- package/dist/components/editor/editor-input.d.ts +1 -0
- package/dist/components/editor/editor-input.js +4 -1
- package/dist/components/editor/editor-scroll.d.ts +1 -0
- package/dist/components/editor/editor-scroll.js +2 -1
- package/dist/components/editor/index.d.ts +19 -3
- package/dist/components/editor/index.js +18 -4
- package/dist/components/editor/tokenizer/base.d.ts +1 -25
- package/dist/components/editor/tokenizer/base.js +0 -172
- package/dist/components/editor/tokenizer/index.d.ts +4 -0
- package/dist/components/editor/tokenizer/index.js +1 -1
- package/dist/components/editor/tokenizer/languages/html.d.ts +3 -2
- package/dist/components/editor/tokenizer/languages/html.js +64 -6
- package/dist/components/editor/tokenizer/languages/javascript.d.ts +0 -3
- package/dist/components/editor/tokenizer/languages/javascript.js +1 -2
- package/dist/components/editor/tokenizer/languages/svelte.d.ts +1 -1
- package/dist/components/editor/tokenizer/languages/svelte.js +6 -1
- package/dist/components/editor/tokenizer/types.d.ts +0 -28
- package/dist/crdt/awareness.d.ts +8 -2
- package/dist/crdt/awareness.js +11 -4
- package/dist/crdt/document.d.ts +10 -1
- package/dist/crdt/document.js +15 -7
- package/dist/crdt/index.d.ts +8 -2
- package/dist/crdt/index.js +5 -2
- package/dist/crdt/undo.d.ts +2 -7
- package/dist/crdt/undo.js +1 -8
- package/dist/index.d.ts +7 -9
- package/dist/index.js +7 -9
- package/dist/services/error-handling.d.ts +2 -11
- package/dist/services/error-handling.js +15 -4
- package/dist/services/lsp-client.d.ts +3 -0
- package/dist/services/lsp-client.js +55 -10
- package/dist/services/optimistic.d.ts +8 -5
- package/dist/services/optimistic.js +36 -10
- package/dist/services/vfs-client.js +11 -3
- package/dist/stores/agents.svelte.js +3 -2
- package/dist/stores/ai-persistence.svelte.js +7 -2
- package/dist/stores/ai.svelte.js +2 -1
- package/dist/stores/collaboration.svelte.d.ts +1 -1
- package/dist/stores/collaboration.svelte.js +3 -2
- package/dist/stores/editor.svelte.js +29 -5
- package/dist/stores/layout.svelte.js +3 -0
- package/dist/stores/plugin.svelte.js +9 -3
- package/dist/stores/vfs.svelte.js +26 -9
- package/dist/styles/theme.css +43 -0
- package/dist/types/vfs.d.ts +15 -1
- package/dist/types/vfs.js +9 -0
- package/dist/utils/language.d.ts +4 -3
- package/dist/utils/language.js +8 -18
- package/package.json +1 -1
- package/dist/components/editor/MinimalEditor.svelte +0 -75
- package/dist/components/editor/MinimalEditor.svelte.d.ts +0 -6
- package/dist/components/editor/MinimalEditor2.svelte +0 -84
- 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 {
|
|
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
|
|
74
|
-
const
|
|
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}@${
|
|
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
|
-
|
|
104
|
-
const
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
//
|
|
628
|
-
this._undoStack[this._undoStack.length - 1] =
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
|
@@ -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
|
|
96
|
-
const
|
|
97
|
-
const
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
31
|
+
export { getTokenizer, tokenize, getTokenClass, tokensToHTML, PlaintextTokenizer, createToken, type Token, type TokenizedLine, type TokenizerState, type TokenType, type LanguageTokenizer } from './tokenizer';
|