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,43 @@
1
+ // This module deals with storing and accessing the
2
+ // username in local storage.
3
+ import { useEffect, useRef } from 'react';
4
+ import { Username } from '../types';
5
+
6
+ const localStoragePropertyName = 'vzCodeUsername';
7
+
8
+ const initialUsernameDefault = 'Anonymous';
9
+
10
+ // Gets the username from local storage.
11
+ export const useInitialUsername = (): Username => {
12
+ let initialUsername: Username = initialUsernameDefault;
13
+
14
+ // If we're in the browser,
15
+ if (typeof window !== 'undefined') {
16
+ //check localStorage for a previously stored width.
17
+ const initialUsernameFromLocalStorage: string | null =
18
+ window.localStorage.getItem(localStoragePropertyName);
19
+
20
+ // If there is a previously stored width,
21
+ if (initialUsernameFromLocalStorage !== null) {
22
+ // use it as the initial width.
23
+ initialUsername = initialUsernameFromLocalStorage;
24
+ }
25
+ } else {
26
+ // If we're not in the browser, use the default initial width.
27
+ }
28
+ return initialUsername;
29
+ };
30
+
31
+ // Stores the username to local storage.
32
+ export const usePersistUsername = (username: Username) => {
33
+ const previousUsername = useRef<Username>(username);
34
+ useEffect(() => {
35
+ if (previousUsername.current !== username) {
36
+ previousUsername.current = username;
37
+ window.localStorage.setItem(
38
+ 'vzCodeUsername',
39
+ username,
40
+ );
41
+ }
42
+ }, [username]);
43
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Utility function to extract file extension from a file name.
3
+ * @param fileName - The name of the file (e.g., "index.html", "script.js")
4
+ * @returns The file extension without the dot (e.g., "html", "js") or undefined if no extension
5
+ */
6
+ export const getFileExtension = (
7
+ fileName: string,
8
+ ): string | undefined => {
9
+ return fileName.split('.').pop();
10
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Utility function to check if a file is an image based on its name.
3
+ * @param fileName - The name of the file (e.g., "image.png", "photo.jpg")
4
+ * @returns true if the file is an image, false otherwise
5
+ */
6
+ export const isImageFile = (fileName: string): boolean => {
7
+ return (
8
+ fileName.match(
9
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
10
+ ) !== null
11
+ );
12
+ };
@@ -0,0 +1,44 @@
1
+ import { VZState, VZAction } from '.';
2
+
3
+ export const setIsAIChatOpenReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState => {
7
+ if (action.type === 'set_is_ai_chat_open') {
8
+ return {
9
+ ...state,
10
+ isAIChatOpen: action.value,
11
+ // When opening AI chat, close search
12
+ isSearchOpen: action.value
13
+ ? false
14
+ : state.isSearchOpen,
15
+ };
16
+ }
17
+ return state;
18
+ };
19
+
20
+ export const toggleAIChatFocusedReducer = (
21
+ state: VZState,
22
+ action: VZAction,
23
+ ): VZState => {
24
+ if (action.type === 'toggle_ai_chat_focused') {
25
+ return {
26
+ ...state,
27
+ aiChatFocused: !state.aiChatFocused,
28
+ };
29
+ }
30
+ return state;
31
+ };
32
+
33
+ export const setAIChatModeReducer = (
34
+ state: VZState,
35
+ action: VZAction,
36
+ ): VZState => {
37
+ if (action.type === 'set_ai_chat_mode') {
38
+ return {
39
+ ...state,
40
+ aiChatMode: action.mode,
41
+ };
42
+ }
43
+ return state;
44
+ };
@@ -0,0 +1,205 @@
1
+ import { assert, describe, expect, it } from 'vitest';
2
+ import { closeTabsReducer } from './closeTabsReducer';
3
+ import { VZAction, VZState, createInitialState } from '.';
4
+ import { defaultTheme } from '../themes';
5
+
6
+ const initialState: VZState = createInitialState({
7
+ defaultTheme,
8
+ });
9
+
10
+ describe('closeTabsReducer', () => {
11
+ it('Closes an existing tab', () => {
12
+ const action: VZAction = {
13
+ type: 'close_tabs',
14
+ fileIdsToClose: ['file1'],
15
+ };
16
+
17
+ const stateWithTabs = {
18
+ ...initialState,
19
+ pane: {
20
+ ...initialState.pane,
21
+ tabList: [{ fileId: 'file1', isTransient: false }],
22
+ activeFileId: 'file1',
23
+ },
24
+ };
25
+
26
+ const newState = closeTabsReducer(
27
+ stateWithTabs,
28
+ action,
29
+ );
30
+
31
+ assert(newState.pane.type === 'leafPane');
32
+ expect(newState.pane.tabList.length).toBe(0);
33
+ expect(newState.pane.activeFileId).toBeNull();
34
+ });
35
+
36
+ it('Keeps other tabs open', () => {
37
+ const action: VZAction = {
38
+ type: 'close_tabs',
39
+ fileIdsToClose: ['file2'],
40
+ };
41
+ const stateWithTabs = {
42
+ ...initialState,
43
+ pane: {
44
+ ...initialState.pane,
45
+ tabList: [
46
+ { fileId: 'file1', isTransient: false },
47
+ { fileId: 'file2', isTransient: false },
48
+ ],
49
+ activeFileId: 'file1',
50
+ },
51
+ };
52
+
53
+ const newState = closeTabsReducer(
54
+ stateWithTabs,
55
+ action,
56
+ );
57
+
58
+ assert(newState.pane.type === 'leafPane');
59
+ expect(newState.pane.tabList.length).toBe(1);
60
+ expect(newState.pane.tabList[0].fileId).toBe('file1');
61
+ expect(newState.pane.activeFileId).toBe('file1');
62
+ });
63
+
64
+ it('Activates previous tab after closing an active tab', () => {
65
+ const action: VZAction = {
66
+ type: 'close_tabs',
67
+ fileIdsToClose: ['file2'],
68
+ };
69
+ const stateWithTabs: VZState = {
70
+ ...initialState,
71
+ pane: {
72
+ ...initialState.pane,
73
+ type: 'leafPane',
74
+ tabList: [
75
+ { fileId: 'file1', isTransient: false },
76
+ { fileId: 'file2', isTransient: false },
77
+ { fileId: 'file3', isTransient: false },
78
+ ],
79
+ activeFileId: 'file2',
80
+ },
81
+ };
82
+
83
+ const newState = closeTabsReducer(
84
+ stateWithTabs,
85
+ action,
86
+ );
87
+
88
+ assert(newState.pane.type === 'leafPane');
89
+ expect(newState.pane.tabList.length).toBe(2);
90
+ expect(newState.pane.activeFileId).toBe('file1'); // should switch to the previous tab
91
+ });
92
+
93
+ it('Does not modify the state if closing non-existing tabs', () => {
94
+ const action: VZAction = {
95
+ type: 'close_tabs',
96
+ fileIdsToClose: ['file4'],
97
+ };
98
+ const stateWithTabs = {
99
+ ...initialState,
100
+ pane: {
101
+ ...initialState.pane,
102
+ tabList: [
103
+ { fileId: 'file1', isTransient: false },
104
+ { fileId: 'file2', isTransient: false },
105
+ { fileId: 'file3', isTransient: false },
106
+ ],
107
+ activeFileId: 'file2',
108
+ },
109
+ };
110
+
111
+ const newState = closeTabsReducer(
112
+ stateWithTabs,
113
+ action,
114
+ );
115
+
116
+ expect(newState).toEqual(stateWithTabs);
117
+ });
118
+
119
+ it('Closes a tab that is not currently active', () => {
120
+ const stateWithMultipleTabs = {
121
+ ...initialState,
122
+ pane: {
123
+ ...initialState.pane,
124
+ tabList: [
125
+ { fileId: 'file1', isTransient: false },
126
+ { fileId: 'file2', isTransient: false },
127
+ { fileId: 'file3', isTransient: false },
128
+ ],
129
+ activeFileId: 'file3',
130
+ },
131
+ };
132
+
133
+ const action: VZAction = {
134
+ type: 'close_tabs',
135
+ fileIdsToClose: ['file2'],
136
+ };
137
+
138
+ const newState = closeTabsReducer(
139
+ stateWithMultipleTabs,
140
+ action,
141
+ );
142
+
143
+ assert(newState.pane.type === 'leafPane');
144
+ expect(newState.pane.tabList.length).toBe(2);
145
+ expect(newState.pane.activeFileId).toBe('file3'); // remains unchanged
146
+ });
147
+
148
+ it('Closes the first tab out of many, when it is active', () => {
149
+ const stateWithMultipleTabs = {
150
+ ...initialState,
151
+ pane: {
152
+ ...initialState.pane,
153
+ tabList: [
154
+ { fileId: 'file1', isTransient: false },
155
+ { fileId: 'file2', isTransient: false },
156
+ { fileId: 'file3', isTransient: false },
157
+ ],
158
+ activeFileId: 'file1',
159
+ },
160
+ };
161
+
162
+ const action: VZAction = {
163
+ type: 'close_tabs',
164
+ fileIdsToClose: ['file1'],
165
+ };
166
+
167
+ const newState = closeTabsReducer(
168
+ stateWithMultipleTabs,
169
+ action,
170
+ );
171
+
172
+ assert(newState.pane.type === 'leafPane');
173
+ expect(newState.pane.tabList.length).toBe(2);
174
+ expect(newState.pane.activeFileId).toBe('file2'); // switches to the next available tab
175
+ });
176
+
177
+ it('Closes multiple tabs at once', () => {
178
+ const stateWithMultipleTabs = {
179
+ ...initialState,
180
+ pane: {
181
+ ...initialState.pane,
182
+ tabList: [
183
+ { fileId: 'file1', isTransient: false },
184
+ { fileId: 'file2', isTransient: false },
185
+ { fileId: 'file3', isTransient: false },
186
+ ],
187
+ activeFileId: 'file2',
188
+ },
189
+ };
190
+
191
+ const action: VZAction = {
192
+ type: 'close_tabs',
193
+ fileIdsToClose: ['file1', 'file2'],
194
+ };
195
+
196
+ const newState = closeTabsReducer(
197
+ stateWithMultipleTabs,
198
+ action,
199
+ );
200
+
201
+ assert(newState.pane.type === 'leafPane');
202
+ expect(newState.pane.tabList.length).toBe(1);
203
+ expect(newState.pane.activeFileId).toBe('file3'); // switches to the next available tab
204
+ });
205
+ });
@@ -0,0 +1,96 @@
1
+ import { VZAction, VZState } from '.';
2
+ import { Pane, PaneId, TabState } from '../../types';
3
+ import { VizFileId } from '@vizhub/viz-types';
4
+ import { findPane } from './findPane';
5
+ import { updatePane } from './updatePane';
6
+
7
+ export const closeTabsReducer = (
8
+ state: VZState,
9
+ action: VZAction,
10
+ ): VZState => {
11
+ if (action.type !== 'close_tabs') {
12
+ return state;
13
+ }
14
+
15
+ const { pane, activePaneId } = state;
16
+ const activePane: Pane = findPane(pane, activePaneId);
17
+
18
+ if (activePane.type !== 'leafPane') {
19
+ throw new Error(
20
+ 'closeTabsReducer: activePane is not a leafPane',
21
+ );
22
+ }
23
+
24
+ // Derive the new tab list by filtering out the tabs to close.
25
+ const newTabList: Array<TabState> =
26
+ activePane.tabList.filter(
27
+ (tabState) =>
28
+ !action.fileIdsToClose.includes(tabState.fileId),
29
+ );
30
+
31
+ // If the active tab wasn't closed, keep it active.
32
+ if (
33
+ !action.fileIdsToClose.includes(
34
+ activePane.activeFileId!,
35
+ )
36
+ ) {
37
+ return {
38
+ ...state,
39
+ pane: updatePane({
40
+ pane,
41
+ activePaneId,
42
+ newTabList,
43
+ }),
44
+ };
45
+ }
46
+
47
+ // Otherwise, we'll need to also find a new active tab.
48
+ let newActiveFileId: VizFileId | null = null;
49
+
50
+ // The question becomes: which tab should be active now?
51
+ // - Either the tab to the left or right of the closed tab.
52
+
53
+ // Try getting the previous available tab first.
54
+ const originalIndex = activePane.tabList.findIndex(
55
+ (tabState) =>
56
+ tabState.fileId === activePane.activeFileId,
57
+ );
58
+ for (let i = originalIndex - 1; i >= 0; i--) {
59
+ if (
60
+ !action.fileIdsToClose.includes(
61
+ activePane.tabList[i].fileId,
62
+ )
63
+ ) {
64
+ newActiveFileId = activePane.tabList[i].fileId;
65
+ break;
66
+ }
67
+ }
68
+
69
+ // If no previous tab available, get the next available one.
70
+ if (!newActiveFileId) {
71
+ for (
72
+ let i = originalIndex + 1;
73
+ i < activePane.tabList.length;
74
+ i++
75
+ ) {
76
+ if (
77
+ !action.fileIdsToClose.includes(
78
+ activePane.tabList[i].fileId,
79
+ )
80
+ ) {
81
+ newActiveFileId = activePane.tabList[i].fileId;
82
+ break;
83
+ }
84
+ }
85
+ }
86
+
87
+ return {
88
+ ...state,
89
+ pane: updatePane({
90
+ pane,
91
+ activePaneId,
92
+ newTabList,
93
+ newActiveFileId,
94
+ }),
95
+ };
96
+ };
@@ -0,0 +1,38 @@
1
+ import { VZState } from '.';
2
+ import { Username } from '../../types';
3
+ import { ThemeLabel } from '../themes';
4
+
5
+ export const createInitialState = ({
6
+ defaultTheme,
7
+ initialUsername = 'Anonymous',
8
+ }: {
9
+ defaultTheme: ThemeLabel;
10
+ initialUsername?: Username;
11
+ }): VZState => ({
12
+ pane: {
13
+ id: 'root',
14
+ type: 'leafPane',
15
+ tabList: [],
16
+ activeFileId: null,
17
+ },
18
+ activePaneId: 'root',
19
+ theme: defaultTheme,
20
+ search: {
21
+ pattern: '',
22
+ results: {},
23
+ focused: false,
24
+ focusedIndex: null,
25
+ focusedChildIndex: null,
26
+ },
27
+ isVisualEditorOpen: false,
28
+ isSearchOpen: false,
29
+ isAIChatOpen: true,
30
+ aiChatFocused: false,
31
+ aiChatMode: 'edit',
32
+ isSettingsOpen: false,
33
+ isDocOpen: false,
34
+ editorWantsFocus: false,
35
+ username: initialUsername,
36
+ enableAutoFollow: true,
37
+ sidebarPresenceIndicators: [],
38
+ });
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const editorNoLongerWantsFocusReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'editor_no_longer_wants_focus'
8
+ ? { ...state, editorWantsFocus: false }
9
+ : state;
@@ -0,0 +1,19 @@
1
+ import { Pane, PaneId } from '../../types';
2
+
3
+ // Recursively finds a pane by id, given a root pane node.
4
+ export const findPane = (
5
+ pane: Pane,
6
+ paneId: PaneId,
7
+ ): Pane => {
8
+ if (pane.id === paneId) {
9
+ return pane;
10
+ }
11
+ if (pane.type === 'splitPane') {
12
+ for (const child of pane.children) {
13
+ const foundPane = findPane(child, paneId);
14
+ if (foundPane) {
15
+ return foundPane;
16
+ }
17
+ }
18
+ }
19
+ };