@lvce-editor/main-area-worker 9.27.0 → 9.28.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.
@@ -599,7 +599,14 @@ class IpcError extends VError {
599
599
  const cause = new Error(message);
600
600
  // @ts-ignore
601
601
  cause.code = code;
602
- cause.stack = stack;
602
+ if (stack) {
603
+ Object.defineProperty(cause, 'stack', {
604
+ configurable: true,
605
+ enumerable: false,
606
+ value: stack,
607
+ writable: true
608
+ });
609
+ }
603
610
  super(cause, betterMessage);
604
611
  } else {
605
612
  super(betterMessage);
@@ -1898,144 +1905,484 @@ const closeTab = (state, groupId, tabId) => {
1898
1905
  return withGroups(stateWithClosedTab, newGroups);
1899
1906
  };
1900
1907
 
1901
- const findTabInState = (state, groupId, tabId) => {
1902
- const {
1903
- layout
1904
- } = state;
1905
- const group = layout.groups.find(g => g.id === groupId);
1906
- return group?.tabs.find(t => t.id === tabId);
1908
+ const handleAttach = async command => {
1909
+ // TODO find a better way to append editors
1910
+ const parentNodeSelector = '.editor-groups-container, .EditorGroup';
1911
+ await invoke('Layout.attachViewlet', parentNodeSelector, command.instanceId);
1907
1912
  };
1908
1913
 
1909
- const saveEditor = async editorUid => {
1910
- return invoke('Editor.save', editorUid);
1914
+ const getTitle$1 = async editorUid => {
1915
+ try {
1916
+ const title = await invoke('Viewlet.getTitle', editorUid);
1917
+ return typeof title === 'string' && title ? title : undefined;
1918
+ } catch {
1919
+ return undefined;
1920
+ }
1921
+ };
1922
+ const createViewlet = async (viewletModuleId, editorUid, tabId, bounds, uri) => {
1923
+ await invoke('Layout.createViewlet', viewletModuleId, editorUid, tabId, bounds, uri);
1924
+ return getTitle$1(editorUid);
1911
1925
  };
1912
1926
 
1913
- const closeTabAndSave = async (state, groupId, tabId) => {
1914
- const tab = findTabInState(state, groupId, tabId);
1915
- if (!tab) {
1916
- return state;
1917
- }
1918
- if (tab.editorUid !== -1 && tab.isDirty) {
1919
- const editorState = await saveEditor(tab.editorUid);
1920
- if (!editorState?.modified && tab.uri) {
1921
- await handleModifiedStatusChange$1(tab.uri, false);
1922
- }
1923
- }
1924
- return closeTab(state, groupId, tabId);
1927
+ const {
1928
+ get,
1929
+ getCommandIds,
1930
+ getKeys: getKeys$1,
1931
+ registerCommands,
1932
+ set,
1933
+ wrapAsyncCommand,
1934
+ wrapGetter,
1935
+ wrapSerialAsyncCommand,
1936
+ wrapSerialCommand
1937
+ } = create$f();
1938
+
1939
+ const create$2 = () => {
1940
+ return Math.random();
1925
1941
  };
1926
1942
 
1927
- const getFocusedGroup = state => {
1943
+ const updateTab = (state, tabId, updates) => {
1928
1944
  const {
1929
1945
  layout
1930
1946
  } = state;
1931
1947
  const {
1932
1948
  groups
1933
1949
  } = layout;
1934
- return groups.find(group => group.focused);
1935
- };
1936
-
1937
- const closeActiveEditor = async state => {
1938
- const focusedGroup = getFocusedGroup(state);
1939
- if (!focusedGroup) {
1940
- return state;
1941
- }
1942
- const {
1943
- activeTabId
1944
- } = focusedGroup;
1945
- if (activeTabId === undefined) {
1946
- return state;
1947
- }
1948
- return closeTabAndSave(state, focusedGroup.id, activeTabId);
1949
- };
1950
-
1951
- const closeAll$1 = state => {
1952
- const entries = state.layout.groups.flatMap((group, groupIndex) => {
1953
- return group.tabs.map((tab, tabIndex) => ({
1954
- group,
1955
- groupIndex,
1956
- tab,
1957
- tabIndex
1958
- }));
1950
+ const updatedGroups = groups.map(group => {
1951
+ const tabIndex = group.tabs.findIndex(t => t.id === tabId);
1952
+ if (tabIndex === -1) {
1953
+ return group;
1954
+ }
1955
+ const updatedTabs = group.tabs.map((tab, index) => {
1956
+ if (index === tabIndex) {
1957
+ return {
1958
+ ...tab,
1959
+ ...updates
1960
+ };
1961
+ }
1962
+ return tab;
1963
+ });
1964
+ return {
1965
+ ...group,
1966
+ tabs: updatedTabs
1967
+ };
1959
1968
  });
1960
- return withEmptyGroups(addClosedTabs(state, entries));
1961
- };
1962
-
1963
- const redistributeSizesWithRemainder = groups => {
1964
- const baseSize = Math.floor(100 / groups.length);
1965
- const remainder = 100 % groups.length;
1966
- return groups.map((group, index) => ({
1967
- ...group,
1968
- size: baseSize + (index === groups.length - 1 ? remainder : 0)
1969
- }));
1969
+ return {
1970
+ ...state,
1971
+ layout: {
1972
+ ...layout,
1973
+ groups: updatedGroups
1974
+ }
1975
+ };
1970
1976
  };
1971
1977
 
1972
- const closeEditorGroup$1 = (state, groupId) => {
1973
- if (Number.isNaN(groupId)) {
1974
- return state;
1975
- }
1978
+ const findTab = (state, tabId) => {
1976
1979
  const {
1977
1980
  layout
1978
1981
  } = state;
1979
1982
  const {
1980
- activeGroupId,
1981
1983
  groups
1982
1984
  } = layout;
1983
- const groupIndex = getGroupIndexById(state, groupId);
1984
- if (groupIndex === -1 || groups.length <= 1) {
1985
- return state;
1985
+ for (const group of groups) {
1986
+ const tab = group.tabs.find(t => t.id === tabId);
1987
+ if (tab) {
1988
+ return tab;
1989
+ }
1986
1990
  }
1987
- const remainingGroups = groups.filter(group => group.id !== groupId);
1988
- const redistributedGroups = redistributeSizesWithRemainder(remainingGroups);
1989
- const newActiveGroupId = activeGroupId === groupId ? remainingGroups[0].id : activeGroupId;
1990
- return withGroupsAndActiveGroup(state, redistributedGroups, newActiveGroupId);
1991
+ return undefined;
1992
+ };
1993
+ const loadFileContent = async path => {
1994
+ const content = await readFile(path);
1995
+ return content;
1996
+ };
1997
+ const getLoadFileErrorMessage = error => {
1998
+ if (!(error instanceof Error)) {
1999
+ return 'Failed to load file content';
2000
+ }
2001
+ if (error.message.includes('EISDIR') || error.message.includes('illegal operation on a directory') || error.message.includes('is a directory')) {
2002
+ return 'Expected a file but received a folder';
2003
+ }
2004
+ return error.message;
1991
2005
  };
2006
+ const loadTabContentAsync = async (tabId, path, requestId, getLatestState) => {
2007
+ try {
2008
+ await loadFileContent(path);
1992
2009
 
1993
- const isFocused = group => {
1994
- return group.focused;
2010
+ // Check for race condition: get the latest state
2011
+ const latestState = getLatestState();
2012
+ const latestTab = findTab(latestState, tabId);
2013
+
2014
+ // If the tab no longer exists, discard this result
2015
+ if (!latestTab) {
2016
+ return latestState;
2017
+ }
2018
+
2019
+ // If the tab is no longer in loading state, discard this result (newer request started)
2020
+ if (latestTab.loadingState !== 'loading') {
2021
+ return latestState;
2022
+ }
2023
+
2024
+ // Assign editorUid if tab doesn't have one yet
2025
+ const editorUid = latestTab.editorUid === -1 ? create$2() : latestTab.editorUid;
2026
+ return updateTab(latestState, tabId, {
2027
+ editorUid,
2028
+ errorMessage: undefined,
2029
+ loadingState: 'loaded'
2030
+ });
2031
+ } catch (error) {
2032
+ // Check for race condition before updating with error
2033
+ const latestState = getLatestState();
2034
+ const latestTab = findTab(latestState, tabId);
2035
+ if (!latestTab) {
2036
+ return latestState;
2037
+ }
2038
+
2039
+ // If the tab is no longer in loading state, discard this result (newer request started)
2040
+ if (latestTab.loadingState !== 'loading') {
2041
+ return latestState;
2042
+ }
2043
+ const errorMessage = getLoadFileErrorMessage(error);
2044
+ return updateTab(latestState, tabId, {
2045
+ errorMessage,
2046
+ loadingState: 'error'
2047
+ });
2048
+ }
1995
2049
  };
1996
- const closeFocusedTab = async state => {
1997
- const {
1998
- layout
1999
- } = state;
2000
- const {
2001
- groups
2002
- } = layout;
2003
- const focusedGroup = groups.find(isFocused);
2004
- if (!focusedGroup) {
2050
+
2051
+ /**
2052
+ * SAFE: Create viewlet for a tab (no visible side effects)
2053
+ * Can be called eagerly when tab is opened - the viewlet loads in the background.
2054
+ */
2055
+ const createViewletForTab = (state, tabId, viewletModuleId, bounds) => {
2056
+ const tab = findTab(state, tabId);
2057
+ if (!tab) {
2005
2058
  return state;
2006
2059
  }
2007
- const {
2008
- activeTabId
2009
- } = focusedGroup;
2010
- if (activeTabId === undefined) {
2060
+
2061
+ // If tab already has an editorUid or is loading/loaded, don't recreate
2062
+ if (tab.editorUid !== -1 || tab.loadingState === 'loading' || tab.loadingState === 'loaded') {
2011
2063
  return state;
2012
2064
  }
2013
- return closeTabAndSave(state, focusedGroup.id, activeTabId);
2065
+ const editorUid = create$2();
2066
+ const newState = updateTab(state, tabId, {
2067
+ editorUid
2068
+ });
2069
+ return newState;
2014
2070
  };
2015
2071
 
2016
- const closeOtherTabs = (state, groupId) => {
2072
+ /**
2073
+ * Called when switching tabs.
2074
+ * With reference nodes, attachment/detachment is handled automatically by virtual DOM rendering.
2075
+ * No attach/detach commands needed - virtual DOM will render the correct reference at each position.
2076
+ */
2077
+ const switchViewlet = (state, fromTabId, toTabId) => {
2078
+ // No commands needed - virtual DOM reference nodes handle attachment automatically
2079
+ // Virtual DOM will:
2080
+ // 1. Remove the old reference node from the previous active tab
2081
+ // 2. Add the new reference node for the now-active tab
2082
+ // This achieves the same effect as detach/attach without explicit commands
2083
+
2084
+ return {
2085
+ commands: [],
2086
+ newState: state
2087
+ };
2088
+ };
2089
+
2090
+ const findTabByEditorUid = (state, editorUid) => {
2017
2091
  const {
2018
2092
  layout
2019
2093
  } = state;
2020
2094
  const {
2021
- activeGroupId,
2022
2095
  groups
2023
2096
  } = layout;
2024
- const targetGroupId = groupId ?? activeGroupId;
2025
- if (targetGroupId === undefined) {
2026
- return state;
2027
- }
2028
- const group = groups.find(g => g.id === targetGroupId);
2029
- if (!group) {
2030
- return state;
2031
- }
2032
- const {
2033
- activeTabId
2034
- } = group;
2035
- if (activeTabId === undefined) {
2036
- return state;
2097
+ for (const group of groups) {
2098
+ const tab = group.tabs.find(t => t.editorUid === editorUid);
2099
+ if (tab) {
2100
+ return tab;
2101
+ }
2037
2102
  }
2038
- const closedTabs = group.tabs.map((tab, tabIndex) => ({
2103
+ return undefined;
2104
+ };
2105
+
2106
+ /**
2107
+ * Called when renderer reports viewlet finished creating.
2108
+ * With reference nodes, attachment is handled automatically by virtual DOM rendering.
2109
+ * No commands needed - state update is sufficient.
2110
+ */
2111
+ const handleViewletReady = (state, editorUid, title) => {
2112
+ if (editorUid === -1) {
2113
+ throw new Error('Invalid editorUid -1');
2114
+ }
2115
+ // Find the tab by viewletRequestId
2116
+ const tab = findTabByEditorUid(state, editorUid);
2117
+ if (!tab) {
2118
+ // Tab was closed, dispose viewlet
2119
+ return state;
2120
+ }
2121
+
2122
+ // Mark viewlet as ready and set instance id
2123
+ // Reference nodes will handle rendering at the correct position automatically
2124
+ const newState = updateTab(state, tab.id, {
2125
+ editorUid,
2126
+ loadingState: 'loaded',
2127
+ ...(title && {
2128
+ title
2129
+ })
2130
+ });
2131
+
2132
+ // No attach commands needed - virtual DOM reference nodes handle positioning
2133
+ return newState;
2134
+ };
2135
+
2136
+ /**
2137
+ * Dispose viewlet when tab is closed.
2138
+ */
2139
+ const disposeViewletForTab = (state, tabId) => {
2140
+ const tab = findTab(state, tabId);
2141
+ if (!tab || tab.editorUid === -1) {
2142
+ return {
2143
+ commands: [],
2144
+ newState: state
2145
+ };
2146
+ }
2147
+ const commands = [{
2148
+ instanceId: tab.editorUid,
2149
+ type: 'dispose'
2150
+ }];
2151
+ return {
2152
+ commands,
2153
+ newState: state
2154
+ };
2155
+ };
2156
+
2157
+ const handleCreate = async command => {
2158
+ // Safe to call - no visible side effects
2159
+
2160
+ const title = await createViewlet(command.viewletModuleId, command.editorUid, command.tabId, command.bounds, command.uri || '');
2161
+
2162
+ // After viewlet is created, mark it as ready
2163
+ // Attachment is handled automatically by virtual DOM reference nodes
2164
+ const {
2165
+ newState: state,
2166
+ oldState
2167
+ } = get(command.uid);
2168
+ const readyState = handleViewletReady(state, command.editorUid, title);
2169
+ set(command.uid, oldState, readyState);
2170
+ return readyState;
2171
+ };
2172
+
2173
+ const handleDetach = async command => {
2174
+ // Hides viewlet but keeps it alive
2175
+
2176
+ await invoke('Viewlet.detach', command.instanceId);
2177
+ };
2178
+
2179
+ const handleDispose = async command => {
2180
+ // Fully destroys viewlet
2181
+
2182
+ await invoke('Viewlet.dispose', command.instanceId);
2183
+ };
2184
+
2185
+ const handleSetBounds = async command => {
2186
+ await invoke('Viewlet.setBounds', command.instanceId, command.bounds);
2187
+ };
2188
+
2189
+ const executeViewletCommand = async command => {
2190
+ switch (command.type) {
2191
+ case 'attach':
2192
+ await handleAttach(command);
2193
+ return;
2194
+ case 'create':
2195
+ await handleCreate(command);
2196
+ return;
2197
+ case 'detach':
2198
+ await handleDetach(command);
2199
+ return;
2200
+ case 'dispose':
2201
+ await handleDispose(command);
2202
+ return;
2203
+ case 'setBounds':
2204
+ await handleSetBounds(command);
2205
+ return;
2206
+ }
2207
+ };
2208
+ const executeViewletCommands = async commands => {
2209
+ for (const command of commands) {
2210
+ await executeViewletCommand(command);
2211
+ }
2212
+ };
2213
+
2214
+ const findTabInState = (state, groupId, tabId) => {
2215
+ const {
2216
+ layout
2217
+ } = state;
2218
+ const group = layout.groups.find(g => g.id === groupId);
2219
+ return group?.tabs.find(t => t.id === tabId);
2220
+ };
2221
+
2222
+ const closeTabWithViewlet = async (state, groupId, tabId) => {
2223
+ const tab = findTabInState(state, groupId, tabId);
2224
+ const commands = [];
2225
+ if (tab && tab.editorUid !== -1) {
2226
+ const {
2227
+ commands: disposeCommands
2228
+ } = disposeViewletForTab(state, tabId);
2229
+ commands.push(...disposeCommands);
2230
+ }
2231
+ const newState = closeTab(state, groupId, tabId);
2232
+ const group = state.layout.groups.find(g => g.id === groupId);
2233
+ const wasActiveTab = group?.activeTabId === tabId;
2234
+ if (wasActiveTab) {
2235
+ const newGroup = newState.layout.groups.find(g => g.id === groupId);
2236
+ const newActiveTabId = newGroup?.activeTabId;
2237
+ if (newActiveTabId !== undefined) {
2238
+ const {
2239
+ commands: switchCommands,
2240
+ newState: switchedState
2241
+ } = switchViewlet(newState);
2242
+ commands.push(...switchCommands);
2243
+ await executeViewletCommands(commands);
2244
+ return switchedState;
2245
+ }
2246
+ }
2247
+ if (commands.length > 0) {
2248
+ await executeViewletCommands(commands);
2249
+ }
2250
+ return newState;
2251
+ };
2252
+
2253
+ const saveEditor = async editorUid => {
2254
+ return invoke('Editor.save', editorUid);
2255
+ };
2256
+
2257
+ const closeTabAndSave = async (state, groupId, tabId) => {
2258
+ const tab = findTabInState(state, groupId, tabId);
2259
+ if (!tab) {
2260
+ return state;
2261
+ }
2262
+ if (tab.editorUid !== -1 && tab.isDirty) {
2263
+ const editorState = await saveEditor(tab.editorUid);
2264
+ if (!editorState?.modified && tab.uri) {
2265
+ await handleModifiedStatusChange$1(tab.uri, false);
2266
+ }
2267
+ }
2268
+ if (tab.uri?.startsWith('simple-browser://')) {
2269
+ return closeTabWithViewlet(state, groupId, tabId);
2270
+ }
2271
+ return closeTab(state, groupId, tabId);
2272
+ };
2273
+
2274
+ const getFocusedGroup = state => {
2275
+ const {
2276
+ layout
2277
+ } = state;
2278
+ const {
2279
+ groups
2280
+ } = layout;
2281
+ return groups.find(group => group.focused);
2282
+ };
2283
+
2284
+ const closeActiveEditor = async state => {
2285
+ const focusedGroup = getFocusedGroup(state);
2286
+ if (!focusedGroup) {
2287
+ return state;
2288
+ }
2289
+ const {
2290
+ activeTabId
2291
+ } = focusedGroup;
2292
+ if (activeTabId === undefined) {
2293
+ return state;
2294
+ }
2295
+ return closeTabAndSave(state, focusedGroup.id, activeTabId);
2296
+ };
2297
+
2298
+ const closeAll$1 = state => {
2299
+ const entries = state.layout.groups.flatMap((group, groupIndex) => {
2300
+ return group.tabs.map((tab, tabIndex) => ({
2301
+ group,
2302
+ groupIndex,
2303
+ tab,
2304
+ tabIndex
2305
+ }));
2306
+ });
2307
+ return withEmptyGroups(addClosedTabs(state, entries));
2308
+ };
2309
+
2310
+ const redistributeSizesWithRemainder = groups => {
2311
+ const baseSize = Math.floor(100 / groups.length);
2312
+ const remainder = 100 % groups.length;
2313
+ return groups.map((group, index) => ({
2314
+ ...group,
2315
+ size: baseSize + (index === groups.length - 1 ? remainder : 0)
2316
+ }));
2317
+ };
2318
+
2319
+ const closeEditorGroup$1 = (state, groupId) => {
2320
+ if (Number.isNaN(groupId)) {
2321
+ return state;
2322
+ }
2323
+ const {
2324
+ layout
2325
+ } = state;
2326
+ const {
2327
+ activeGroupId,
2328
+ groups
2329
+ } = layout;
2330
+ const groupIndex = getGroupIndexById(state, groupId);
2331
+ if (groupIndex === -1 || groups.length <= 1) {
2332
+ return state;
2333
+ }
2334
+ const remainingGroups = groups.filter(group => group.id !== groupId);
2335
+ const redistributedGroups = redistributeSizesWithRemainder(remainingGroups);
2336
+ const newActiveGroupId = activeGroupId === groupId ? remainingGroups[0].id : activeGroupId;
2337
+ return withGroupsAndActiveGroup(state, redistributedGroups, newActiveGroupId);
2338
+ };
2339
+
2340
+ const isFocused = group => {
2341
+ return group.focused;
2342
+ };
2343
+ const closeFocusedTab = async state => {
2344
+ const {
2345
+ layout
2346
+ } = state;
2347
+ const {
2348
+ groups
2349
+ } = layout;
2350
+ const focusedGroup = groups.find(isFocused);
2351
+ if (!focusedGroup) {
2352
+ return state;
2353
+ }
2354
+ const {
2355
+ activeTabId
2356
+ } = focusedGroup;
2357
+ if (activeTabId === undefined) {
2358
+ return state;
2359
+ }
2360
+ return closeTabAndSave(state, focusedGroup.id, activeTabId);
2361
+ };
2362
+
2363
+ const closeOtherTabs = (state, groupId) => {
2364
+ const {
2365
+ layout
2366
+ } = state;
2367
+ const {
2368
+ activeGroupId,
2369
+ groups
2370
+ } = layout;
2371
+ const targetGroupId = groupId ?? activeGroupId;
2372
+ if (targetGroupId === undefined) {
2373
+ return state;
2374
+ }
2375
+ const group = groups.find(g => g.id === targetGroupId);
2376
+ if (!group) {
2377
+ return state;
2378
+ }
2379
+ const {
2380
+ activeTabId
2381
+ } = group;
2382
+ if (activeTabId === undefined) {
2383
+ return state;
2384
+ }
2385
+ const closedTabs = group.tabs.map((tab, tabIndex) => ({
2039
2386
  group,
2040
2387
  groupIndex: groups.findIndex(g => g.id === targetGroupId),
2041
2388
  tab,
@@ -2125,473 +2472,188 @@ const closeTabsByUris = (state, uris) => {
2125
2472
  while (nextState) {
2126
2473
  currentState = nextState;
2127
2474
  nextState = closeFirstTabByUri(currentState, uri);
2128
- }
2129
- }
2130
- return currentState;
2131
- };
2132
-
2133
- const getGroupById = (state, groupId) => {
2134
- return state.layout.groups.find(group => group.id === groupId);
2135
- };
2136
-
2137
- const closeTabsRight = (state, groupId) => {
2138
- const {
2139
- layout
2140
- } = state;
2141
- const {
2142
- groups
2143
- } = layout;
2144
- const group = getGroupById(state, groupId);
2145
- if (!group) {
2146
- return state;
2147
- }
2148
- const {
2149
- activeTabId,
2150
- tabs
2151
- } = group;
2152
- if (activeTabId === undefined) {
2153
- return state;
2154
- }
2155
- const activeTabIndex = tabs.findIndex(tab => tab.id === activeTabId);
2156
- if (activeTabIndex === -1) {
2157
- return state;
2158
- }
2159
- const newTabs = tabs.slice(0, activeTabIndex + 1);
2160
- if (newTabs.length === tabs.length) {
2161
- return state;
2162
- }
2163
- const groupIndex = groups.findIndex(g => g.id === groupId);
2164
- const closedTabs = tabs.slice(activeTabIndex + 1).map((tab, index) => ({
2165
- group,
2166
- groupIndex,
2167
- tab,
2168
- tabIndex: activeTabIndex + index + 1
2169
- }));
2170
- const newGroups = groups.map(g => {
2171
- if (g.id === groupId) {
2172
- return {
2173
- ...g,
2174
- isEmpty: newTabs.length === 0,
2175
- tabs: newTabs
2176
- };
2177
- }
2178
- return g;
2179
- });
2180
- return withGroups(addClosedTabs(state, closedTabs), newGroups);
2181
- };
2182
-
2183
- const findGroupById = (state, groupId) => {
2184
- const {
2185
- layout
2186
- } = state;
2187
- const {
2188
- groups
2189
- } = layout;
2190
- return groups.find(group => group.id === groupId);
2191
- };
2192
-
2193
- const copyIntoNewWindow$1 = async state => {
2194
- const {
2195
- activeGroupId
2196
- } = state.layout;
2197
- const group = findGroupById(state, activeGroupId ?? 0);
2198
- if (!group) {
2199
- return state;
2200
- }
2201
- const {
2202
- activeTabId
2203
- } = group;
2204
- if (activeTabId === undefined) {
2205
- return state;
2206
- }
2207
- const tab = group.tabs.find(tab => tab.id === activeTabId);
2208
- if (!tab?.uri) {
2209
- return state;
2210
- }
2211
- await invoke('ElectronWindow.openNewWithUri', tab.uri);
2212
- return state;
2213
- };
2214
-
2215
- const copyPath$1 = async (state, path) => {
2216
- string(path);
2217
- await writeClipBoardText(path);
2218
- return state;
2219
- };
2220
-
2221
- const copyRelativePath$1 = async (state, path) => {
2222
- string(path);
2223
- // TODO use clipboard worker
2224
- const relativePath = await invoke('Workspace.pathBaseName', path);
2225
- await invoke('ClipBoard.writeText', relativePath);
2226
- return state;
2227
- };
2228
-
2229
- const Horizontal = 1;
2230
- const Vertical = 2;
2231
- const isLayoutDirection = direction => {
2232
- return direction === Horizontal || direction === Vertical;
2233
- };
2234
- const normalizeLayoutDirection = direction => {
2235
- if (isLayoutDirection(direction)) {
2236
- return direction;
2237
- }
2238
- if (direction === 'horizontal') {
2239
- return Horizontal;
2240
- }
2241
- if (direction === 'vertical') {
2242
- return Vertical;
2243
- }
2244
- return undefined;
2245
- };
2246
-
2247
- const {
2248
- get,
2249
- getCommandIds,
2250
- getKeys: getKeys$1,
2251
- registerCommands,
2252
- set,
2253
- wrapAsyncCommand,
2254
- wrapGetter,
2255
- wrapSerialAsyncCommand,
2256
- wrapSerialCommand
2257
- } = create$f();
2258
-
2259
- const create$2 = (uid, uri, x, y, width, height, platform, assetDir, tabHeight = 35) => {
2260
- const state = {
2261
- assetDir,
2262
- closedTabs: [],
2263
- fileIconCache: {},
2264
- height,
2265
- homeDirUri: '',
2266
- iframes: [],
2267
- initial: true,
2268
- layout: {
2269
- activeGroupId: undefined,
2270
- direction: Horizontal,
2271
- groups: []
2272
- },
2273
- maxOpenEditorGroups: Infinity,
2274
- maxOpenEditors: Infinity,
2275
- minGroupHeightPx: 80,
2276
- minGroupWidthPx: 250,
2277
- platform,
2278
- splitButtonEnabled: false,
2279
- tabHeight,
2280
- uid,
2281
- width,
2282
- workspaceuri: uri,
2283
- x,
2284
- y
2285
- };
2286
- set(uid, state, state);
2287
- };
2288
-
2289
- const isEqual$1 = (oldState, newState) => {
2290
- return false;
2291
- };
2292
-
2293
- const isEqual = (oldState, newState) => {
2294
- return oldState.layout === newState.layout;
2295
- };
2296
-
2297
- const RenderItems = 4;
2298
- const RenderCss = 10;
2299
- const RenderIncremental = 11;
2300
-
2301
- const modules = [isEqual, isEqual$1];
2302
- const numbers = [RenderIncremental, RenderCss];
2303
-
2304
- const diff = (oldState, newState) => {
2305
- const diffResult = [];
2306
- for (let i = 0; i < modules.length; i++) {
2307
- const fn = modules[i];
2308
- if (!fn(oldState, newState)) {
2309
- diffResult.push(numbers[i]);
2310
- }
2311
- }
2312
- return diffResult;
2313
- };
2314
-
2315
- const diff2 = uid => {
2316
- const {
2317
- newState,
2318
- oldState
2319
- } = get(uid);
2320
- const result = diff(oldState, newState);
2321
- return result;
2322
- };
2323
-
2324
- const handleAttach = async command => {
2325
- // TODO find a better way to append editors
2326
- const parentNodeSelector = '.editor-groups-container, .EditorGroup';
2327
- await invoke('Layout.attachViewlet', parentNodeSelector, command.instanceId);
2328
- };
2329
-
2330
- const getTitle$1 = async editorUid => {
2331
- try {
2332
- const title = await invoke('Viewlet.getTitle', editorUid);
2333
- return typeof title === 'string' && title ? title : undefined;
2334
- } catch {
2335
- return undefined;
2336
- }
2337
- };
2338
- const createViewlet = async (viewletModuleId, editorUid, tabId, bounds, uri) => {
2339
- await invoke('Layout.createViewlet', viewletModuleId, editorUid, tabId, bounds, uri);
2340
- return getTitle$1(editorUid);
2341
- };
2342
-
2343
- const create$1 = () => {
2344
- return Math.random();
2345
- };
2346
-
2347
- const updateTab = (state, tabId, updates) => {
2348
- const {
2349
- layout
2350
- } = state;
2351
- const {
2352
- groups
2353
- } = layout;
2354
- const updatedGroups = groups.map(group => {
2355
- const tabIndex = group.tabs.findIndex(t => t.id === tabId);
2356
- if (tabIndex === -1) {
2357
- return group;
2358
- }
2359
- const updatedTabs = group.tabs.map((tab, index) => {
2360
- if (index === tabIndex) {
2361
- return {
2362
- ...tab,
2363
- ...updates
2364
- };
2365
- }
2366
- return tab;
2367
- });
2368
- return {
2369
- ...group,
2370
- tabs: updatedTabs
2371
- };
2372
- });
2373
- return {
2374
- ...state,
2375
- layout: {
2376
- ...layout,
2377
- groups: updatedGroups
2378
- }
2379
- };
2380
- };
2381
-
2382
- const findTab = (state, tabId) => {
2383
- const {
2384
- layout
2385
- } = state;
2386
- const {
2387
- groups
2388
- } = layout;
2389
- for (const group of groups) {
2390
- const tab = group.tabs.find(t => t.id === tabId);
2391
- if (tab) {
2392
- return tab;
2393
- }
2394
- }
2395
- return undefined;
2396
- };
2397
- const loadFileContent = async path => {
2398
- const content = await readFile(path);
2399
- return content;
2400
- };
2401
- const getLoadFileErrorMessage = error => {
2402
- if (!(error instanceof Error)) {
2403
- return 'Failed to load file content';
2404
- }
2405
- if (error.message.includes('EISDIR') || error.message.includes('illegal operation on a directory') || error.message.includes('is a directory')) {
2406
- return 'Expected a file but received a folder';
2407
- }
2408
- return error.message;
2409
- };
2410
- const loadTabContentAsync = async (tabId, path, requestId, getLatestState) => {
2411
- try {
2412
- await loadFileContent(path);
2413
-
2414
- // Check for race condition: get the latest state
2415
- const latestState = getLatestState();
2416
- const latestTab = findTab(latestState, tabId);
2417
-
2418
- // If the tab no longer exists, discard this result
2419
- if (!latestTab) {
2420
- return latestState;
2421
- }
2422
-
2423
- // If the tab is no longer in loading state, discard this result (newer request started)
2424
- if (latestTab.loadingState !== 'loading') {
2425
- return latestState;
2426
- }
2427
-
2428
- // Assign editorUid if tab doesn't have one yet
2429
- const editorUid = latestTab.editorUid === -1 ? create$1() : latestTab.editorUid;
2430
- return updateTab(latestState, tabId, {
2431
- editorUid,
2432
- errorMessage: undefined,
2433
- loadingState: 'loaded'
2434
- });
2435
- } catch (error) {
2436
- // Check for race condition before updating with error
2437
- const latestState = getLatestState();
2438
- const latestTab = findTab(latestState, tabId);
2439
- if (!latestTab) {
2440
- return latestState;
2441
- }
2442
-
2443
- // If the tab is no longer in loading state, discard this result (newer request started)
2444
- if (latestTab.loadingState !== 'loading') {
2445
- return latestState;
2446
- }
2447
- const errorMessage = getLoadFileErrorMessage(error);
2448
- return updateTab(latestState, tabId, {
2449
- errorMessage,
2450
- loadingState: 'error'
2451
- });
2475
+ }
2452
2476
  }
2477
+ return currentState;
2453
2478
  };
2454
2479
 
2455
- /**
2456
- * SAFE: Create viewlet for a tab (no visible side effects)
2457
- * Can be called eagerly when tab is opened - the viewlet loads in the background.
2458
- */
2459
- const createViewletForTab = (state, tabId, viewletModuleId, bounds) => {
2460
- const tab = findTab(state, tabId);
2461
- if (!tab) {
2480
+ const getGroupById = (state, groupId) => {
2481
+ return state.layout.groups.find(group => group.id === groupId);
2482
+ };
2483
+
2484
+ const closeTabsRight = (state, groupId) => {
2485
+ const {
2486
+ layout
2487
+ } = state;
2488
+ const {
2489
+ groups
2490
+ } = layout;
2491
+ const group = getGroupById(state, groupId);
2492
+ if (!group) {
2462
2493
  return state;
2463
2494
  }
2464
-
2465
- // If tab already has an editorUid or is loading/loaded, don't recreate
2466
- if (tab.editorUid !== -1 || tab.loadingState === 'loading' || tab.loadingState === 'loaded') {
2495
+ const {
2496
+ activeTabId,
2497
+ tabs
2498
+ } = group;
2499
+ if (activeTabId === undefined) {
2467
2500
  return state;
2468
2501
  }
2469
- const editorUid = create$1();
2470
- const newState = updateTab(state, tabId, {
2471
- editorUid
2502
+ const activeTabIndex = tabs.findIndex(tab => tab.id === activeTabId);
2503
+ if (activeTabIndex === -1) {
2504
+ return state;
2505
+ }
2506
+ const newTabs = tabs.slice(0, activeTabIndex + 1);
2507
+ if (newTabs.length === tabs.length) {
2508
+ return state;
2509
+ }
2510
+ const groupIndex = groups.findIndex(g => g.id === groupId);
2511
+ const closedTabs = tabs.slice(activeTabIndex + 1).map((tab, index) => ({
2512
+ group,
2513
+ groupIndex,
2514
+ tab,
2515
+ tabIndex: activeTabIndex + index + 1
2516
+ }));
2517
+ const newGroups = groups.map(g => {
2518
+ if (g.id === groupId) {
2519
+ return {
2520
+ ...g,
2521
+ isEmpty: newTabs.length === 0,
2522
+ tabs: newTabs
2523
+ };
2524
+ }
2525
+ return g;
2472
2526
  });
2473
- return newState;
2474
- };
2475
-
2476
- /**
2477
- * Called when switching tabs.
2478
- * With reference nodes, attachment/detachment is handled automatically by virtual DOM rendering.
2479
- * No attach/detach commands needed - virtual DOM will render the correct reference at each position.
2480
- */
2481
- const switchViewlet = (state, fromTabId, toTabId) => {
2482
- // No commands needed - virtual DOM reference nodes handle attachment automatically
2483
- // Virtual DOM will:
2484
- // 1. Remove the old reference node from the previous active tab
2485
- // 2. Add the new reference node for the now-active tab
2486
- // This achieves the same effect as detach/attach without explicit commands
2487
-
2488
- return {
2489
- commands: [],
2490
- newState: state
2491
- };
2527
+ return withGroups(addClosedTabs(state, closedTabs), newGroups);
2492
2528
  };
2493
2529
 
2494
- const findTabByEditorUid = (state, editorUid) => {
2530
+ const findGroupById = (state, groupId) => {
2495
2531
  const {
2496
2532
  layout
2497
2533
  } = state;
2498
2534
  const {
2499
2535
  groups
2500
2536
  } = layout;
2501
- for (const group of groups) {
2502
- const tab = group.tabs.find(t => t.editorUid === editorUid);
2503
- if (tab) {
2504
- return tab;
2505
- }
2506
- }
2507
- return undefined;
2537
+ return groups.find(group => group.id === groupId);
2508
2538
  };
2509
2539
 
2510
- /**
2511
- * Called when renderer reports viewlet finished creating.
2512
- * With reference nodes, attachment is handled automatically by virtual DOM rendering.
2513
- * No commands needed - state update is sufficient.
2514
- */
2515
- const handleViewletReady = (state, editorUid, title) => {
2516
- if (editorUid === -1) {
2517
- throw new Error('Invalid editorUid -1');
2540
+ const copyIntoNewWindow$1 = async state => {
2541
+ const {
2542
+ activeGroupId
2543
+ } = state.layout;
2544
+ const group = findGroupById(state, activeGroupId ?? 0);
2545
+ if (!group) {
2546
+ return state;
2518
2547
  }
2519
- // Find the tab by viewletRequestId
2520
- const tab = findTabByEditorUid(state, editorUid);
2521
- if (!tab) {
2522
- // Tab was closed, dispose viewlet
2548
+ const {
2549
+ activeTabId
2550
+ } = group;
2551
+ if (activeTabId === undefined) {
2523
2552
  return state;
2524
2553
  }
2525
-
2526
- // Mark viewlet as ready and set instance id
2527
- // Reference nodes will handle rendering at the correct position automatically
2528
- const newState = updateTab(state, tab.id, {
2529
- editorUid,
2530
- loadingState: 'loaded',
2531
- ...(title && {
2532
- title
2533
- })
2534
- });
2535
-
2536
- // No attach commands needed - virtual DOM reference nodes handle positioning
2537
- return newState;
2554
+ const tab = group.tabs.find(tab => tab.id === activeTabId);
2555
+ if (!tab?.uri) {
2556
+ return state;
2557
+ }
2558
+ await invoke('ElectronWindow.openNewWithUri', tab.uri);
2559
+ return state;
2538
2560
  };
2539
2561
 
2540
- const handleCreate = async command => {
2541
- // Safe to call - no visible side effects
2542
-
2543
- const title = await createViewlet(command.viewletModuleId, command.editorUid, command.tabId, command.bounds, command.uri || '');
2544
-
2545
- // After viewlet is created, mark it as ready
2546
- // Attachment is handled automatically by virtual DOM reference nodes
2547
- const {
2548
- newState: state,
2549
- oldState
2550
- } = get(command.uid);
2551
- const readyState = handleViewletReady(state, command.editorUid, title);
2552
- set(command.uid, oldState, readyState);
2553
- return readyState;
2562
+ const copyPath$1 = async (state, path) => {
2563
+ string(path);
2564
+ await writeClipBoardText(path);
2565
+ return state;
2554
2566
  };
2555
2567
 
2556
- const handleDetach = async command => {
2557
- // Hides viewlet but keeps it alive
2568
+ const copyRelativePath$1 = async (state, path) => {
2569
+ string(path);
2570
+ // TODO use clipboard worker
2571
+ const relativePath = await invoke('Workspace.pathBaseName', path);
2572
+ await invoke('ClipBoard.writeText', relativePath);
2573
+ return state;
2574
+ };
2558
2575
 
2559
- await invoke('Viewlet.detach', command.instanceId);
2576
+ const Horizontal = 1;
2577
+ const Vertical = 2;
2578
+ const isLayoutDirection = direction => {
2579
+ return direction === Horizontal || direction === Vertical;
2580
+ };
2581
+ const normalizeLayoutDirection = direction => {
2582
+ if (isLayoutDirection(direction)) {
2583
+ return direction;
2584
+ }
2585
+ if (direction === 'horizontal') {
2586
+ return Horizontal;
2587
+ }
2588
+ if (direction === 'vertical') {
2589
+ return Vertical;
2590
+ }
2591
+ return undefined;
2560
2592
  };
2561
2593
 
2562
- const handleDispose = async command => {
2563
- // Fully destroys viewlet
2594
+ const create$1 = (uid, uri, x, y, width, height, platform, assetDir, tabHeight = 35) => {
2595
+ const state = {
2596
+ assetDir,
2597
+ closedTabs: [],
2598
+ fileIconCache: {},
2599
+ height,
2600
+ homeDirUri: '',
2601
+ iframes: [],
2602
+ initial: true,
2603
+ layout: {
2604
+ activeGroupId: undefined,
2605
+ direction: Horizontal,
2606
+ groups: []
2607
+ },
2608
+ maxOpenEditorGroups: Infinity,
2609
+ maxOpenEditors: Infinity,
2610
+ minGroupHeightPx: 80,
2611
+ minGroupWidthPx: 250,
2612
+ platform,
2613
+ splitButtonEnabled: false,
2614
+ tabHeight,
2615
+ uid,
2616
+ width,
2617
+ workspaceuri: uri,
2618
+ x,
2619
+ y
2620
+ };
2621
+ set(uid, state, state);
2622
+ };
2564
2623
 
2565
- await invoke('Viewlet.dispose', command.instanceId);
2624
+ const isEqual$1 = (oldState, newState) => {
2625
+ return false;
2566
2626
  };
2567
2627
 
2568
- const handleSetBounds = async command => {
2569
- await invoke('Viewlet.setBounds', command.instanceId, command.bounds);
2628
+ const isEqual = (oldState, newState) => {
2629
+ return oldState.layout === newState.layout;
2570
2630
  };
2571
2631
 
2572
- const executeViewletCommand = async command => {
2573
- switch (command.type) {
2574
- case 'attach':
2575
- await handleAttach(command);
2576
- return;
2577
- case 'create':
2578
- await handleCreate(command);
2579
- return;
2580
- case 'detach':
2581
- await handleDetach(command);
2582
- return;
2583
- case 'dispose':
2584
- await handleDispose(command);
2585
- return;
2586
- case 'setBounds':
2587
- await handleSetBounds(command);
2588
- return;
2632
+ const RenderItems = 4;
2633
+ const RenderCss = 10;
2634
+ const RenderIncremental = 11;
2635
+
2636
+ const modules = [isEqual, isEqual$1];
2637
+ const numbers = [RenderIncremental, RenderCss];
2638
+
2639
+ const diff = (oldState, newState) => {
2640
+ const diffResult = [];
2641
+ for (let i = 0; i < modules.length; i++) {
2642
+ const fn = modules[i];
2643
+ if (!fn(oldState, newState)) {
2644
+ diffResult.push(numbers[i]);
2645
+ }
2589
2646
  }
2647
+ return diffResult;
2590
2648
  };
2591
- const executeViewletCommands = async commands => {
2592
- for (const command of commands) {
2593
- await executeViewletCommand(command);
2594
- }
2649
+
2650
+ const diff2 = uid => {
2651
+ const {
2652
+ newState,
2653
+ oldState
2654
+ } = get(uid);
2655
+ const result = diff(oldState, newState);
2656
+ return result;
2595
2657
  };
2596
2658
 
2597
2659
  const requestIdState = {
@@ -3485,9 +3547,9 @@ const createEmptyGroup$1 = (state, uri, requestId, preview = false, title = getL
3485
3547
  const {
3486
3548
  groups
3487
3549
  } = layout;
3488
- const groupId = create$1();
3489
- const tabId = create$1();
3490
- const editorUid = create$1();
3550
+ const groupId = create$2();
3551
+ const tabId = create$2();
3552
+ const editorUid = create$2();
3491
3553
  const newTab = {
3492
3554
  editorInput,
3493
3555
  editorType,
@@ -3528,7 +3590,7 @@ const openTab = (state, groupId, tab) => {
3528
3590
  };
3529
3591
  const newTab = 'id' in normalizedTab && normalizedTab.id !== undefined ? normalizedTab : {
3530
3592
  ...normalizedTab,
3531
- id: create$1()
3593
+ id: create$2()
3532
3594
  };
3533
3595
  const {
3534
3596
  layout
@@ -3615,8 +3677,8 @@ const ensureActiveGroup = (state, uri, preview = false, title = getLabel(uri), e
3615
3677
  }
3616
3678
 
3617
3679
  // Create a new tab with the URI in the active group
3618
- const tabId = create$1();
3619
- const editorUid = create$1();
3680
+ const tabId = create$2();
3681
+ const editorUid = create$2();
3620
3682
  const newTab = {
3621
3683
  editorInput,
3622
3684
  editorType,
@@ -4149,7 +4211,7 @@ const splitEditorGroup$1 = (state, groupId, direction) => {
4149
4211
  if (!sourceGroup) {
4150
4212
  return state;
4151
4213
  }
4152
- const newGroupId = create$1();
4214
+ const newGroupId = create$2();
4153
4215
  const splitLayoutDirection = getSplitLayoutDirection(direction);
4154
4216
  const baseNewGroup = {
4155
4217
  activeTabId: undefined,
@@ -4366,8 +4428,8 @@ const newFile = async state => {
4366
4428
  getActiveTabId(newState);
4367
4429
 
4368
4430
  // Create a new empty tab
4369
- const tabId = create$1();
4370
- const editorUid = create$1();
4431
+ const tabId = create$2();
4432
+ const editorUid = create$2();
4371
4433
  const newTab = {
4372
4434
  editorType: 'text',
4373
4435
  editorUid,
@@ -4861,7 +4923,7 @@ const createViewlets = async (layout, viewletModuleIds, bounds) => {
4861
4923
  for (const group of layout.groups) {
4862
4924
  const activeTab = group.tabs.find(tab => tab.id === group.activeTabId);
4863
4925
  if (activeTab && viewletModuleIds[activeTab.id]) {
4864
- const editorUid = activeTab.editorUid === -1 ? create$1() : activeTab.editorUid;
4926
+ const editorUid = activeTab.editorUid === -1 ? create$2() : activeTab.editorUid;
4865
4927
  editorUids[activeTab.id] = editorUid;
4866
4928
  const title = await createViewlet(viewletModuleIds[activeTab.id], editorUid, activeTab.id, bounds, activeTab.uri);
4867
4929
  if (title) {
@@ -5049,15 +5111,15 @@ const getMenuEntryArgs = groupId => {
5049
5111
  };
5050
5112
 
5051
5113
  const emptyObject = {};
5052
- const RE_PLACEHOLDER = /\{(PH\d+)\}/g;
5053
5114
  const i18nString = (key, placeholders = emptyObject) => {
5054
5115
  if (placeholders === emptyObject) {
5055
5116
  return key;
5056
5117
  }
5057
- const replacer = (match, rest) => {
5058
- return placeholders[rest];
5059
- };
5060
- return key.replaceAll(RE_PLACEHOLDER, replacer);
5118
+ let result = key;
5119
+ for (const [placeholder, replacement] of Object.entries(placeholders)) {
5120
+ result = result.split(`{${placeholder}}`).join(String(replacement));
5121
+ }
5122
+ return result;
5061
5123
  };
5062
5124
 
5063
5125
  const Close = 'Close';
@@ -6500,7 +6562,7 @@ const createEmptyGroup = () => {
6500
6562
  return {
6501
6563
  activeTabId: undefined,
6502
6564
  focused: false,
6503
- id: create$1(),
6565
+ id: create$2(),
6504
6566
  isEmpty: true,
6505
6567
  size: 100,
6506
6568
  tabs: []
@@ -6675,7 +6737,7 @@ const splitDown = (state, groupId) => {
6675
6737
 
6676
6738
  // If there are no groups, create an initial empty group first
6677
6739
  if (groups.length === 0) {
6678
- const initialGroupId = create$1();
6740
+ const initialGroupId = create$2();
6679
6741
  const initialGroup = {
6680
6742
  activeTabId: undefined,
6681
6743
  focused: true,
@@ -6710,7 +6772,7 @@ const splitLeft = (state, groupId) => {
6710
6772
  groups
6711
6773
  } = layout;
6712
6774
  if (groups.length === 0) {
6713
- const initialGroupId = create$1();
6775
+ const initialGroupId = create$2();
6714
6776
  const initialGroup = {
6715
6777
  activeTabId: undefined,
6716
6778
  focused: true,
@@ -6744,7 +6806,7 @@ const splitRight = (state, groupId) => {
6744
6806
 
6745
6807
  // If there are no groups, create an initial empty group first
6746
6808
  if (groups.length === 0) {
6747
- const initialGroupId = create$1();
6809
+ const initialGroupId = create$2();
6748
6810
  const initialGroup = {
6749
6811
  activeTabId: undefined,
6750
6812
  focused: true,
@@ -6775,7 +6837,7 @@ const splitUp = (state, groupId) => {
6775
6837
  groups
6776
6838
  } = layout;
6777
6839
  if (groups.length === 0) {
6778
- const initialGroupId = create$1();
6840
+ const initialGroupId = create$2();
6779
6841
  const initialGroup = {
6780
6842
  activeTabId: undefined,
6781
6843
  focused: true,
@@ -6838,7 +6900,7 @@ const commandMap = {
6838
6900
  'MainArea.copyIntoNewWindow': wrapSerialCommand(copyIntoNewWindow$1),
6839
6901
  'MainArea.copyPath': wrapSerialCommand(copyPath$1),
6840
6902
  'MainArea.copyRelativePath': wrapSerialCommand(copyRelativePath$1),
6841
- 'MainArea.create': create$2,
6903
+ 'MainArea.create': create$1,
6842
6904
  'MainArea.diff2': diff2,
6843
6905
  'MainArea.flipEditorLayout': wrapSerialCommand(flipLayout),
6844
6906
  'MainArea.focusNext': wrapSerialCommand(focusNextTab),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lvce-editor/main-area-worker",
3
- "version": "9.27.0",
3
+ "version": "9.28.0",
4
4
  "description": "Main Area Worker",
5
5
  "repository": {
6
6
  "type": "git",