@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.
- 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 +13 -5
- package/dist/components/editor/tokenizer/languages/javascript.js +69 -57
- 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
package/dist/crdt/awareness.d.ts
CHANGED
|
@@ -28,10 +28,16 @@ export interface AwarenessProtocol {
|
|
|
28
28
|
onUsersChange(callback: (users: AwarenessUser[]) => void): () => void;
|
|
29
29
|
destroy(): void;
|
|
30
30
|
}
|
|
31
|
+
export interface CreateAwarenessProtocolOptions {
|
|
32
|
+
destroyAwareness?: boolean;
|
|
33
|
+
}
|
|
31
34
|
/**
|
|
32
|
-
* Create an awareness protocol instance
|
|
35
|
+
* Create an awareness protocol instance.
|
|
36
|
+
*
|
|
37
|
+
* Pass a provider-owned Awareness instance to broadcast presence through that
|
|
38
|
+
* provider. Passing a Y.Doc creates a standalone Awareness instance.
|
|
33
39
|
*/
|
|
34
|
-
export declare function createAwarenessProtocol(
|
|
40
|
+
export declare function createAwarenessProtocol(source: YDoc | Awareness, options?: CreateAwarenessProtocolOptions): AwarenessProtocol;
|
|
35
41
|
/**
|
|
36
42
|
* Generate a random color for a user
|
|
37
43
|
*/
|
package/dist/crdt/awareness.js
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Awareness } from 'y-protocols/awareness';
|
|
5
5
|
/**
|
|
6
|
-
* Create an awareness protocol instance
|
|
6
|
+
* Create an awareness protocol instance.
|
|
7
|
+
*
|
|
8
|
+
* Pass a provider-owned Awareness instance to broadcast presence through that
|
|
9
|
+
* provider. Passing a Y.Doc creates a standalone Awareness instance.
|
|
7
10
|
*/
|
|
8
|
-
export function createAwarenessProtocol(
|
|
9
|
-
const
|
|
11
|
+
export function createAwarenessProtocol(source, options = {}) {
|
|
12
|
+
const ownsAwareness = source instanceof Awareness;
|
|
13
|
+
const awareness = ownsAwareness ? source : new Awareness(source);
|
|
14
|
+
const shouldDestroyAwareness = options.destroyAwareness ?? !ownsAwareness;
|
|
10
15
|
const userChangeCallbacks = new Set();
|
|
11
16
|
// Track awareness changes
|
|
12
17
|
awareness.on('change', () => {
|
|
@@ -75,7 +80,9 @@ export function createAwarenessProtocol(doc) {
|
|
|
75
80
|
return () => userChangeCallbacks.delete(callback);
|
|
76
81
|
},
|
|
77
82
|
destroy() {
|
|
78
|
-
|
|
83
|
+
if (shouldDestroyAwareness) {
|
|
84
|
+
awareness.destroy();
|
|
85
|
+
}
|
|
79
86
|
userChangeCallbacks.clear();
|
|
80
87
|
}
|
|
81
88
|
};
|
package/dist/crdt/document.d.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as Y from 'yjs';
|
|
6
6
|
import type { DocumentOptions } from './types';
|
|
7
|
+
export interface CollaborativeUndoManagerOptions {
|
|
8
|
+
captureTimeout?: number;
|
|
9
|
+
trackedOrigins?: Set<unknown>;
|
|
10
|
+
deleteFilter?: (item: Y.Item) => boolean;
|
|
11
|
+
}
|
|
7
12
|
export declare class CollaborativeDocument {
|
|
8
13
|
readonly doc: Y.Doc;
|
|
9
14
|
readonly id: string;
|
|
@@ -49,7 +54,11 @@ export declare class CollaborativeDocument {
|
|
|
49
54
|
/**
|
|
50
55
|
* Setup undo manager
|
|
51
56
|
*/
|
|
52
|
-
|
|
57
|
+
getUndoManager(options?: CollaborativeUndoManagerOptions): Y.UndoManager;
|
|
58
|
+
/**
|
|
59
|
+
* Check whether this document currently owns an undo manager.
|
|
60
|
+
*/
|
|
61
|
+
hasUndoManager(): boolean;
|
|
53
62
|
/**
|
|
54
63
|
* Track additional origins for undo
|
|
55
64
|
*/
|
package/dist/crdt/document.js
CHANGED
|
@@ -18,7 +18,7 @@ export class CollaborativeDocument {
|
|
|
18
18
|
}
|
|
19
19
|
// Setup undo manager if enabled
|
|
20
20
|
if (options.enableUndo !== false) {
|
|
21
|
-
this.
|
|
21
|
+
this.getUndoManager({ captureTimeout: options.undoCaptureTimeout });
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
@@ -97,20 +97,28 @@ export class CollaborativeDocument {
|
|
|
97
97
|
/**
|
|
98
98
|
* Setup undo manager
|
|
99
99
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
getUndoManager(options = {}) {
|
|
101
|
+
if (this.undoManager)
|
|
102
|
+
return this.undoManager;
|
|
103
|
+
const { captureTimeout = 500, trackedOrigins = new Set([null, 'local']), deleteFilter } = options;
|
|
102
104
|
this.undoManager = new Y.UndoManager(this.getText(), {
|
|
103
105
|
trackedOrigins,
|
|
104
|
-
captureTimeout
|
|
106
|
+
captureTimeout,
|
|
107
|
+
deleteFilter
|
|
105
108
|
});
|
|
109
|
+
return this.undoManager;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Check whether this document currently owns an undo manager.
|
|
113
|
+
*/
|
|
114
|
+
hasUndoManager() {
|
|
115
|
+
return this.undoManager !== null;
|
|
106
116
|
}
|
|
107
117
|
/**
|
|
108
118
|
* Track additional origins for undo
|
|
109
119
|
*/
|
|
110
120
|
trackOrigin(origin) {
|
|
111
|
-
|
|
112
|
-
this.undoManager.trackedOrigins.add(origin);
|
|
113
|
-
}
|
|
121
|
+
this.getUndoManager().trackedOrigins.add(origin);
|
|
114
122
|
}
|
|
115
123
|
/**
|
|
116
124
|
* Undo last change
|
package/dist/crdt/index.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { CollaborativeDocument } from './document';
|
|
6
6
|
export { CollaborativeProvider } from './provider';
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
7
|
+
export { createProvider } from './provider';
|
|
8
|
+
export { createAwarenessProtocol, generateUserColor, getInitials } from './awareness';
|
|
9
|
+
export { createUndoManager, createUserUndoManager } from './undo';
|
|
10
|
+
export { default as CollaborativeEditor } from '../components/editor/CollaborativeEditor.svelte';
|
|
11
|
+
export { CRDTBinding, createCRDTBinding, createRelativePosition, resolveRelativePosition } from '../components/editor/core/crdt-binding';
|
|
9
12
|
export type * from './types';
|
|
13
|
+
export type { AwarenessProtocol, AwarenessUser, CreateAwarenessProtocolOptions } from './awareness';
|
|
14
|
+
export type { UndoManagerInstance, UndoManagerOptions, UndoManagerState } from './undo';
|
|
15
|
+
export type { CRDTBindingConfig, CRDTPosition, RelativePosition } from '../components/editor/core/crdt-binding';
|
package/dist/crdt/index.js
CHANGED
|
@@ -4,5 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { CollaborativeDocument } from './document';
|
|
6
6
|
export { CollaborativeProvider } from './provider';
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
7
|
+
export { createProvider } from './provider';
|
|
8
|
+
export { createAwarenessProtocol, generateUserColor, getInitials } from './awareness';
|
|
9
|
+
export { createUndoManager, createUserUndoManager } from './undo';
|
|
10
|
+
export { default as CollaborativeEditor } from '../components/editor/CollaborativeEditor.svelte';
|
|
11
|
+
export { CRDTBinding, createCRDTBinding, createRelativePosition, resolveRelativePosition } from '../components/editor/core/crdt-binding';
|
package/dist/crdt/undo.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Undo manager for CRDT documents
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
export interface UndoManagerOptions {
|
|
7
|
-
captureTimeout?: number;
|
|
8
|
-
trackedOrigins?: Set<unknown>;
|
|
9
|
-
deleteFilter?: (item: Y.Item) => boolean;
|
|
10
|
-
}
|
|
4
|
+
import type { CollaborativeDocument, CollaborativeUndoManagerOptions } from './document';
|
|
5
|
+
export type UndoManagerOptions = CollaborativeUndoManagerOptions;
|
|
11
6
|
export interface UndoManagerState {
|
|
12
7
|
canUndo: boolean;
|
|
13
8
|
canRedo: boolean;
|
package/dist/crdt/undo.js
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Undo manager for CRDT documents
|
|
3
3
|
*/
|
|
4
|
-
import * as Y from 'yjs';
|
|
5
4
|
/**
|
|
6
5
|
* Create an undo manager for a collaborative document
|
|
7
6
|
*/
|
|
8
7
|
export function createUndoManager(document, options = {}) {
|
|
9
|
-
const
|
|
10
|
-
const text = document.getText();
|
|
11
|
-
const manager = new Y.UndoManager(text, {
|
|
12
|
-
captureTimeout,
|
|
13
|
-
trackedOrigins,
|
|
14
|
-
deleteFilter
|
|
15
|
-
});
|
|
8
|
+
const manager = document.getUndoManager(options);
|
|
16
9
|
const stateCallbacks = new Set();
|
|
17
10
|
function getState() {
|
|
18
11
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - All UI components built from scratch (no UI libraries)
|
|
9
9
|
* - Native Svelte 5 runes for state management
|
|
10
10
|
* - Custom code editor (no CodeMirror dependency)
|
|
11
|
-
* - Yjs for CRDT collaboration (optional
|
|
11
|
+
* - Yjs for CRDT collaboration (optional via the ./crdt entry)
|
|
12
12
|
*
|
|
13
13
|
* ============================================================================
|
|
14
14
|
* PUBLIC API SURFACE — what belongs in this root barrel
|
|
@@ -111,11 +111,6 @@ export { default as Editor } from './components/editor/Editor.svelte';
|
|
|
111
111
|
* @public - Stable API
|
|
112
112
|
*/
|
|
113
113
|
export { default as CustomEditor } from './components/editor/CustomEditor.svelte';
|
|
114
|
-
/**
|
|
115
|
-
* Collaborative editor with CRDT support
|
|
116
|
-
* @experimental - requires the optional yjs peers; API may change in future versions
|
|
117
|
-
*/
|
|
118
|
-
export { default as CollaborativeEditor } from './components/editor/CollaborativeEditor.svelte';
|
|
119
114
|
/**
|
|
120
115
|
* Editor tab bar component
|
|
121
116
|
* @public - Stable API
|
|
@@ -142,14 +137,17 @@ export { default as FileExplorer, type FileChangeStatus, type GitFileStatus } fr
|
|
|
142
137
|
*
|
|
143
138
|
* Functions and types marked below:
|
|
144
139
|
* createEditorState, createNavigation, createKeyboardHandler,
|
|
145
|
-
* createDefaultKeybindings,
|
|
140
|
+
* createDefaultKeybindings,
|
|
146
141
|
* EditorState, Position, Selection, Line, ChangeEvent,
|
|
147
|
-
* Navigation, KeyboardHandler, Keybinding,
|
|
142
|
+
* Navigation, KeyboardHandler, Keybinding,
|
|
148
143
|
* Cursor, CursorManager, CursorManagerConfig
|
|
149
144
|
*
|
|
150
145
|
* @public - Stable API
|
|
151
146
|
*/
|
|
152
|
-
export { createEditorState,
|
|
147
|
+
export { createEditorState, type EditorState, type Position, type Selection, type Line, type ChangeEvent } from './components/editor/core/state';
|
|
148
|
+
export { createNavigation, type Navigation } from './components/editor/core/navigation';
|
|
149
|
+
export { createKeyboardHandler, createDefaultKeybindings, type KeyboardHandler, type Keybinding } from './components/editor/core/keybindings';
|
|
150
|
+
export { type Cursor, type CursorManager, type CursorManagerConfig } from './components/editor/core/multi-cursor';
|
|
153
151
|
/**
|
|
154
152
|
* Editor theme utilities and types
|
|
155
153
|
* @public - Stable API
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - All UI components built from scratch (no UI libraries)
|
|
9
9
|
* - Native Svelte 5 runes for state management
|
|
10
10
|
* - Custom code editor (no CodeMirror dependency)
|
|
11
|
-
* - Yjs for CRDT collaboration (optional
|
|
11
|
+
* - Yjs for CRDT collaboration (optional via the ./crdt entry)
|
|
12
12
|
*
|
|
13
13
|
* ============================================================================
|
|
14
14
|
* PUBLIC API SURFACE — what belongs in this root barrel
|
|
@@ -117,11 +117,6 @@ export { default as Editor } from './components/editor/Editor.svelte';
|
|
|
117
117
|
* @public - Stable API
|
|
118
118
|
*/
|
|
119
119
|
export { default as CustomEditor } from './components/editor/CustomEditor.svelte';
|
|
120
|
-
/**
|
|
121
|
-
* Collaborative editor with CRDT support
|
|
122
|
-
* @experimental - requires the optional yjs peers; API may change in future versions
|
|
123
|
-
*/
|
|
124
|
-
export { default as CollaborativeEditor } from './components/editor/CollaborativeEditor.svelte';
|
|
125
120
|
/**
|
|
126
121
|
* Editor tab bar component
|
|
127
122
|
* @public - Stable API
|
|
@@ -148,14 +143,17 @@ export { default as FileExplorer } from './components/editor/FileExplorer.svelte
|
|
|
148
143
|
*
|
|
149
144
|
* Functions and types marked below:
|
|
150
145
|
* createEditorState, createNavigation, createKeyboardHandler,
|
|
151
|
-
* createDefaultKeybindings,
|
|
146
|
+
* createDefaultKeybindings,
|
|
152
147
|
* EditorState, Position, Selection, Line, ChangeEvent,
|
|
153
|
-
* Navigation, KeyboardHandler, Keybinding,
|
|
148
|
+
* Navigation, KeyboardHandler, Keybinding,
|
|
154
149
|
* Cursor, CursorManager, CursorManagerConfig
|
|
155
150
|
*
|
|
156
151
|
* @public - Stable API
|
|
157
152
|
*/
|
|
158
|
-
export { createEditorState
|
|
153
|
+
export { createEditorState } from './components/editor/core/state';
|
|
154
|
+
export { createNavigation } from './components/editor/core/navigation';
|
|
155
|
+
export { createKeyboardHandler, createDefaultKeybindings } from './components/editor/core/keybindings';
|
|
156
|
+
export {} from './components/editor/core/multi-cursor';
|
|
159
157
|
/**
|
|
160
158
|
* Editor theme utilities and types
|
|
161
159
|
* @public - Stable API
|
|
@@ -3,17 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides structured error types, recovery strategies, and user-friendly error messages.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
code: VFSErrorCode;
|
|
9
|
-
statusCode?: number;
|
|
10
|
-
path?: string;
|
|
11
|
-
workspaceId?: string;
|
|
12
|
-
retryable: boolean;
|
|
13
|
-
userMessage: string;
|
|
14
|
-
technicalDetails?: string;
|
|
15
|
-
recoveryOptions: RecoveryOption[];
|
|
16
|
-
}
|
|
6
|
+
import { VFSError, type VFSErrorCode } from '../types/vfs';
|
|
7
|
+
export { VFSError, type VFSErrorCode };
|
|
17
8
|
export interface RecoveryOption {
|
|
18
9
|
id: string;
|
|
19
10
|
label: string;
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides structured error types, recovery strategies, and user-friendly error messages.
|
|
5
5
|
*/
|
|
6
|
+
import { VFSError } from '../types/vfs';
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Error Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
export { VFSError };
|
|
6
11
|
// ============================================================================
|
|
7
12
|
// Error Factory
|
|
8
13
|
// ============================================================================
|
|
@@ -10,9 +15,7 @@
|
|
|
10
15
|
* Create a structured VFS error
|
|
11
16
|
*/
|
|
12
17
|
export function createVFSError(code, message, options = {}) {
|
|
13
|
-
const error = new
|
|
14
|
-
error.name = 'VFSError';
|
|
15
|
-
error.code = code;
|
|
18
|
+
const error = new VFSError(message, code, options.cause);
|
|
16
19
|
error.statusCode = options.statusCode;
|
|
17
20
|
error.path = options.path;
|
|
18
21
|
error.workspaceId = options.workspaceId;
|
|
@@ -29,6 +32,14 @@ export function createVFSError(code, message, options = {}) {
|
|
|
29
32
|
export function parseError(error, context) {
|
|
30
33
|
// Already a VFSError
|
|
31
34
|
if (isVFSError(error)) {
|
|
35
|
+
error.path ??= context?.path;
|
|
36
|
+
error.workspaceId ??= context?.workspaceId;
|
|
37
|
+
error.retryable = isRetryableError(error.code);
|
|
38
|
+
error.userMessage = getUserMessage(error.code, error.path);
|
|
39
|
+
error.technicalDetails ??= error.message;
|
|
40
|
+
if (error.recoveryOptions.length === 0) {
|
|
41
|
+
error.recoveryOptions = getRecoveryOptions(error.code);
|
|
42
|
+
}
|
|
32
43
|
return error;
|
|
33
44
|
}
|
|
34
45
|
// Network/fetch errors
|
|
@@ -97,7 +108,7 @@ function parseStatusCode(status, message, context) {
|
|
|
97
108
|
* Check if error is a VFSError
|
|
98
109
|
*/
|
|
99
110
|
export function isVFSError(error) {
|
|
100
|
-
return error instanceof
|
|
111
|
+
return error instanceof VFSError;
|
|
101
112
|
}
|
|
102
113
|
/**
|
|
103
114
|
* Check if error code is retryable
|
|
@@ -13,6 +13,7 @@ export declare class LSPClient {
|
|
|
13
13
|
private _capabilities;
|
|
14
14
|
private reconnectAttempts;
|
|
15
15
|
private reconnectTimeout;
|
|
16
|
+
private intentionalDisconnect;
|
|
16
17
|
private openDocuments;
|
|
17
18
|
private diagnosticsCache;
|
|
18
19
|
private eventHandlers;
|
|
@@ -28,6 +29,8 @@ export declare class LSPClient {
|
|
|
28
29
|
private emitEvent;
|
|
29
30
|
private setState;
|
|
30
31
|
connect(): Promise<void>;
|
|
32
|
+
private cleanupSocket;
|
|
33
|
+
private getReconnectDelay;
|
|
31
34
|
private handleDisconnect;
|
|
32
35
|
disconnect(): Promise<void>;
|
|
33
36
|
private initialize;
|
|
@@ -109,6 +109,7 @@ export class LSPClient {
|
|
|
109
109
|
_capabilities = null;
|
|
110
110
|
reconnectAttempts = 0;
|
|
111
111
|
reconnectTimeout = null;
|
|
112
|
+
intentionalDisconnect = false;
|
|
112
113
|
// Document tracking
|
|
113
114
|
openDocuments = new Map();
|
|
114
115
|
diagnosticsCache = new Map();
|
|
@@ -177,11 +178,24 @@ export class LSPClient {
|
|
|
177
178
|
if (this._state !== 'disconnected' && this._state !== 'error') {
|
|
178
179
|
return;
|
|
179
180
|
}
|
|
181
|
+
this.intentionalDisconnect = false;
|
|
182
|
+
if (this.reconnectTimeout) {
|
|
183
|
+
clearTimeout(this.reconnectTimeout);
|
|
184
|
+
this.reconnectTimeout = null;
|
|
185
|
+
}
|
|
186
|
+
this.cleanupSocket();
|
|
180
187
|
this.setState('connecting');
|
|
181
188
|
return new Promise((resolve, reject) => {
|
|
182
189
|
try {
|
|
183
|
-
|
|
184
|
-
this.ws
|
|
190
|
+
const socket = new WebSocket(this.config.serverUrl);
|
|
191
|
+
this.ws = socket;
|
|
192
|
+
socket.onopen = async () => {
|
|
193
|
+
if (socket !== this.ws)
|
|
194
|
+
return;
|
|
195
|
+
if (this.reconnectTimeout) {
|
|
196
|
+
clearTimeout(this.reconnectTimeout);
|
|
197
|
+
this.reconnectTimeout = null;
|
|
198
|
+
}
|
|
185
199
|
this.reconnectAttempts = 0;
|
|
186
200
|
this.setState('connected');
|
|
187
201
|
try {
|
|
@@ -192,17 +206,21 @@ export class LSPClient {
|
|
|
192
206
|
reject(err);
|
|
193
207
|
}
|
|
194
208
|
};
|
|
195
|
-
|
|
196
|
-
this.handleDisconnect();
|
|
209
|
+
socket.onclose = () => {
|
|
210
|
+
this.handleDisconnect(socket);
|
|
197
211
|
};
|
|
198
|
-
|
|
212
|
+
socket.onerror = (_event) => {
|
|
213
|
+
if (socket !== this.ws)
|
|
214
|
+
return;
|
|
199
215
|
const error = new Error('WebSocket error');
|
|
200
216
|
this.emitEvent('onError', error);
|
|
201
217
|
if (this._state === 'connecting') {
|
|
202
218
|
reject(error);
|
|
203
219
|
}
|
|
204
220
|
};
|
|
205
|
-
|
|
221
|
+
socket.onmessage = (event) => {
|
|
222
|
+
if (socket !== this.ws)
|
|
223
|
+
return;
|
|
206
224
|
this.handleMessage(event.data);
|
|
207
225
|
};
|
|
208
226
|
}
|
|
@@ -212,7 +230,29 @@ export class LSPClient {
|
|
|
212
230
|
}
|
|
213
231
|
});
|
|
214
232
|
}
|
|
215
|
-
|
|
233
|
+
cleanupSocket(socket = this.ws) {
|
|
234
|
+
if (!socket)
|
|
235
|
+
return;
|
|
236
|
+
socket.onopen = null;
|
|
237
|
+
socket.onclose = null;
|
|
238
|
+
socket.onerror = null;
|
|
239
|
+
socket.onmessage = null;
|
|
240
|
+
if (socket.readyState !== WebSocket.CLOSING) {
|
|
241
|
+
socket.close();
|
|
242
|
+
}
|
|
243
|
+
if (socket === this.ws) {
|
|
244
|
+
this.ws = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
getReconnectDelay(attempt) {
|
|
248
|
+
const baseDelay = this.config.reconnectDelay ?? 1000;
|
|
249
|
+
const exponentialDelay = baseDelay * 2 ** (attempt - 1);
|
|
250
|
+
const jitter = 0.75 + Math.random() * 0.5;
|
|
251
|
+
return Math.round(exponentialDelay * jitter);
|
|
252
|
+
}
|
|
253
|
+
handleDisconnect(socket = this.ws) {
|
|
254
|
+
const wasIntentional = this.intentionalDisconnect;
|
|
255
|
+
this.cleanupSocket(socket);
|
|
216
256
|
this.setState('disconnected');
|
|
217
257
|
// Cancel pending requests
|
|
218
258
|
for (const [_id, pending] of this.pendingRequests) {
|
|
@@ -220,11 +260,15 @@ export class LSPClient {
|
|
|
220
260
|
pending.reject(new Error('Connection closed'));
|
|
221
261
|
}
|
|
222
262
|
this.pendingRequests.clear();
|
|
263
|
+
if (wasIntentional) {
|
|
264
|
+
this.intentionalDisconnect = false;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
223
267
|
// Auto-reconnect
|
|
224
268
|
if (this.config.autoReconnect &&
|
|
225
269
|
this.reconnectAttempts < (this.config.maxReconnectAttempts ?? 5)) {
|
|
226
270
|
this.reconnectAttempts++;
|
|
227
|
-
const delay =
|
|
271
|
+
const delay = this.getReconnectDelay(this.reconnectAttempts);
|
|
228
272
|
if (this.config.debug) {
|
|
229
273
|
console.log(`[LSP] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
230
274
|
}
|
|
@@ -236,6 +280,7 @@ export class LSPClient {
|
|
|
236
280
|
}
|
|
237
281
|
}
|
|
238
282
|
async disconnect() {
|
|
283
|
+
this.intentionalDisconnect = true;
|
|
239
284
|
if (this.reconnectTimeout) {
|
|
240
285
|
clearTimeout(this.reconnectTimeout);
|
|
241
286
|
this.reconnectTimeout = null;
|
|
@@ -251,10 +296,10 @@ export class LSPClient {
|
|
|
251
296
|
// Ignore errors during shutdown
|
|
252
297
|
}
|
|
253
298
|
}
|
|
254
|
-
this.
|
|
255
|
-
this.ws = null;
|
|
299
|
+
this.cleanupSocket();
|
|
256
300
|
}
|
|
257
301
|
this.setState('disconnected');
|
|
302
|
+
this.intentionalDisconnect = false;
|
|
258
303
|
}
|
|
259
304
|
// ============================================================================
|
|
260
305
|
// Initialize
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export interface OptimisticOperation<T> {
|
|
8
8
|
id: string;
|
|
9
|
+
scopeKey?: string;
|
|
9
10
|
type: string;
|
|
10
11
|
payload: T;
|
|
11
12
|
timestamp: number;
|
|
@@ -53,6 +54,7 @@ type CommitFn<T> = () => Promise<T>;
|
|
|
53
54
|
* ```
|
|
54
55
|
*/
|
|
55
56
|
export declare function optimisticUpdate<T, R>(options: {
|
|
57
|
+
scopeKey?: string;
|
|
56
58
|
type: string;
|
|
57
59
|
payload: T;
|
|
58
60
|
apply: () => void;
|
|
@@ -84,6 +86,7 @@ export declare function createOptimisticState<T>(initialValue: T): {
|
|
|
84
86
|
* Batch multiple optimistic operations
|
|
85
87
|
*/
|
|
86
88
|
export declare function batchOptimisticUpdates(operations: Array<{
|
|
89
|
+
scopeKey?: string;
|
|
87
90
|
type: string;
|
|
88
91
|
payload: unknown;
|
|
89
92
|
apply: () => void;
|
|
@@ -95,9 +98,9 @@ export declare function batchOptimisticUpdates(operations: Array<{
|
|
|
95
98
|
failedCount: number;
|
|
96
99
|
}>;
|
|
97
100
|
/**
|
|
98
|
-
* Get
|
|
101
|
+
* Get pending operations, optionally limited to one document/workspace scope.
|
|
99
102
|
*/
|
|
100
|
-
export declare function getPendingOperations(): OptimisticOperation<unknown>[];
|
|
103
|
+
export declare function getPendingOperations(scopeKey?: string): OptimisticOperation<unknown>[];
|
|
101
104
|
/**
|
|
102
105
|
* Get operation by ID
|
|
103
106
|
*/
|
|
@@ -105,11 +108,11 @@ export declare function getOperation(id: string): OptimisticOperation<unknown> |
|
|
|
105
108
|
/**
|
|
106
109
|
* Cancel a pending operation
|
|
107
110
|
*/
|
|
108
|
-
export declare function cancelOperation(id: string): Promise<boolean>;
|
|
111
|
+
export declare function cancelOperation(id: string, scopeKey?: string): Promise<boolean>;
|
|
109
112
|
/**
|
|
110
|
-
* Cancel
|
|
113
|
+
* Cancel pending operations, optionally limited to one document/workspace scope.
|
|
111
114
|
*/
|
|
112
|
-
export declare function cancelAllOperations(): Promise<void>;
|
|
115
|
+
export declare function cancelAllOperations(scopeKey?: string): Promise<void>;
|
|
113
116
|
export interface ConflictInfo {
|
|
114
117
|
type: 'version' | 'lock' | 'concurrent';
|
|
115
118
|
localVersion?: number;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Provides utilities for optimistic UI updates with automatic rollback on failure.
|
|
5
5
|
* Implements a queue-based system for managing pending operations.
|
|
6
6
|
*/
|
|
7
|
+
const DEFAULT_SCOPE_KEY = '__global__';
|
|
7
8
|
// ============================================================================
|
|
8
9
|
// Operation Queue
|
|
9
10
|
// ============================================================================
|
|
@@ -38,8 +39,10 @@ const rollbackFunctions = new Map();
|
|
|
38
39
|
export async function optimisticUpdate(options) {
|
|
39
40
|
const { type, payload, apply, rollback, commit, config = {} } = options;
|
|
40
41
|
const { maxRetries = 3, retryDelay = 1000, onCommit, onRollback, onRetry } = config;
|
|
42
|
+
const scopeKey = options.scopeKey ?? getScopeKeyFromPayload(payload);
|
|
41
43
|
const operation = {
|
|
42
44
|
id: crypto.randomUUID(),
|
|
45
|
+
scopeKey,
|
|
43
46
|
type,
|
|
44
47
|
payload,
|
|
45
48
|
timestamp: Date.now(),
|
|
@@ -85,7 +88,7 @@ export async function optimisticUpdate(options) {
|
|
|
85
88
|
operation.retryCount++;
|
|
86
89
|
if (operation.retryCount <= maxRetries) {
|
|
87
90
|
onRetry?.(operation, operation.retryCount);
|
|
88
|
-
await delay(retryDelay
|
|
91
|
+
await delay(getRetryDelay(retryDelay, operation.retryCount));
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
}
|
|
@@ -227,10 +230,10 @@ export async function batchOptimisticUpdates(operations, config) {
|
|
|
227
230
|
// Queue Management
|
|
228
231
|
// ============================================================================
|
|
229
232
|
/**
|
|
230
|
-
* Get
|
|
233
|
+
* Get pending operations, optionally limited to one document/workspace scope.
|
|
231
234
|
*/
|
|
232
|
-
export function getPendingOperations() {
|
|
233
|
-
return Array.from(operationQueue.values()).filter((op) => op.status === 'pending');
|
|
235
|
+
export function getPendingOperations(scopeKey) {
|
|
236
|
+
return Array.from(operationQueue.values()).filter((op) => op.status === 'pending' && (!scopeKey || op.scopeKey === scopeKey));
|
|
234
237
|
}
|
|
235
238
|
/**
|
|
236
239
|
* Get operation by ID
|
|
@@ -241,9 +244,11 @@ export function getOperation(id) {
|
|
|
241
244
|
/**
|
|
242
245
|
* Cancel a pending operation
|
|
243
246
|
*/
|
|
244
|
-
export async function cancelOperation(id) {
|
|
247
|
+
export async function cancelOperation(id, scopeKey) {
|
|
245
248
|
const operation = operationQueue.get(id);
|
|
246
|
-
if (!operation ||
|
|
249
|
+
if (!operation ||
|
|
250
|
+
operation.status !== 'pending' ||
|
|
251
|
+
(scopeKey && operation.scopeKey !== scopeKey)) {
|
|
247
252
|
return false;
|
|
248
253
|
}
|
|
249
254
|
const rollback = rollbackFunctions.get(id);
|
|
@@ -262,12 +267,12 @@ export async function cancelOperation(id) {
|
|
|
262
267
|
return true;
|
|
263
268
|
}
|
|
264
269
|
/**
|
|
265
|
-
* Cancel
|
|
270
|
+
* Cancel pending operations, optionally limited to one document/workspace scope.
|
|
266
271
|
*/
|
|
267
|
-
export async function cancelAllOperations() {
|
|
268
|
-
const pending = getPendingOperations();
|
|
272
|
+
export async function cancelAllOperations(scopeKey) {
|
|
273
|
+
const pending = getPendingOperations(scopeKey);
|
|
269
274
|
for (const op of pending) {
|
|
270
|
-
await cancelOperation(op.id);
|
|
275
|
+
await cancelOperation(op.id, scopeKey);
|
|
271
276
|
}
|
|
272
277
|
}
|
|
273
278
|
/**
|
|
@@ -319,6 +324,27 @@ export function parseConflictDetails(error) {
|
|
|
319
324
|
function delay(ms) {
|
|
320
325
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
321
326
|
}
|
|
327
|
+
function getScopeKeyFromPayload(payload) {
|
|
328
|
+
if (payload && typeof payload === 'object') {
|
|
329
|
+
const candidate = payload;
|
|
330
|
+
if (typeof candidate.scopeKey === 'string')
|
|
331
|
+
return candidate.scopeKey;
|
|
332
|
+
if (typeof candidate.workspaceId === 'string' && typeof candidate.path === 'string') {
|
|
333
|
+
return `${candidate.workspaceId}:${candidate.path}`;
|
|
334
|
+
}
|
|
335
|
+
if (typeof candidate.workspace === 'string' && typeof candidate.path === 'string') {
|
|
336
|
+
return `${candidate.workspace}:${candidate.path}`;
|
|
337
|
+
}
|
|
338
|
+
if (typeof candidate.path === 'string')
|
|
339
|
+
return candidate.path;
|
|
340
|
+
}
|
|
341
|
+
return DEFAULT_SCOPE_KEY;
|
|
342
|
+
}
|
|
343
|
+
function getRetryDelay(baseDelay, attempt) {
|
|
344
|
+
const exponentialDelay = baseDelay * 2 ** (attempt - 1);
|
|
345
|
+
const jitter = 0.75 + Math.random() * 0.5;
|
|
346
|
+
return Math.round(exponentialDelay * jitter);
|
|
347
|
+
}
|
|
322
348
|
/**
|
|
323
349
|
* Create a debounced optimistic updater
|
|
324
350
|
*/
|
|
@@ -55,7 +55,7 @@ async function request(path, options = {}) {
|
|
|
55
55
|
if (err instanceof VFSError)
|
|
56
56
|
throw err;
|
|
57
57
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
58
|
-
throw new VFSError('Request timeout', '
|
|
58
|
+
throw new VFSError('Request timeout', 'TIMEOUT');
|
|
59
59
|
}
|
|
60
60
|
throw new VFSError(err instanceof Error ? err.message : 'Unknown error', 'NETWORK_ERROR', err);
|
|
61
61
|
}
|
|
@@ -65,14 +65,22 @@ async function request(path, options = {}) {
|
|
|
65
65
|
}
|
|
66
66
|
function mapStatusToErrorCode(status) {
|
|
67
67
|
switch (status) {
|
|
68
|
-
case
|
|
69
|
-
return 'FILE_NOT_FOUND';
|
|
68
|
+
case 401:
|
|
70
69
|
case 403:
|
|
71
70
|
return 'PERMISSION_DENIED';
|
|
71
|
+
case 404:
|
|
72
|
+
return 'FILE_NOT_FOUND';
|
|
72
73
|
case 409:
|
|
73
74
|
return 'VERSION_CONFLICT';
|
|
74
75
|
case 423:
|
|
75
76
|
return 'FILE_LOCKED';
|
|
77
|
+
case 429:
|
|
78
|
+
return 'RATE_LIMITED';
|
|
79
|
+
case 500:
|
|
80
|
+
case 502:
|
|
81
|
+
case 503:
|
|
82
|
+
case 504:
|
|
83
|
+
return 'SERVER_ERROR';
|
|
76
84
|
default:
|
|
77
85
|
return 'NETWORK_ERROR';
|
|
78
86
|
}
|