@eclipse-lyra/core 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/api/base-classes.d.ts +7 -0
  2. package/dist/api/base-classes.d.ts.map +1 -0
  3. package/dist/api/constants.d.ts +2 -0
  4. package/dist/api/constants.d.ts.map +1 -0
  5. package/dist/api/index.d.ts +6 -0
  6. package/dist/api/index.d.ts.map +1 -0
  7. package/dist/api/index.js +84 -0
  8. package/dist/api/index.js.map +1 -0
  9. package/dist/api/services.d.ts +28 -0
  10. package/dist/api/services.d.ts.map +1 -0
  11. package/dist/api/types.d.ts +11 -0
  12. package/dist/api/types.d.ts.map +1 -0
  13. package/dist/commands/editor.d.ts +1 -0
  14. package/dist/commands/editor.d.ts.map +1 -0
  15. package/dist/commands/files.d.ts +2 -0
  16. package/dist/commands/files.d.ts.map +1 -0
  17. package/dist/commands/global.d.ts +1 -0
  18. package/dist/commands/global.d.ts.map +1 -0
  19. package/dist/commands/index.d.ts +1 -0
  20. package/dist/commands/index.d.ts.map +1 -0
  21. package/dist/commands/open-view-as-editor.d.ts +2 -0
  22. package/dist/commands/open-view-as-editor.d.ts.map +1 -0
  23. package/dist/commands/version-info.d.ts +2 -0
  24. package/dist/commands/version-info.d.ts.map +1 -0
  25. package/dist/components/app-selector.d.ts +17 -0
  26. package/dist/components/app-selector.d.ts.map +1 -0
  27. package/dist/components/app-switcher.d.ts +13 -0
  28. package/dist/components/app-switcher.d.ts.map +1 -0
  29. package/dist/components/command.d.ts +31 -0
  30. package/dist/components/command.d.ts.map +1 -0
  31. package/dist/components/extensions.d.ts +32 -0
  32. package/dist/components/extensions.d.ts.map +1 -0
  33. package/dist/components/fastviews.d.ts +34 -0
  34. package/dist/components/fastviews.d.ts.map +1 -0
  35. package/dist/components/filebrowser.d.ts +45 -0
  36. package/dist/components/filebrowser.d.ts.map +1 -0
  37. package/dist/components/index.d.ts +1 -0
  38. package/dist/components/index.d.ts.map +1 -0
  39. package/dist/components/language-selector.d.ts +12 -0
  40. package/dist/components/language-selector.d.ts.map +1 -0
  41. package/dist/components/log-terminal.d.ts +36 -0
  42. package/dist/components/log-terminal.d.ts.map +1 -0
  43. package/dist/components/part-name.d.ts +12 -0
  44. package/dist/components/part-name.d.ts.map +1 -0
  45. package/dist/components/tasks.d.ts +13 -0
  46. package/dist/components/tasks.d.ts.map +1 -0
  47. package/dist/contributions/default-ui-contributions.d.ts +2 -0
  48. package/dist/contributions/default-ui-contributions.d.ts.map +1 -0
  49. package/dist/contributions/index.d.ts +1 -0
  50. package/dist/contributions/index.d.ts.map +1 -0
  51. package/dist/contributions/marketplace-catalog-contributions.d.ts +2 -0
  52. package/dist/contributions/marketplace-catalog-contributions.d.ts.map +1 -0
  53. package/dist/core/app-host-config.d.ts +7 -0
  54. package/dist/core/app-host-config.d.ts.map +1 -0
  55. package/dist/core/apploader.d.ts +214 -0
  56. package/dist/core/apploader.d.ts.map +1 -0
  57. package/dist/core/appstate.d.ts +12 -0
  58. package/dist/core/appstate.d.ts.map +1 -0
  59. package/dist/core/commandregistry.d.ts +79 -0
  60. package/dist/core/commandregistry.d.ts.map +1 -0
  61. package/dist/core/config.d.ts +15 -0
  62. package/dist/core/config.d.ts.map +1 -0
  63. package/dist/core/constants.d.ts +22 -0
  64. package/dist/core/constants.d.ts.map +1 -0
  65. package/dist/core/contributionregistry.d.ts +53 -0
  66. package/dist/core/contributionregistry.d.ts.map +1 -0
  67. package/dist/core/di.d.ts +18 -0
  68. package/dist/core/di.d.ts.map +1 -0
  69. package/dist/core/dialogservice.d.ts +33 -0
  70. package/dist/core/dialogservice.d.ts.map +1 -0
  71. package/dist/core/editorregistry.d.ts +87 -0
  72. package/dist/core/editorregistry.d.ts.map +1 -0
  73. package/dist/core/esmsh-service.d.ts +40 -0
  74. package/dist/core/esmsh-service.d.ts.map +1 -0
  75. package/dist/core/events.d.ts +7 -0
  76. package/dist/core/events.d.ts.map +1 -0
  77. package/dist/core/events.js +63 -0
  78. package/dist/core/events.js.map +1 -0
  79. package/dist/core/extensionregistry.d.ts +98 -0
  80. package/dist/core/extensionregistry.d.ts.map +1 -0
  81. package/dist/core/filesys/common.d.ts +122 -0
  82. package/dist/core/filesys/common.d.ts.map +1 -0
  83. package/dist/core/filesys/fs-access.d.ts +31 -0
  84. package/dist/core/filesys/fs-access.d.ts.map +1 -0
  85. package/dist/core/filesys/index.d.ts +5 -0
  86. package/dist/core/filesys/index.d.ts.map +1 -0
  87. package/dist/core/filesys/indexeddb.d.ts +41 -0
  88. package/dist/core/filesys/indexeddb.d.ts.map +1 -0
  89. package/dist/core/filesys/opfs.d.ts +14 -0
  90. package/dist/core/filesys/opfs.d.ts.map +1 -0
  91. package/dist/core/i18n.d.ts +50 -0
  92. package/dist/core/i18n.d.ts.map +1 -0
  93. package/dist/core/index.d.ts +1 -0
  94. package/dist/core/index.d.ts.map +1 -0
  95. package/dist/core/keybindings.d.ts +67 -0
  96. package/dist/core/keybindings.d.ts.map +1 -0
  97. package/dist/core/logger.d.ts +44 -0
  98. package/dist/core/logger.d.ts.map +1 -0
  99. package/dist/core/marketplaceregistry.d.ts +25 -0
  100. package/dist/core/marketplaceregistry.d.ts.map +1 -0
  101. package/dist/core/packageinfoservice.d.ts +16 -0
  102. package/dist/core/packageinfoservice.d.ts.map +1 -0
  103. package/dist/core/persistenceservice.d.ts +6 -0
  104. package/dist/core/persistenceservice.d.ts.map +1 -0
  105. package/dist/core/settingsservice.d.ts +54 -0
  106. package/dist/core/settingsservice.d.ts.map +1 -0
  107. package/dist/core/signals.d.ts +3 -0
  108. package/dist/core/signals.d.ts.map +1 -0
  109. package/dist/core/taskservice.d.ts +20 -0
  110. package/dist/core/taskservice.d.ts.map +1 -0
  111. package/dist/core/toast.d.ts +4 -0
  112. package/dist/core/toast.d.ts.map +1 -0
  113. package/dist/core/tree-utils.d.ts +16 -0
  114. package/dist/core/tree-utils.d.ts.map +1 -0
  115. package/dist/dialogs/confirm-dialog.d.ts +14 -0
  116. package/dist/dialogs/confirm-dialog.d.ts.map +1 -0
  117. package/dist/dialogs/index.d.ts +5 -0
  118. package/dist/dialogs/index.d.ts.map +1 -0
  119. package/dist/dialogs/info-dialog.d.ts +13 -0
  120. package/dist/dialogs/info-dialog.d.ts.map +1 -0
  121. package/dist/dialogs/navigable-info-dialog.d.ts +33 -0
  122. package/dist/dialogs/navigable-info-dialog.d.ts.map +1 -0
  123. package/dist/dialogs/prompt-dialog.d.ts +21 -0
  124. package/dist/dialogs/prompt-dialog.d.ts.map +1 -0
  125. package/dist/externals/lit.d.ts +21 -0
  126. package/dist/externals/lit.d.ts.map +1 -0
  127. package/dist/externals/lit.js +24 -0
  128. package/dist/externals/lit.js.map +1 -0
  129. package/dist/externals/third-party.d.ts +7 -0
  130. package/dist/externals/third-party.d.ts.map +1 -0
  131. package/dist/externals/third-party.js +7 -0
  132. package/dist/externals/third-party.js.map +1 -0
  133. package/dist/externals/webawesome.d.ts +1 -0
  134. package/dist/externals/webawesome.d.ts.map +1 -0
  135. package/dist/externals/webawesome.js +75 -0
  136. package/dist/externals/webawesome.js.map +1 -0
  137. package/dist/i18n/extensions.json.d.ts +42 -0
  138. package/dist/i18n/fastviews.json.d.ts +13 -0
  139. package/dist/i18n/filebrowser.json.d.ts +29 -0
  140. package/dist/i18n/index.d.ts +2 -0
  141. package/dist/i18n/index.d.ts.map +1 -0
  142. package/dist/i18n/logterminal.json.d.ts +45 -0
  143. package/dist/i18n/partname.json.d.ts +15 -0
  144. package/dist/i18n/tasks.json.d.ts +15 -0
  145. package/dist/i18n/workspace.json.d.ts +15 -0
  146. package/dist/icon-DN6fp0dg.js +487 -0
  147. package/dist/icon-DN6fp0dg.js.map +1 -0
  148. package/dist/index.d.ts +2 -0
  149. package/dist/index.d.ts.map +1 -0
  150. package/dist/index.js +84 -0
  151. package/dist/index.js.map +1 -0
  152. package/dist/layouts/standard-layout.d.ts +16 -0
  153. package/dist/layouts/standard-layout.d.ts.map +1 -0
  154. package/dist/nocontent-BhrN6yxJ.js +48 -0
  155. package/dist/nocontent-BhrN6yxJ.js.map +1 -0
  156. package/dist/parts/container.d.ts +4 -0
  157. package/dist/parts/container.d.ts.map +1 -0
  158. package/dist/parts/contextmenu.d.ts +50 -0
  159. package/dist/parts/contextmenu.d.ts.map +1 -0
  160. package/dist/parts/dialog-content.d.ts +9 -0
  161. package/dist/parts/dialog-content.d.ts.map +1 -0
  162. package/dist/parts/element.d.ts +36 -0
  163. package/dist/parts/element.d.ts.map +1 -0
  164. package/dist/parts/index.d.ts +1 -0
  165. package/dist/parts/index.d.ts.map +1 -0
  166. package/dist/parts/index.js +3 -0
  167. package/dist/parts/index.js.map +1 -0
  168. package/dist/parts/part.d.ts +96 -0
  169. package/dist/parts/part.d.ts.map +1 -0
  170. package/dist/parts/resizable-grid.d.ts +31 -0
  171. package/dist/parts/resizable-grid.d.ts.map +1 -0
  172. package/dist/parts/tabs.d.ts +75 -0
  173. package/dist/parts/tabs.d.ts.map +1 -0
  174. package/dist/parts/toolbar.d.ts +35 -0
  175. package/dist/parts/toolbar.d.ts.map +1 -0
  176. package/dist/resizable-grid-BRH3MyZK.js +3813 -0
  177. package/dist/resizable-grid-BRH3MyZK.js.map +1 -0
  178. package/dist/standard-layout-BSGa06lP.js +4907 -0
  179. package/dist/standard-layout-BSGa06lP.js.map +1 -0
  180. package/dist/widgets/icon.d.ts +10 -0
  181. package/dist/widgets/icon.d.ts.map +1 -0
  182. package/dist/widgets/index.d.ts +1 -0
  183. package/dist/widgets/index.d.ts.map +1 -0
  184. package/dist/widgets/index.js +3 -0
  185. package/dist/widgets/index.js.map +1 -0
  186. package/dist/widgets/nocontent.d.ts +13 -0
  187. package/dist/widgets/nocontent.d.ts.map +1 -0
  188. package/dist/widgets/widget.d.ts +25 -0
  189. package/dist/widgets/widget.d.ts.map +1 -0
  190. package/package.json +81 -0
  191. package/src/api/base-classes.ts +10 -0
  192. package/src/api/constants.ts +3 -0
  193. package/src/api/index.ts +31 -0
  194. package/src/api/services.ts +58 -0
  195. package/src/api/types.ts +46 -0
  196. package/src/commands/editor.ts +1 -0
  197. package/src/commands/files.ts +425 -0
  198. package/src/commands/global.ts +198 -0
  199. package/src/commands/index.ts +6 -0
  200. package/src/commands/open-view-as-editor.ts +28 -0
  201. package/src/commands/version-info.ts +214 -0
  202. package/src/components/app-selector.ts +233 -0
  203. package/src/components/app-switcher.ts +126 -0
  204. package/src/components/command.ts +236 -0
  205. package/src/components/extensions.ts +615 -0
  206. package/src/components/fastviews.ts +314 -0
  207. package/src/components/filebrowser.ts +518 -0
  208. package/src/components/index.ts +9 -0
  209. package/src/components/language-selector.ts +166 -0
  210. package/src/components/log-terminal.ts +337 -0
  211. package/src/components/part-name.ts +54 -0
  212. package/src/components/tasks.ts +275 -0
  213. package/src/contributions/default-ui-contributions.ts +44 -0
  214. package/src/contributions/index.ts +3 -0
  215. package/src/contributions/marketplace-catalog-contributions.ts +6 -0
  216. package/src/core/app-host-config.ts +23 -0
  217. package/src/core/apploader.ts +630 -0
  218. package/src/core/appstate.ts +15 -0
  219. package/src/core/commandregistry.ts +210 -0
  220. package/src/core/config.ts +29 -0
  221. package/src/core/constants.ts +29 -0
  222. package/src/core/contributionregistry.ts +81 -0
  223. package/src/core/di.ts +54 -0
  224. package/src/core/dialogservice.ts +266 -0
  225. package/src/core/editorregistry.ts +347 -0
  226. package/src/core/esmsh-service.ts +404 -0
  227. package/src/core/events.ts +68 -0
  228. package/src/core/extensionregistry.ts +399 -0
  229. package/src/core/filesys/common.ts +474 -0
  230. package/src/core/filesys/fs-access.ts +339 -0
  231. package/src/core/filesys/index.ts +5 -0
  232. package/src/core/filesys/indexeddb.ts +596 -0
  233. package/src/core/filesys/opfs.ts +95 -0
  234. package/src/core/i18n.ts +221 -0
  235. package/src/core/index.ts +51 -0
  236. package/src/core/keybindings.ts +274 -0
  237. package/src/core/logger.ts +187 -0
  238. package/src/core/marketplaceregistry.ts +197 -0
  239. package/src/core/packageinfoservice.ts +56 -0
  240. package/src/core/persistenceservice.ts +46 -0
  241. package/src/core/settingsservice.ts +191 -0
  242. package/src/core/signals.ts +18 -0
  243. package/src/core/taskservice.ts +72 -0
  244. package/src/core/toast.ts +21 -0
  245. package/src/core/tree-utils.ts +24 -0
  246. package/src/dialogs/confirm-dialog.ts +72 -0
  247. package/src/dialogs/index.ts +4 -0
  248. package/src/dialogs/info-dialog.ts +67 -0
  249. package/src/dialogs/navigable-info-dialog.ts +256 -0
  250. package/src/dialogs/prompt-dialog.ts +123 -0
  251. package/src/externals/lit.ts +36 -0
  252. package/src/externals/third-party.ts +10 -0
  253. package/src/externals/webawesome.ts +76 -0
  254. package/src/i18n/extensions.json +39 -0
  255. package/src/i18n/fastviews.json +10 -0
  256. package/src/i18n/filebrowser.json +27 -0
  257. package/src/i18n/index.ts +25 -0
  258. package/src/i18n/logterminal.json +42 -0
  259. package/src/i18n/partname.json +12 -0
  260. package/src/i18n/tasks.json +12 -0
  261. package/src/i18n/workspace.json +12 -0
  262. package/src/icons/icons.txt +3 -0
  263. package/src/icons/js.svg +6 -0
  264. package/src/icons/jupyter.svg +18 -0
  265. package/src/icons/python.svg +15 -0
  266. package/src/index.ts +3 -0
  267. package/src/layouts/standard-layout.ts +174 -0
  268. package/src/parts/container.ts +4 -0
  269. package/src/parts/contextmenu.ts +266 -0
  270. package/src/parts/dialog-content.ts +31 -0
  271. package/src/parts/element.ts +100 -0
  272. package/src/parts/index.ts +5 -0
  273. package/src/parts/part.ts +158 -0
  274. package/src/parts/resizable-grid.ts +366 -0
  275. package/src/parts/tabs.ts +581 -0
  276. package/src/parts/toolbar.ts +260 -0
  277. package/src/vite-env.d.ts +16 -0
  278. package/src/widgets/icon.ts +38 -0
  279. package/src/widgets/index.ts +2 -0
  280. package/src/widgets/nocontent.ts +40 -0
  281. package/src/widgets/widget.ts +92 -0
@@ -0,0 +1,596 @@
1
+ import {
2
+ File,
3
+ Directory,
4
+ FileContentType,
5
+ type FileContentsOptions,
6
+ type GetResourceOptions,
7
+ type Resource,
8
+ TOPIC_WORKSPACE_CHANGED,
9
+ workspaceService,
10
+ } from "./common";
11
+ import { publish } from "../events";
12
+
13
+ type IDBEntryType = 'file' | 'dir';
14
+
15
+ interface IDBEntry {
16
+ type: IDBEntryType;
17
+ content?: Blob;
18
+ mimeType?: string;
19
+ }
20
+
21
+ const IDB_WORKSPACE_DB_NAME = 'eclipse-lyra-workspace-idb';
22
+ const IDB_WORKSPACE_STORE_NAME = 'files';
23
+
24
+ let idbWorkspacePromise: Promise<IDBDatabase> | null = null;
25
+
26
+ async function getWorkspaceIDB(): Promise<IDBDatabase> {
27
+ if (typeof indexedDB === 'undefined') {
28
+ throw new Error('IndexedDB is not available in this environment');
29
+ }
30
+ if (!idbWorkspacePromise) {
31
+ idbWorkspacePromise = new Promise((resolve, reject) => {
32
+ const request = indexedDB.open(IDB_WORKSPACE_DB_NAME, 1);
33
+ request.onerror = () => reject(request.error);
34
+ request.onsuccess = () => resolve(request.result);
35
+ request.onupgradeneeded = (e) => {
36
+ const db = (e.target as IDBOpenDBRequest).result;
37
+ if (!db.objectStoreNames.contains(IDB_WORKSPACE_STORE_NAME)) {
38
+ db.createObjectStore(IDB_WORKSPACE_STORE_NAME);
39
+ }
40
+ };
41
+ });
42
+ }
43
+ return idbWorkspacePromise;
44
+ }
45
+
46
+ async function getNextIndexedDBName(): Promise<string> {
47
+ const baseName = 'IndexedDB';
48
+ const folders = await workspaceService.getFolders();
49
+ const existingNames = new Set(
50
+ folders
51
+ .filter(f => f.type === 'indexeddb')
52
+ .map(f => f.name)
53
+ );
54
+
55
+ if (!existingNames.has(baseName)) {
56
+ return baseName;
57
+ }
58
+
59
+ let index = 1;
60
+ // Find the smallest n such that "IndexedDB (n)" is unused
61
+ // to keep names stable and predictable.
62
+ // This is O(k) in number of existing IndexedDB roots, which is small.
63
+ // We avoid gaps intentionally (e.g. after deleting "(1)") to keep UX simple.
64
+ while (existingNames.has(`${baseName} (${index})`)) {
65
+ index += 1;
66
+ }
67
+ return `${baseName} (${index})`;
68
+ }
69
+
70
+ function normalizePath(path: string): string {
71
+ if (!path) return '';
72
+ return path.split('/').filter(Boolean).join('/');
73
+ }
74
+
75
+ function joinPath(base: string, name: string): string {
76
+ const cleanBase = normalizePath(base);
77
+ const cleanName = normalizePath(name);
78
+ if (!cleanBase) return cleanName;
79
+ if (!cleanName) return cleanBase;
80
+ return `${cleanBase}/${cleanName}`;
81
+ }
82
+
83
+ function storageKey(rootId: string, path: string): string {
84
+ const norm = normalizePath(path);
85
+ return norm ? `${rootId}/${norm}` : rootId;
86
+ }
87
+
88
+ function storagePrefix(rootId: string, path: string): string {
89
+ const norm = normalizePath(path);
90
+ return norm ? `${rootId}/${norm}/` : `${rootId}/`;
91
+ }
92
+
93
+ async function idbGet(rootId: string, path: string): Promise<IDBEntry | undefined> {
94
+ const db = await getWorkspaceIDB();
95
+ const tx = db.transaction(IDB_WORKSPACE_STORE_NAME, 'readonly');
96
+ const store = tx.objectStore(IDB_WORKSPACE_STORE_NAME);
97
+ const key = path ? storageKey(rootId, path) : rootId;
98
+ return await new Promise((resolve, reject) => {
99
+ const req = store.get(key);
100
+ req.onsuccess = () => resolve(req.result as IDBEntry | undefined);
101
+ req.onerror = () => reject(req.error);
102
+ });
103
+ }
104
+
105
+ async function idbPut(rootId: string, path: string, entry: IDBEntry): Promise<void> {
106
+ const db = await getWorkspaceIDB();
107
+ const tx = db.transaction(IDB_WORKSPACE_STORE_NAME, 'readwrite');
108
+ const store = tx.objectStore(IDB_WORKSPACE_STORE_NAME);
109
+ const key = path ? storageKey(rootId, path) : rootId;
110
+ await new Promise<void>((resolve, reject) => {
111
+ const req = store.put(entry, key);
112
+ req.onsuccess = () => resolve();
113
+ req.onerror = () => reject(req.error);
114
+ });
115
+ }
116
+
117
+ async function idbDelete(rootId: string, path: string): Promise<void> {
118
+ const db = await getWorkspaceIDB();
119
+ const tx = db.transaction(IDB_WORKSPACE_STORE_NAME, 'readwrite');
120
+ const store = tx.objectStore(IDB_WORKSPACE_STORE_NAME);
121
+ const key = path ? storageKey(rootId, path) : rootId;
122
+ await new Promise<void>((resolve, reject) => {
123
+ const req = store.delete(key);
124
+ req.onsuccess = () => resolve();
125
+ req.onerror = () => reject(req.error);
126
+ });
127
+ }
128
+
129
+ async function idbDeleteTree(rootId: string, rootPath: string): Promise<void> {
130
+ const db = await getWorkspaceIDB();
131
+ const tx = db.transaction(IDB_WORKSPACE_STORE_NAME, 'readwrite');
132
+ const store = tx.objectStore(IDB_WORKSPACE_STORE_NAME);
133
+ const prefix = storageKey(rootId, rootPath);
134
+ const prefixWithSlash = prefix + '/';
135
+ const cursorReq = store.openCursor();
136
+
137
+ await new Promise<void>((resolve, reject) => {
138
+ cursorReq.onerror = () => reject(cursorReq.error);
139
+ cursorReq.onsuccess = (ev) => {
140
+ const cursor = (ev.target as IDBRequest<IDBCursorWithValue | null>).result;
141
+ if (!cursor) {
142
+ resolve();
143
+ return;
144
+ }
145
+ const key = String(cursor.key);
146
+ if (key === prefix || key.startsWith(prefixWithSlash)) {
147
+ cursor.delete();
148
+ }
149
+ cursor.continue();
150
+ };
151
+ });
152
+ }
153
+
154
+ async function idbRenameTree(rootId: string, oldPath: string, newPath: string): Promise<void> {
155
+ const db = await getWorkspaceIDB();
156
+ const tx = db.transaction(IDB_WORKSPACE_STORE_NAME, 'readwrite');
157
+ const store = tx.objectStore(IDB_WORKSPACE_STORE_NAME);
158
+ const oldPrefix = storageKey(rootId, oldPath);
159
+ const newPrefix = storageKey(rootId, newPath);
160
+ const cursorReq = store.openCursor();
161
+
162
+ const operations: Array<() => void> = [];
163
+
164
+ await new Promise<void>((resolve, reject) => {
165
+ cursorReq.onerror = () => reject(cursorReq.error);
166
+ cursorReq.onsuccess = (ev) => {
167
+ const cursor = (ev.target as IDBRequest<IDBCursorWithValue | null>).result;
168
+ if (!cursor) {
169
+ resolve();
170
+ return;
171
+ }
172
+ const key = String(cursor.key);
173
+ if (key === oldPrefix || key.startsWith(oldPrefix + '/')) {
174
+ const suffix = key.slice(oldPrefix.length);
175
+ const newKey = newPrefix + suffix;
176
+ const value = cursor.value as IDBEntry;
177
+ operations.push(() => {
178
+ cursor.delete();
179
+ store.put(value, newKey);
180
+ });
181
+ }
182
+ cursor.continue();
183
+ };
184
+ });
185
+
186
+ for (const op of operations) {
187
+ op();
188
+ }
189
+ }
190
+
191
+ async function idbListChildrenOfDir(rootId: string, dirPath: string): Promise<Array<{ name: string; entry: IDBEntry; type: IDBEntryType }>> {
192
+ const db = await getWorkspaceIDB();
193
+ const tx = db.transaction(IDB_WORKSPACE_STORE_NAME, 'readonly');
194
+ const store = tx.objectStore(IDB_WORKSPACE_STORE_NAME);
195
+ const prefix = storagePrefix(rootId, dirPath);
196
+ const cursorReq = store.openCursor();
197
+
198
+ const dirNames = new Set<string>();
199
+ const fileEntries = new Map<string, IDBEntry>();
200
+
201
+ await new Promise<void>((resolve, reject) => {
202
+ cursorReq.onerror = () => reject(cursorReq.error);
203
+ cursorReq.onsuccess = (ev) => {
204
+ const cursor = (ev.target as IDBRequest<IDBCursorWithValue | null>).result;
205
+ if (!cursor) {
206
+ resolve();
207
+ return;
208
+ }
209
+ const key = String(cursor.key);
210
+ const entry = cursor.value as IDBEntry;
211
+
212
+ if (!key.startsWith(prefix)) {
213
+ cursor.continue();
214
+ return;
215
+ }
216
+
217
+ const rest = key.slice(prefix.length);
218
+ if (!rest) {
219
+ cursor.continue();
220
+ return;
221
+ }
222
+
223
+ const idx = rest.indexOf('/');
224
+ const childName = idx === -1 ? rest : rest.slice(0, idx);
225
+
226
+ if (idx === -1) {
227
+ if (entry.type === 'dir') {
228
+ dirNames.add(childName);
229
+ } else {
230
+ fileEntries.set(childName, entry);
231
+ }
232
+ } else {
233
+ dirNames.add(childName);
234
+ }
235
+
236
+ cursor.continue();
237
+ };
238
+ });
239
+
240
+ const result: Array<{ name: string; entry: IDBEntry; type: IDBEntryType }> = [];
241
+ for (const name of dirNames) {
242
+ result.push({ name, entry: { type: 'dir' }, type: 'dir' });
243
+ }
244
+ for (const [name, entry] of fileEntries) {
245
+ if (!dirNames.has(name)) {
246
+ result.push({ name, entry, type: 'file' });
247
+ }
248
+ }
249
+ return result;
250
+ }
251
+
252
+ function getRootIdFromParent(parent: Directory): string {
253
+ return parent instanceof IDBDirectoryResource ? parent.getRootId() : '';
254
+ }
255
+
256
+ export class IDBFileResource extends File {
257
+ private readonly path: string;
258
+ private readonly parent: Directory;
259
+
260
+ constructor(path: string, parent: Directory) {
261
+ super();
262
+ this.path = normalizePath(path);
263
+ this.parent = parent;
264
+ }
265
+
266
+ getName(): string {
267
+ const parts = this.path.split('/');
268
+ return parts[parts.length - 1] || '';
269
+ }
270
+
271
+ getParent(): Directory {
272
+ return this.parent;
273
+ }
274
+
275
+ private getRootId(): string {
276
+ return getRootIdFromParent(this.parent);
277
+ }
278
+
279
+ async delete(): Promise<void> {
280
+ await idbDelete(this.getRootId(), this.path);
281
+ publish(TOPIC_WORKSPACE_CHANGED, workspaceService.getWorkspaceSync() ?? this.getWorkspace());
282
+ }
283
+
284
+ async getContents(options?: FileContentsOptions): Promise<any> {
285
+ const entry = await idbGet(this.getRootId(), this.path);
286
+ let raw = (entry as any)?.content as Blob | string | undefined;
287
+
288
+ if (typeof raw === 'string') {
289
+ const migratedBlob = new Blob([raw], { type: entry?.mimeType || 'text/plain' });
290
+ raw = migratedBlob;
291
+ if (entry) {
292
+ entry.content = migratedBlob;
293
+ await idbPut(this.getRootId(), this.path, entry);
294
+ }
295
+ }
296
+
297
+ if (!options || options.contentType === FileContentType.TEXT) {
298
+ if (!raw) {
299
+ return '';
300
+ }
301
+ return await raw.text();
302
+ }
303
+
304
+ let blob: Blob;
305
+ if (raw) {
306
+ blob = raw;
307
+ } else {
308
+ blob = new Blob([], { type: entry?.mimeType });
309
+ }
310
+
311
+ if (options.blob) {
312
+ return blob;
313
+ }
314
+
315
+ if (options.uri) {
316
+ return URL.createObjectURL(blob);
317
+ }
318
+
319
+ return blob.stream();
320
+ }
321
+
322
+ async saveContents(contents: any, _options?: FileContentsOptions): Promise<void> {
323
+ let blob: Blob;
324
+ let mimeType: string | undefined;
325
+
326
+ if (contents instanceof Blob) {
327
+ blob = contents;
328
+ mimeType = contents.type || undefined;
329
+ } else if (typeof contents === 'string') {
330
+ mimeType = 'text/plain';
331
+ blob = new Blob([contents], { type: mimeType });
332
+ } else {
333
+ const text = String(contents ?? '');
334
+ mimeType = 'text/plain';
335
+ blob = new Blob([text], { type: mimeType });
336
+ }
337
+
338
+ await idbPut(this.getRootId(), this.path, { type: 'file', content: blob, mimeType });
339
+ publish(TOPIC_WORKSPACE_CHANGED, workspaceService.getWorkspaceSync() ?? this.getWorkspace());
340
+ }
341
+
342
+ async size(): Promise<number | null> {
343
+ const entry = await idbGet(this.getRootId(), this.path);
344
+ const content = entry?.content;
345
+ if (!content) return null;
346
+ return content.size;
347
+ }
348
+
349
+ async copyTo(targetPath: string): Promise<void> {
350
+ const contents = await this.getContents({ blob: true });
351
+ const targetFile = await this.getWorkspace().getResource(targetPath, { create: true }) as File;
352
+ if (!targetFile) {
353
+ throw new Error(`Failed to create target file: ${targetPath}`);
354
+ }
355
+ await targetFile.saveContents(contents);
356
+ }
357
+
358
+ async rename(newName: string): Promise<void> {
359
+ if (this.getName() === newName) {
360
+ return;
361
+ }
362
+ const parentDir = this.getParent();
363
+ const parentPath = parentDir instanceof IDBDirectoryResource ? parentDir.getPath() : '';
364
+ const newPath = joinPath(parentPath, newName);
365
+
366
+ const rootId = this.getRootId();
367
+ const entry = await idbGet(rootId, this.path);
368
+ if (!entry) {
369
+ throw new Error('File not found in IndexedDB');
370
+ }
371
+
372
+ await idbDelete(rootId, this.path);
373
+ await idbPut(rootId, newPath, entry);
374
+
375
+ publish(TOPIC_WORKSPACE_CHANGED, workspaceService.getWorkspaceSync() ?? this.getWorkspace());
376
+ }
377
+ }
378
+
379
+ export class IDBDirectoryResource extends Directory {
380
+ private readonly path: string;
381
+ private readonly parent?: Directory;
382
+
383
+ constructor(path: string, parent?: Directory) {
384
+ super();
385
+ this.path = normalizePath(path);
386
+ this.parent = parent;
387
+ }
388
+
389
+ getPath(): string {
390
+ return this.path;
391
+ }
392
+
393
+ getName(): string {
394
+ if (!this.path) {
395
+ return '';
396
+ }
397
+ const parts = this.path.split('/');
398
+ return parts[parts.length - 1];
399
+ }
400
+
401
+ getParent(): Directory | undefined {
402
+ return this.parent;
403
+ }
404
+
405
+ getRoot(): IDBDirectoryResource {
406
+ const p = this.getParent();
407
+ if (!p) return this;
408
+ return (p as IDBDirectoryResource).getRoot();
409
+ }
410
+
411
+ getRootId(): string {
412
+ const r = this.getRoot();
413
+ return r instanceof IDBRootDirectory ? r.getRootId() : '';
414
+ }
415
+
416
+ async listChildren(_forceRefresh: boolean): Promise<Resource[]> {
417
+ const childrenInfo = await idbListChildrenOfDir(this.getRootId(), this.path);
418
+ const result: Resource[] = [];
419
+ for (const child of childrenInfo) {
420
+ const childPath = joinPath(this.path, child.name);
421
+ if (child.type === 'dir') {
422
+ result.push(new IDBDirectoryResource(childPath, this));
423
+ } else {
424
+ result.push(new IDBFileResource(childPath, this));
425
+ }
426
+ }
427
+ return result;
428
+ }
429
+
430
+ async getResource(path: string, options?: GetResourceOptions): Promise<Resource | null> {
431
+ if (!path) {
432
+ throw new Error('No path provided');
433
+ }
434
+
435
+ const segments = path.split('/').filter(s => s.trim());
436
+ let currentDir: IDBDirectoryResource = this;
437
+
438
+ for (let i = 0; i < segments.length; i++) {
439
+ const segment = segments[i];
440
+ const isLast = i === segments.length - 1;
441
+ const currentPath = currentDir.getPath();
442
+ const candidatePath = joinPath(currentPath, segment);
443
+ const rootId = this.getRootId();
444
+
445
+ const entry = await idbGet(rootId, candidatePath);
446
+
447
+ if (!entry) {
448
+ if (!options?.create) {
449
+ return null;
450
+ }
451
+
452
+ if (isLast) {
453
+ await idbPut(rootId, candidatePath, { type: 'file', content: new Blob([]) });
454
+ publish(TOPIC_WORKSPACE_CHANGED, workspaceService.getWorkspaceSync() ?? this.getWorkspace());
455
+ return new IDBFileResource(candidatePath, currentDir);
456
+ }
457
+
458
+ await idbPut(rootId, candidatePath, { type: 'dir' });
459
+ currentDir = new IDBDirectoryResource(candidatePath, currentDir);
460
+ continue;
461
+ }
462
+
463
+ if (isLast) {
464
+ if (entry.type === 'dir') {
465
+ return new IDBDirectoryResource(candidatePath, currentDir);
466
+ }
467
+ return new IDBFileResource(candidatePath, currentDir);
468
+ }
469
+
470
+ if (entry.type !== 'dir') {
471
+ return null;
472
+ }
473
+
474
+ currentDir = new IDBDirectoryResource(candidatePath, currentDir);
475
+ }
476
+
477
+ return currentDir;
478
+ }
479
+
480
+ touch(): void {
481
+ publish(TOPIC_WORKSPACE_CHANGED, workspaceService.getWorkspaceSync() ?? this.getWorkspace());
482
+ }
483
+
484
+ async delete(name?: string, _recursive: boolean = true): Promise<void> {
485
+ if (!name) {
486
+ const parent = this.getParent();
487
+ if (parent instanceof IDBDirectoryResource) {
488
+ await parent.delete(this.getName());
489
+ return;
490
+ }
491
+ return;
492
+ }
493
+
494
+ const targetPath = joinPath(this.path, name);
495
+ await idbDeleteTree(this.getRootId(), targetPath);
496
+ publish(TOPIC_WORKSPACE_CHANGED, workspaceService.getWorkspaceSync() ?? this.getWorkspace());
497
+ }
498
+
499
+ async copyTo(targetPath: string): Promise<void> {
500
+ for (const resource of await this.listChildren(false)) {
501
+ const childTarget = [targetPath, resource.getName()].join('/');
502
+ await resource.copyTo(childTarget);
503
+ }
504
+ }
505
+
506
+ async rename(newName: string): Promise<void> {
507
+ if (this.getName() === newName) {
508
+ return;
509
+ }
510
+ const parentDir = this.getParent();
511
+ if (!(parentDir instanceof IDBDirectoryResource)) {
512
+ throw new Error('Cannot rename IndexedDB root directory');
513
+ }
514
+ const oldPath = this.getPath();
515
+ const newPath = joinPath(parentDir.getPath(), newName);
516
+ await idbRenameTree(this.getRootId(), oldPath, newPath);
517
+ publish(TOPIC_WORKSPACE_CHANGED, workspaceService.getWorkspaceSync() ?? this.getWorkspace());
518
+ }
519
+ }
520
+
521
+ export class IDBRootDirectory extends IDBDirectoryResource {
522
+ private readonly displayName: string;
523
+ private readonly rootId: string;
524
+
525
+ constructor(displayName: string, rootId: string) {
526
+ super('');
527
+ this.displayName = displayName || 'IndexedDB';
528
+ this.rootId = rootId;
529
+ }
530
+
531
+ getRootId(): string {
532
+ return this.rootId;
533
+ }
534
+
535
+ getName(): string {
536
+ return this.displayName;
537
+ }
538
+
539
+ getParent(): Directory | undefined {
540
+ return undefined;
541
+ }
542
+
543
+ async rename(_newName: string): Promise<void> {
544
+ const name = String(_newName ?? '').trim();
545
+ if (!name || name === this.displayName) {
546
+ return;
547
+ }
548
+ // Update the in-memory display name and persist the change via workspaceService.
549
+ (this as any).displayName = name;
550
+ await workspaceService.updateFolderName(this, name);
551
+ }
552
+ }
553
+
554
+ function generateRootId(): string {
555
+ return typeof crypto !== 'undefined' && crypto.randomUUID
556
+ ? crypto.randomUUID()
557
+ : 'default-' + Math.random().toString(36).slice(2) + Date.now().toString(36);
558
+ }
559
+
560
+ // Register IndexedDB workspace contribution
561
+ workspaceService.registerContribution({
562
+ type: 'indexeddb',
563
+ name: 'idb',
564
+
565
+ canHandle(input: any): boolean {
566
+ return input && typeof input === 'object' && input.indexeddb === true;
567
+ },
568
+
569
+ async connect(input: { indexeddb: true; name?: string }): Promise<Directory> {
570
+ await getWorkspaceIDB();
571
+ const explicitName = input.name && String(input.name).trim();
572
+ const name = explicitName && explicitName.length > 0
573
+ ? explicitName
574
+ : await getNextIndexedDBName();
575
+ const rootId = generateRootId();
576
+ return new IDBRootDirectory(name, rootId);
577
+ },
578
+
579
+ async restore(data: any): Promise<Directory | undefined> {
580
+ if (data && typeof data === 'object' && data.indexeddb === true && data.rootId) {
581
+ await getWorkspaceIDB();
582
+ const name = (data.name && String(data.name).trim()) || 'IndexedDB';
583
+ return new IDBRootDirectory(name, String(data.rootId));
584
+ }
585
+ return undefined;
586
+ },
587
+
588
+ async persist(workspace: Directory): Promise<any> {
589
+ if (workspace instanceof IDBRootDirectory) {
590
+ return { indexeddb: true, name: workspace.getName(), rootId: workspace.getRootId() };
591
+ }
592
+ return null;
593
+ }
594
+ });
595
+
596
+
@@ -0,0 +1,95 @@
1
+ import { publish } from "../events";
2
+ import {
3
+ Directory,
4
+ type GetResourceOptions,
5
+ type Resource,
6
+ TOPIC_WORKSPACE_CHANGED,
7
+ workspaceService,
8
+ } from "./common";
9
+
10
+ const OPFS_DISPLAY_NAME = '.opfs';
11
+
12
+ async function getOPFSRoot(): Promise<FileSystemDirectoryHandle> {
13
+ if (typeof navigator === 'undefined' || !navigator.storage?.getDirectory) {
14
+ throw new Error('OPFS is not available in this environment');
15
+ }
16
+ return await navigator.storage.getDirectory();
17
+ }
18
+
19
+ export class OPFSRootDirectory extends Directory {
20
+ constructor(private readonly inner: Directory) {
21
+ super();
22
+ }
23
+
24
+ getName(): string {
25
+ return OPFS_DISPLAY_NAME;
26
+ }
27
+
28
+ getParent(): Directory | undefined {
29
+ return this.inner.getParent();
30
+ }
31
+
32
+ async listChildren(forceRefresh: boolean): Promise<Resource[]> {
33
+ return this.inner.listChildren(forceRefresh);
34
+ }
35
+
36
+ async getResource(path: string, options?: GetResourceOptions): Promise<Resource | null> {
37
+ return this.inner.getResource(path, options);
38
+ }
39
+
40
+ touch(): void {
41
+ this.inner.touch();
42
+ }
43
+
44
+ async delete(name?: string, recursive?: boolean): Promise<void> {
45
+ return this.inner.delete(name, recursive);
46
+ }
47
+
48
+ async copyTo(targetPath: string): Promise<void> {
49
+ return this.inner.copyTo(targetPath);
50
+ }
51
+
52
+ async rename(newName: string): Promise<void> {
53
+ return this.inner.rename(newName);
54
+ }
55
+ }
56
+
57
+ // Register OPFS workspace contribution
58
+ workspaceService.registerContribution({
59
+ type: 'opfs',
60
+ name: 'opfs',
61
+
62
+ canHandle(input: any): boolean {
63
+ return input && typeof input === 'object' && input.opfs === true;
64
+ },
65
+
66
+ async connect(_input: { opfs: true }): Promise<Directory> {
67
+ const root = await getOPFSRoot();
68
+ // We wrap the underlying FileSysDirHandleResource root in an OPFSRootDirectory
69
+ // for a stable display name and clear separation in the workspace tree.
70
+ const fsModule = await import('./fs-access');
71
+ const FileSysDirHandleResource = fsModule.FileSysDirHandleResource;
72
+ const inner = new FileSysDirHandleResource(root);
73
+ return new OPFSRootDirectory(inner);
74
+ },
75
+
76
+ async restore(data: any): Promise<Directory | undefined> {
77
+ if (data && typeof data === 'object' && data.opfs === true) {
78
+ const root = await getOPFSRoot();
79
+ const fsModule = await import('./fs-access');
80
+ const FileSysDirHandleResource = fsModule.FileSysDirHandleResource;
81
+ const inner = new FileSysDirHandleResource(root);
82
+ return new OPFSRootDirectory(inner);
83
+ }
84
+ return undefined;
85
+ },
86
+
87
+ async persist(workspace: Directory): Promise<any> {
88
+ if (workspace instanceof OPFSRootDirectory) {
89
+ return { opfs: true };
90
+ }
91
+ return null;
92
+ }
93
+ });
94
+
95
+