vzcode 1.41.0 → 1.42.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-DKLiE98a.css → index-CP9o_hN5.css} +1 -1
- package/dist/assets/{index-DPAeYEiH.js → index-kPLgjbcb.js} +86 -86
- package/dist/index.html +2 -2
- package/package.json +5 -5
- package/src/client/App/style.scss +2 -2
- package/src/client/CodeEditor/json1PresenceDisplay.ts +40 -12
- package/src/client/VZRight.tsx +17 -16
- package/src/client/VZSidebar/AIChat/MessageList.tsx +18 -27
- package/src/client/VZSidebar/AIChat/index.tsx +0 -2
- package/src/client/VZSidebar/aiCopyPaste.ts +8 -1
- package/src/client/VZSidebar/index.tsx +10 -8
- package/src/client/vzReducer/searchReducer.test.ts +250 -0
- package/src/client/vzReducer/searchReducer.ts +3 -1
- package/src/{ot.js → ot.ts} +11 -0
- package/src/runCode.ts +4 -23
- package/src/server/aiChatHandler/aiEditing.ts +10 -7
- package/src/server/aiChatHandler/chatOperations.ts +144 -58
- package/src/server/aiChatHandler/errorHandling.ts +1 -1
- package/src/server/aiChatHandler/index.ts +8 -2
- package/src/server/aiChatHandler/llmStreaming.ts +76 -40
- package/src/submitOperation.ts +1 -1
- package/src/client/diff.js +0 -12
- package/src/runCode.js +0 -48
- package/src/submitOperation.js +0 -16
- /package/src/{randomId.js → randomId.ts} +0 -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-kPLgjbcb.js"></script>
|
|
24
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CP9o_hN5.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.42.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"diff-match-patch": "^1.0.5",
|
|
148
148
|
"dotenv": "^17.2.1",
|
|
149
149
|
"editcodewithai": "^2.3.0",
|
|
150
|
-
"eslint-linter-browserify": "^9.
|
|
150
|
+
"eslint-linter-browserify": "^9.32.0",
|
|
151
151
|
"express": "^5.1.0",
|
|
152
152
|
"ignore": "^7.0.5",
|
|
153
153
|
"json0-ot-diff": "^1.1.2",
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
"@types/react-dom": "^18",
|
|
176
176
|
"@vitejs/plugin-react": "^4.7.0",
|
|
177
177
|
"concurrently": "^9.2.0",
|
|
178
|
-
"cross-env": "^
|
|
178
|
+
"cross-env": "^10.0.0",
|
|
179
179
|
"eslint": "^9.32.0",
|
|
180
180
|
"globals": "^16.3.0",
|
|
181
181
|
"npm-check-updates": "^18.0.2",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"vitest": "^3.2.4"
|
|
188
188
|
},
|
|
189
189
|
"optionalDependencies": {
|
|
190
|
-
"@rollup/rollup-darwin-arm64": "^4.
|
|
191
|
-
"@rollup/rollup-win32-x64-msvc": "^4.
|
|
190
|
+
"@rollup/rollup-darwin-arm64": "^4.46.1",
|
|
191
|
+
"@rollup/rollup-win32-x64-msvc": "^4.46.1"
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -41,6 +41,12 @@ export const json1PresenceDisplay = ({
|
|
|
41
41
|
//Added variable for cursor position
|
|
42
42
|
cursorPosition = {};
|
|
43
43
|
|
|
44
|
+
// Flag to prevent multiple pending updates
|
|
45
|
+
pendingUpdate = false;
|
|
46
|
+
|
|
47
|
+
// Flag to prevent multiple pending scroll updates
|
|
48
|
+
pendingScrollUpdate = false;
|
|
49
|
+
|
|
44
50
|
constructor(view: EditorView) {
|
|
45
51
|
// Initialize decorations to empty array so CodeMirror doesn't crash.
|
|
46
52
|
this.decorations = RangeSet.of([]);
|
|
@@ -146,13 +152,22 @@ export const json1PresenceDisplay = ({
|
|
|
146
152
|
true,
|
|
147
153
|
);
|
|
148
154
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
// Safely dispatch decoration updates without causing race conditions
|
|
156
|
+
// Use requestAnimationFrame to ensure we're not in the middle of an update
|
|
157
|
+
if (!this.pendingUpdate) {
|
|
158
|
+
this.pendingUpdate = true;
|
|
159
|
+
requestAnimationFrame(() => {
|
|
160
|
+
this.pendingUpdate = false;
|
|
161
|
+
// Check if view is still valid and not currently updating
|
|
162
|
+
if (view.state && view.dom.isConnected) {
|
|
163
|
+
view.dispatch({
|
|
164
|
+
annotations: [
|
|
165
|
+
presenceAnnotation.of(true),
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
154
169
|
});
|
|
155
|
-
}
|
|
170
|
+
}
|
|
156
171
|
|
|
157
172
|
if (enableAutoFollowRef.current) {
|
|
158
173
|
this.scrollToCursor(view);
|
|
@@ -162,12 +177,25 @@ export const json1PresenceDisplay = ({
|
|
|
162
177
|
}
|
|
163
178
|
// Method to scroll the view to keep the cursor in view
|
|
164
179
|
scrollToCursor(view) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
// Debounce scroll updates to prevent conflicts with typing
|
|
181
|
+
if (!this.pendingScrollUpdate) {
|
|
182
|
+
this.pendingScrollUpdate = true;
|
|
183
|
+
requestAnimationFrame(() => {
|
|
184
|
+
this.pendingScrollUpdate = false;
|
|
185
|
+
// Check if view is still valid
|
|
186
|
+
if (view.state && view.dom.isConnected) {
|
|
187
|
+
for (const id in this.cursorPosition) {
|
|
188
|
+
//getting the cursor position of the other cursor
|
|
189
|
+
const cursorPos = this.cursorPosition[id];
|
|
190
|
+
view.dispatch({
|
|
191
|
+
//if the other person's cursor has jumped off screen, we will follow it by scrolling there directly.
|
|
192
|
+
effects:
|
|
193
|
+
EditorView.scrollIntoView(cursorPos),
|
|
194
|
+
});
|
|
195
|
+
// Only scroll to the first cursor to avoid multiple dispatches
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
171
199
|
});
|
|
172
200
|
}
|
|
173
201
|
}
|
package/src/client/VZRight.tsx
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useContext,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
} from 'react';
|
|
2
7
|
import {
|
|
3
8
|
createRuntime,
|
|
4
9
|
VizHubRuntime,
|
|
@@ -9,12 +14,11 @@ import { vizFilesToFileCollection } from '@vizhub/viz-utils';
|
|
|
9
14
|
|
|
10
15
|
const enableIframe = true;
|
|
11
16
|
|
|
12
|
-
// Singleton runtime instance
|
|
13
|
-
// TODO use refs, and cleanup on unmount
|
|
14
|
-
let runtime: VizHubRuntime = null;
|
|
15
|
-
let isFirstRun = true;
|
|
16
|
-
|
|
17
17
|
export const VZRight = () => {
|
|
18
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
19
|
+
const runtimeRef = useRef<VizHubRuntime | null>(null);
|
|
20
|
+
const isFirstRunRef = useRef(true);
|
|
21
|
+
|
|
18
22
|
// Get access to the current files.
|
|
19
23
|
const { content } = useContext(VZCodeContext);
|
|
20
24
|
|
|
@@ -32,13 +36,10 @@ export const VZRight = () => {
|
|
|
32
36
|
if (!files) return;
|
|
33
37
|
|
|
34
38
|
// Initialize the runtime only once
|
|
35
|
-
if (!
|
|
39
|
+
if (!runtimeRef.current && iframeRef.current) {
|
|
36
40
|
const worker = new BuildWorker();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
) as HTMLIFrameElement;
|
|
40
|
-
runtime = createRuntime({
|
|
41
|
-
iframe,
|
|
41
|
+
runtimeRef.current = createRuntime({
|
|
42
|
+
iframe: iframeRef.current,
|
|
42
43
|
worker,
|
|
43
44
|
setBuildErrorMessage: (error) => {
|
|
44
45
|
if (error) {
|
|
@@ -49,14 +50,14 @@ export const VZRight = () => {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// Run code in the iframe
|
|
52
|
-
if (
|
|
53
|
-
|
|
53
|
+
if (isFirstRunRef.current || isInteracting) {
|
|
54
|
+
runtimeRef.current?.run({
|
|
54
55
|
files,
|
|
55
56
|
enableHotReloading: true,
|
|
56
57
|
enableSourcemap: true,
|
|
57
58
|
vizId: 'example-viz',
|
|
58
59
|
});
|
|
59
|
-
|
|
60
|
+
isFirstRunRef.current = false;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// TODO use refs, and cleanup on unmount
|
|
@@ -69,7 +70,7 @@ export const VZRight = () => {
|
|
|
69
70
|
return (
|
|
70
71
|
<div className="right">
|
|
71
72
|
{enableIframe ? (
|
|
72
|
-
<iframe
|
|
73
|
+
<iframe ref={iframeRef}></iframe>
|
|
73
74
|
) : null}
|
|
74
75
|
</div>
|
|
75
76
|
);
|
|
@@ -5,29 +5,18 @@ import {
|
|
|
5
5
|
memo,
|
|
6
6
|
} from 'react';
|
|
7
7
|
import { Message } from './Message';
|
|
8
|
-
import { StreamingMessage } from './StreamingMessage';
|
|
9
8
|
import { TypingIndicator } from './TypingIndicator';
|
|
10
|
-
|
|
11
|
-
interface MessageData {
|
|
12
|
-
id: string;
|
|
13
|
-
role: 'user' | 'assistant';
|
|
14
|
-
content: string;
|
|
15
|
-
timestamp: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface MessageListProps {
|
|
19
|
-
messages: MessageData[];
|
|
20
|
-
aiScratchpad?: string;
|
|
21
|
-
aiStatus?: string;
|
|
22
|
-
isLoading: boolean;
|
|
23
|
-
}
|
|
9
|
+
import { VizChatMessage } from '@vizhub/viz-types';
|
|
24
10
|
|
|
25
11
|
const MessageListComponent = ({
|
|
26
12
|
messages,
|
|
27
|
-
aiScratchpad,
|
|
28
13
|
aiStatus,
|
|
29
14
|
isLoading,
|
|
30
|
-
}:
|
|
15
|
+
}: {
|
|
16
|
+
messages: VizChatMessage[];
|
|
17
|
+
aiStatus?: string;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
}) => {
|
|
31
20
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
32
21
|
|
|
33
22
|
const scrollToBottom = useCallback(() => {
|
|
@@ -38,7 +27,17 @@ const MessageListComponent = ({
|
|
|
38
27
|
|
|
39
28
|
useEffect(() => {
|
|
40
29
|
scrollToBottom();
|
|
41
|
-
}, [messages
|
|
30
|
+
}, [messages]);
|
|
31
|
+
|
|
32
|
+
// Check if AI generation has started (last message is from assistant)
|
|
33
|
+
const lastMessage = messages[messages.length - 1];
|
|
34
|
+
const aiGenerationStarted =
|
|
35
|
+
lastMessage?.role === 'assistant' &&
|
|
36
|
+
lastMessage.content !== '';
|
|
37
|
+
|
|
38
|
+
// Show typing indicator only when loading and AI generation hasn't started yet
|
|
39
|
+
const showTypingIndicator =
|
|
40
|
+
isLoading && !aiGenerationStarted;
|
|
42
41
|
|
|
43
42
|
return (
|
|
44
43
|
<div className="ai-chat-messages">
|
|
@@ -52,15 +51,7 @@ const MessageListComponent = ({
|
|
|
52
51
|
/>
|
|
53
52
|
))}
|
|
54
53
|
|
|
55
|
-
{
|
|
56
|
-
{aiScratchpad && (
|
|
57
|
-
<StreamingMessage
|
|
58
|
-
content={aiScratchpad}
|
|
59
|
-
status={aiStatus}
|
|
60
|
-
/>
|
|
61
|
-
)}
|
|
62
|
-
|
|
63
|
-
{isLoading && !aiScratchpad && <TypingIndicator />}
|
|
54
|
+
{showTypingIndicator && <TypingIndicator />}
|
|
64
55
|
<div ref={messagesEndRef} />
|
|
65
56
|
</div>
|
|
66
57
|
);
|
|
@@ -27,7 +27,6 @@ export const AIChat = () => {
|
|
|
27
27
|
// Get current chat data from content
|
|
28
28
|
const currentChat = content?.chats?.[currentChatId];
|
|
29
29
|
const rawMessages = currentChat?.messages || [];
|
|
30
|
-
const aiScratchpad = currentChat?.aiScratchpad;
|
|
31
30
|
const aiStatus = currentChat?.aiStatus;
|
|
32
31
|
|
|
33
32
|
// Transform messages to ensure they have required id field - memoized to avoid recreation
|
|
@@ -87,7 +86,6 @@ export const AIChat = () => {
|
|
|
87
86
|
<div className="ai-chat-container">
|
|
88
87
|
<MessageList
|
|
89
88
|
messages={messages}
|
|
90
|
-
aiScratchpad={aiScratchpad}
|
|
91
89
|
aiStatus={aiStatus}
|
|
92
90
|
isLoading={isLoading}
|
|
93
91
|
/>
|
|
@@ -17,7 +17,14 @@ export const createAICopyPasteHandlers = (
|
|
|
17
17
|
) => {
|
|
18
18
|
// Copy for AI - formats all files and copies to clipboard
|
|
19
19
|
const handleCopyForAI = async () => {
|
|
20
|
-
if (!files)
|
|
20
|
+
if (!files || Object.keys(files).length === 0) {
|
|
21
|
+
setCopyButtonText('No files to copy');
|
|
22
|
+
setTimeout(
|
|
23
|
+
() => setCopyButtonText('Copy for AI'),
|
|
24
|
+
2000,
|
|
25
|
+
);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
21
28
|
|
|
22
29
|
try {
|
|
23
30
|
const fileCollection =
|
|
@@ -546,15 +546,17 @@ export const VZSidebar = ({
|
|
|
546
546
|
)}
|
|
547
547
|
</div>
|
|
548
548
|
</div>
|
|
549
|
-
{!isAIChatOpen && !isSearchOpen &&
|
|
549
|
+
{!isAIChatOpen && !isSearchOpen && (
|
|
550
550
|
<div className="ai-buttons">
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
551
|
+
{filesExist && (
|
|
552
|
+
<button
|
|
553
|
+
className="ai-button copy-button"
|
|
554
|
+
onClick={handleCopyForAI}
|
|
555
|
+
title="Copy files formatted for AI"
|
|
556
|
+
>
|
|
557
|
+
{copyButtonText}
|
|
558
|
+
</button>
|
|
559
|
+
)}
|
|
558
560
|
<button
|
|
559
561
|
className="ai-button paste-button"
|
|
560
562
|
onClick={handlePasteForAI}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { setSearchResultsReducer } from './searchReducer';
|
|
3
|
+
import { VZAction, VZState, createInitialState } from '.';
|
|
4
|
+
import { defaultTheme } from '../themes';
|
|
5
|
+
|
|
6
|
+
// Mock ShareDBDoc for testing
|
|
7
|
+
const createMockShareDBDoc = (files: any) => ({
|
|
8
|
+
data: { files },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const initialState: VZState = createInitialState({
|
|
12
|
+
defaultTheme,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('searchReducer', () => {
|
|
16
|
+
describe('setSearchResultsReducer', () => {
|
|
17
|
+
it('should find case-insensitive matches', () => {
|
|
18
|
+
const mockFiles = {
|
|
19
|
+
'file1.js': {
|
|
20
|
+
name: 'file1.js',
|
|
21
|
+
text: 'const Hello = "world";\nconst goodbye = "HELLO";',
|
|
22
|
+
},
|
|
23
|
+
'file2.js': {
|
|
24
|
+
name: 'file2.js',
|
|
25
|
+
text: 'function hello() {\n return "Hello World";\n}',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const stateWithSearch = {
|
|
30
|
+
...initialState,
|
|
31
|
+
search: {
|
|
32
|
+
...initialState.search,
|
|
33
|
+
pattern: 'hello', // lowercase search pattern
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const action: VZAction = {
|
|
38
|
+
type: 'set_search_results',
|
|
39
|
+
files: createMockShareDBDoc(mockFiles),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const newState = setSearchResultsReducer(
|
|
43
|
+
stateWithSearch,
|
|
44
|
+
action,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Should find matches in both files despite different cases
|
|
48
|
+
expect(
|
|
49
|
+
Object.keys(newState.search.results),
|
|
50
|
+
).toHaveLength(2);
|
|
51
|
+
|
|
52
|
+
// Check file1.js results
|
|
53
|
+
const file1Results =
|
|
54
|
+
newState.search.results['file1.js'];
|
|
55
|
+
expect(file1Results).toBeDefined();
|
|
56
|
+
expect(file1Results.matches).toHaveLength(2); // "Hello" and "HELLO"
|
|
57
|
+
expect(file1Results.matches[0].line).toBe(1);
|
|
58
|
+
expect(file1Results.matches[0].index).toBe(6); // position of "Hello"
|
|
59
|
+
expect(file1Results.matches[1].line).toBe(2);
|
|
60
|
+
expect(file1Results.matches[1].index).toBe(18); // position of "HELLO"
|
|
61
|
+
|
|
62
|
+
// Check file2.js results
|
|
63
|
+
const file2Results =
|
|
64
|
+
newState.search.results['file2.js'];
|
|
65
|
+
expect(file2Results).toBeDefined();
|
|
66
|
+
expect(file2Results.matches).toHaveLength(2); // "hello" and "Hello"
|
|
67
|
+
expect(file2Results.matches[0].line).toBe(1);
|
|
68
|
+
expect(file2Results.matches[0].index).toBe(9); // position of "hello"
|
|
69
|
+
expect(file2Results.matches[1].line).toBe(2);
|
|
70
|
+
expect(file2Results.matches[1].index).toBe(10); // position of "Hello"
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should find exact case matches', () => {
|
|
74
|
+
const mockFiles = {
|
|
75
|
+
'file1.js': {
|
|
76
|
+
name: 'file1.js',
|
|
77
|
+
text: 'const hello = "world";',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const stateWithSearch = {
|
|
82
|
+
...initialState,
|
|
83
|
+
search: {
|
|
84
|
+
...initialState.search,
|
|
85
|
+
pattern: 'hello', // exact case match
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const action: VZAction = {
|
|
90
|
+
type: 'set_search_results',
|
|
91
|
+
files: createMockShareDBDoc(mockFiles),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const newState = setSearchResultsReducer(
|
|
95
|
+
stateWithSearch,
|
|
96
|
+
action,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
Object.keys(newState.search.results),
|
|
101
|
+
).toHaveLength(1);
|
|
102
|
+
const file1Results =
|
|
103
|
+
newState.search.results['file1.js'];
|
|
104
|
+
expect(file1Results.matches).toHaveLength(1);
|
|
105
|
+
expect(file1Results.matches[0].line).toBe(1);
|
|
106
|
+
expect(file1Results.matches[0].index).toBe(6);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should not find matches when pattern does not exist', () => {
|
|
110
|
+
const mockFiles = {
|
|
111
|
+
'file1.js': {
|
|
112
|
+
name: 'file1.js',
|
|
113
|
+
text: 'const world = "test";',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const stateWithSearch = {
|
|
118
|
+
...initialState,
|
|
119
|
+
search: {
|
|
120
|
+
...initialState.search,
|
|
121
|
+
pattern: 'hello',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const action: VZAction = {
|
|
126
|
+
type: 'set_search_results',
|
|
127
|
+
files: createMockShareDBDoc(mockFiles),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const newState = setSearchResultsReducer(
|
|
131
|
+
stateWithSearch,
|
|
132
|
+
action,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(
|
|
136
|
+
Object.keys(newState.search.results),
|
|
137
|
+
).toHaveLength(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should handle uppercase search pattern', () => {
|
|
141
|
+
const mockFiles = {
|
|
142
|
+
'file1.js': {
|
|
143
|
+
name: 'file1.js',
|
|
144
|
+
text: 'const Hello = "world";\nfunction hello() {}',
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const stateWithSearch = {
|
|
149
|
+
...initialState,
|
|
150
|
+
search: {
|
|
151
|
+
...initialState.search,
|
|
152
|
+
pattern: 'HELLO', // uppercase search pattern
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const action: VZAction = {
|
|
157
|
+
type: 'set_search_results',
|
|
158
|
+
files: createMockShareDBDoc(mockFiles),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const newState = setSearchResultsReducer(
|
|
162
|
+
stateWithSearch,
|
|
163
|
+
action,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(
|
|
167
|
+
Object.keys(newState.search.results),
|
|
168
|
+
).toHaveLength(1);
|
|
169
|
+
const file1Results =
|
|
170
|
+
newState.search.results['file1.js'];
|
|
171
|
+
expect(file1Results.matches).toHaveLength(2); // Should find both "Hello" and "hello"
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle mixed case search pattern', () => {
|
|
175
|
+
const mockFiles = {
|
|
176
|
+
'file1.js': {
|
|
177
|
+
name: 'file1.js',
|
|
178
|
+
text: 'const HeLLo = "world";\nconst hello = "test";\nconst HELLO = "end";',
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const stateWithSearch = {
|
|
183
|
+
...initialState,
|
|
184
|
+
search: {
|
|
185
|
+
...initialState.search,
|
|
186
|
+
pattern: 'HeLLo', // mixed case search pattern
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const action: VZAction = {
|
|
191
|
+
type: 'set_search_results',
|
|
192
|
+
files: createMockShareDBDoc(mockFiles),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const newState = setSearchResultsReducer(
|
|
196
|
+
stateWithSearch,
|
|
197
|
+
action,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(
|
|
201
|
+
Object.keys(newState.search.results),
|
|
202
|
+
).toHaveLength(1);
|
|
203
|
+
const file1Results =
|
|
204
|
+
newState.search.results['file1.js'];
|
|
205
|
+
expect(file1Results.matches).toHaveLength(3); // Should find all three variations
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should skip files without text content', () => {
|
|
209
|
+
const mockFiles = {
|
|
210
|
+
'file1.js': {
|
|
211
|
+
name: 'file1.js',
|
|
212
|
+
text: 'const hello = "world";',
|
|
213
|
+
},
|
|
214
|
+
'file2.js': {
|
|
215
|
+
name: 'file2.js',
|
|
216
|
+
// no text property
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const stateWithSearch = {
|
|
221
|
+
...initialState,
|
|
222
|
+
search: {
|
|
223
|
+
...initialState.search,
|
|
224
|
+
pattern: 'hello',
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const action: VZAction = {
|
|
229
|
+
type: 'set_search_results',
|
|
230
|
+
files: createMockShareDBDoc(mockFiles),
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const newState = setSearchResultsReducer(
|
|
234
|
+
stateWithSearch,
|
|
235
|
+
action,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Should only find results in file1.js
|
|
239
|
+
expect(
|
|
240
|
+
Object.keys(newState.search.results),
|
|
241
|
+
).toHaveLength(1);
|
|
242
|
+
expect(
|
|
243
|
+
newState.search.results['file1.js'],
|
|
244
|
+
).toBeDefined();
|
|
245
|
+
expect(
|
|
246
|
+
newState.search.results['file2.js'],
|
|
247
|
+
).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -24,7 +24,9 @@ function searchPattern(
|
|
|
24
24
|
const matches = [];
|
|
25
25
|
|
|
26
26
|
for (let j = 0; j < lines.length; j++) {
|
|
27
|
-
const index = lines[j]
|
|
27
|
+
const index = lines[j]
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.indexOf(pattern.toLowerCase());
|
|
28
30
|
|
|
29
31
|
if (index !== -1) {
|
|
30
32
|
matches.push({
|
package/src/{ot.js → ot.ts}
RENAMED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// This file is the central point where the OT types are imported.
|
|
3
3
|
// Localized to one file so it's easy to change it in future.
|
|
4
4
|
import OTJSON1Presence from 'sharedb-client-browser/dist/ot-json1-presence-umd.cjs';
|
|
5
|
+
import jsondiff from 'json0-ot-diff';
|
|
6
|
+
import diffMatchPatch from 'diff-match-patch';
|
|
5
7
|
|
|
6
8
|
export const { json1Presence, textUnicode } =
|
|
7
9
|
OTJSON1Presence;
|
|
@@ -11,3 +13,12 @@ export const otType = json1Presence.type;
|
|
|
11
13
|
|
|
12
14
|
// Applies an OT op to an object.
|
|
13
15
|
export const apply = otType.apply;
|
|
16
|
+
|
|
17
|
+
export const diff = (a, b) =>
|
|
18
|
+
jsondiff(
|
|
19
|
+
a,
|
|
20
|
+
b,
|
|
21
|
+
diffMatchPatch,
|
|
22
|
+
json1Presence,
|
|
23
|
+
textUnicode,
|
|
24
|
+
);
|
package/src/runCode.ts
CHANGED
|
@@ -1,31 +1,14 @@
|
|
|
1
1
|
import { SubmitOperation } from './types';
|
|
2
2
|
import { VizContent } from '@vizhub/viz-types';
|
|
3
|
-
import { createSubmitOperation } from './submitOperation';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Creates a runCode function that triggers code execution by flashing `isInteracting` to `true`.
|
|
7
6
|
* This works for both client-side (with submitOperation) and server-side (with ShareDB document).
|
|
8
7
|
*/
|
|
9
8
|
export const createRunCodeFunction = (
|
|
10
|
-
|
|
11
|
-
| SubmitOperation
|
|
12
|
-
| { data: any; submitOp: (ops: any) => void },
|
|
9
|
+
submitOperation: SubmitOperation,
|
|
13
10
|
) => {
|
|
14
11
|
return () => {
|
|
15
|
-
let submitOperation: SubmitOperation;
|
|
16
|
-
|
|
17
|
-
// Check if this is a client-side submitOperation or server-side ShareDB document
|
|
18
|
-
if (typeof submitOperationOrDoc === 'function') {
|
|
19
|
-
// Client-side: already a submitOperation function
|
|
20
|
-
submitOperation =
|
|
21
|
-
submitOperationOrDoc as SubmitOperation;
|
|
22
|
-
} else {
|
|
23
|
-
// Server-side: create submitOperation from ShareDB document
|
|
24
|
-
submitOperation = createSubmitOperation(
|
|
25
|
-
submitOperationOrDoc,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
12
|
// Use the unified submitOperation approach for both client and server
|
|
30
13
|
submitOperation((content: VizContent) => ({
|
|
31
14
|
...content,
|
|
@@ -39,7 +22,7 @@ export const createRunCodeFunction = (
|
|
|
39
22
|
submitOperation(
|
|
40
23
|
({ isInteracting, ...newDocument }) => newDocument,
|
|
41
24
|
);
|
|
42
|
-
},
|
|
25
|
+
}, 100);
|
|
43
26
|
};
|
|
44
27
|
};
|
|
45
28
|
|
|
@@ -48,11 +31,9 @@ export const createRunCodeFunction = (
|
|
|
48
31
|
* This is useful for maintaining compatibility with existing code that expects a ref.
|
|
49
32
|
*/
|
|
50
33
|
export const createRunCodeRef = (
|
|
51
|
-
|
|
52
|
-
| SubmitOperation
|
|
53
|
-
| { data: any; submitOp: (ops: any) => void },
|
|
34
|
+
submitOperation: SubmitOperation,
|
|
54
35
|
) => {
|
|
55
36
|
return {
|
|
56
|
-
current: createRunCodeFunction(
|
|
37
|
+
current: createRunCodeFunction(submitOperation),
|
|
57
38
|
};
|
|
58
39
|
};
|