vzcode 1.37.0 → 1.39.0
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/dist/assets/{index-BRU-VQpy.js → index-0y6ZxgSv.js} +171 -107
- package/dist/assets/{index-lvLw6dCW.css → index-DKLiE98a.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -7
- package/src/client/CodeEditor/getOrCreateEditor.ts +0 -3
- package/src/client/CodeEditor/index.tsx +0 -1
- package/src/client/CodeEditor/json1PresenceDisplay.ts +4 -18
- package/src/client/VZCodeContext.tsx +11 -0
- package/src/client/VZSidebar/aiCopyPaste.ts +124 -0
- package/src/client/VZSidebar/index.tsx +44 -12
- package/src/client/VZSidebar/styles.scss +49 -0
- package/src/client/index.ts +1 -0
- package/src/client/useKeyboardShortcuts.ts +7 -1
- package/src/client/usePresenceAutoFollow.ts +86 -0
- package/src/client/useRunCode.tsx +2 -16
- package/src/client/useSubmitOperation.ts +4 -11
- package/src/runCode.js +48 -0
- package/src/runCode.ts +58 -0
- package/src/server/aiChatHandler/aiEditing.js +32 -30
- package/src/server/aiChatHandler/chatOperations.js +132 -7
- package/src/server/aiChatHandler/index.js +17 -23
- package/src/server/aiChatHandler/llmStreaming.js +144 -32
- package/src/server/index.js +19 -9
- package/src/submitOperation.js +16 -0
- package/src/submitOperation.ts +25 -0
package/dist/index.html
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
21
21
|
rel="stylesheet"
|
|
22
22
|
/>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
24
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-0y6ZxgSv.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DKLiE98a.css">
|
|
25
25
|
</head>
|
|
26
26
|
<body>
|
|
27
27
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"@codemirror/state": "^6.5.2",
|
|
115
115
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
116
116
|
"@codemirror/view": "^6.38.1",
|
|
117
|
-
"@langchain/core": "^0.3.
|
|
117
|
+
"@langchain/core": "^0.3.66",
|
|
118
118
|
"@langchain/openai": "^0.6.2",
|
|
119
119
|
"@lezer/highlight": "^1.2.1",
|
|
120
120
|
"@livekit/components-react": "^2.9.14",
|
|
@@ -139,21 +139,19 @@
|
|
|
139
139
|
"body-parser": "^2.2.0",
|
|
140
140
|
"codemirror": "^6.0.2",
|
|
141
141
|
"codemirror-copilot": "^0.0.7",
|
|
142
|
-
"codemirror-ot": "^4.
|
|
142
|
+
"codemirror-ot": "^4.5.0",
|
|
143
143
|
"color-hash": "^2.0.2",
|
|
144
144
|
"comlink": "^4.4.2",
|
|
145
145
|
"d3-array": "^3.2.4",
|
|
146
146
|
"diff-match-patch": "^1.0.5",
|
|
147
147
|
"dotenv": "^17.2.0",
|
|
148
|
-
"editcodewithai": "^2.
|
|
149
|
-
"eslint": "^9.31.0",
|
|
148
|
+
"editcodewithai": "^2.2.0",
|
|
150
149
|
"eslint-linter-browserify": "^9.31.0",
|
|
151
150
|
"express": "^5.1.0",
|
|
152
|
-
"globals": "^16.3.0",
|
|
153
151
|
"ignore": "^7.0.5",
|
|
154
152
|
"json0-ot-diff": "^1.1.2",
|
|
155
153
|
"livekit-server-sdk": "^2.13.1",
|
|
156
|
-
"llm-code-format": "^2.0
|
|
154
|
+
"llm-code-format": "^2.1.0",
|
|
157
155
|
"ngrok": "^5.0.0-beta.2",
|
|
158
156
|
"npm": "^11.4.2",
|
|
159
157
|
"open": "^10.2.0",
|
|
@@ -171,11 +169,14 @@
|
|
|
171
169
|
"ws": "^8.18.3"
|
|
172
170
|
},
|
|
173
171
|
"devDependencies": {
|
|
172
|
+
"@eslint/js": "^9.31.0",
|
|
174
173
|
"@types/react": "^18",
|
|
175
174
|
"@types/react-dom": "^18",
|
|
176
175
|
"@vitejs/plugin-react": "^4.7.0",
|
|
177
176
|
"concurrently": "^9.2.0",
|
|
178
177
|
"cross-env": "^7.0.3",
|
|
178
|
+
"eslint": "^9.31.0",
|
|
179
|
+
"globals": "^16.3.0",
|
|
179
180
|
"npm-check-updates": "^18.0.1",
|
|
180
181
|
"prettier": "^3.6.2",
|
|
181
182
|
"sass": "^1.89.2",
|
|
@@ -176,7 +176,6 @@ export const getOrCreateEditor = async ({
|
|
|
176
176
|
usernameRef,
|
|
177
177
|
customInteractRules,
|
|
178
178
|
enableAutoFollowRef,
|
|
179
|
-
openTab,
|
|
180
179
|
aiCopilotEndpoint,
|
|
181
180
|
esLintSource,
|
|
182
181
|
rainbowBracketsEnabled = true,
|
|
@@ -213,7 +212,6 @@ export const getOrCreateEditor = async ({
|
|
|
213
212
|
// Ref to a boolean that determines whether to
|
|
214
213
|
// enable auto-following the cursors of remote users.
|
|
215
214
|
enableAutoFollowRef: React.MutableRefObject<boolean>;
|
|
216
|
-
openTab: (tabState: TabState) => void;
|
|
217
215
|
aiCopilotEndpoint?: string;
|
|
218
216
|
esLintSource: (
|
|
219
217
|
view: EditorView,
|
|
@@ -285,7 +283,6 @@ export const getOrCreateEditor = async ({
|
|
|
285
283
|
path: textPath,
|
|
286
284
|
docPresence,
|
|
287
285
|
enableAutoFollowRef,
|
|
288
|
-
openTab,
|
|
289
286
|
}),
|
|
290
287
|
);
|
|
291
288
|
}
|
|
@@ -6,20 +6,14 @@ import {
|
|
|
6
6
|
} from '@codemirror/view';
|
|
7
7
|
import { Annotation, RangeSet } from '@codemirror/state';
|
|
8
8
|
import { assignUserColor } from '../presenceColor';
|
|
9
|
-
import { VizFileId } from '@vizhub/viz-types';
|
|
10
9
|
import {
|
|
11
10
|
Presence,
|
|
12
11
|
PresenceId,
|
|
13
|
-
TabState,
|
|
14
12
|
Username,
|
|
15
13
|
} from '../../types';
|
|
16
14
|
|
|
17
|
-
const
|
|
15
|
+
const DEBUG = true;
|
|
18
16
|
|
|
19
|
-
// export let enableAutoFollow = false;
|
|
20
|
-
// export const toggleAutoFollowButton = () => {
|
|
21
|
-
// enableAutoFollow = !enableAutoFollow;
|
|
22
|
-
// };
|
|
23
17
|
// Deals with receiving the broadcasted presence cursor locations
|
|
24
18
|
// from other clients and displaying them.
|
|
25
19
|
//
|
|
@@ -32,12 +26,10 @@ export const json1PresenceDisplay = ({
|
|
|
32
26
|
path,
|
|
33
27
|
docPresence,
|
|
34
28
|
enableAutoFollowRef,
|
|
35
|
-
openTab,
|
|
36
29
|
}: {
|
|
37
30
|
path: Array<string>;
|
|
38
31
|
docPresence: any;
|
|
39
32
|
enableAutoFollowRef: React.MutableRefObject<boolean>;
|
|
40
|
-
openTab: (tabState: TabState) => void;
|
|
41
33
|
}) => [
|
|
42
34
|
ViewPlugin.fromClass(
|
|
43
35
|
class {
|
|
@@ -68,7 +60,7 @@ export const json1PresenceDisplay = ({
|
|
|
68
60
|
docPresence.on(
|
|
69
61
|
'receive',
|
|
70
62
|
(id: PresenceId, presence: Presence) => {
|
|
71
|
-
if (
|
|
63
|
+
if (DEBUG) {
|
|
72
64
|
console.log(
|
|
73
65
|
`Received presence for id ${id}`,
|
|
74
66
|
presence,
|
|
@@ -94,14 +86,8 @@ export const json1PresenceDisplay = ({
|
|
|
94
86
|
// Otherwise, delete the presence state.
|
|
95
87
|
delete presenceState[id];
|
|
96
88
|
|
|
97
|
-
//
|
|
98
|
-
// in
|
|
99
|
-
if (enableAutoFollowRef.current) {
|
|
100
|
-
openTab({
|
|
101
|
-
fileId: presence.start[1] as VizFileId,
|
|
102
|
-
isTransient: true,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
89
|
+
// Note: Auto-follow tab opening is now handled by usePresenceAutoFollow hook
|
|
90
|
+
// in VZCodeContext, which works independently of CodeMirror extensions
|
|
105
91
|
}
|
|
106
92
|
// Update decorations to reflect new presence state.
|
|
107
93
|
// TODO consider mutating this rather than recomputing it on each change.
|
|
@@ -37,6 +37,7 @@ import { useRunCode } from './useRunCode';
|
|
|
37
37
|
import { useURLSync } from './useURLSync';
|
|
38
38
|
import { createInitialState, vzReducer } from './vzReducer';
|
|
39
39
|
import { findPane } from './vzReducer/findPane';
|
|
40
|
+
import { usePresenceAutoFollow } from './usePresenceAutoFollow';
|
|
40
41
|
|
|
41
42
|
// This context centralizes all the "smart" logic
|
|
42
43
|
// to do with the application state. This includes
|
|
@@ -408,6 +409,16 @@ export const VZCodeProvider = ({
|
|
|
408
409
|
const [hoveredItemId, setHoveredItemId] =
|
|
409
410
|
useState<ItemId | null>(null);
|
|
410
411
|
|
|
412
|
+
// Handle presence-based auto-following
|
|
413
|
+
// This hook manages opening tabs when presence is received on files
|
|
414
|
+
// that are not currently open, independent of CodeMirror extensions
|
|
415
|
+
usePresenceAutoFollow({
|
|
416
|
+
docPresence,
|
|
417
|
+
enableAutoFollow,
|
|
418
|
+
openTab,
|
|
419
|
+
activePane,
|
|
420
|
+
});
|
|
421
|
+
|
|
411
422
|
// Livekit Voice Chat Modal
|
|
412
423
|
|
|
413
424
|
const [voiceChatModalOpen, setVoiceChatModalOpen] =
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatMarkdownFiles,
|
|
3
|
+
parseMarkdownFiles,
|
|
4
|
+
} from 'llm-code-format';
|
|
5
|
+
import {
|
|
6
|
+
FORMAT_INSTRUCTIONS,
|
|
7
|
+
mergeFileChanges,
|
|
8
|
+
} from 'editcodewithai';
|
|
9
|
+
import { VizFiles } from '@vizhub/viz-types';
|
|
10
|
+
import { vizFilesToFileCollection } from '@vizhub/viz-utils';
|
|
11
|
+
|
|
12
|
+
export const createAICopyPasteHandlers = (
|
|
13
|
+
files: VizFiles,
|
|
14
|
+
submitOperation: any,
|
|
15
|
+
setCopyButtonText: (text: string) => void,
|
|
16
|
+
setPasteButtonText: (text: string) => void,
|
|
17
|
+
) => {
|
|
18
|
+
// Copy for AI - formats all files and copies to clipboard
|
|
19
|
+
const handleCopyForAI = async () => {
|
|
20
|
+
if (!files) return;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const fileCollection =
|
|
24
|
+
vizFilesToFileCollection(files);
|
|
25
|
+
|
|
26
|
+
// Format files for AI consumption
|
|
27
|
+
const formattedFiles =
|
|
28
|
+
formatMarkdownFiles(fileCollection) +
|
|
29
|
+
'\n\n' +
|
|
30
|
+
FORMAT_INSTRUCTIONS.whole;
|
|
31
|
+
|
|
32
|
+
// Copy to clipboard
|
|
33
|
+
await navigator.clipboard.writeText(formattedFiles);
|
|
34
|
+
|
|
35
|
+
// Show success feedback
|
|
36
|
+
setCopyButtonText('Copied!');
|
|
37
|
+
setTimeout(
|
|
38
|
+
() => setCopyButtonText('Copy for AI'),
|
|
39
|
+
2000,
|
|
40
|
+
);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Failed to copy files for AI:', error);
|
|
43
|
+
setCopyButtonText('Error');
|
|
44
|
+
setTimeout(
|
|
45
|
+
() => setCopyButtonText('Copy for AI'),
|
|
46
|
+
2000,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Paste for AI - parses clipboard content and updates files
|
|
52
|
+
const handlePasteForAI = async () => {
|
|
53
|
+
if (!submitOperation) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Read from clipboard
|
|
57
|
+
const clipboardText =
|
|
58
|
+
await navigator.clipboard.readText();
|
|
59
|
+
|
|
60
|
+
// Handle various line endings
|
|
61
|
+
const normalized = clipboardText.replace(
|
|
62
|
+
/\r\n?/g,
|
|
63
|
+
'\n',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!normalized.trim()) {
|
|
67
|
+
setPasteButtonText('Empty clipboard');
|
|
68
|
+
setTimeout(
|
|
69
|
+
() => setPasteButtonText('Paste for AI'),
|
|
70
|
+
2000,
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse the markdown files format
|
|
76
|
+
const parsed = parseMarkdownFiles(normalized, 'bold');
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
parsed.files &&
|
|
80
|
+
Object.keys(parsed.files).length > 0
|
|
81
|
+
) {
|
|
82
|
+
// Use mergeFileChanges utility from editcodewithai
|
|
83
|
+
const mergedFiles = mergeFileChanges(
|
|
84
|
+
files,
|
|
85
|
+
parsed.files,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Update files using the submit operation
|
|
89
|
+
submitOperation((document) => ({
|
|
90
|
+
...document,
|
|
91
|
+
files: mergedFiles,
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
const fileCount = Object.keys(parsed.files).length;
|
|
95
|
+
setPasteButtonText('Pasted!');
|
|
96
|
+
setTimeout(
|
|
97
|
+
() => setPasteButtonText('Paste for AI'),
|
|
98
|
+
2000,
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
setPasteButtonText('No files found');
|
|
102
|
+
setTimeout(
|
|
103
|
+
() => setPasteButtonText('Paste for AI'),
|
|
104
|
+
2000,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(
|
|
109
|
+
'Failed to paste files from AI:',
|
|
110
|
+
error,
|
|
111
|
+
);
|
|
112
|
+
setPasteButtonText('Error');
|
|
113
|
+
setTimeout(
|
|
114
|
+
() => setPasteButtonText('Paste for AI'),
|
|
115
|
+
2000,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
handleCopyForAI,
|
|
122
|
+
handlePasteForAI,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
@@ -35,15 +35,13 @@ import { AIChat } from './AIChat';
|
|
|
35
35
|
import { Listing } from './Listing';
|
|
36
36
|
import { Search } from './Search';
|
|
37
37
|
import { useDragAndDrop } from './useDragAndDrop';
|
|
38
|
+
import { createAICopyPasteHandlers } from './aiCopyPaste';
|
|
38
39
|
import {
|
|
39
40
|
enableLiveKit,
|
|
40
41
|
enableAIChat,
|
|
41
42
|
} from '../featureFlags';
|
|
42
43
|
import './styles.scss';
|
|
43
44
|
|
|
44
|
-
// TODO turn this UI back on when we are actually detecting
|
|
45
|
-
// the connection status.
|
|
46
|
-
// See https://github.com/vizhub-core/vzcode/issues/456
|
|
47
45
|
const enableConnectionStatus = true;
|
|
48
46
|
|
|
49
47
|
export const VZSidebar = ({
|
|
@@ -146,10 +144,8 @@ export const VZSidebar = ({
|
|
|
146
144
|
toggleAutoFollow,
|
|
147
145
|
docPresence,
|
|
148
146
|
updatePresenceIndicator,
|
|
149
|
-
sidebarPresenceIndicators,
|
|
150
|
-
liveKitConnection,
|
|
151
|
-
setLiveKitConnection,
|
|
152
147
|
setVoiceChatModalOpen,
|
|
148
|
+
submitOperation,
|
|
153
149
|
} = useContext(VZCodeContext);
|
|
154
150
|
|
|
155
151
|
const fileTree = useMemo(
|
|
@@ -163,6 +159,24 @@ export const VZSidebar = ({
|
|
|
163
159
|
setIsSettingsOpen(true);
|
|
164
160
|
}, []);
|
|
165
161
|
|
|
162
|
+
// State for AI operation feedback
|
|
163
|
+
const [copyButtonText, setCopyButtonText] =
|
|
164
|
+
useState('Copy for AI');
|
|
165
|
+
const [pasteButtonText, setPasteButtonText] =
|
|
166
|
+
useState('Paste for AI');
|
|
167
|
+
|
|
168
|
+
// Create AI copy/paste handlers
|
|
169
|
+
const { handleCopyForAI, handlePasteForAI } = useMemo(
|
|
170
|
+
() =>
|
|
171
|
+
createAICopyPasteHandlers(
|
|
172
|
+
files,
|
|
173
|
+
submitOperation,
|
|
174
|
+
setCopyButtonText,
|
|
175
|
+
setPasteButtonText,
|
|
176
|
+
),
|
|
177
|
+
[files, submitOperation],
|
|
178
|
+
);
|
|
179
|
+
|
|
166
180
|
const { sidebarWidth } = useContext(
|
|
167
181
|
SplitPaneResizeContext,
|
|
168
182
|
);
|
|
@@ -210,17 +224,17 @@ export const VZSidebar = ({
|
|
|
210
224
|
presenceId: PresenceId,
|
|
211
225
|
update: Presence,
|
|
212
226
|
) => {
|
|
227
|
+
// Sometimes this can be null, so we check.
|
|
228
|
+
// This can happen when the user disconnects.
|
|
229
|
+
// If this happens, we do not update the presence indicator.
|
|
230
|
+
// TODO test removal of presence indicator.
|
|
231
|
+
if (!update) return;
|
|
232
|
+
|
|
213
233
|
const presenceIndicator: PresenceIndicator = {
|
|
214
234
|
username: update.username,
|
|
215
235
|
fileId: update.start[1] as VizFileId,
|
|
216
236
|
};
|
|
217
237
|
|
|
218
|
-
// console.log('Got presence!');
|
|
219
|
-
// // console.log({presenceId,update})
|
|
220
|
-
// console.log(
|
|
221
|
-
// JSON.stringify(presenceIndicator, null, 2),
|
|
222
|
-
// );
|
|
223
|
-
|
|
224
238
|
updatePresenceIndicator(presenceIndicator);
|
|
225
239
|
};
|
|
226
240
|
|
|
@@ -532,6 +546,24 @@ export const VZSidebar = ({
|
|
|
532
546
|
)}
|
|
533
547
|
</div>
|
|
534
548
|
</div>
|
|
549
|
+
{!isAIChatOpen && !isSearchOpen && filesExist && (
|
|
550
|
+
<div className="ai-buttons">
|
|
551
|
+
<button
|
|
552
|
+
className="ai-button copy-button"
|
|
553
|
+
onClick={handleCopyForAI}
|
|
554
|
+
title="Copy files formatted for AI"
|
|
555
|
+
>
|
|
556
|
+
{copyButtonText}
|
|
557
|
+
</button>
|
|
558
|
+
<button
|
|
559
|
+
className="ai-button paste-button"
|
|
560
|
+
onClick={handlePasteForAI}
|
|
561
|
+
title="Paste files from AI"
|
|
562
|
+
>
|
|
563
|
+
{pasteButtonText}
|
|
564
|
+
</button>
|
|
565
|
+
</div>
|
|
566
|
+
)}
|
|
535
567
|
{enableConnectionStatus && (
|
|
536
568
|
<div className="connection-status">
|
|
537
569
|
{isConnecting
|
|
@@ -297,6 +297,55 @@
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
+
|
|
301
|
+
.ai-buttons {
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
gap: 8px;
|
|
305
|
+
padding: 10px 10px 0 10px;
|
|
306
|
+
border-top: 1px solid var(--vh-color-neutral-02);
|
|
307
|
+
|
|
308
|
+
.ai-button {
|
|
309
|
+
padding: 8px 12px;
|
|
310
|
+
border: 1px solid var(--vh-color-neutral-03);
|
|
311
|
+
border-radius: 6px;
|
|
312
|
+
background: var(--vh-color-neutral-01);
|
|
313
|
+
color: var(--vh-color-neutral-04);
|
|
314
|
+
font-size: 12px;
|
|
315
|
+
font-weight: 500;
|
|
316
|
+
font-family: var(--vzcode-font-family);
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
transition: all 0.2s ease;
|
|
319
|
+
|
|
320
|
+
&:hover {
|
|
321
|
+
background: var(--vh-color-hover-dark);
|
|
322
|
+
border-color: var(--vh-color-neutral-04);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
&:active {
|
|
326
|
+
background: var(--vh-color-neutral-03);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
&.copy-button {
|
|
330
|
+
border-color: var(--vh-color-primary-03);
|
|
331
|
+
|
|
332
|
+
&:hover {
|
|
333
|
+
background: var(--vh-color-hover-dark);
|
|
334
|
+
border-color: var(--vh-color-primary-04);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
&.paste-button {
|
|
339
|
+
border-color: var(--vh-color-success-03);
|
|
340
|
+
|
|
341
|
+
&:hover {
|
|
342
|
+
background: var(--vh-color-hover-dark);
|
|
343
|
+
border-color: var(--vh-color-success-04);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
300
349
|
.name {
|
|
301
350
|
display: flex;
|
|
302
351
|
align-items: center;
|
package/src/client/index.ts
CHANGED
|
@@ -378,7 +378,13 @@ export const useKeyboardShortcuts = ({
|
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
|
|
381
|
+
let position: number;
|
|
382
|
+
try {
|
|
383
|
+
position = editor.posAtDOM(element);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
// Element is not within the current editor's document
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
382
388
|
const identifier: SyntaxNode = tree.resolveInner(
|
|
383
389
|
position,
|
|
384
390
|
1,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
2
|
+
import { VZCodeContext } from './VZCodeContext';
|
|
3
|
+
import { Presence, PresenceId, TabState } from '../types';
|
|
4
|
+
import { VizFileId } from '@vizhub/viz-types';
|
|
5
|
+
|
|
6
|
+
const DEBUG = true;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom hook that handles auto-following users by opening tabs when presence
|
|
10
|
+
* is received on files that are not currently open. This logic is independent
|
|
11
|
+
* of CodeMirror extensions and works regardless of editor cache state.
|
|
12
|
+
*/
|
|
13
|
+
export const usePresenceAutoFollow = ({
|
|
14
|
+
docPresence,
|
|
15
|
+
enableAutoFollow,
|
|
16
|
+
openTab,
|
|
17
|
+
activePane,
|
|
18
|
+
}) => {
|
|
19
|
+
// Track enableAutoFollow on a ref for use in event handlers
|
|
20
|
+
const enableAutoFollowRef = useRef(enableAutoFollow);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
enableAutoFollowRef.current = enableAutoFollow;
|
|
23
|
+
}, [enableAutoFollow]);
|
|
24
|
+
|
|
25
|
+
// Track active file for comparison
|
|
26
|
+
const activeFileIdRef = useRef(activePane?.activeFileId);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
activeFileIdRef.current = activePane?.activeFileId;
|
|
29
|
+
}, [activePane?.activeFileId]);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!docPresence) return;
|
|
33
|
+
|
|
34
|
+
const handlePresenceReceive = (
|
|
35
|
+
id: PresenceId,
|
|
36
|
+
presence: Presence,
|
|
37
|
+
) => {
|
|
38
|
+
if (DEBUG) {
|
|
39
|
+
console.log(
|
|
40
|
+
`[usePresenceAutoFollow] Received presence for id ${id}`,
|
|
41
|
+
presence,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// If presence is null, the user has disconnected
|
|
46
|
+
if (!presence) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If auto-follow is not enabled, do nothing
|
|
51
|
+
if (!enableAutoFollowRef.current) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Extract the file ID from the presence path
|
|
56
|
+
// Presence path structure: [path, to, file, fileId, position]
|
|
57
|
+
// We want the fileId which should be at index 1 for files
|
|
58
|
+
const presenceFileId = presence.start[1] as VizFileId;
|
|
59
|
+
|
|
60
|
+
// If the presence is for the currently active file, do nothing
|
|
61
|
+
// (the CodeMirror extension will handle displaying the cursor)
|
|
62
|
+
if (presenceFileId === activeFileIdRef.current) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If the presence is for a different file, open that tab
|
|
67
|
+
DEBUG &&
|
|
68
|
+
console.log(
|
|
69
|
+
`[usePresenceAutoFollow] Auto-opening tab for ${presence.username} in file ${presenceFileId}`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
openTab({
|
|
73
|
+
fileId: presenceFileId,
|
|
74
|
+
isTransient: true,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Subscribe to presence changes
|
|
79
|
+
docPresence.on('receive', handlePresenceReceive);
|
|
80
|
+
|
|
81
|
+
// Cleanup subscription on unmount
|
|
82
|
+
return () => {
|
|
83
|
+
docPresence.off('receive', handlePresenceReceive);
|
|
84
|
+
};
|
|
85
|
+
}, [docPresence, openTab]);
|
|
86
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { SubmitOperation } from '../types';
|
|
3
|
-
import {
|
|
3
|
+
import { createRunCodeFunction } from '../runCode';
|
|
4
4
|
|
|
5
5
|
// Runs the code by flashing `isInteracting` to `true`.
|
|
6
6
|
export const useRunCode = (
|
|
@@ -8,21 +8,7 @@ export const useRunCode = (
|
|
|
8
8
|
) => {
|
|
9
9
|
const runCodeRef = useRef(null);
|
|
10
10
|
useEffect(() => {
|
|
11
|
-
const runCode = ()
|
|
12
|
-
submitOperation((content: VizContent) => ({
|
|
13
|
-
...content,
|
|
14
|
-
isInteracting: true,
|
|
15
|
-
}));
|
|
16
|
-
setTimeout(() => {
|
|
17
|
-
// This somewhat cryptic logic
|
|
18
|
-
// deletes the `isInteracting` property
|
|
19
|
-
// from the document.
|
|
20
|
-
submitOperation(
|
|
21
|
-
({ isInteracting, ...newDocument }) =>
|
|
22
|
-
newDocument,
|
|
23
|
-
);
|
|
24
|
-
}, 0);
|
|
25
|
-
};
|
|
11
|
+
const runCode = createRunCodeFunction(submitOperation);
|
|
26
12
|
runCodeRef.current = runCode;
|
|
27
13
|
}, [submitOperation]);
|
|
28
14
|
return runCodeRef;
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { ShareDBDoc } from '../types'; // Assuming ShareDBDoc is already generic
|
|
3
|
-
import {
|
|
3
|
+
import { createSubmitOperation } from '../submitOperation';
|
|
4
4
|
|
|
5
5
|
// A generic helper function to submit diff-based operations to ShareDB
|
|
6
6
|
export const useSubmitOperation = <T>(
|
|
7
7
|
shareDBDoc: ShareDBDoc<T>,
|
|
8
8
|
): ((next: (data: T) => T) => void) => {
|
|
9
9
|
const submitOperation: (next: (data: T) => T) => void =
|
|
10
|
-
useCallback(
|
|
11
|
-
(
|
|
12
|
-
|
|
13
|
-
const op = diff(data, next(data));
|
|
14
|
-
if (op && shareDBDoc) {
|
|
15
|
-
shareDBDoc.submitOp(op);
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
[shareDBDoc],
|
|
19
|
-
);
|
|
10
|
+
useCallback(() => {
|
|
11
|
+
return createSubmitOperation(shareDBDoc);
|
|
12
|
+
}, [shareDBDoc])();
|
|
20
13
|
|
|
21
14
|
return submitOperation;
|
|
22
15
|
};
|
package/src/runCode.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createSubmitOperation } from './submitOperation.js';
|
|
2
|
+
// TODO migrate the server-side code to use TypeScript,
|
|
3
|
+
// and delete this file once the migration is complete (use src/runCode.ts instead).
|
|
4
|
+
/**
|
|
5
|
+
* Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
|
|
6
|
+
* This works for both client-side (with submitOperation) and server-side (with ShareDB document).
|
|
7
|
+
*/
|
|
8
|
+
export const createRunCodeFunction = (
|
|
9
|
+
submitOperationOrDoc,
|
|
10
|
+
) => {
|
|
11
|
+
return () => {
|
|
12
|
+
let submitOperation;
|
|
13
|
+
|
|
14
|
+
// Check if this is a client-side submitOperation or server-side ShareDB document
|
|
15
|
+
if (typeof submitOperationOrDoc === 'function') {
|
|
16
|
+
// Client-side: already a submitOperation function
|
|
17
|
+
submitOperation = submitOperationOrDoc;
|
|
18
|
+
} else {
|
|
19
|
+
// Server-side: create submitOperation from ShareDB document
|
|
20
|
+
submitOperation = createSubmitOperation(
|
|
21
|
+
submitOperationOrDoc,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Use the unified submitOperation approach for both client and server
|
|
26
|
+
submitOperation((content) => ({
|
|
27
|
+
...content,
|
|
28
|
+
isInteracting: true,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
submitOperation((content) => ({
|
|
33
|
+
...content,
|
|
34
|
+
isInteracting: false,
|
|
35
|
+
}));
|
|
36
|
+
}, 0);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a runCodeRef object compatible with React refs.
|
|
42
|
+
* This is useful for maintaining compatibility with existing code that expects a ref.
|
|
43
|
+
*/
|
|
44
|
+
export const createRunCodeRef = (submitOperationOrDoc) => {
|
|
45
|
+
return {
|
|
46
|
+
current: createRunCodeFunction(submitOperationOrDoc),
|
|
47
|
+
};
|
|
48
|
+
};
|