vzcode 0.27.0 → 0.29.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-836bdce7.css +1 -0
  3. package/dist/assets/index-c7f894a9.js +115 -0
  4. package/dist/assets/worker-a2979249.js +366 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +11 -5
  7. package/src/client/AIAssist.ts +137 -0
  8. package/src/client/AIAssistWidget/index.tsx +101 -0
  9. package/src/client/AIAssistWidget/style.scss +10 -0
  10. package/src/client/App/VZCodeContext.tsx +251 -0
  11. package/src/client/App/VZLeft.tsx +56 -0
  12. package/src/client/App/VZMiddle.tsx +91 -0
  13. package/src/client/App/VZRight.tsx +15 -0
  14. package/src/client/App/index.tsx +104 -0
  15. package/src/client/{style.scss → App/style.scss} +14 -3
  16. package/src/client/App/useShareDB.ts +103 -0
  17. package/src/client/CodeEditor/getOrCreateEditor.ts +70 -7
  18. package/src/client/CodeEditor/index.tsx +24 -0
  19. package/src/client/CodeEditor/json1PresenceDisplay.ts +38 -9
  20. package/src/client/CodeEditor/typeScriptCompletions.ts +108 -0
  21. package/src/client/CodeEditor/typeScriptLinter.ts +45 -0
  22. package/src/client/CodeErrorOverlay/index.tsx +44 -8
  23. package/src/client/CodeErrorOverlay/style.scss +22 -11
  24. package/src/client/SplitPaneResizeContext.tsx +195 -39
  25. package/src/client/TabList/index.tsx +1 -27
  26. package/src/client/{Resizer → VZResizer}/index.tsx +37 -13
  27. package/src/client/VZSettings.tsx +1 -1
  28. package/src/client/VZSidebar/CreateFileModal.tsx +93 -0
  29. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +42 -24
  30. package/src/client/VZSidebar/index.tsx +116 -33
  31. package/src/client/VZSidebar/styles.scss +21 -1
  32. package/src/client/useFileCRUD.ts +19 -19
  33. package/src/client/useOpenDirectories.ts +12 -8
  34. package/src/client/usePrettier/index.ts +13 -1
  35. package/src/client/useTypeScript/index.ts +44 -0
  36. package/src/client/useTypeScript/requestTypes.ts +29 -0
  37. package/src/client/useTypeScript/worker.ts +222 -0
  38. package/src/client/usernameLocalStorage.ts +1 -1
  39. package/src/client/vzReducer/createInitialState.ts +4 -5
  40. package/src/index.ts +5 -1
  41. package/src/server/handleAIAssist.js +133 -0
  42. package/src/server/index.js +63 -1
  43. package/src/types.ts +78 -1
  44. package/dist/assets/index-5f9f376e.css +0 -1
  45. package/dist/assets/index-70b6d947.js +0 -111
  46. package/src/client/App.tsx +0 -286
  47. /package/src/client/{Resizer → VZResizer}/styles.scss +0 -0
@@ -0,0 +1,91 @@
1
+ import { useContext } from 'react';
2
+ import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
3
+ import { TabList } from '../TabList';
4
+ import { CodeEditor } from '../CodeEditor';
5
+ import { CodeErrorOverlay } from '../CodeErrorOverlay';
6
+ import { PresenceNotifications } from '../PresenceNotifications';
7
+ import { AIAssistWidget } from '../AIAssistWidget';
8
+ import { VZCodeContext } from './VZCodeContext';
9
+
10
+ // The middle portion of the VZCode environment, containing:
11
+ // * The list of tabs at the top
12
+ // * The code editor itself
13
+ // * The code error overlay
14
+ // * The presence notifications
15
+ // * The UI for AI Assist
16
+ export const VZMiddle = () => {
17
+ const { codeEditorWidth } = useContext(
18
+ SplitPaneResizeContext,
19
+ );
20
+
21
+ // TODO leverage this context in deeper levels of the component tree.
22
+ const {
23
+ content,
24
+ shareDBDoc,
25
+ submitOperation,
26
+ localPresence,
27
+ docPresence,
28
+ files,
29
+ createFile,
30
+ activeFileId,
31
+ setActiveFileId,
32
+ tabList,
33
+ openTab,
34
+ closeTabs,
35
+ theme,
36
+ username,
37
+ editorCache,
38
+ editorWantsFocus,
39
+ editorNoLongerWantsFocus,
40
+ errorMessage,
41
+ typeScriptWorker,
42
+ } = useContext(VZCodeContext);
43
+
44
+ return activeFileId !== null ? (
45
+ <div
46
+ className="middle"
47
+ style={{ width: codeEditorWidth + 'px' }}
48
+ >
49
+ <TabList
50
+ files={files}
51
+ tabList={tabList}
52
+ activeFileId={activeFileId}
53
+ setActiveFileId={setActiveFileId}
54
+ openTab={openTab}
55
+ closeTabs={closeTabs}
56
+ createFile={createFile}
57
+ />
58
+ {content && activeFileId ? (
59
+ <CodeEditor
60
+ shareDBDoc={shareDBDoc}
61
+ submitOperation={submitOperation}
62
+ localPresence={localPresence}
63
+ docPresence={docPresence}
64
+ activeFileId={activeFileId}
65
+ theme={theme}
66
+ editorCache={editorCache}
67
+ editorWantsFocus={editorWantsFocus}
68
+ editorNoLongerWantsFocus={
69
+ editorNoLongerWantsFocus
70
+ }
71
+ username={username}
72
+ typeScriptWorker={typeScriptWorker}
73
+ tabList={tabList}
74
+ />
75
+ ) : null}
76
+ <CodeErrorOverlay errorMessage={errorMessage} />
77
+ <PresenceNotifications
78
+ docPresence={docPresence}
79
+ localPresence={localPresence}
80
+ />
81
+ {content && activeFileId ? (
82
+ <AIAssistWidget
83
+ activeFileId={activeFileId}
84
+ shareDBDoc={shareDBDoc}
85
+ editorCache={editorCache}
86
+ tabList={tabList}
87
+ />
88
+ ) : null}
89
+ </div>
90
+ ) : null;
91
+ };
@@ -0,0 +1,15 @@
1
+ // Displays the Vite devserver iframe.
2
+ // TODO test this out, think this through.
3
+ // Purpose: support folks using VZCode as the editor
4
+ // for their Vite-based projects.
5
+ const enableIframe = false;
6
+
7
+ export const VZRight = () => {
8
+ return (
9
+ <div className="right">
10
+ {enableIframe ? (
11
+ <iframe src="http://localhost:5173/"></iframe>
12
+ ) : null}
13
+ </div>
14
+ );
15
+ };
@@ -0,0 +1,104 @@
1
+ import { useContext } from 'react';
2
+ import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
3
+ import { json1Presence } from '../../ot';
4
+ import { Username } from '../../types';
5
+ // @ts-ignore
6
+ import PrettierWorker from '../usePrettier/worker?worker';
7
+ // @ts-ignore
8
+ import TypeScriptWorker from '../useTypeScript/worker?worker';
9
+ import { SplitPaneResizeProvider } from '../SplitPaneResizeContext';
10
+ import { VZResizer } from '../VZResizer';
11
+ import {
12
+ useInitialUsername,
13
+ usePersistUsername,
14
+ } from '../usernameLocalStorage';
15
+ import { useShareDB } from './useShareDB';
16
+ import { VZLeft } from './VZLeft';
17
+ import { VZMiddle } from './VZMiddle';
18
+ import { VZRight } from './VZRight';
19
+ import {
20
+ VZCodeContext,
21
+ VZCodeProvider,
22
+ } from './VZCodeContext';
23
+ import './style.scss';
24
+
25
+ // Instantiate the Prettier worker.
26
+ const prettierWorker = new PrettierWorker();
27
+
28
+ // Instantiate the TypeScript worker.
29
+ const typeScriptWorker = new TypeScriptWorker();
30
+
31
+ // Register our custom JSON1 OT type that supports presence.
32
+ // See https://github.com/vizhub-core/json1-presence
33
+ ShareDBClient.types.register(json1Presence.type);
34
+
35
+ // Establish the singleton ShareDB connection over WebSockets.
36
+ // TODO consider using reconnecting WebSocket
37
+ const { Connection } = ShareDBClient;
38
+ const wsProtocol =
39
+ window.location.protocol === 'https:'
40
+ ? 'wss://'
41
+ : 'ws://';
42
+ const socket = new WebSocket(
43
+ wsProtocol + window.location.host + '/ws',
44
+ );
45
+ const connection = new Connection(socket);
46
+
47
+ // Stores the username to local storage.
48
+ // TODO consider if there's a cleaner pattern for this.
49
+ // This makes sense in VZCode itself, but for an app with
50
+ // authentication that wraps VZCode, it is not required.
51
+ const PersistUsername = () => {
52
+ const { username } = useContext(VZCodeContext);
53
+ usePersistUsername(username);
54
+ return null;
55
+ };
56
+
57
+ function App() {
58
+ const {
59
+ shareDBDoc,
60
+ content,
61
+ localPresence,
62
+ docPresence,
63
+ } = useShareDB({
64
+ connection,
65
+ });
66
+
67
+ // Get the initial username from localStorage.
68
+ const initialUsername: Username = useInitialUsername();
69
+
70
+ // Feature flag for enabling the right-side panel.
71
+ // Useful for debugging dual split pane functionality.
72
+ // We may want to add this as an actual VZCode feature,
73
+ // for running the code that the VZCode user is developing
74
+ // in the same browser window as the VZCode editor,
75
+ // so that multiple browser windows are not required.
76
+ const enableRightPanel = true;
77
+
78
+ return (
79
+ <SplitPaneResizeProvider>
80
+ <VZCodeProvider
81
+ content={content}
82
+ shareDBDoc={shareDBDoc}
83
+ localPresence={localPresence}
84
+ docPresence={docPresence}
85
+ prettierWorker={prettierWorker}
86
+ typeScriptWorker={typeScriptWorker}
87
+ initialUsername={initialUsername}
88
+ >
89
+ <div className="app">
90
+ <VZLeft />
91
+ <VZMiddle />
92
+ {enableRightPanel ? <VZRight /> : null}
93
+ <VZResizer side="left" />
94
+ {enableRightPanel ? (
95
+ <VZResizer side="right" />
96
+ ) : null}
97
+ </div>
98
+ <PersistUsername />
99
+ </VZCodeProvider>
100
+ </SplitPaneResizeProvider>
101
+ );
102
+ }
103
+
104
+ export default App;
@@ -49,16 +49,27 @@ body,
49
49
  position: relative;
50
50
 
51
51
  .left,
52
- .right {
52
+ .middle {
53
53
  display: flex;
54
54
 
55
55
  /* To suport PrettierErrorOverlay, which is position: absolute */
56
56
  position: relative;
57
57
  }
58
58
 
59
- .right {
60
- flex: 1;
59
+ .middle {
60
+ // flex: 1;
61
61
  flex-direction: column;
62
62
  min-width: 0;
63
63
  }
64
+
65
+ .right {
66
+ flex: 1;
67
+ background-color: white;
68
+
69
+ iframe {
70
+ width: 100%;
71
+ height: 100%;
72
+ border: none;
73
+ }
74
+ }
64
75
  }
@@ -0,0 +1,103 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { randomId } from '../../randomId';
3
+ import { ShareDBDoc, VZCodeContent } from '../../types';
4
+
5
+ // Set up the connection to ShareDB.
6
+ export const useShareDB = ({
7
+ connection,
8
+ }: {
9
+ connection: any;
10
+ }) => {
11
+ // The ShareDB document.
12
+ const [shareDBDoc, setShareDBDoc] =
13
+ useState<ShareDBDoc<VZCodeContent> | null>(null);
14
+
15
+ // Local ShareDB presence, for broadcasting our cursor position
16
+ // so other clients can see it.
17
+ // See https://share.github.io/sharedb/api/local-presence
18
+ const [localPresence, setLocalPresence] = useState(null);
19
+
20
+ // The document-level presence object, which emits
21
+ // changes in remote presence.
22
+ const [docPresence, setDocPresence] = useState(null);
23
+
24
+ // The `doc.data` part of the ShareDB document,
25
+ // updated on each change to decouple rendering from ShareDB.
26
+ // Starts out as `null` until the document is loaded.
27
+ const [content, setContent] =
28
+ useState<VZCodeContent | null>(null);
29
+
30
+ useEffect(() => {
31
+ // Since there is only ever a single document,
32
+ // these things are pretty arbitrary.
33
+ // * `collection` - the ShareDB collection to use
34
+ // * `id` - the id of the ShareDB document to use
35
+ const collection = 'documents';
36
+ const id = '1';
37
+
38
+ // Initialize the ShareDB document.
39
+ const shareDBDoc = connection.get(collection, id);
40
+
41
+ // Subscribe to the document to get updates.
42
+ // This callback gets called once only.
43
+ shareDBDoc.subscribe(() => {
44
+ // Expose ShareDB doc to downstream logic.
45
+ setShareDBDoc(shareDBDoc);
46
+
47
+ // Set initial data.
48
+ setContent(shareDBDoc.data);
49
+
50
+ // Listen for all changes and update `data`.
51
+ // This decouples rendering logic from ShareDB.
52
+ // This callback gets called on each change.
53
+ shareDBDoc.on('op', () => {
54
+ // TODO consider excluding file contents from this,
55
+ // because currently we are re-rendering the entire
56
+ // document on each file change (each keystroke while editing).
57
+ // This is not required, because no part of the app outside
58
+ // of the CodeMirror integration uses those file contents.
59
+ // The Sidebar only needs to know file names, not contents.
60
+ setContent(shareDBDoc.data);
61
+ });
62
+
63
+ // Set up presence.
64
+ // See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js#L53
65
+ const docPresence = connection.getDocPresence(
66
+ collection,
67
+ id,
68
+ );
69
+
70
+ // Subscribe to receive remote presence updates.
71
+ docPresence.subscribe(function (error) {
72
+ if (error) throw error;
73
+ });
74
+
75
+ // Set up our local presence for broadcasting this client's presence.
76
+ const generateTimestampedId = () => {
77
+ const timestamp = Date.now().toString(36);
78
+ const randomPart = randomId();
79
+ return `${timestamp}-${randomPart}`;
80
+ };
81
+ setLocalPresence(
82
+ docPresence.create(generateTimestampedId()),
83
+ );
84
+
85
+ // Store docPresence so child components can listen for changes.
86
+ setDocPresence(docPresence);
87
+ });
88
+
89
+ // TODO unsubscribe from presence
90
+ // TODO unsubscribe from doc
91
+ return () => {
92
+ // shareDBDoc.destroy();
93
+ // docPresence.destroy();
94
+ };
95
+ }, []);
96
+
97
+ return {
98
+ shareDBDoc,
99
+ content,
100
+ localPresence,
101
+ docPresence,
102
+ };
103
+ };
@@ -4,10 +4,13 @@ import {
4
4
  EditorState,
5
5
  } from '@codemirror/state';
6
6
  import { javascript } from '@codemirror/lang-javascript';
7
+ import { json } from '@codemirror/lang-json';
7
8
  import { markdown } from '@codemirror/lang-markdown';
8
9
  import { html } from '@codemirror/lang-html';
9
10
  import { css } from '@codemirror/lang-css';
10
11
  import { json1Sync } from 'codemirror-ot';
12
+ import { autocompletion } from '@codemirror/autocomplete';
13
+ import { Diagnostic, linter } from '@codemirror/lint';
11
14
  import { json1Presence, textUnicode } from '../../ot';
12
15
  import {
13
16
  FileId,
@@ -28,18 +31,31 @@ import {
28
31
  EditorCacheValue,
29
32
  } from '../useEditorCache';
30
33
  import { ThemeLabel, themeOptionsByLabel } from '../themes';
34
+ import { AIAssist } from '../AIAssist';
35
+ import { typeScriptCompletions } from './typeScriptCompletions';
36
+ import { typeScriptLinter } from './typeScriptLinter';
37
+ import { TabState } from '../vzReducer';
38
+
39
+ // Feature flag to enable TypeScript completions & TypeScript Linter.
40
+ const enableTypeScriptCompletions = true;
41
+ const enableTypeScriptLinter = true;
42
+
43
+ // Enables TypeScript +JSX support in CodeMirror.
44
+ const tsx = () =>
45
+ javascript({ jsx: true, typescript: true });
31
46
 
32
47
  // Language extensions for CodeMirror.
33
48
  // Keys are file extensions.
34
49
  // Values are CodeMirror extensions.
35
50
  // TODO consider moving this to a separate file.
36
51
  const languageExtensions = {
37
- js: javascript,
38
- json: javascript,
39
- jsx: javascript,
40
- ts: javascript,
41
- html: html,
42
- css: css,
52
+ json,
53
+ tsx,
54
+ js: tsx,
55
+ jsx: tsx,
56
+ ts: tsx,
57
+ html,
58
+ css,
43
59
  md: markdown,
44
60
  };
45
61
 
@@ -66,6 +82,10 @@ export const getOrCreateEditor = ({
66
82
  onInteract,
67
83
  editorCache,
68
84
  usernameRef,
85
+ aiAssistEndpoint,
86
+ aiAssistOptions,
87
+ typeScriptWorker,
88
+ tabList,
69
89
  }: {
70
90
  fileId: FileId;
71
91
 
@@ -85,6 +105,12 @@ export const getOrCreateEditor = ({
85
105
  onInteract?: () => void;
86
106
  editorCache: EditorCache;
87
107
  usernameRef: React.MutableRefObject<Username>;
108
+ aiAssistEndpoint?: string;
109
+ aiAssistOptions?: {
110
+ [key: string]: any;
111
+ };
112
+ typeScriptWorker: Worker;
113
+ tabList: Array<TabState>;
88
114
  }): EditorCacheValue => {
89
115
  // Cache hit
90
116
  if (editorCache.has(fileId)) {
@@ -157,7 +183,7 @@ export const getOrCreateEditor = ({
157
183
  // using a Compartment.
158
184
  const languageCompartment = new Compartment();
159
185
  // See https://github.com/vizhub-core/vzcode/issues/55
160
- const getLanguageExtension = (fileExtension) => {
186
+ const getLanguageExtension = (fileExtension: string) => {
161
187
  const languageFunc = languageExtensions[fileExtension];
162
188
  return languageFunc ? languageFunc() : undefined;
163
189
  };
@@ -200,6 +226,43 @@ export const getOrCreateEditor = ({
200
226
 
201
227
  extensions.push(rotationIndicator);
202
228
 
229
+ extensions.push(
230
+ AIAssist({
231
+ shareDBDoc,
232
+ fileId,
233
+ tabList,
234
+ aiAssistEndpoint,
235
+ aiAssistOptions,
236
+ }),
237
+ );
238
+
239
+ // Add the extension that provides TypeScript completions.
240
+ if (enableTypeScriptCompletions) {
241
+ extensions.push(
242
+ autocompletion({
243
+ override: [
244
+ typeScriptCompletions({
245
+ typeScriptWorker,
246
+ fileName: name,
247
+ }),
248
+ ],
249
+ }),
250
+ );
251
+ }
252
+ if (enableTypeScriptLinter) {
253
+ extensions.push(
254
+ linter(
255
+ typeScriptLinter({
256
+ typeScriptWorker,
257
+ fileName: name,
258
+ shareDBDoc,
259
+ fileId,
260
+ }) as unknown as () => Diagnostic[],
261
+ //Needs the unknown because we are returning a Promise<Diagnostic>
262
+ ),
263
+ );
264
+ }
265
+
203
266
  const editor = new EditorView({
204
267
  state: EditorState.create({
205
268
  doc: text,
@@ -17,6 +17,7 @@ import {
17
17
  } from '../useEditorCache';
18
18
  import { getOrCreateEditor } from './getOrCreateEditor';
19
19
  import './style.scss';
20
+ import { TabState } from '../vzReducer';
20
21
 
21
22
  export const CodeEditor = ({
22
23
  activeFileId,
@@ -30,6 +31,10 @@ export const CodeEditor = ({
30
31
  editorWantsFocus,
31
32
  editorNoLongerWantsFocus,
32
33
  username,
34
+ aiAssistEndpoint,
35
+ aiAssistOptions,
36
+ typeScriptWorker,
37
+ tabList,
33
38
  }: {
34
39
  activeFileId: FileId;
35
40
  shareDBDoc: ShareDBDoc<VZCodeContent> | null;
@@ -51,6 +56,18 @@ export const CodeEditor = ({
51
56
  // Signals that the editor no longer wants focus.
52
57
  editorNoLongerWantsFocus: () => void;
53
58
  username: Username;
59
+
60
+ // The server endpoint for the AI Assist service.
61
+ aiAssistEndpoint?: string;
62
+
63
+ // Additional options to pass to the AI Assist service,
64
+ // an object with string values.
65
+ aiAssistOptions?: {
66
+ [key: string]: string;
67
+ };
68
+ typeScriptWorker: Worker;
69
+
70
+ tabList: Array<TabState>;
54
71
  }) => {
55
72
  const ref = useRef<HTMLDivElement>(null);
56
73
 
@@ -98,6 +115,10 @@ export const CodeEditor = ({
98
115
  onInteract,
99
116
  editorCache,
100
117
  usernameRef,
118
+ aiAssistEndpoint,
119
+ aiAssistOptions,
120
+ typeScriptWorker,
121
+ tabList,
101
122
  }),
102
123
  [
103
124
  activeFileId,
@@ -109,6 +130,9 @@ export const CodeEditor = ({
109
130
  onInteract,
110
131
  editorCache,
111
132
  usernameRef,
133
+ aiAssistEndpoint,
134
+ aiAssistOptions,
135
+ typeScriptWorker,
112
136
  ],
113
137
  );
114
138
 
@@ -5,6 +5,8 @@ import {
5
5
  Decoration,
6
6
  } from '@codemirror/view';
7
7
  import { Annotation, RangeSet } from '@codemirror/state';
8
+ import ColorHash from 'color-hash';
9
+ import { Username } from '../../types';
8
10
 
9
11
  // Deals with receiving the broadcasted presence cursor locations
10
12
  // from other clients and displaying them.
@@ -50,8 +52,9 @@ export const json1PresenceDisplay = ({
50
52
  const presence = presenceState[id];
51
53
  const { start, end } = presence;
52
54
  const from = start[start.length - 1];
53
- // TODO support selection ranges (first attempt introduced layout errors)
54
55
  const to = end[end.length - 1];
56
+ const userColor = new ColorHash().rgb(id);
57
+ const { username } = presence;
55
58
  if (from === to) {
56
59
  return {
57
60
  from,
@@ -59,7 +62,11 @@ export const json1PresenceDisplay = ({
59
62
  value: Decoration.widget({
60
63
  side: -1,
61
64
  block: false,
62
- widget: new PresenceWidget(id),
65
+ widget: new PresenceWidget(
66
+ id,
67
+ userColor,
68
+ username,
69
+ ),
63
70
  }),
64
71
  };
65
72
  } else {
@@ -70,8 +77,8 @@ export const json1PresenceDisplay = ({
70
77
  class: 'cm-json1-presence',
71
78
  attributes: {
72
79
  style: `
73
- background-color: rgba(255, 255, 0, 0.5);
74
- box-shadow: 1px 0 0 yellow;
80
+ background-color: rgba(${userColor}, 0.75);
81
+ mix-blend-mode: luminosity;
75
82
  `,
76
83
  },
77
84
  }),
@@ -122,9 +129,17 @@ const pathMatches = (path, presence) => {
122
129
  // Displays a single remote presence cursor.
123
130
  class PresenceWidget extends WidgetType {
124
131
  id: string;
125
- constructor(id: string) {
132
+ color: string;
133
+ username: Username;
134
+ constructor(
135
+ id: string,
136
+ color: string,
137
+ username: Username,
138
+ ) {
126
139
  super();
127
140
  this.id = id;
141
+ this.color = color;
142
+ this.username = username;
128
143
  }
129
144
 
130
145
  eq(other: PresenceWidget) {
@@ -135,14 +150,30 @@ class PresenceWidget extends WidgetType {
135
150
  const span = document.createElement('span');
136
151
  span.setAttribute('aria-hidden', 'true');
137
152
  span.className = 'cm-json1-presence';
138
-
139
153
  // This child is what actually displays the presence.
140
154
  // Nested so that the layout is not impacted.
141
155
  //
142
156
  // The initial attempt using the top level span to render
143
157
  // the cursor caused a wonky layout with adjacent characters shifting
144
158
  // left and right by 1 pixel or so.
145
- span.appendChild(document.createElement('div'));
159
+ const div = document.createElement('div');
160
+ span.appendChild(div);
161
+ div.style.borderLeft = `1px solid rgba(${this.color})`;
162
+
163
+ // background color behind username
164
+ const userDiv = document.createElement('div');
165
+ userDiv.className = 'remote-cursor-username';
166
+ userDiv.style.top = `-20px`;
167
+ userDiv.style.height = `20px`;
168
+ userDiv.style.width = `${this.username.length * 11}px`;
169
+ userDiv.style.backgroundColor = `rgba(${this.color})`;
170
+ userDiv.style.color = `black`;
171
+ userDiv.style.textAlign = `center`;
172
+ userDiv.appendChild(
173
+ document.createTextNode(this.username),
174
+ );
175
+ span.appendChild(userDiv);
176
+
146
177
  return span;
147
178
  }
148
179
 
@@ -150,7 +181,6 @@ class PresenceWidget extends WidgetType {
150
181
  return false;
151
182
  }
152
183
  }
153
-
154
184
  const presenceTheme = EditorView.baseTheme({
155
185
  '.cm-json1-presence': {
156
186
  position: 'relative',
@@ -161,6 +191,5 @@ const presenceTheme = EditorView.baseTheme({
161
191
  bottom: '0',
162
192
  left: '0',
163
193
  right: '0',
164
- borderLeft: '1px solid yellow',
165
194
  },
166
195
  });