vzcode 2.3.0 → 2.4.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 (196) hide show
  1. package/package.json +3 -2
  2. package/src/cli.ts +208 -0
  3. package/src/client/AIAssist/AIAssistCodeMirrorKeyMap.ts +42 -0
  4. package/src/client/AIAssist/AIAssistWidget/index.tsx +135 -0
  5. package/src/client/AIAssist/AIAssistWidget/style.scss +47 -0
  6. package/src/client/AIAssist/Spinner.jsx +76 -0
  7. package/src/client/AIAssist/formatFile.ts +27 -0
  8. package/src/client/AIAssist/generateFilesContext.ts +25 -0
  9. package/src/client/AIAssist/index.ts +2 -0
  10. package/src/client/AIAssist/startAIAssist.ts +95 -0
  11. package/src/client/App/index.tsx +152 -0
  12. package/src/client/App/style.scss +82 -0
  13. package/src/client/App/usePending.ts +51 -0
  14. package/src/client/App/useShareDB.ts +161 -0
  15. package/src/client/CodeEditor/Copilot/index.ts +74 -0
  16. package/src/client/CodeEditor/InteractiveWidgets/boolToggler.ts +12 -0
  17. package/src/client/CodeEditor/InteractiveWidgets/colorPicker.ts +68 -0
  18. package/src/client/CodeEditor/InteractiveWidgets/colorsInTextPlugin.ts +174 -0
  19. package/src/client/CodeEditor/InteractiveWidgets/highlightWidgets.ts +79 -0
  20. package/src/client/CodeEditor/InteractiveWidgets/index.ts +44 -0
  21. package/src/client/CodeEditor/InteractiveWidgets/markdownCheckboxToggler.ts +12 -0
  22. package/src/client/CodeEditor/InteractiveWidgets/numberDragger.ts +20 -0
  23. package/src/client/CodeEditor/InteractiveWidgets/rotateWidget.ts +143 -0
  24. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.test.ts +48 -0
  25. package/src/client/CodeEditor/InteractiveWidgets/urlClicker.ts +12 -0
  26. package/src/client/CodeEditor/InteractiveWidgets/vec2Slider.ts +25 -0
  27. package/src/client/CodeEditor/basicSetup.ts +73 -0
  28. package/src/client/CodeEditor/getOrCreateEditor.tsx +615 -0
  29. package/src/client/CodeEditor/index.tsx +202 -0
  30. package/src/client/CodeEditor/json1PresenceBroadcast.ts +41 -0
  31. package/src/client/CodeEditor/json1PresenceDisplay.ts +350 -0
  32. package/src/client/CodeEditor/rainbowBrackets.ts +122 -0
  33. package/src/client/CodeEditor/style.scss +156 -0
  34. package/src/client/CodeEditor/typescriptExtension/worker.ts +32 -0
  35. package/src/client/CodeErrorOverlay/index.tsx +77 -0
  36. package/src/client/CodeErrorOverlay/style.scss +33 -0
  37. package/src/client/Fonts/fonts.ts +8 -0
  38. package/src/client/Icons/AdjustmentSVG.tsx +19 -0
  39. package/src/client/Icons/BugSVG.tsx +20 -0
  40. package/src/client/Icons/CloseSVG.tsx +19 -0
  41. package/src/client/Icons/CssSVG.tsx +16 -0
  42. package/src/client/Icons/DirectoryArrowSVG.tsx +19 -0
  43. package/src/client/Icons/EditSVG.tsx +19 -0
  44. package/src/client/Icons/FileSVG.tsx +19 -0
  45. package/src/client/Icons/FolderSVG.tsx +20 -0
  46. package/src/client/Icons/GearSVG.tsx +16 -0
  47. package/src/client/Icons/HtmlSVG.tsx +16 -0
  48. package/src/client/Icons/JavaScriptSVG.tsx +16 -0
  49. package/src/client/Icons/JsonSVG.tsx +16 -0
  50. package/src/client/Icons/MarkdownSVG.tsx +16 -0
  51. package/src/client/Icons/MicOffSVG.tsx +35 -0
  52. package/src/client/Icons/MicSVG.tsx +25 -0
  53. package/src/client/Icons/NewSVG.tsx +19 -0
  54. package/src/client/Icons/PinSVG.tsx +14 -0
  55. package/src/client/Icons/PlaySVG.tsx +16 -0
  56. package/src/client/Icons/QuestionMarkSVG.tsx +18 -0
  57. package/src/client/Icons/ReactSVG.tsx +16 -0
  58. package/src/client/Icons/SearchSVG.tsx +20 -0
  59. package/src/client/Icons/SparklesSVG.tsx +23 -0
  60. package/src/client/Icons/SplitEditorSVG.tsx +34 -0
  61. package/src/client/Icons/StopSVG.tsx +15 -0
  62. package/src/client/Icons/SvelteSVG.tsx +16 -0
  63. package/src/client/Icons/TrashSVG.tsx +19 -0
  64. package/src/client/Icons/TypeScriptSVG.tsx +16 -0
  65. package/src/client/Icons/index.tsx +26 -0
  66. package/src/client/ImageViewer/index.tsx +91 -0
  67. package/src/client/ImageViewer/style.scss +41 -0
  68. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  69. package/src/client/PresenceNotifications/index.tsx +158 -0
  70. package/src/client/PresenceNotifications/style.scss +17 -0
  71. package/src/client/RunCodeWidget/index.tsx +117 -0
  72. package/src/client/RunCodeWidget/style.scss +24 -0
  73. package/src/client/SplitPaneResizeContext.tsx +159 -0
  74. package/src/client/TabList/Tab.tsx +73 -0
  75. package/src/client/TabList/index.tsx +35 -0
  76. package/src/client/TabList/style.scss +51 -0
  77. package/src/client/VZCodeContext/index.tsx +29 -0
  78. package/src/client/VZCodeContext/types.ts +248 -0
  79. package/src/client/VZCodeContext/useVZCodeState.ts +602 -0
  80. package/src/client/VZKeyboardShortcutsDoc.tsx +148 -0
  81. package/src/client/VZLeft.tsx +28 -0
  82. package/src/client/VZMiddle.tsx +172 -0
  83. package/src/client/VZResizer/index.tsx +128 -0
  84. package/src/client/VZResizer/styles.scss +33 -0
  85. package/src/client/VZRight.tsx +113 -0
  86. package/src/client/VZSettings.tsx +275 -0
  87. package/src/client/VZSidebar/AIChat/ChatInput.tsx +243 -0
  88. package/src/client/VZSidebar/AIChat/DiffView.scss +136 -0
  89. package/src/client/VZSidebar/AIChat/DiffView.tsx +176 -0
  90. package/src/client/VZSidebar/AIChat/Message.tsx +79 -0
  91. package/src/client/VZSidebar/AIChat/MessageList.tsx +190 -0
  92. package/src/client/VZSidebar/AIChat/StreamingMessage.tsx +35 -0
  93. package/src/client/VZSidebar/AIChat/ThinkingScratchpad.tsx +156 -0
  94. package/src/client/VZSidebar/AIChat/TypingIndicator.tsx +19 -0
  95. package/src/client/VZSidebar/AIChat/index.tsx +357 -0
  96. package/src/client/VZSidebar/AIChat/styles.scss +646 -0
  97. package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
  98. package/src/client/VZSidebar/CreateDirModal.tsx +111 -0
  99. package/src/client/VZSidebar/CreateFileModal.tsx +111 -0
  100. package/src/client/VZSidebar/DeleteConfirmationModal.tsx +76 -0
  101. package/src/client/VZSidebar/DirectoryListing.tsx +89 -0
  102. package/src/client/VZSidebar/EmptyState.tsx +11 -0
  103. package/src/client/VZSidebar/FileListing.tsx +91 -0
  104. package/src/client/VZSidebar/FileTypeIcon.tsx +49 -0
  105. package/src/client/VZSidebar/Item.tsx +237 -0
  106. package/src/client/VZSidebar/Listing.tsx +47 -0
  107. package/src/client/VZSidebar/Search.tsx +527 -0
  108. package/src/client/VZSidebar/VisualEditor.tsx +282 -0
  109. package/src/client/VZSidebar/VoiceChatModal.tsx +121 -0
  110. package/src/client/VZSidebar/aiCopyPaste.ts +199 -0
  111. package/src/client/VZSidebar/index.tsx +728 -0
  112. package/src/client/VZSidebar/styles.scss +612 -0
  113. package/src/client/VZSidebar/useDragAndDrop.tsx +408 -0
  114. package/src/client/bootstrap.ts +26 -0
  115. package/src/client/buildWorker.ts +3 -0
  116. package/src/client/featureFlags.ts +8 -0
  117. package/src/client/generateRequestId.ts +6 -0
  118. package/src/client/getFileTree.ts +53 -0
  119. package/src/client/index.ts +19 -0
  120. package/src/client/main.jsx +10 -0
  121. package/src/client/presenceColor.ts +8 -0
  122. package/src/client/shouldTriggerRun.ts +31 -0
  123. package/src/client/sortFileTree.ts +21 -0
  124. package/src/client/tabsSearchParameters.test.ts +79 -0
  125. package/src/client/tabsSearchParameters.ts +137 -0
  126. package/src/client/themes/index.ts +7 -0
  127. package/src/client/themes/themes.ts +77 -0
  128. package/src/client/themes/uiwImports.ts +32 -0
  129. package/src/client/themes/useDynamicTheme.ts +29 -0
  130. package/src/client/themes/vizhubTheme/colors.ts +80 -0
  131. package/src/client/themes/vizhubTheme/index.ts +193 -0
  132. package/src/client/useActions.ts +288 -0
  133. package/src/client/useESLint/index.ts +72 -0
  134. package/src/client/useESLint/worker.ts +113 -0
  135. package/src/client/useEditorCache.ts +35 -0
  136. package/src/client/useFileCRUD.ts +228 -0
  137. package/src/client/useKeyboardShortcuts.ts +451 -0
  138. package/src/client/useOpenDirectories.ts +99 -0
  139. package/src/client/usePresenceAutoFollow.ts +85 -0
  140. package/src/client/usePrettier/index.ts +178 -0
  141. package/src/client/usePrettier/worker.ts +109 -0
  142. package/src/client/useRunCode.tsx +15 -0
  143. package/src/client/useRuntimeError.ts +32 -0
  144. package/src/client/useSubmitOperation.ts +15 -0
  145. package/src/client/useURLSync.ts +137 -0
  146. package/src/client/usernameLocalStorage.ts +43 -0
  147. package/src/client/utils/fileExtension.ts +10 -0
  148. package/src/client/utils/isImageFile.ts +12 -0
  149. package/src/client/vzReducer/aiChatReducer.ts +44 -0
  150. package/src/client/vzReducer/closeTabReducer.test.ts +205 -0
  151. package/src/client/vzReducer/closeTabsReducer.ts +96 -0
  152. package/src/client/vzReducer/createInitialState.ts +38 -0
  153. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  154. package/src/client/vzReducer/findPane.ts +19 -0
  155. package/src/client/vzReducer/index.ts +250 -0
  156. package/src/client/vzReducer/openTabReducer.test.ts +127 -0
  157. package/src/client/vzReducer/openTabReducer.ts +74 -0
  158. package/src/client/vzReducer/searchReducer.test.ts +292 -0
  159. package/src/client/vzReducer/searchReducer.ts +181 -0
  160. package/src/client/vzReducer/setActiveFileIdReducer.ts +28 -0
  161. package/src/client/vzReducer/setActiveFileLeftRightReducer.ts +76 -0
  162. package/src/client/vzReducer/setIsDocOpenReducer.ts +9 -0
  163. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  164. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  165. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  166. package/src/client/vzReducer/splitCurrentPaneReducer.ts +66 -0
  167. package/src/client/vzReducer/toggleAutoFollowReducer.ts +12 -0
  168. package/src/client/vzReducer/updatePane.ts +40 -0
  169. package/src/client/vzReducer/updatePresenceIndicatorReducer.ts +30 -0
  170. package/src/client/vzReducer/visualEditorReducer.ts +9 -0
  171. package/src/ot.ts +24 -0
  172. package/src/randomId.ts +15 -0
  173. package/src/reactRouterExports.ts +15 -0
  174. package/src/runCode.ts +27 -0
  175. package/src/server/aiChatHandler/aiEditing.ts +151 -0
  176. package/src/server/aiChatHandler/chatOperations.ts +476 -0
  177. package/src/server/aiChatHandler/errorHandling.ts +66 -0
  178. package/src/server/aiChatHandler/index.ts +199 -0
  179. package/src/server/aiChatHandler/llmStreaming.ts +419 -0
  180. package/src/server/aiChatHandler/validation.ts +24 -0
  181. package/src/server/computeInitialDocument.ts +220 -0
  182. package/src/server/config.ts +24 -0
  183. package/src/server/featureFlags.ts +8 -0
  184. package/src/server/generateAIResponse.ts +188 -0
  185. package/src/server/handleAIAssist.ts +53 -0
  186. package/src/server/handleAIChatMessage.ts +2 -0
  187. package/src/server/handleAICopilot.ts +97 -0
  188. package/src/server/index.ts +379 -0
  189. package/src/server/isDirectory.ts +1 -0
  190. package/src/server/livekit.ts +24 -0
  191. package/src/server/prettier.ts +115 -0
  192. package/src/server/setupEnv.ts +8 -0
  193. package/src/submitOperation.ts +25 -0
  194. package/src/types.ts +205 -0
  195. package/src/utils/fileDiff.ts +117 -0
  196. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,202 @@
1
+ import {
2
+ useRef,
3
+ useLayoutEffect,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ } from 'react';
9
+ import { Username } from '../../types';
10
+ import { EditorCacheValue } from '../useEditorCache';
11
+ import { getOrCreateEditor } from './getOrCreateEditor';
12
+ import { VZCodeContext } from '../VZCodeContext';
13
+ import { InteractRule } from '@replit/codemirror-interact';
14
+ import { EditorView } from 'codemirror'; // Import EditorView
15
+ import { Diagnostic } from '@codemirror/lint'; // Import Diagnostic
16
+ import './style.scss';
17
+
18
+ // The path in the ShareDB document where the files live.
19
+ const filesPath = ['files'];
20
+
21
+ export const CodeEditor = ({
22
+ customInteractRules,
23
+ aiCopilotEndpoint,
24
+ esLintSource,
25
+ }: {
26
+ customInteractRules?: Array<InteractRule>;
27
+ aiCopilotEndpoint?: string;
28
+ esLintSource: (
29
+ view: EditorView,
30
+ ) => Promise<readonly Diagnostic[]>;
31
+ }) => {
32
+ const {
33
+ activePane,
34
+ shareDBDoc,
35
+ content,
36
+ submitOperation,
37
+ localPresence,
38
+ docPresence,
39
+ username,
40
+ editorCache,
41
+ editorWantsFocus,
42
+ editorNoLongerWantsFocus,
43
+ theme,
44
+ codeEditorRef,
45
+ enableAutoFollow,
46
+ setIsAIChatOpen,
47
+ setAIChatMessage,
48
+ handleSendMessage,
49
+ } = useContext(VZCodeContext);
50
+
51
+ // Set `doc.data.isInteracting` to `true` when the user is interacting
52
+ // via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
53
+ const interactTimeoutRef = useRef(null);
54
+
55
+ const onInteract = useCallback(() => {
56
+ // Set `isInteracting: true` if not already set.
57
+ if (!interactTimeoutRef.current) {
58
+ submitOperation((document) => ({
59
+ ...document,
60
+ isInteracting: true,
61
+ }));
62
+ } else {
63
+ clearTimeout(interactTimeoutRef.current);
64
+ }
65
+
66
+ // Set `isInteracting: undefined` after a delay,
67
+ // so that when the runId changes in the future,
68
+ // it will trigger a hard re-run, not a hot reload.
69
+ interactTimeoutRef.current = setTimeout(() => {
70
+ interactTimeoutRef.current = null;
71
+
72
+ // This logic deletes the `isInteracting` property from the document
73
+ submitOperation(
74
+ ({ isInteracting, ...newDocument }) => ({
75
+ ...newDocument,
76
+ }),
77
+ );
78
+ }, 800);
79
+ }, [submitOperation]);
80
+
81
+ // Track username on a ref, so that we can use it in the
82
+ // presence object's `username` property.
83
+ const usernameRef = useRef<Username>(username);
84
+ usernameRef.current = username;
85
+
86
+ // Track enableAutoFollow on a ref, so that we can use it in the
87
+ // CodeMirror extension.
88
+ const enableAutoFollowRef = useRef(enableAutoFollow);
89
+ useEffect(() => {
90
+ enableAutoFollowRef.current = enableAutoFollow;
91
+ }, [enableAutoFollow]);
92
+
93
+ // State to hold the editor cache value
94
+ const [editorCacheValue, setEditorCacheValue] =
95
+ useState<EditorCacheValue | null>(null);
96
+
97
+ // Get the editor corresponding to the active file.
98
+ // Looks in `editorCache` first, and if not found, creates a new editor.
99
+ useEffect(() => {
100
+ if (!shareDBDoc) return;
101
+ let isMounted = true;
102
+
103
+ const initEditor = async () => {
104
+ const editor = await getOrCreateEditor({
105
+ fileId: activePane.activeFileId,
106
+ shareDBDoc,
107
+ filesPath,
108
+ localPresence,
109
+ docPresence,
110
+ theme,
111
+ onInteract,
112
+ editorCache,
113
+ usernameRef,
114
+ customInteractRules,
115
+ enableAutoFollowRef,
116
+ aiCopilotEndpoint,
117
+ esLintSource,
118
+ setIsAIChatOpen,
119
+ setAIChatMessage,
120
+ handleSendMessage,
121
+ });
122
+
123
+ if (isMounted) {
124
+ setEditorCacheValue(editor);
125
+ }
126
+ };
127
+
128
+ initEditor();
129
+
130
+ return () => {
131
+ isMounted = false;
132
+ };
133
+ }, [
134
+ activePane,
135
+ shareDBDoc,
136
+ filesPath,
137
+ localPresence,
138
+ docPresence,
139
+ theme,
140
+ onInteract,
141
+ editorCache,
142
+ usernameRef,
143
+ aiCopilotEndpoint,
144
+ esLintSource,
145
+ ]);
146
+
147
+ // Every time the active file switches from one file to another,
148
+ // the editor corresponding to the old file is removed from the DOM,
149
+ // and the editor corresponding to the new file is added to the DOM.
150
+ useLayoutEffect(() => {
151
+ // Guard against cases where page is still loading.
152
+ if (!codeEditorRef.current) return;
153
+ if (!content) return;
154
+ if (!editorCacheValue) return; // Guard against null editorCacheValue
155
+ // if (openSplitPane) return;
156
+
157
+ // Add the editor and apply the prior scroll position to the DOM.
158
+ codeEditorRef.current.appendChild(
159
+ editorCacheValue.editor.dom,
160
+ );
161
+ editorCacheValue.editor.scrollDOM.scrollTo({
162
+ top: editorCacheValue.scrollPosition ?? 0,
163
+ });
164
+
165
+ return () => {
166
+ // Remove the old editor from the DOM and store the current scroll position.
167
+ // This happens every time `activeFileId` changes.
168
+ if (
169
+ editorCacheValue &&
170
+ codeEditorRef.current?.contains(
171
+ editorCacheValue.editor.dom,
172
+ )
173
+ ) {
174
+ editorCacheValue.scrollPosition =
175
+ editorCacheValue.editor.scrollDOM.scrollTop;
176
+ codeEditorRef.current.removeChild(
177
+ editorCacheValue.editor.dom,
178
+ );
179
+ }
180
+ };
181
+ }, [shareDBDoc, editorCacheValue]);
182
+
183
+ // Focus the editor
184
+ useEffect(() => {
185
+ if (editorWantsFocus && editorCacheValue) {
186
+ editorCacheValue.editor.focus();
187
+ editorNoLongerWantsFocus();
188
+ }
189
+ }, [
190
+ editorWantsFocus,
191
+ editorCacheValue,
192
+ editorNoLongerWantsFocus,
193
+ ]);
194
+
195
+ return (
196
+ <div
197
+ className="vz-code-editor"
198
+ ref={codeEditorRef}
199
+ tabIndex={-1}
200
+ />
201
+ );
202
+ };
@@ -0,0 +1,41 @@
1
+ import { ViewUpdate } from '@codemirror/view';
2
+ import { EditorView } from 'codemirror';
3
+
4
+ // Deals with broadcasting changes in cursor location and selection.
5
+ export const json1PresenceBroadcast = ({
6
+ path,
7
+ localPresence,
8
+ usernameRef,
9
+ }) =>
10
+ // See https://discuss.codemirror.net/t/codemirror-6-proper-way-to-listen-for-changes/2395/10
11
+ EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
12
+ // If this update modified the cursor / selection,
13
+ // we broadcast the selection update via ShareDB presence.
14
+
15
+ if (viewUpdate.selectionSet) {
16
+ // Isolate the single selection to use for presence.
17
+ // Unfortunately JSON1 with presence does not yet
18
+ // support multiple selections.
19
+ // See https://github.com/ottypes/json1/pull/25#issuecomment-1459616521
20
+ const selection =
21
+ viewUpdate.state.selection.ranges[0];
22
+ const { from, to } = selection;
23
+
24
+ // Translate this into the form expected by json1Presence.
25
+ const presence = {
26
+ start: [...path, from],
27
+ end: [...path, to],
28
+
29
+ // We use a ref here so that the presence always broadcasts
30
+ // the latest username, in the case that it has been changed
31
+ // in the Settings mid-session.
32
+ username: usernameRef.current,
33
+ };
34
+
35
+ // Broadcast presence to remote clients!
36
+ // See https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js#L71
37
+ localPresence.submit(presence, (error) => {
38
+ if (error) throw error;
39
+ });
40
+ }
41
+ });
@@ -0,0 +1,350 @@
1
+ import {
2
+ ViewPlugin,
3
+ EditorView,
4
+ WidgetType,
5
+ Decoration,
6
+ } from '@codemirror/view';
7
+ import { Annotation, RangeSet } from '@codemirror/state';
8
+ import { assignUserColor } from '../presenceColor';
9
+ import {
10
+ Presence,
11
+ PresenceId,
12
+ Username,
13
+ } from '../../types';
14
+
15
+ const DEBUG = false;
16
+
17
+ // Deals with receiving the broadcasted presence cursor locations
18
+ // from other clients and displaying them.
19
+ //
20
+ // Inspired by
21
+ // * https://github.com/yjs/y-codemirror.next/blob/main/src/y-remote-selections.js
22
+ // * https://codemirror.net/examples/decoration/
23
+ // * https://github.com/share/sharedb/blob/master/examples/rich-text-presence/client.js
24
+ // * https://share.github.io/sharedb/presence
25
+ export const json1PresenceDisplay = ({
26
+ path,
27
+ docPresence,
28
+ enableAutoFollowRef,
29
+ }: {
30
+ path: Array<string>;
31
+ docPresence: any;
32
+ enableAutoFollowRef: React.MutableRefObject<boolean>;
33
+ }) => [
34
+ ViewPlugin.fromClass(
35
+ class {
36
+ // The decorations to display.
37
+ // This is a RangeSet of Decoration objects.
38
+ // See https://codemirror.net/6/docs/ref/#view.Decoration
39
+ decorations: RangeSet<Decoration>;
40
+
41
+ //Added variable for cursor position
42
+ cursorPosition = {};
43
+
44
+ // Track previous cursor positions to detect which cursors actually moved
45
+ previousCursorPositions = {};
46
+
47
+ // Flag to prevent multiple pending updates
48
+ pendingUpdate = false;
49
+
50
+ // Flag to prevent multiple pending scroll updates
51
+ pendingScrollUpdate = false;
52
+
53
+ constructor(view: EditorView) {
54
+ // Initialize decorations to empty array so CodeMirror doesn't crash.
55
+ this.decorations = RangeSet.of([]);
56
+
57
+ // Mutable state local to this closure representing aggregated presence.
58
+ // * Keys are presence ids
59
+ // * Values are presence objects as defined by ot-json1-presence
60
+ const presenceState: Record<PresenceId, Presence> =
61
+ {};
62
+
63
+ // Add the scroll event listener
64
+ //This runs for the arrow key scrolling, it should result in the users scrolling to eachother's location.
65
+ // view.dom.addEventListener('scroll', () => {
66
+ // this.scrollToCursor(view);
67
+ // });
68
+ // Receive remote presence changes.
69
+ docPresence.on(
70
+ 'receive',
71
+ (id: PresenceId, presence: Presence) => {
72
+ if (DEBUG) {
73
+ console.log(
74
+ `Received presence for id ${id}`,
75
+ presence,
76
+ );
77
+ }
78
+
79
+ // If presence is null, the user has disconnected.
80
+ // We must remove them from our local state.
81
+ if (!presence) {
82
+ delete presenceState[id];
83
+ // Also remove their cursor position to prevent errors.
84
+ delete this.cursorPosition[id];
85
+ delete this.previousCursorPositions[id];
86
+ } else {
87
+ // Otherwise, the user is active. Check if the presence
88
+ // is for the current file.
89
+ const isPresenceInCurrentFile = pathMatches(
90
+ path,
91
+ presence,
92
+ );
93
+
94
+ // If it's the current file, add/update their state.
95
+ if (isPresenceInCurrentFile) {
96
+ presenceState[id] = presence;
97
+ } else {
98
+ // If it's for another file, remove them from this view's state.
99
+ delete presenceState[id];
100
+ delete this.cursorPosition[id];
101
+ delete this.previousCursorPositions[id];
102
+ }
103
+ }
104
+
105
+ const presenceDecorations = [];
106
+
107
+ for (const id of Object.keys(presenceState)) {
108
+ const presence: Presence = presenceState[id];
109
+ const { start, end } = presence;
110
+ const from = +start[start.length - 1];
111
+ const to = +end[end.length - 1];
112
+ const userColor = assignUserColor(
113
+ presence.username,
114
+ );
115
+ const { username } = presence;
116
+
117
+ // Check if this cursor actually moved by comparing with previous position
118
+ const previousPosition =
119
+ this.previousCursorPositions[id];
120
+ const cursorMoved = previousPosition !== from;
121
+
122
+ presenceDecorations.push({
123
+ from,
124
+ to: from,
125
+ value: Decoration.widget({
126
+ side: -1,
127
+ block: false,
128
+ widget: new PresenceWidget(
129
+ '' + Math.random(),
130
+ userColor,
131
+ username,
132
+ cursorMoved, // Pass whether this cursor moved
133
+ ),
134
+ }),
135
+ });
136
+
137
+ if (from !== to) {
138
+ presenceDecorations.push({
139
+ from,
140
+ to,
141
+ value: Decoration.mark({
142
+ class: 'cm-json1-presence',
143
+ attributes: {
144
+ style: `
145
+ background-color: rgba(${userColor}, 0.75);
146
+ mix-blend-mode: luminosity;
147
+ `,
148
+ },
149
+ }),
150
+ });
151
+ }
152
+ if (view.state.doc.length >= from) {
153
+ this.cursorPosition[id] = from;
154
+ // Update previous position for next comparison
155
+ this.previousCursorPositions[id] = from;
156
+ } else {
157
+ // The cursor position is invalid, so remove it.
158
+ delete this.cursorPosition[id];
159
+ delete this.previousCursorPositions[id];
160
+ }
161
+ }
162
+
163
+ this.decorations = Decoration.set(
164
+ presenceDecorations,
165
+ true,
166
+ );
167
+
168
+ // Safely dispatch decoration updates without causing race conditions
169
+ // Use requestAnimationFrame to ensure we're not in the middle of an update
170
+ if (!this.pendingUpdate) {
171
+ this.pendingUpdate = true;
172
+ requestAnimationFrame(() => {
173
+ this.pendingUpdate = false;
174
+ // Check if view is still valid and not currently updating
175
+ if (view.state && view.dom.isConnected) {
176
+ view.dispatch({
177
+ annotations: [
178
+ presenceAnnotation.of(true),
179
+ ],
180
+ });
181
+ }
182
+ });
183
+ }
184
+
185
+ if (enableAutoFollowRef.current) {
186
+ this.scrollToCursor(view);
187
+ }
188
+ },
189
+ );
190
+ }
191
+ // Method to scroll the view to keep the cursor in view
192
+ scrollToCursor(view) {
193
+ // Debounce scroll updates to prevent conflicts with typing
194
+ if (!this.pendingScrollUpdate) {
195
+ this.pendingScrollUpdate = true;
196
+ requestAnimationFrame(() => {
197
+ this.pendingScrollUpdate = false;
198
+ // Check if view is still valid
199
+ if (view.state && view.dom.isConnected) {
200
+ for (const id in this.cursorPosition) {
201
+ //getting the cursor position of the other cursor
202
+ const cursorPos = this.cursorPosition[id];
203
+ view.dispatch({
204
+ //if the other person's cursor has jumped off screen, we will follow it by scrolling there directly.
205
+ effects:
206
+ EditorView.scrollIntoView(cursorPos),
207
+ });
208
+ // Only scroll to the first cursor to avoid multiple dispatches
209
+ break;
210
+ }
211
+ }
212
+ });
213
+ }
214
+ }
215
+ },
216
+ {
217
+ decorations: (v) => v.decorations,
218
+ },
219
+ ),
220
+ presenceTheme,
221
+ ];
222
+
223
+ const presenceAnnotation = Annotation.define();
224
+
225
+ // Checks that the path of this file
226
+ // matches the path of the presence.
227
+ // * If true is returned, the presence is in this file.
228
+ // * If false is returned, the presence is in another file.
229
+ // Assumption: start and end path are the same except the cursor position.
230
+ const pathMatches = (path, presence) => {
231
+ for (let i = 0; i < path.length; i++) {
232
+ if (path[i] !== presence.start[i]) {
233
+ return false;
234
+ }
235
+ }
236
+ return true;
237
+ };
238
+
239
+ // Displays a single remote presence cursor.
240
+ class PresenceWidget extends WidgetType {
241
+ id: string;
242
+ color: string;
243
+ username: Username;
244
+ timeout: number;
245
+ cursorMoved: boolean;
246
+ constructor(
247
+ id: string,
248
+ color: string,
249
+ username: Username,
250
+ cursorMoved: boolean = true, // Default to true for backward compatibility
251
+ ) {
252
+ super();
253
+ this.id = id;
254
+ this.color = color;
255
+ this.username = username;
256
+ this.cursorMoved = cursorMoved;
257
+ }
258
+
259
+ eq(other: PresenceWidget) {
260
+ return (
261
+ other.id === this.id &&
262
+ other.cursorMoved === this.cursorMoved
263
+ );
264
+ // return false;
265
+ }
266
+
267
+ toDOM() {
268
+ // console.log('inside toDOM');
269
+ const span = document.createElement('span');
270
+ span.setAttribute('aria-hidden', 'true');
271
+ span.className = 'cm-json1-presence';
272
+ // This child is what actually displays the presence.
273
+ // Nested so that the layout is not impacted.
274
+ //
275
+ // The initial attempt using the top level span to render
276
+ // the cursor caused a wonky layout with adjacent characters shifting
277
+ // left and right by 1 pixel or so.
278
+ const div = document.createElement('div');
279
+ span.appendChild(div);
280
+ div.style.borderLeft = `1px solid rgba(${this.color})`;
281
+
282
+ // background color behind username
283
+ const userDiv = document.createElement('div');
284
+ userDiv.className = 'remote-cursor-username';
285
+ userDiv.style.top = `-20px`;
286
+ userDiv.style.height = `20px`;
287
+ userDiv.style.width = `${this.username.length * 12}px`;
288
+ userDiv.style.backgroundColor = `rgba(${this.color})`;
289
+ userDiv.style.color = `black`;
290
+ // userDiv.style.textAlign = `center`;
291
+ userDiv.appendChild(
292
+ document.createTextNode(this.username),
293
+ );
294
+ span.appendChild(userDiv);
295
+
296
+ // Only reset opacity and start timeout for cursors that actually moved
297
+ if (this.cursorMoved) {
298
+ // Start with full opacity when cursor moves
299
+ userDiv.style.opacity = '1';
300
+
301
+ // Clear any existing timeout to prevent interference
302
+ if (this.timeout) {
303
+ window.clearTimeout(this.timeout);
304
+ }
305
+
306
+ // after 2 seconds of inactivity, username is made less visible
307
+ this.timeout = window.setTimeout(() => {
308
+ // userDiv.style.backgroundColor = `rgba(${this.color}, 0.2)`;
309
+ // userDiv.style.color = 'rgba(0,0,0,0.2)';
310
+ userDiv.style.opacity = '0.3';
311
+ }, 2000);
312
+ } else {
313
+ // For cursors that didn't move, keep their reduced opacity
314
+ userDiv.style.opacity = '0.3';
315
+ }
316
+
317
+ return span;
318
+ }
319
+
320
+ // TODO try to use this instead of toDOM
321
+ // updateDOM(dom: HTMLElement, view: EditorView) {
322
+ // console.log('inside updateDOM');
323
+ // dom.style.opacity = '1';
324
+ // window.clearTimeout(this.timeout);
325
+
326
+ // // after 2 seconds of inactivity, username is made less visible
327
+ // this.timeout = window.setTimeout(() => {
328
+ // // userDiv.style.backgroundColor = `rgba(${this.color}, 0.2)`;
329
+ // // userDiv.style.color = 'rgba(0,0,0,0.2)';
330
+ // dom.style.opacity = '0.3';
331
+ // }, 2000);
332
+
333
+ // return false;
334
+ // }
335
+ ignoreEvent() {
336
+ return false;
337
+ }
338
+ }
339
+ const presenceTheme = EditorView.baseTheme({
340
+ '.cm-json1-presence': {
341
+ position: 'relative',
342
+ },
343
+ '.cm-json1-presence > div': {
344
+ position: 'absolute',
345
+ top: '0',
346
+ bottom: '0',
347
+ left: '0',
348
+ right: '0',
349
+ },
350
+ });