@lvce-editor/main-area-worker 9.27.0 → 9.28.1

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,700 +1905,769 @@ 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) => ({
2039
- group,
2040
- groupIndex: groups.findIndex(g => g.id === targetGroupId),
2041
- tab,
2042
- tabIndex
2043
- })).filter(entry => entry.tab.id !== activeTabId);
2044
- const newGroups = groups.map(g => {
2045
- if (g.id === targetGroupId) {
2046
- const newTabs = g.tabs.filter(tab => tab.id === activeTabId);
2047
- return {
2048
- ...g,
2049
- activeTabId,
2050
- isEmpty: newTabs.length === 0,
2051
- tabs: newTabs
2052
- };
2053
- }
2054
- return g;
2055
- });
2056
- return {
2057
- ...addClosedTabs(state, closedTabs),
2058
- layout: {
2059
- ...layout,
2060
- groups: newGroups
2061
- }
2062
- };
2103
+ return undefined;
2063
2104
  };
2064
2105
 
2065
- const getNextActiveTabId = (tabs, newTabs, activeTabId) => {
2066
- if (activeTabId === undefined) {
2067
- return undefined;
2068
- }
2069
- if (newTabs.some(tab => tab.id === activeTabId)) {
2070
- return activeTabId;
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');
2071
2114
  }
2072
- const activeTabIndex = tabs.findIndex(tab => tab.id === activeTabId);
2073
- if (activeTabIndex === -1 || newTabs.length === 0) {
2074
- return undefined;
2115
+ // Find the tab by viewletRequestId
2116
+ const tab = findTabByEditorUid(state, editorUid);
2117
+ if (!tab) {
2118
+ // Tab was closed, dispose viewlet
2119
+ return state;
2075
2120
  }
2076
- return newTabs[Math.min(activeTabIndex, newTabs.length - 1)].id;
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;
2077
2134
  };
2078
- const closeSavedInGroup = group => {
2079
- const {
2080
- activeTabId,
2081
- tabs
2082
- } = group;
2083
- const newTabs = tabs.filter(tab => tab.isDirty);
2084
- if (newTabs.length === tabs.length) {
2085
- return group;
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
+ };
2086
2146
  }
2087
- const newActiveTabId = getNextActiveTabId(tabs, newTabs, activeTabId);
2147
+ const commands = [{
2148
+ instanceId: tab.editorUid,
2149
+ type: 'dispose'
2150
+ }];
2088
2151
  return {
2089
- ...group,
2090
- activeTabId: newActiveTabId,
2091
- isEmpty: newTabs.length === 0,
2092
- tabs: newTabs
2152
+ commands,
2153
+ newState: state
2093
2154
  };
2094
2155
  };
2095
- const closeSaved$1 = state => {
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
2096
2164
  const {
2097
- groups
2098
- } = state.layout;
2099
- const closedTabs = groups.flatMap((group, groupIndex) => {
2100
- return group.tabs.map((tab, tabIndex) => ({
2101
- group,
2102
- groupIndex,
2103
- tab,
2104
- tabIndex
2105
- })).filter(entry => !entry.tab.isDirty);
2106
- });
2107
- const newGroups = groups.map(closeSavedInGroup);
2108
- return withGroups(addClosedTabs(state, closedTabs), newGroups);
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;
2109
2171
  };
2110
2172
 
2111
- const closeFirstTabByUri = (state, uri) => {
2112
- const matchingGroup = state.layout.groups.find(group => {
2113
- return group.tabs.some(tab => tab.uri === uri);
2114
- });
2115
- const matchingTab = matchingGroup?.tabs.find(tab => tab.uri === uri);
2116
- if (!matchingGroup || !matchingTab) {
2117
- return undefined;
2118
- }
2119
- return closeTab(state, matchingGroup.id, matchingTab.id);
2173
+ const handleDetach = async command => {
2174
+ // Hides viewlet but keeps it alive
2175
+
2176
+ await invoke('Viewlet.detach', command.instanceId);
2120
2177
  };
2121
- const closeTabsByUris = (state, uris) => {
2122
- let currentState = state;
2123
- for (const uri of uris) {
2124
- let nextState = closeFirstTabByUri(currentState, uri);
2125
- while (nextState) {
2126
- currentState = nextState;
2127
- nextState = closeFirstTabByUri(currentState, uri);
2128
- }
2129
- }
2130
- return currentState;
2178
+
2179
+ const handleDispose = async command => {
2180
+ // Fully destroys viewlet
2181
+
2182
+ await invoke('Viewlet.dispose', command.instanceId);
2131
2183
  };
2132
2184
 
2133
- const getGroupById = (state, groupId) => {
2134
- return state.layout.groups.find(group => group.id === groupId);
2185
+ const handleSetBounds = async command => {
2186
+ await invoke('Viewlet.setBounds', command.instanceId, command.bounds);
2135
2187
  };
2136
2188
 
2137
- const closeTabsRight = (state, groupId) => {
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) => {
2138
2215
  const {
2139
2216
  layout
2140
2217
  } = 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;
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
+ }
2154
2246
  }
2155
- const activeTabIndex = tabs.findIndex(tab => tab.id === activeTabId);
2156
- if (activeTabIndex === -1) {
2157
- return state;
2247
+ if (commands.length > 0) {
2248
+ await executeViewletCommands(commands);
2158
2249
  }
2159
- const newTabs = tabs.slice(0, activeTabIndex + 1);
2160
- if (newTabs.length === tabs.length) {
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) {
2161
2260
  return state;
2162
2261
  }
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
- };
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);
2177
2266
  }
2178
- return g;
2179
- });
2180
- return withGroups(addClosedTabs(state, closedTabs), newGroups);
2267
+ }
2268
+ if (tab.uri?.startsWith('simple-browser://')) {
2269
+ return closeTabWithViewlet(state, groupId, tabId);
2270
+ }
2271
+ return closeTab(state, groupId, tabId);
2181
2272
  };
2182
2273
 
2183
- const findGroupById = (state, groupId) => {
2274
+ const getFocusedGroup = state => {
2184
2275
  const {
2185
2276
  layout
2186
2277
  } = state;
2187
2278
  const {
2188
2279
  groups
2189
2280
  } = layout;
2190
- return groups.find(group => group.id === groupId);
2281
+ return groups.find(group => group.focused);
2191
2282
  };
2192
2283
 
2193
- const copyIntoNewWindow$1 = async state => {
2194
- const {
2195
- activeGroupId
2196
- } = state.layout;
2197
- const group = findGroupById(state, activeGroupId ?? 0);
2198
- if (!group) {
2284
+ const closeActiveEditor = async state => {
2285
+ const focusedGroup = getFocusedGroup(state);
2286
+ if (!focusedGroup) {
2199
2287
  return state;
2200
2288
  }
2201
2289
  const {
2202
2290
  activeTabId
2203
- } = group;
2291
+ } = focusedGroup;
2204
2292
  if (activeTabId === undefined) {
2205
2293
  return state;
2206
2294
  }
2207
- const tab = group.tabs.find(tab => tab.id === activeTabId);
2208
- if (!tab?.uri) {
2209
- return state;
2295
+ return closeTabAndSave(state, focusedGroup.id, activeTabId);
2296
+ };
2297
+
2298
+ const disposeEditors = async editorUids => {
2299
+ // Dispose all child viewlets in the renderer worker
2300
+ for (const editorUid of editorUids) {
2301
+ try {
2302
+ await invoke('Viewlet.dispose', editorUid);
2303
+ } catch {
2304
+ // Silently ignore errors during disposal
2305
+ // Viewlet may already be disposed or renderer worker may not be available
2306
+ }
2210
2307
  }
2211
- await invoke('ElectronWindow.openNewWithUri', tab.uri);
2212
- return state;
2213
2308
  };
2214
2309
 
2215
- const copyPath$1 = async (state, path) => {
2216
- string(path);
2217
- await writeClipBoardText(path);
2218
- return state;
2310
+ const closeAll$1 = async state => {
2311
+ const editorUids = state.layout.groups.flatMap(group => group.tabs.map(tab => tab.editorUid)).filter(editorUid => editorUid !== -1);
2312
+ await disposeEditors(editorUids);
2313
+ const entries = state.layout.groups.flatMap((group, groupIndex) => {
2314
+ return group.tabs.map((tab, tabIndex) => ({
2315
+ group,
2316
+ groupIndex,
2317
+ tab,
2318
+ tabIndex
2319
+ }));
2320
+ });
2321
+ return withEmptyGroups(addClosedTabs(state, entries));
2219
2322
  };
2220
2323
 
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;
2324
+ const redistributeSizesWithRemainder = groups => {
2325
+ const baseSize = Math.floor(100 / groups.length);
2326
+ const remainder = 100 % groups.length;
2327
+ return groups.map((group, index) => ({
2328
+ ...group,
2329
+ size: baseSize + (index === groups.length - 1 ? remainder : 0)
2330
+ }));
2227
2331
  };
2228
2332
 
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;
2333
+ const closeEditorGroup$1 = (state, groupId) => {
2334
+ if (Number.isNaN(groupId)) {
2335
+ return state;
2240
2336
  }
2241
- if (direction === 'vertical') {
2242
- return Vertical;
2337
+ const {
2338
+ layout
2339
+ } = state;
2340
+ const {
2341
+ activeGroupId,
2342
+ groups
2343
+ } = layout;
2344
+ const groupIndex = getGroupIndexById(state, groupId);
2345
+ if (groupIndex === -1 || groups.length <= 1) {
2346
+ return state;
2243
2347
  }
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;
2348
+ const remainingGroups = groups.filter(group => group.id !== groupId);
2349
+ const redistributedGroups = redistributeSizesWithRemainder(remainingGroups);
2350
+ const newActiveGroupId = activeGroupId === groupId ? remainingGroups[0].id : activeGroupId;
2351
+ return withGroupsAndActiveGroup(state, redistributedGroups, newActiveGroupId);
2291
2352
  };
2292
2353
 
2293
- const isEqual = (oldState, newState) => {
2294
- return oldState.layout === newState.layout;
2354
+ const isFocused = group => {
2355
+ return group.focused;
2295
2356
  };
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
- }
2357
+ const closeFocusedTab = async state => {
2358
+ const {
2359
+ layout
2360
+ } = state;
2361
+ const {
2362
+ groups
2363
+ } = layout;
2364
+ const focusedGroup = groups.find(isFocused);
2365
+ if (!focusedGroup) {
2366
+ return state;
2311
2367
  }
2312
- return diffResult;
2313
- };
2314
-
2315
- const diff2 = uid => {
2316
2368
  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;
2369
+ activeTabId
2370
+ } = focusedGroup;
2371
+ if (activeTabId === undefined) {
2372
+ return state;
2336
2373
  }
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();
2374
+ return closeTabAndSave(state, focusedGroup.id, activeTabId);
2345
2375
  };
2346
2376
 
2347
- const updateTab = (state, tabId, updates) => {
2377
+ const closeOtherTabs = (state, groupId) => {
2348
2378
  const {
2349
2379
  layout
2350
2380
  } = state;
2351
2381
  const {
2382
+ activeGroupId,
2352
2383
  groups
2353
2384
  } = layout;
2354
- const updatedGroups = groups.map(group => {
2355
- const tabIndex = group.tabs.findIndex(t => t.id === tabId);
2356
- if (tabIndex === -1) {
2357
- return group;
2385
+ const targetGroupId = groupId ?? activeGroupId;
2386
+ if (targetGroupId === undefined) {
2387
+ return state;
2388
+ }
2389
+ const group = groups.find(g => g.id === targetGroupId);
2390
+ if (!group) {
2391
+ return state;
2392
+ }
2393
+ const {
2394
+ activeTabId
2395
+ } = group;
2396
+ if (activeTabId === undefined) {
2397
+ return state;
2398
+ }
2399
+ const closedTabs = group.tabs.map((tab, tabIndex) => ({
2400
+ group,
2401
+ groupIndex: groups.findIndex(g => g.id === targetGroupId),
2402
+ tab,
2403
+ tabIndex
2404
+ })).filter(entry => entry.tab.id !== activeTabId);
2405
+ const newGroups = groups.map(g => {
2406
+ if (g.id === targetGroupId) {
2407
+ const newTabs = g.tabs.filter(tab => tab.id === activeTabId);
2408
+ return {
2409
+ ...g,
2410
+ activeTabId,
2411
+ isEmpty: newTabs.length === 0,
2412
+ tabs: newTabs
2413
+ };
2358
2414
  }
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
- };
2415
+ return g;
2372
2416
  });
2373
2417
  return {
2374
- ...state,
2418
+ ...addClosedTabs(state, closedTabs),
2375
2419
  layout: {
2376
2420
  ...layout,
2377
- groups: updatedGroups
2421
+ groups: newGroups
2378
2422
  }
2379
2423
  };
2380
2424
  };
2381
2425
 
2382
- const findTab = (state, tabId) => {
2383
- const {
2384
- layout
2385
- } = state;
2426
+ const getNextActiveTabId = (tabs, newTabs, activeTabId) => {
2427
+ if (activeTabId === undefined) {
2428
+ return undefined;
2429
+ }
2430
+ if (newTabs.some(tab => tab.id === activeTabId)) {
2431
+ return activeTabId;
2432
+ }
2433
+ const activeTabIndex = tabs.findIndex(tab => tab.id === activeTabId);
2434
+ if (activeTabIndex === -1 || newTabs.length === 0) {
2435
+ return undefined;
2436
+ }
2437
+ return newTabs[Math.min(activeTabIndex, newTabs.length - 1)].id;
2438
+ };
2439
+ const closeSavedInGroup = group => {
2386
2440
  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
- }
2441
+ activeTabId,
2442
+ tabs
2443
+ } = group;
2444
+ const newTabs = tabs.filter(tab => tab.isDirty);
2445
+ if (newTabs.length === tabs.length) {
2446
+ return group;
2394
2447
  }
2395
- return undefined;
2448
+ const newActiveTabId = getNextActiveTabId(tabs, newTabs, activeTabId);
2449
+ return {
2450
+ ...group,
2451
+ activeTabId: newActiveTabId,
2452
+ isEmpty: newTabs.length === 0,
2453
+ tabs: newTabs
2454
+ };
2396
2455
  };
2397
- const loadFileContent = async path => {
2398
- const content = await readFile(path);
2399
- return content;
2456
+ const closeSaved$1 = state => {
2457
+ const {
2458
+ groups
2459
+ } = state.layout;
2460
+ const closedTabs = groups.flatMap((group, groupIndex) => {
2461
+ return group.tabs.map((tab, tabIndex) => ({
2462
+ group,
2463
+ groupIndex,
2464
+ tab,
2465
+ tabIndex
2466
+ })).filter(entry => !entry.tab.isDirty);
2467
+ });
2468
+ const newGroups = groups.map(closeSavedInGroup);
2469
+ return withGroups(addClosedTabs(state, closedTabs), newGroups);
2400
2470
  };
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';
2471
+
2472
+ const closeFirstTabByUri = (state, uri) => {
2473
+ const matchingGroup = state.layout.groups.find(group => {
2474
+ return group.tabs.some(tab => tab.uri === uri);
2475
+ });
2476
+ const matchingTab = matchingGroup?.tabs.find(tab => tab.uri === uri);
2477
+ if (!matchingGroup || !matchingTab) {
2478
+ return undefined;
2407
2479
  }
2408
- return error.message;
2480
+ return closeTab(state, matchingGroup.id, matchingTab.id);
2409
2481
  };
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;
2482
+ const closeTabsByUris = (state, uris) => {
2483
+ let currentState = state;
2484
+ for (const uri of uris) {
2485
+ let nextState = closeFirstTabByUri(currentState, uri);
2486
+ while (nextState) {
2487
+ currentState = nextState;
2488
+ nextState = closeFirstTabByUri(currentState, uri);
2446
2489
  }
2447
- const errorMessage = getLoadFileErrorMessage(error);
2448
- return updateTab(latestState, tabId, {
2449
- errorMessage,
2450
- loadingState: 'error'
2451
- });
2452
2490
  }
2491
+ return currentState;
2453
2492
  };
2454
2493
 
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) {
2494
+ const getGroupById = (state, groupId) => {
2495
+ return state.layout.groups.find(group => group.id === groupId);
2496
+ };
2497
+
2498
+ const closeTabsRight = (state, groupId) => {
2499
+ const {
2500
+ layout
2501
+ } = state;
2502
+ const {
2503
+ groups
2504
+ } = layout;
2505
+ const group = getGroupById(state, groupId);
2506
+ if (!group) {
2462
2507
  return state;
2463
2508
  }
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') {
2509
+ const {
2510
+ activeTabId,
2511
+ tabs
2512
+ } = group;
2513
+ if (activeTabId === undefined) {
2467
2514
  return state;
2468
2515
  }
2469
- const editorUid = create$1();
2470
- const newState = updateTab(state, tabId, {
2471
- editorUid
2516
+ const activeTabIndex = tabs.findIndex(tab => tab.id === activeTabId);
2517
+ if (activeTabIndex === -1) {
2518
+ return state;
2519
+ }
2520
+ const newTabs = tabs.slice(0, activeTabIndex + 1);
2521
+ if (newTabs.length === tabs.length) {
2522
+ return state;
2523
+ }
2524
+ const groupIndex = groups.findIndex(g => g.id === groupId);
2525
+ const closedTabs = tabs.slice(activeTabIndex + 1).map((tab, index) => ({
2526
+ group,
2527
+ groupIndex,
2528
+ tab,
2529
+ tabIndex: activeTabIndex + index + 1
2530
+ }));
2531
+ const newGroups = groups.map(g => {
2532
+ if (g.id === groupId) {
2533
+ return {
2534
+ ...g,
2535
+ isEmpty: newTabs.length === 0,
2536
+ tabs: newTabs
2537
+ };
2538
+ }
2539
+ return g;
2472
2540
  });
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
- };
2541
+ return withGroups(addClosedTabs(state, closedTabs), newGroups);
2492
2542
  };
2493
2543
 
2494
- const findTabByEditorUid = (state, editorUid) => {
2544
+ const findGroupById = (state, groupId) => {
2495
2545
  const {
2496
2546
  layout
2497
2547
  } = state;
2498
2548
  const {
2499
2549
  groups
2500
2550
  } = 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;
2551
+ return groups.find(group => group.id === groupId);
2508
2552
  };
2509
2553
 
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');
2554
+ const copyIntoNewWindow$1 = async state => {
2555
+ const {
2556
+ activeGroupId
2557
+ } = state.layout;
2558
+ const group = findGroupById(state, activeGroupId ?? 0);
2559
+ if (!group) {
2560
+ return state;
2518
2561
  }
2519
- // Find the tab by viewletRequestId
2520
- const tab = findTabByEditorUid(state, editorUid);
2521
- if (!tab) {
2522
- // Tab was closed, dispose viewlet
2562
+ const {
2563
+ activeTabId
2564
+ } = group;
2565
+ if (activeTabId === undefined) {
2523
2566
  return state;
2524
2567
  }
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;
2568
+ const tab = group.tabs.find(tab => tab.id === activeTabId);
2569
+ if (!tab?.uri) {
2570
+ return state;
2571
+ }
2572
+ await invoke('ElectronWindow.openNewWithUri', tab.uri);
2573
+ return state;
2538
2574
  };
2539
2575
 
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;
2576
+ const copyPath$1 = async (state, path) => {
2577
+ string(path);
2578
+ await writeClipBoardText(path);
2579
+ return state;
2554
2580
  };
2555
2581
 
2556
- const handleDetach = async command => {
2557
- // Hides viewlet but keeps it alive
2582
+ const copyRelativePath$1 = async (state, path) => {
2583
+ string(path);
2584
+ // TODO use clipboard worker
2585
+ const relativePath = await invoke('Workspace.pathBaseName', path);
2586
+ await invoke('ClipBoard.writeText', relativePath);
2587
+ return state;
2588
+ };
2558
2589
 
2559
- await invoke('Viewlet.detach', command.instanceId);
2590
+ const Horizontal = 1;
2591
+ const Vertical = 2;
2592
+ const isLayoutDirection = direction => {
2593
+ return direction === Horizontal || direction === Vertical;
2594
+ };
2595
+ const normalizeLayoutDirection = direction => {
2596
+ if (isLayoutDirection(direction)) {
2597
+ return direction;
2598
+ }
2599
+ if (direction === 'horizontal') {
2600
+ return Horizontal;
2601
+ }
2602
+ if (direction === 'vertical') {
2603
+ return Vertical;
2604
+ }
2605
+ return undefined;
2560
2606
  };
2561
2607
 
2562
- const handleDispose = async command => {
2563
- // Fully destroys viewlet
2608
+ const create$1 = (uid, uri, x, y, width, height, platform, assetDir, tabHeight = 35) => {
2609
+ const state = {
2610
+ assetDir,
2611
+ closedTabs: [],
2612
+ fileIconCache: {},
2613
+ height,
2614
+ homeDirUri: '',
2615
+ iframes: [],
2616
+ initial: true,
2617
+ layout: {
2618
+ activeGroupId: undefined,
2619
+ direction: Horizontal,
2620
+ groups: []
2621
+ },
2622
+ maxOpenEditorGroups: Infinity,
2623
+ maxOpenEditors: Infinity,
2624
+ minGroupHeightPx: 80,
2625
+ minGroupWidthPx: 250,
2626
+ platform,
2627
+ splitButtonEnabled: false,
2628
+ tabHeight,
2629
+ uid,
2630
+ width,
2631
+ workspaceuri: uri,
2632
+ x,
2633
+ y
2634
+ };
2635
+ set(uid, state, state);
2636
+ };
2564
2637
 
2565
- await invoke('Viewlet.dispose', command.instanceId);
2638
+ const isEqual$1 = (oldState, newState) => {
2639
+ return false;
2566
2640
  };
2567
2641
 
2568
- const handleSetBounds = async command => {
2569
- await invoke('Viewlet.setBounds', command.instanceId, command.bounds);
2642
+ const isEqual = (oldState, newState) => {
2643
+ return oldState.layout === newState.layout;
2570
2644
  };
2571
2645
 
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;
2646
+ const RenderItems = 4;
2647
+ const RenderCss = 10;
2648
+ const RenderIncremental = 11;
2649
+
2650
+ const modules = [isEqual, isEqual$1];
2651
+ const numbers = [RenderIncremental, RenderCss];
2652
+
2653
+ const diff = (oldState, newState) => {
2654
+ const diffResult = [];
2655
+ for (let i = 0; i < modules.length; i++) {
2656
+ const fn = modules[i];
2657
+ if (!fn(oldState, newState)) {
2658
+ diffResult.push(numbers[i]);
2659
+ }
2589
2660
  }
2661
+ return diffResult;
2590
2662
  };
2591
- const executeViewletCommands = async commands => {
2592
- for (const command of commands) {
2593
- await executeViewletCommand(command);
2594
- }
2663
+
2664
+ const diff2 = uid => {
2665
+ const {
2666
+ newState,
2667
+ oldState
2668
+ } = get(uid);
2669
+ const result = diff(oldState, newState);
2670
+ return result;
2595
2671
  };
2596
2672
 
2597
2673
  const requestIdState = {
@@ -2609,6 +2685,7 @@ const getEditorInputEditorType = editorInput => {
2609
2685
  case 'process-explorer':
2610
2686
  case 'running-extensions':
2611
2687
  case 'video':
2688
+ case 'webview':
2612
2689
  return 'custom';
2613
2690
  case 'editor':
2614
2691
  return 'text';
@@ -2622,6 +2699,7 @@ const getEditorInputUri = editorInput => {
2622
2699
  case 'editor':
2623
2700
  case 'image':
2624
2701
  case 'video':
2702
+ case 'webview':
2625
2703
  return editorInput.uri;
2626
2704
  case 'extension-detail-view':
2627
2705
  return `extension-detail://${editorInput.extensionId}`;
@@ -2726,6 +2804,9 @@ const getNormalizedEditorInput = tab => {
2726
2804
  editorInput: tabEditorInput,
2727
2805
  uri: tabUri
2728
2806
  } = tab ?? {};
2807
+ if (tabEditorInput?.type === 'editor' && tabEditorInput.forceText || tabEditorInput?.type === 'webview') {
2808
+ return tabEditorInput;
2809
+ }
2729
2810
  let uri;
2730
2811
  if (typeof tabUri === 'string') {
2731
2812
  uri = tabUri;
@@ -2854,11 +2935,11 @@ const getSelectedTabBounds = state => {
2854
2935
  };
2855
2936
  };
2856
2937
 
2857
- const getViewletModuleId$1 = async uri => {
2938
+ const getViewletModuleId$1 = async (uri, opener) => {
2858
2939
  // Query RendererWorker for viewlet module ID (optional, may fail in tests)
2859
2940
  let viewletModuleId;
2860
2941
  try {
2861
- viewletModuleId = await invoke('Layout.getModuleId', uri);
2942
+ viewletModuleId = opener === undefined ? await invoke('Layout.getModuleId', uri) : await invoke('Layout.getModuleId', uri, opener);
2862
2943
  } catch {
2863
2944
  // Viewlet creation is optional - silently ignore if RendererWorker isn't available
2864
2945
  }
@@ -2877,15 +2958,18 @@ const getViewletModuleIdForEditorInput = async editorInput => {
2877
2958
  case 'diff-editor':
2878
2959
  return DiffEditor;
2879
2960
  case 'editor':
2961
+ return editorInput.forceText ? EditorText : getViewletModuleId$1(editorInput.uri);
2962
+ case 'extension-detail-view':
2963
+ return ExtensionDetail;
2880
2964
  case 'image':
2881
2965
  case 'video':
2882
2966
  return getViewletModuleId$1(editorInput.uri);
2883
- case 'extension-detail-view':
2884
- return ExtensionDetail;
2885
2967
  case 'process-explorer':
2886
2968
  return ProcessExplorer;
2887
2969
  case 'running-extensions':
2888
2970
  return RunningExtensions;
2971
+ case 'webview':
2972
+ return getViewletModuleId$1(editorInput.uri, editorInput.providerId);
2889
2973
  }
2890
2974
  };
2891
2975
 
@@ -3434,6 +3518,7 @@ const getEditorInputTitle = editorInput => {
3434
3518
  case 'editor':
3435
3519
  case 'image':
3436
3520
  case 'video':
3521
+ case 'webview':
3437
3522
  return getLabel(editorInput.uri);
3438
3523
  case 'extension-detail-view':
3439
3524
  return editorInput.extensionId;
@@ -3485,9 +3570,9 @@ const createEmptyGroup$1 = (state, uri, requestId, preview = false, title = getL
3485
3570
  const {
3486
3571
  groups
3487
3572
  } = layout;
3488
- const groupId = create$1();
3489
- const tabId = create$1();
3490
- const editorUid = create$1();
3573
+ const groupId = create$2();
3574
+ const tabId = create$2();
3575
+ const editorUid = create$2();
3491
3576
  const newTab = {
3492
3577
  editorInput,
3493
3578
  editorType,
@@ -3528,7 +3613,7 @@ const openTab = (state, groupId, tab) => {
3528
3613
  };
3529
3614
  const newTab = 'id' in normalizedTab && normalizedTab.id !== undefined ? normalizedTab : {
3530
3615
  ...normalizedTab,
3531
- id: create$1()
3616
+ id: create$2()
3532
3617
  };
3533
3618
  const {
3534
3619
  layout
@@ -3615,8 +3700,8 @@ const ensureActiveGroup = (state, uri, preview = false, title = getLabel(uri), e
3615
3700
  }
3616
3701
 
3617
3702
  // Create a new tab with the URI in the active group
3618
- const tabId = create$1();
3619
- const editorUid = create$1();
3703
+ const tabId = create$2();
3704
+ const editorUid = create$2();
3620
3705
  const newTab = {
3621
3706
  editorInput,
3622
3707
  editorType,
@@ -4149,7 +4234,7 @@ const splitEditorGroup$1 = (state, groupId, direction) => {
4149
4234
  if (!sourceGroup) {
4150
4235
  return state;
4151
4236
  }
4152
- const newGroupId = create$1();
4237
+ const newGroupId = create$2();
4153
4238
  const splitLayoutDirection = getSplitLayoutDirection(direction);
4154
4239
  const baseNewGroup = {
4155
4240
  activeTabId: undefined,
@@ -4366,8 +4451,8 @@ const newFile = async state => {
4366
4451
  getActiveTabId(newState);
4367
4452
 
4368
4453
  // Create a new empty tab
4369
- const tabId = create$1();
4370
- const editorUid = create$1();
4454
+ const tabId = create$2();
4455
+ const editorUid = create$2();
4371
4456
  const newTab = {
4372
4457
  editorType: 'text',
4373
4458
  editorUid,
@@ -4861,7 +4946,7 @@ const createViewlets = async (layout, viewletModuleIds, bounds) => {
4861
4946
  for (const group of layout.groups) {
4862
4947
  const activeTab = group.tabs.find(tab => tab.id === group.activeTabId);
4863
4948
  if (activeTab && viewletModuleIds[activeTab.id]) {
4864
- const editorUid = activeTab.editorUid === -1 ? create$1() : activeTab.editorUid;
4949
+ const editorUid = activeTab.editorUid === -1 ? create$2() : activeTab.editorUid;
4865
4950
  editorUids[activeTab.id] = editorUid;
4866
4951
  const title = await createViewlet(viewletModuleIds[activeTab.id], editorUid, activeTab.id, bounds, activeTab.uri);
4867
4952
  if (title) {
@@ -5049,15 +5134,15 @@ const getMenuEntryArgs = groupId => {
5049
5134
  };
5050
5135
 
5051
5136
  const emptyObject = {};
5052
- const RE_PLACEHOLDER = /\{(PH\d+)\}/g;
5053
5137
  const i18nString = (key, placeholders = emptyObject) => {
5054
5138
  if (placeholders === emptyObject) {
5055
5139
  return key;
5056
5140
  }
5057
- const replacer = (match, rest) => {
5058
- return placeholders[rest];
5059
- };
5060
- return key.replaceAll(RE_PLACEHOLDER, replacer);
5141
+ let result = key;
5142
+ for (const [placeholder, replacement] of Object.entries(placeholders)) {
5143
+ result = result.split(`{${placeholder}}`).join(String(replacement));
5144
+ }
5145
+ return result;
5061
5146
  };
5062
5147
 
5063
5148
  const Close = 'Close';
@@ -5141,7 +5226,7 @@ const copyPath = () => {
5141
5226
  const copyRelativePath = () => {
5142
5227
  return i18nString(CopyRelativePath);
5143
5228
  };
5144
- const reopenEditorWith = () => {
5229
+ const reopenEditorWith$1 = () => {
5145
5230
  return i18nString(ReopenEditorWith);
5146
5231
  };
5147
5232
  const share = () => {
@@ -5308,10 +5393,10 @@ const getMenuEntries$1 = state => {
5308
5393
  id: 'copyRelativePath',
5309
5394
  label: copyRelativePath()
5310
5395
  }, menuEntrySeparator, {
5311
- command: '',
5396
+ command: 'Main.reopenEditorWith',
5312
5397
  flags: None,
5313
5398
  id: 'reopenEditorWith',
5314
- label: reopenEditorWith()
5399
+ label: reopenEditorWith$1()
5315
5400
  }, menuEntrySeparator, {
5316
5401
  command: '',
5317
5402
  flags: None,
@@ -6396,6 +6481,101 @@ const renderEventListeners = () => {
6396
6481
  }];
6397
6482
  };
6398
6483
 
6484
+ const getViewProviderEntries = async uri => {
6485
+ const textEditorEntry = {
6486
+ id: 'editor',
6487
+ label: 'Text Editor',
6488
+ type: 'editor'
6489
+ };
6490
+ try {
6491
+ const providers = await invoke('WebView.getWebViews');
6492
+ const matchingProviders = providers.filter(provider => provider?.id && provider.selector?.some(selector => uri.endsWith(selector)));
6493
+ return [textEditorEntry, ...matchingProviders.map(provider => {
6494
+ const titleElement = provider.elements?.find(element => element.type === 'title');
6495
+ return {
6496
+ id: provider.id,
6497
+ label: provider.name || provider.title || titleElement?.value || provider.id,
6498
+ type: 'webview'
6499
+ };
6500
+ })];
6501
+ } catch {
6502
+ return [textEditorEntry];
6503
+ }
6504
+ };
6505
+ const reopenEditorWith = async context => {
6506
+ const initialState = context.getState();
6507
+ const {
6508
+ activeGroupId,
6509
+ groups
6510
+ } = initialState.layout;
6511
+ const activeGroup = groups.find(group => group.id === activeGroupId);
6512
+ const initialTab = activeGroup?.tabs.find(tab => tab.id === activeGroup.activeTabId);
6513
+ if (!initialTab?.uri) {
6514
+ return;
6515
+ }
6516
+ const entries = await getViewProviderEntries(initialTab.uri);
6517
+ const items = entries.map(entry => ({
6518
+ description: '',
6519
+ label: entry.label,
6520
+ value: entry
6521
+ }));
6522
+ const selected = await invoke('ExtensionHostQuickPick.showQuickPick', {
6523
+ items,
6524
+ placeholder: 'Select Editor'
6525
+ });
6526
+ if (!selected) {
6527
+ return;
6528
+ }
6529
+ const editorInput = selected.type === 'editor' ? {
6530
+ forceText: true,
6531
+ type: 'editor',
6532
+ uri: initialTab.uri
6533
+ } : {
6534
+ providerId: selected.id,
6535
+ type: 'webview',
6536
+ uri: initialTab.uri
6537
+ };
6538
+ const viewletModuleId = await getViewletModuleIdForEditorInput(editorInput);
6539
+ if (!viewletModuleId) {
6540
+ return;
6541
+ }
6542
+ const latestState = context.getState();
6543
+ const latestTab = findTabById(latestState, initialTab.id);
6544
+ if (!latestTab) {
6545
+ return;
6546
+ }
6547
+ const oldEditorUid = latestTab.tab.editorUid;
6548
+ const resetState = updateTab(latestState, initialTab.id, {
6549
+ editorInput,
6550
+ editorType: getEditorInputEditorType(editorInput),
6551
+ editorUid: -1,
6552
+ errorMessage: undefined,
6553
+ loadingState: undefined
6554
+ });
6555
+ const bounds = {
6556
+ height: resetState.height - resetState.tabHeight,
6557
+ width: resetState.width,
6558
+ x: resetState.x,
6559
+ y: resetState.y + resetState.tabHeight
6560
+ };
6561
+ const stateWithViewlet = createViewletForTab(resetState, initialTab.id);
6562
+ await context.updateState(() => stateWithViewlet);
6563
+ const tabWithViewlet = findTabById(stateWithViewlet, initialTab.id);
6564
+ if (!tabWithViewlet || tabWithViewlet.tab.editorUid === -1) {
6565
+ return;
6566
+ }
6567
+ const renderedTitle = await createViewlet(viewletModuleId, tabWithViewlet.tab.editorUid, initialTab.id, bounds, initialTab.uri);
6568
+ const readyState = handleViewletReady(context.getState(), tabWithViewlet.tab.editorUid, renderedTitle);
6569
+ await context.updateState(() => readyState);
6570
+ if (oldEditorUid !== -1) {
6571
+ try {
6572
+ await invoke('Viewlet.dispose', oldEditorUid);
6573
+ } catch {
6574
+ // Reopening should still proceed if the previous viewlet was already disposed.
6575
+ }
6576
+ }
6577
+ };
6578
+
6399
6579
  const getLatestStoredState = (uid, fallbackState, referenceTabId, referenceTabUri, allowMissingReference = false) => {
6400
6580
  const stateFromStore = get(uid);
6401
6581
  if (!stateFromStore) {
@@ -6500,7 +6680,7 @@ const createEmptyGroup = () => {
6500
6680
  return {
6501
6681
  activeTabId: undefined,
6502
6682
  focused: false,
6503
- id: create$1(),
6683
+ id: create$2(),
6504
6684
  isEmpty: true,
6505
6685
  size: 100,
6506
6686
  tabs: []
@@ -6675,7 +6855,7 @@ const splitDown = (state, groupId) => {
6675
6855
 
6676
6856
  // If there are no groups, create an initial empty group first
6677
6857
  if (groups.length === 0) {
6678
- const initialGroupId = create$1();
6858
+ const initialGroupId = create$2();
6679
6859
  const initialGroup = {
6680
6860
  activeTabId: undefined,
6681
6861
  focused: true,
@@ -6710,7 +6890,7 @@ const splitLeft = (state, groupId) => {
6710
6890
  groups
6711
6891
  } = layout;
6712
6892
  if (groups.length === 0) {
6713
- const initialGroupId = create$1();
6893
+ const initialGroupId = create$2();
6714
6894
  const initialGroup = {
6715
6895
  activeTabId: undefined,
6716
6896
  focused: true,
@@ -6744,7 +6924,7 @@ const splitRight = (state, groupId) => {
6744
6924
 
6745
6925
  // If there are no groups, create an initial empty group first
6746
6926
  if (groups.length === 0) {
6747
- const initialGroupId = create$1();
6927
+ const initialGroupId = create$2();
6748
6928
  const initialGroup = {
6749
6929
  activeTabId: undefined,
6750
6930
  focused: true,
@@ -6775,7 +6955,7 @@ const splitUp = (state, groupId) => {
6775
6955
  groups
6776
6956
  } = layout;
6777
6957
  if (groups.length === 0) {
6778
- const initialGroupId = create$1();
6958
+ const initialGroupId = create$2();
6779
6959
  const initialGroup = {
6780
6960
  activeTabId: undefined,
6781
6961
  focused: true,
@@ -6838,7 +7018,7 @@ const commandMap = {
6838
7018
  'MainArea.copyIntoNewWindow': wrapSerialCommand(copyIntoNewWindow$1),
6839
7019
  'MainArea.copyPath': wrapSerialCommand(copyPath$1),
6840
7020
  'MainArea.copyRelativePath': wrapSerialCommand(copyRelativePath$1),
6841
- 'MainArea.create': create$2,
7021
+ 'MainArea.create': create$1,
6842
7022
  'MainArea.diff2': diff2,
6843
7023
  'MainArea.flipEditorLayout': wrapSerialCommand(flipLayout),
6844
7024
  'MainArea.focusNext': wrapSerialCommand(focusNextTab),
@@ -6878,6 +7058,7 @@ const commandMap = {
6878
7058
  'MainArea.refresh': wrapSerialCommand(refresh),
6879
7059
  'MainArea.render2': render2,
6880
7060
  'MainArea.renderEventListeners': renderEventListeners,
7061
+ 'MainArea.reopenEditorWith': wrapSerialAsyncCommand(reopenEditorWith),
6881
7062
  'MainArea.resize': wrapGetter(handleResize),
6882
7063
  'MainArea.restoreClosedTab': wrapSerialCommand(restoreClosedTab),
6883
7064
  'MainArea.save': wrapSerialCommand(save),