vzcode 0.21.0 → 0.24.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.
@@ -1,5 +1,18 @@
1
1
  import interact from '@replit/codemirror-interact';
2
2
 
3
+ import {
4
+ ViewPlugin,
5
+ Decoration,
6
+ WidgetType,
7
+ keymap,
8
+ } from '@codemirror/view';
9
+ import {
10
+ Annotation,
11
+ Extension,
12
+ RangeSet,
13
+ } from '@codemirror/state';
14
+ import { EditorView } from 'codemirror';
15
+
3
16
  // Interactive code widgets.
4
17
  // * Number dragger
5
18
  // * Boolean toggler
@@ -15,10 +28,49 @@ export const widgets = ({
15
28
  }) =>
16
29
  interact({
17
30
  rules: [
31
+ // hex color picker
32
+ // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
33
+ {
34
+ regexp: /\"\#([0-9]|[A-F]|[a-f]){6}\"/g,
35
+ cursor: 'pointer',
36
+ onClick(text, setText, e) {
37
+ const res =
38
+ /\"(?<hex>\#([0-9]|[A-F]|[a-f]){6})\"/.exec(
39
+ text,
40
+ );
41
+ const startingColor = res.groups?.hex;
42
+
43
+ const sel = document.createElement('input');
44
+ sel.type = 'color';
45
+
46
+ sel.value = startingColor.toLowerCase();
47
+
48
+ //valueIsUpper maintains style of user's code. It keeps the case of a-f the same case as the original."
49
+ const valueIsUpper =
50
+ startingColor.toUpperCase() === startingColor;
51
+
52
+ const updateHex = (e: Event) => {
53
+ const el = e.target as HTMLInputElement;
54
+
55
+ if (el.value) {
56
+ setText(
57
+ `"${
58
+ valueIsUpper
59
+ ? el.value.toUpperCase()
60
+ : el.value
61
+ }"`,
62
+ );
63
+ }
64
+ };
65
+ sel.addEventListener('input', updateHex);
66
+ sel.click();
67
+ },
68
+ },
69
+
18
70
  // a rule for a number dragger
19
71
  {
20
72
  // the regexp matching the value
21
- regexp: /-?\b\d+\.?\d*\b/g,
73
+ regexp: /(?<!\#)-?\b\d+\.?\d*\b/g,
22
74
  // set cursor to "ew-resize" on hover
23
75
  cursor: 'ew-resize',
24
76
  // change number value based on mouse X movement on drag
@@ -43,7 +95,7 @@ export const widgets = ({
43
95
  }
44
96
  },
45
97
  },
46
- //vec2 slider
98
+ // vec2 slider
47
99
  // Inspired by: https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L61
48
100
  {
49
101
  regexp:
@@ -64,7 +116,7 @@ export const widgets = ({
64
116
  );
65
117
  },
66
118
  },
67
- //color picker
119
+ // rgb color picker
68
120
  // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L71
69
121
  //TODO: create color picker for hsl colors
70
122
  {
@@ -82,6 +134,7 @@ export const widgets = ({
82
134
  //sel will open the color picker when sel.click is called.
83
135
  const sel = document.createElement('input');
84
136
  sel.type = 'color';
137
+
85
138
  if (!isNaN(r + g + b))
86
139
  sel.value = rgb2Hex(r, g, b);
87
140
 
@@ -148,3 +201,185 @@ const hex2RGB = (hex: string): [number, number, number] => {
148
201
  // Inspired by https://github.com/replit/codemirror-interact/blob/master/dev/index.ts#L117
149
202
  const rgb2Hex = (r: number, g: number, b: number): string =>
150
203
  '#' + r.toString(16) + g.toString(16) + b.toString(16);
204
+
205
+ const colorCircleTheme = EditorView.baseTheme({
206
+ '.color-circle-parent': { display: 'inline-block' },
207
+ });
208
+
209
+ const colorCircleAnnotation = Annotation.define();
210
+
211
+ export const colorsInTextPlugin: Extension = [
212
+ ViewPlugin.fromClass(
213
+ class {
214
+ decorations: any;
215
+ view: EditorView;
216
+ constructor(view: EditorView) {
217
+ this.decorations = RangeSet.of([]);
218
+ this.view = view;
219
+ }
220
+ },
221
+ {
222
+ decorations: (v) => {
223
+ const colorInfos = [];
224
+
225
+ const lines = v.view.state.doc.iter();
226
+ let line = lines.next();
227
+
228
+ //Offset is the number of characters before the hex so the circle can be placed properly.
229
+ let offset = 0;
230
+ while (!line.done) {
231
+ if (line.value === '\n') {
232
+ offset++;
233
+ line = lines.next();
234
+ continue;
235
+ }
236
+ const hexColorOccurances = line.value.matchAll(
237
+ /\"\#([0-9]|[A-F]|[a-f]){6}\"/g,
238
+ );
239
+ let hexOccurance = hexColorOccurances.next();
240
+ while (!hexOccurance.done) {
241
+ const offsetColorInfo = hexOccurance.value;
242
+ offsetColorInfo.index += offset;
243
+ colorInfos.push(offsetColorInfo);
244
+ hexOccurance = hexColorOccurances.next();
245
+ }
246
+ offset += line.value.length;
247
+ line = lines.next();
248
+ }
249
+
250
+ return Decoration.set(
251
+ colorInfos.map((colorInfo) => {
252
+ return {
253
+ //9 is the length of the hex color string including quotation marks
254
+ from: colorInfo.index + 9,
255
+ to: colorInfo.index + 9,
256
+ value: Decoration.widget({
257
+ side: -1,
258
+ widget: new ColorWidget(colorInfo[0]),
259
+ }),
260
+ };
261
+ }),
262
+ );
263
+ },
264
+ },
265
+ ),
266
+ colorCircleTheme,
267
+ ];
268
+
269
+ class ColorWidget extends WidgetType {
270
+ color: string;
271
+ constructor(color: string) {
272
+ super();
273
+ this.color = color;
274
+ }
275
+
276
+ eq(widget: ColorWidget): boolean {
277
+ // TODO consider possibly adding a random ID to support multiple instances with the same color
278
+ return widget.color === this.color;
279
+ }
280
+
281
+ toDOM(view: EditorView): HTMLElement {
282
+ const parent = document.createElement('div');
283
+
284
+ parent.setAttribute('style', 'width:10px;height:10px');
285
+ parent.className = 'color-circle-parent';
286
+ const svg = document.createElementNS(
287
+ 'http://www.w3.org/2000/svg',
288
+ 'svg',
289
+ );
290
+ const colorCircle = document.createElementNS(
291
+ 'http://www.w3.org/2000/svg',
292
+ 'circle',
293
+ );
294
+ colorCircle.setAttributeNS(
295
+ null,
296
+ 'fill',
297
+ this.color.replace(/\"/g, ''),
298
+ );
299
+ colorCircle.setAttributeNS(null, 'r', '5');
300
+ colorCircle.setAttributeNS(null, 'cx', '5');
301
+ colorCircle.setAttributeNS(null, 'cy', '5');
302
+
303
+ svg.setAttributeNS(null, 'viewBox', '0 0 10 10');
304
+ svg.setAttributeNS(null, 'width', '10');
305
+ svg.setAttributeNS(null, 'height', '10');
306
+
307
+ svg.appendChild(colorCircle);
308
+
309
+ parent.appendChild(svg);
310
+ return parent;
311
+ }
312
+ ignoreEvent() {
313
+ return false;
314
+ }
315
+ }
316
+
317
+ export const highlightWidgets = ViewPlugin.fromClass(
318
+ class {
319
+ showHighlight: boolean;
320
+ view: EditorView;
321
+ constructor(view: EditorView) {
322
+ this.showHighlight = false;
323
+ this.view = view;
324
+ }
325
+ },
326
+ {
327
+ decorations: (v) => {
328
+ if (!v.showHighlight) {
329
+ return Decoration.none;
330
+ }
331
+
332
+ const interactOpportunities = [];
333
+ const lines = v.view.state.doc.iter();
334
+ let line = lines.next();
335
+
336
+ //Offset is the number of characters before the regex so the highlighting can be placed properly.
337
+ let offset = 0;
338
+ while (!line.done) {
339
+ if (line.value === '\n') {
340
+ offset++;
341
+ line = lines.next();
342
+ continue;
343
+ }
344
+ const interactiveOccurances = line.value.matchAll(
345
+ //The below line contains all of the Regexes for all of the interactive widgets.
346
+ /\"\#([0-9]|[A-F]|[a-f]){6}\"|rotate\(-?\d*\.?\d*\)|https?:\/\/[^ "]+|vec2\(-?\b\d+\.?\d*\b\s*(,\s*-?\b\d+\.?\d*\b)?\)|true|false|(?<!\#)-?\b\d+\.?\d*\b|rgb\(.*\)/dg,
347
+ );
348
+ let interactOccurance =
349
+ interactiveOccurances.next();
350
+ while (!interactOccurance.done) {
351
+ const offsetInteract = interactOccurance.value;
352
+ offsetInteract.indices[0][0] += offset;
353
+ offsetInteract.indices[0][1] += offset;
354
+ interactOpportunities.push(offsetInteract);
355
+ interactOccurance = interactiveOccurances.next();
356
+ }
357
+ offset += line.value.length;
358
+ line = lines.next();
359
+ }
360
+ return Decoration.set(
361
+ interactOpportunities.map((opportunity) => {
362
+ return {
363
+ from: opportunity.indices[0][0],
364
+ to: opportunity.indices[0][1],
365
+ value: Decoration.mark({
366
+ class: 'cm-interact ',
367
+ }),
368
+ };
369
+ }),
370
+ );
371
+ },
372
+ eventHandlers: {
373
+ keydown(event, view) {
374
+ if (event.key == 'Alt') {
375
+ this.showHighlight = true;
376
+ }
377
+ },
378
+ keyup(event, view) {
379
+ if (event.key == 'Alt') {
380
+ this.showHighlight = false;
381
+ }
382
+ },
383
+ },
384
+ },
385
+ );
@@ -1,6 +1,8 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useRef } from 'react';
2
2
  import './style.scss';
3
3
 
4
+ type PresenceId = string;
5
+
4
6
  // A presence notification is a message that is displayed when a user joins or leaves a session.
5
7
  // It has the following properties:
6
8
  // - `user` is the username of the user who joined or left the session.
@@ -10,7 +12,7 @@ import './style.scss';
10
12
  type PresenceNotification = {
11
13
  user: string;
12
14
  join: boolean;
13
- presenceId: string;
15
+ presenceId: PresenceId;
14
16
  };
15
17
 
16
18
  export const PresenceNotifications = ({
@@ -21,86 +23,117 @@ export const PresenceNotifications = ({
21
23
  const [presenceNotifications, setPresenceNotifications] =
22
24
  useState<Array<PresenceNotification>>([]);
23
25
 
24
- // // console.log('docPresence:', docPresence);
25
- // const [visible, setVisible] = useState(true);
26
- // const [user, setUser] = useState('');
27
-
28
- // // `true` means user has joined the session.
29
- // // `false` means user has left the session.
30
- // const [join, setJoin] = useState<boolean>(true);
26
+ // const [presenceIds, setPresenceIds] = useState<
27
+ // Array<any>
28
+ // >([]);
29
+ const alreadyJoinedPresenceIds = useRef<Set<PresenceId>>(
30
+ new Set(),
31
+ );
31
32
 
32
33
  useEffect(() => {
33
34
  // console.log('useEffect triggered.');
34
35
  if (docPresence) {
35
36
  // See https://share.github.io/sharedb/presence
36
37
  docPresence.on('receive', (presenceId, update) => {
38
+ // Reason for notifications appearing on every cursor movement:
39
+ // code doesn't check if a notification about a user joining was previously sent
40
+
41
+ // Attempted solutions:
42
+ // - create an array called presenceIds that keeps track of the presences that join and leave
43
+ // - if presenceId is in presenceIds: local user was already alerted that a remote user joined
44
+ // - if presenceId is not in presenceIds: local user needs to be notified about a new user
45
+ // - problems:
46
+ // - the array is always empty inside useEffect (array works outside of useEffect)
47
+ // - causes check to fail
48
+ // - check docPresence.remotePresences, which keeps track of all the remote presences on the doc
49
+ // - see https://share.github.io/sharedb/api/presence
50
+ // - if presenceId is in docPresence.remotePresences: local user was already alerted that a remote user joined
51
+ // - if presenceId is not in docPresence.remotePresences: local user needs to be notified about a new user
52
+ // - problems:
53
+ // - useEffects is called when docPresence changes (in some cases, it's when the remote presence changes)
54
+ // - remotePresences already has the remote presence in the array, so check fails
55
+ // - could add another property to presence (true: already notified, false: need to be notified)
56
+ // - doesn't seem like an ideal solution
57
+
58
+ // console.log('docPresence:', docPresence);
59
+ // console.log('docPresence remotePresences:', docPresence.remotePresences);
60
+ // console.log('check for presence:', docPresence.remotePresences[presenceId]);
61
+
37
62
  // TODO figure out how to get username from `presenceId`.
38
63
  const user = 'someone';
39
64
 
40
65
  // `true` means user has joined the session.
41
66
  // `false` means user has left the session.
42
- const join = update === null;
43
-
44
- // Add an entry to the presence notifications array.
45
- setPresenceNotifications((prev) => [
46
- ...prev,
47
- { user, join, presenceId },
48
- ]);
49
-
50
- // Remove it after 5 seconds.
51
- setTimeout(() => {
52
- setPresenceNotifications((prev) =>
53
- prev.filter(
54
- (notification) =>
55
- notification.presenceId !== presenceId,
56
- ),
57
- );
58
- }, 5000);
59
- });
67
+ const join = update !== null;
68
+
69
+ console.log('join', join);
60
70
 
61
- // console.log('inside if of useEffect.');
62
- // docPresence.on('receive', (presence) => {
63
- // console.log('presence', presence);
64
-
65
- // // Add an entry to the presence notifications array.
66
- // setPresenceNotifications((prev) => [
67
- // ...prev,
68
- // { user: presence.username, join: presence === null },
69
- // ]);
70
-
71
- // console.log('receive triggered.');
72
- // if (presence === null) {
73
- // setJoin(false);
74
- // }
75
- // const { username } = presence;
76
- // setUser(username);
77
- // });
78
-
79
- // const timer = setTimeout(() => {
80
- // setVisible(false);
81
- // }, 10000);
82
- return () => {
83
- // clearTimeout(timer);
84
- };
85
- } else {
86
- // setVisible(false);
71
+ // Figure out if we have ever seen this user before.
72
+
73
+ // if (join && docPresence.remotePresences[presenceId] === undefined){
74
+ if (join) {
75
+ if (
76
+ !alreadyJoinedPresenceIds.current.has(
77
+ presenceId,
78
+ )
79
+ ) {
80
+ alreadyJoinedPresenceIds.current.add(
81
+ presenceId,
82
+ );
83
+ setPresenceNotifications((prev) => [
84
+ ...prev,
85
+ { user, join, presenceId },
86
+ ]);
87
+
88
+ // Remove it after 5 seconds.
89
+ setTimeout(() => {
90
+ setPresenceNotifications((prev) =>
91
+ prev.filter(
92
+ (notification) =>
93
+ notification.presenceId !==
94
+ presenceId &&
95
+ notification.join !== join,
96
+ ),
97
+ );
98
+ }, 5000);
99
+ }
100
+ } else if (!join) {
101
+ setPresenceNotifications((prev) => [
102
+ ...prev,
103
+ { user, join, presenceId },
104
+ ]);
105
+
106
+ // Remove it after 5 seconds.
107
+ // TODO clean up this duplication.
108
+ setTimeout(() => {
109
+ setPresenceNotifications((prev) =>
110
+ prev.filter(
111
+ (notification) =>
112
+ notification.presenceId !== presenceId &&
113
+ notification.join !== join,
114
+ ),
115
+ );
116
+ }, 5000);
117
+ }
118
+ });
87
119
  }
88
120
  }, [docPresence]);
89
121
 
90
- console.log(
91
- 'presenceNotifications',
92
- presenceNotifications,
93
- );
122
+ // console.log(
123
+ // 'presenceNotifications',
124
+ // presenceNotifications,
125
+ // );
126
+ // console.log(
127
+ // 'presenceIds',
128
+ // presenceIds,
129
+ // );
94
130
 
95
131
  return presenceNotifications.length > 0 ? (
96
132
  <div className="vz-notification">
97
- {presenceNotifications.map((notification) => {
133
+ {presenceNotifications.map((notification, i) => {
98
134
  const { user, join } = notification;
99
135
  return (
100
- <div
101
- key={notification.presenceId}
102
- className="vz-notification-item"
103
- >
136
+ <div key={i} className="vz-notification-item">
104
137
  {join
105
138
  ? `${user} has joined the session.`
106
139
  : `${user} has left the session.`}
@@ -109,20 +142,4 @@ export const PresenceNotifications = ({
109
142
  })}
110
143
  </div>
111
144
  ) : null;
112
- // return null;
113
-
114
- // return (
115
- // <Modal
116
- // className="vz-notification"
117
- // show={visible}
118
- // onHide={() => setVisible(false)}
119
- // animation={false}
120
- // >
121
- // <Modal.Body>
122
- // {join
123
- // ? `${user} has joined the session.`
124
- // : `${user} has left the session.`}
125
- // </Modal.Body>
126
- // </Modal>
127
- // );
128
145
  };
@@ -0,0 +1,9 @@
1
+ import './style.scss';
2
+
3
+ export const PrettierErrorOverlay = ({ prettierError }) => {
4
+ return prettierError !== null ? (
5
+ <pre className="prettier-error-overlay">
6
+ {prettierError}
7
+ </pre>
8
+ ) : null;
9
+ };
@@ -0,0 +1,9 @@
1
+ .prettier-error-overlay {
2
+ color: rgb(255, 53, 53);
3
+ background-color: rgba(0, 0, 0, 0.6);
4
+ position: absolute;
5
+ bottom: 0;
6
+ left: 0;
7
+ right: 0;
8
+ pointer-events: none;
9
+ }
@@ -1,18 +1,13 @@
1
1
  import { useCallback, useRef } from 'react';
2
2
  import { Button, Modal, Form } from './bootstrap';
3
- import Select from 'react-select';
4
- import {
5
- ThemeLabel,
6
- themeOptionsByLabel,
7
- themes,
8
- } from './themes';
3
+ import { ThemeLabel, themes } from './themes';
9
4
 
10
- const saveTimes = [
11
- { value: 1, label: '1 second' },
12
- { value: 5, label: '5 seconds' },
13
- { value: 10, label: '10 seconds' },
14
- { value: 30, label: '30 seconds' },
15
- ];
5
+ // const saveTimes = [
6
+ // { value: 1, label: '1 second' },
7
+ // { value: 5, label: '5 seconds' },
8
+ // { value: 10, label: '10 seconds' },
9
+ // { value: 30, label: '30 seconds' },
10
+ // ];
16
11
 
17
12
  export let username = 'Anonymous';
18
13
 
@@ -27,23 +22,26 @@ export const Settings = ({
27
22
  setTheme: (theme: ThemeLabel) => void;
28
23
  theme: ThemeLabel;
29
24
  }) => {
30
- const handleChange = useCallback((selectedOption) => {
31
- setTheme(selectedOption.label);
25
+ const handleThemeChange = useCallback((event) => {
26
+ const selectedValue = event.target.value;
27
+ console.log(selectedValue);
28
+ setTheme(selectedValue);
32
29
  }, []);
33
30
 
34
- const handleSaveTimeChange = useCallback(
35
- (selectedOption) => {
36
- const time = [{ value: selectedOption.value }];
37
- fetch('/saveTime', {
38
- method: 'POST',
39
- headers: {
40
- 'Content-Type': 'application/json',
41
- },
42
- body: JSON.stringify(time),
43
- });
44
- },
45
- [],
46
- );
31
+ // TODO https://github.com/vizhub-core/vzcode/issues/95
32
+ // const handleSaveTimeChange = useCallback(
33
+ // (selectedOption) => {
34
+ // const time = [{ value: selectedOption.value }];
35
+ // fetch('/saveTime', {
36
+ // method: 'POST',
37
+ // headers: {
38
+ // 'Content-Type': 'application/json',
39
+ // },
40
+ // body: JSON.stringify(time),
41
+ // });
42
+ // },
43
+ // [],
44
+ // );
47
45
 
48
46
  const usernameRef = useRef<HTMLInputElement>(null);
49
47
  const handleUsernameChange = () => {
@@ -76,16 +74,26 @@ export const Settings = ({
76
74
  </Form.Group>
77
75
  <Form.Group className="mb-3" controlId="formFork">
78
76
  <Form.Label>Theme</Form.Label>
79
- <Select
80
- options={themes}
81
- onChange={handleChange}
82
- value={themeOptionsByLabel[theme]}
83
- />
77
+
78
+ <select
79
+ className="form-select"
80
+ value={theme}
81
+ onChange={handleThemeChange}
82
+ >
83
+ {themes.map(({ label }) => (
84
+ <option key={label} value={label}>
85
+ {label}
86
+ </option>
87
+ ))}
88
+ </select>
89
+
84
90
  <Form.Text className="text-muted">
85
91
  Select a color theme for the editor
86
92
  </Form.Text>
87
93
  </Form.Group>
88
- <Form.Group className="mb-3" controlId="formFork">
94
+ {/*
95
+ TODO https://github.com/vizhub-core/vzcode/issues/95
96
+ <Form.Group className="mb-3" controlId="formFork">
89
97
  <Form.Label>Auto-Save Time</Form.Label>
90
98
  <Select
91
99
  options={saveTimes}
@@ -94,7 +102,7 @@ export const Settings = ({
94
102
  <Form.Text className="text-muted">
95
103
  Select an auto save time
96
104
  </Form.Text>
97
- </Form.Group>
105
+ </Form.Group> */}
98
106
  </Modal.Body>
99
107
  <Modal.Footer>
100
108
  <Button variant="primary" onClick={onClose}>
@@ -33,10 +33,13 @@ export const DirectoryListing = ({
33
33
  toggleDirectory(path);
34
34
  }, [toggleDirectory]);
35
35
 
36
- const handleDeleteClick = useCallback(() => {
37
- // https://github.com/vizhub-core/vzcode/issues/102
38
- console.log('TODO handleDeleteDirectoryClick');
39
- }, []);
36
+ const handleDeleteClick = useCallback(
37
+ (event) => {
38
+ // https://github.com/vizhub-core/vzcode/issues/102
39
+ handleDeleteFileClick(path, event);
40
+ },
41
+ [handleDeleteFileClick, path],
42
+ );
40
43
 
41
44
  const handleRenameClick = useCallback(() => {
42
45
  // https://github.com/vizhub-core/vzcode/issues/103
@@ -41,11 +41,11 @@ export const FileListing = ({
41
41
 
42
42
  return (
43
43
  <Item
44
+ name={name}
44
45
  handleClick={handleClick}
45
46
  handleDeleteClick={handleDeleteClick}
46
47
  handleRenameClick={handleRenameClick}
47
48
  isDir={false}
48
- name={name}
49
49
  >
50
50
  {name}
51
51
  </Item>