vzcode 0.9.0 → 0.10.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-80e9e6df.js → index-8a130b74.js} +32 -32
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/client/App.tsx +66 -33
- package/src/client/CodeEditor/getOrCreateEditor.ts +4 -1
- package/src/client/CodeEditor/index.tsx +4 -1
- package/src/client/CodeEditor/widgets.ts +4 -1
- package/src/server/computeInitialDocument.js +16 -6
- package/src/server/index.js +46 -8
package/dist/index.html
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
19
19
|
rel="stylesheet"
|
|
20
20
|
/>
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-8a130b74.js"></script>
|
|
22
22
|
<link rel="stylesheet" href="/assets/index-ccd52a89.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body>
|
package/package.json
CHANGED
package/src/client/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
|
|
3
3
|
import { json1Presence } from '../ot';
|
|
4
4
|
import { randomId } from '../randomId';
|
|
@@ -24,6 +24,12 @@ const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
|
24
24
|
const socket = new WebSocket(wsProtocol + window.location.host + '/ws');
|
|
25
25
|
const connection = new Connection(socket);
|
|
26
26
|
|
|
27
|
+
// The ShareDB document type.
|
|
28
|
+
type Document = {
|
|
29
|
+
files: Files;
|
|
30
|
+
isInteracting: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
27
33
|
function App() {
|
|
28
34
|
// The ShareDB document.
|
|
29
35
|
const [shareDBDoc, setShareDBDoc] = useState(null);
|
|
@@ -39,7 +45,8 @@ function App() {
|
|
|
39
45
|
|
|
40
46
|
// The `doc.data` part of the ShareDB document,
|
|
41
47
|
// updated on each change to decouple rendering from ShareDB.
|
|
42
|
-
|
|
48
|
+
// Starts out as `null` until the document is loaded.
|
|
49
|
+
const [data, setData] = useState<Document | null>(null);
|
|
43
50
|
|
|
44
51
|
// The id of the currently open file tab.
|
|
45
52
|
const [activeFileId, setActiveFileId] = useState<FileId>(null);
|
|
@@ -116,39 +123,60 @@ function App() {
|
|
|
116
123
|
};
|
|
117
124
|
}, []);
|
|
118
125
|
|
|
126
|
+
// A helper function to submit operations to the ShareDB document
|
|
127
|
+
const submitOperation = useCallback(
|
|
128
|
+
(updateFunction: (document: Document) => Document) => {
|
|
129
|
+
const currentDocument = shareDBDoc.data;
|
|
130
|
+
const nextDocument = updateFunction(currentDocument);
|
|
131
|
+
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
132
|
+
},
|
|
133
|
+
[shareDBDoc],
|
|
134
|
+
);
|
|
135
|
+
|
|
119
136
|
// Called when a file in the sidebar is double-clicked.
|
|
120
137
|
const handleRenameFileClick = useCallback(
|
|
121
138
|
(fileId: FileId) => {
|
|
139
|
+
// TODO better UX, maybe Bootstrap modal? Maybe edit inline?
|
|
122
140
|
const newName = prompt('Enter new name');
|
|
141
|
+
|
|
123
142
|
if (newName) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
143
|
+
submitOperation((document) => ({
|
|
144
|
+
...document,
|
|
145
|
+
files: {
|
|
146
|
+
...document.files,
|
|
147
|
+
[fileId]: {
|
|
148
|
+
...document.files[fileId],
|
|
149
|
+
name: newName,
|
|
150
|
+
},
|
|
130
151
|
},
|
|
131
|
-
};
|
|
132
|
-
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
152
|
+
}));
|
|
133
153
|
}
|
|
134
154
|
},
|
|
135
|
-
[
|
|
155
|
+
[submitOperation],
|
|
136
156
|
);
|
|
137
157
|
|
|
138
158
|
const deleteFile = useCallback(
|
|
139
159
|
(fileId: FileId) => {
|
|
140
|
-
// Close the tab in case it's open.
|
|
141
|
-
// TODO consistently use either "key" or "fileId" naming
|
|
142
160
|
closeTab(fileId);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
submitOperation((document) => {
|
|
162
|
+
const updatedFiles = { ...document.files };
|
|
163
|
+
delete updatedFiles[fileId];
|
|
164
|
+
return { ...document, files: updatedFiles };
|
|
165
|
+
});
|
|
148
166
|
},
|
|
149
|
-
[
|
|
167
|
+
[submitOperation, closeTab],
|
|
150
168
|
);
|
|
151
169
|
|
|
170
|
+
const createFile = useCallback(() => {
|
|
171
|
+
const name = prompt('Enter new file name');
|
|
172
|
+
if (name) {
|
|
173
|
+
submitOperation((document) => ({
|
|
174
|
+
...document,
|
|
175
|
+
files: { ...document.files, [randomId()]: { name, text: '' } },
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
}, [submitOperation]);
|
|
179
|
+
|
|
152
180
|
// TODO prompt the user "Are you sure?"
|
|
153
181
|
const handleDeleteFileClick = useCallback(
|
|
154
182
|
(fileId: FileId, event: React.MouseEvent) => {
|
|
@@ -160,28 +188,32 @@ function App() {
|
|
|
160
188
|
[deleteFile],
|
|
161
189
|
);
|
|
162
190
|
|
|
163
|
-
const createFile = useCallback(() => {
|
|
164
|
-
// TODO better UI, maybe Bootstrap modal? Maybe edit inline?
|
|
165
|
-
const name = prompt('Enter new file name');
|
|
166
|
-
if (!name) return;
|
|
167
|
-
const currentDocument = shareDBDoc.data;
|
|
168
|
-
const nextDocument = {
|
|
169
|
-
...currentDocument,
|
|
170
|
-
[randomId()]: { name, text: '' },
|
|
171
|
-
};
|
|
172
|
-
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
173
|
-
}, [shareDBDoc]);
|
|
174
|
-
|
|
175
191
|
const handleSettingsClose = useCallback(() => {
|
|
176
192
|
setIsSettingsOpen(false);
|
|
177
193
|
}, []);
|
|
178
194
|
|
|
195
|
+
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
196
|
+
// via interactive code widgets, and `false` when they are not.
|
|
197
|
+
const interactTimeoutRef = useRef(null);
|
|
198
|
+
const handleInteract = useCallback(() => {
|
|
199
|
+
if (!interactTimeoutRef.current) {
|
|
200
|
+
submitOperation((document) => ({ ...document, isInteracting: true }));
|
|
201
|
+
} else {
|
|
202
|
+
clearTimeout(interactTimeoutRef.current);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interactTimeoutRef.current = setTimeout(() => {
|
|
206
|
+
interactTimeoutRef.current = null;
|
|
207
|
+
submitOperation((document) => ({ ...document, isInteracting: false }));
|
|
208
|
+
}, 800);
|
|
209
|
+
}, [submitOperation]);
|
|
210
|
+
|
|
179
211
|
return (
|
|
180
212
|
<div className="app">
|
|
181
213
|
<div className="left">
|
|
182
214
|
<Sidebar
|
|
183
215
|
createFile={createFile}
|
|
184
|
-
files={data}
|
|
216
|
+
files={data?.files}
|
|
185
217
|
handleRenameFileClick={handleRenameFileClick}
|
|
186
218
|
handleDeleteFileClick={handleDeleteFileClick}
|
|
187
219
|
handleFileClick={openTab}
|
|
@@ -197,7 +229,7 @@ function App() {
|
|
|
197
229
|
</div>
|
|
198
230
|
<div className="right">
|
|
199
231
|
<TabList
|
|
200
|
-
files={data}
|
|
232
|
+
files={data?.files}
|
|
201
233
|
tabList={tabList}
|
|
202
234
|
activeFileId={activeFileId}
|
|
203
235
|
setActiveFileId={setActiveFileId}
|
|
@@ -210,6 +242,7 @@ function App() {
|
|
|
210
242
|
docPresence={docPresence}
|
|
211
243
|
activeFileId={activeFileId}
|
|
212
244
|
theme={theme}
|
|
245
|
+
onInteract={handleInteract}
|
|
213
246
|
/>
|
|
214
247
|
) : null}
|
|
215
248
|
</div>
|
|
@@ -10,6 +10,7 @@ import { FileId, ShareDBDoc } from '../../types';
|
|
|
10
10
|
import { json1PresenceBroadcast } from './json1PresenceBroadcast';
|
|
11
11
|
import { json1PresenceDisplay } from './json1PresenceDisplay';
|
|
12
12
|
import { widgets } from './widgets';
|
|
13
|
+
|
|
13
14
|
// Singleton cache of CodeMirror instances
|
|
14
15
|
// These are created, but never destroyed.
|
|
15
16
|
// TODO consider invalidating this cache at some point.
|
|
@@ -36,6 +37,7 @@ export const getOrCreateEditor = ({
|
|
|
36
37
|
localPresence,
|
|
37
38
|
docPresence,
|
|
38
39
|
theme,
|
|
40
|
+
onInteract,
|
|
39
41
|
}: {
|
|
40
42
|
fileId: FileId;
|
|
41
43
|
shareDBDoc: ShareDBDoc<any>;
|
|
@@ -43,6 +45,7 @@ export const getOrCreateEditor = ({
|
|
|
43
45
|
localPresence: any;
|
|
44
46
|
docPresence: any;
|
|
45
47
|
theme: any;
|
|
48
|
+
onInteract?: () => void;
|
|
46
49
|
}) => {
|
|
47
50
|
const data = shareDBDoc.data;
|
|
48
51
|
const textPath = [...filesPath, fileId, 'text'];
|
|
@@ -97,7 +100,7 @@ export const getOrCreateEditor = ({
|
|
|
97
100
|
extensions.push(markdown());
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
extensions.push(widgets());
|
|
103
|
+
extensions.push(widgets({ onInteract }));
|
|
101
104
|
|
|
102
105
|
let editor = editorCache.get(fileId);
|
|
103
106
|
if (!editor) {
|
|
@@ -10,7 +10,8 @@ export const CodeEditor = ({
|
|
|
10
10
|
localPresence,
|
|
11
11
|
docPresence,
|
|
12
12
|
theme = defaultTheme,
|
|
13
|
-
filesPath = [],
|
|
13
|
+
filesPath = ['files'],
|
|
14
|
+
onInteract,
|
|
14
15
|
}: {
|
|
15
16
|
activeFileId: FileId;
|
|
16
17
|
shareDBDoc: any;
|
|
@@ -21,6 +22,7 @@ export const CodeEditor = ({
|
|
|
21
22
|
// The path of the files object in the ShareDB document.
|
|
22
23
|
// Defaults to the root of the document.
|
|
23
24
|
filesPath?: string[];
|
|
25
|
+
onInteract?: () => void;
|
|
24
26
|
}) => {
|
|
25
27
|
const ref = useRef<HTMLDivElement>(null);
|
|
26
28
|
|
|
@@ -38,6 +40,7 @@ export const CodeEditor = ({
|
|
|
38
40
|
docPresence,
|
|
39
41
|
// TODO refactor this, make dynamic themes work
|
|
40
42
|
theme: themesByLabel[theme],
|
|
43
|
+
onInteract,
|
|
41
44
|
});
|
|
42
45
|
ref.current.appendChild(editor.dom);
|
|
43
46
|
|
|
@@ -7,7 +7,8 @@ import interact from '@replit/codemirror-interact';
|
|
|
7
7
|
// * TODO color picker
|
|
8
8
|
// Inspired by:
|
|
9
9
|
// https://github.com/replit/codemirror-interact/blob/master/dev/index.ts
|
|
10
|
-
|
|
10
|
+
// `onInteract` is called when the user interacts with a widget.
|
|
11
|
+
export const widgets = ({ onInteract }: { onInteract?: () => void }) =>
|
|
11
12
|
interact({
|
|
12
13
|
rules: [
|
|
13
14
|
// a rule for a number dragger
|
|
@@ -18,6 +19,7 @@ export const widgets = () =>
|
|
|
18
19
|
cursor: 'ew-resize',
|
|
19
20
|
// change number value based on mouse X movement on drag
|
|
20
21
|
onDrag: (text, setText, e) => {
|
|
22
|
+
if (onInteract) onInteract();
|
|
21
23
|
const newVal = Number(text) + e.movementX;
|
|
22
24
|
if (isNaN(newVal)) return;
|
|
23
25
|
setText(newVal.toString());
|
|
@@ -28,6 +30,7 @@ export const widgets = () =>
|
|
|
28
30
|
regexp: /true|false/g,
|
|
29
31
|
cursor: 'pointer',
|
|
30
32
|
onClick: (text, setText) => {
|
|
33
|
+
if (onInteract) onInteract();
|
|
31
34
|
switch (text) {
|
|
32
35
|
case 'true':
|
|
33
36
|
return setText('false');
|
|
@@ -15,11 +15,21 @@ export const computeInitialDocument = ({ fullPath }) => {
|
|
|
15
15
|
const unsearchedDirectories = [''];
|
|
16
16
|
|
|
17
17
|
// Initialize the document using our data structure for representing files.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const initialDocument = {
|
|
19
|
+
// `files`
|
|
20
|
+
// * Keys are file ids, which are random numbers.
|
|
21
|
+
// * Values are objects with properties:
|
|
22
|
+
// * text - the text content of the file
|
|
23
|
+
// * name - the file name
|
|
24
|
+
files: {},
|
|
25
|
+
|
|
26
|
+
// `isInteracting`
|
|
27
|
+
// * Whether the user is currently interacting with the document
|
|
28
|
+
// using an interactive code widget such as number dragger.
|
|
29
|
+
// * When true, the auto-save to the file system is changed to be
|
|
30
|
+
// more frequent (throttled not debounced).
|
|
31
|
+
isInteracting: false,
|
|
32
|
+
};
|
|
23
33
|
|
|
24
34
|
// Stack for recursively traversing directories.
|
|
25
35
|
let files = [];
|
|
@@ -51,7 +61,7 @@ export const computeInitialDocument = ({ fullPath }) => {
|
|
|
51
61
|
|
|
52
62
|
files.forEach((file) => {
|
|
53
63
|
const id = randomId();
|
|
54
|
-
initialDocument[id] = {
|
|
64
|
+
initialDocument.files[id] = {
|
|
55
65
|
text: isDirectory(file)
|
|
56
66
|
? null
|
|
57
67
|
: fs.readFileSync(path.join(fullPath, file), 'utf-8'),
|
package/src/server/index.js
CHANGED
|
@@ -32,6 +32,8 @@ ShareDB.types.register(json1Presence.type);
|
|
|
32
32
|
|
|
33
33
|
const app = express();
|
|
34
34
|
|
|
35
|
+
// TODO make this configurable
|
|
36
|
+
// See https://github.com/vizhub-core/vzcode/issues/95
|
|
35
37
|
app.post('/saveTime', (req, res) => {
|
|
36
38
|
//autoSaveDebounceTimeMS = req.body.autoSaveDebounceTimeMS;
|
|
37
39
|
console.log('autoSaveDebounceTimeMS', req.body);
|
|
@@ -67,10 +69,14 @@ let previousDocument = initialDocument;
|
|
|
67
69
|
|
|
68
70
|
// Saves the files that changed.
|
|
69
71
|
const save = () => {
|
|
72
|
+
// console.log('saving');
|
|
70
73
|
const currentDocument = shareDBDoc.data;
|
|
71
74
|
|
|
72
75
|
// Take a look at each file (key) previously and currently.
|
|
73
|
-
const allKeys = Object.keys({
|
|
76
|
+
const allKeys = Object.keys({
|
|
77
|
+
...currentDocument.files,
|
|
78
|
+
...previousDocument.files,
|
|
79
|
+
});
|
|
74
80
|
for (const key of allKeys) {
|
|
75
81
|
const previous = previousDocument[key];
|
|
76
82
|
const current = currentDocument[key];
|
|
@@ -101,14 +107,46 @@ const save = () => {
|
|
|
101
107
|
previousDocument = currentDocument;
|
|
102
108
|
};
|
|
103
109
|
|
|
104
|
-
// Listen for when users modify files.
|
|
105
|
-
// Files get written to disk after `autoSaveDebounceTimeMS`
|
|
106
|
-
// milliseconds of inactivity.
|
|
107
|
-
let timeout;
|
|
110
|
+
// // Listen for when users modify files.
|
|
111
|
+
// // Files get written to disk after `autoSaveDebounceTimeMS`
|
|
112
|
+
// // milliseconds of inactivity.
|
|
113
|
+
// let timeout;
|
|
114
|
+
// shareDBDoc.subscribe(() => {
|
|
115
|
+
// shareDBDoc.on('op', () => {
|
|
116
|
+
// // console.log(shareDBDoc.data.isInteracting);
|
|
117
|
+
// clearTimeout(timeout);
|
|
118
|
+
// timeout = setTimeout(save, autoSaveDebounceTimeMS);
|
|
119
|
+
// });
|
|
120
|
+
// });
|
|
121
|
+
|
|
122
|
+
const throttleTimeMS = 200;
|
|
123
|
+
|
|
124
|
+
let lastExecutedTime = Date.now();
|
|
125
|
+
|
|
126
|
+
// Function to throttle the saving
|
|
127
|
+
function throttleSave() {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
if (now - lastExecutedTime > throttleTimeMS) {
|
|
130
|
+
save();
|
|
131
|
+
lastExecutedTime = now;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Function to debounce the saving
|
|
136
|
+
let debounceTimeout;
|
|
137
|
+
function debounceSave() {
|
|
138
|
+
clearTimeout(debounceTimeout);
|
|
139
|
+
debounceTimeout = setTimeout(save, autoSaveDebounceTimeMS);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Subscribe to listen for modifications
|
|
108
143
|
shareDBDoc.subscribe(() => {
|
|
109
|
-
shareDBDoc.on('op', (
|
|
110
|
-
|
|
111
|
-
|
|
144
|
+
shareDBDoc.on('op', () => {
|
|
145
|
+
if (shareDBDoc.data.isInteracting) {
|
|
146
|
+
throttleSave();
|
|
147
|
+
} else {
|
|
148
|
+
debounceSave();
|
|
149
|
+
}
|
|
112
150
|
});
|
|
113
151
|
});
|
|
114
152
|
|