@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,518 @@
1
+
2
+ import { css, html, TemplateResult, nothing } from 'lit'
3
+ import { customElement, state } from 'lit/decorators.js'
4
+ import { LyraPart } from "../parts/part";
5
+ import {
6
+ Directory,
7
+ File,
8
+ Resource,
9
+ TOPIC_WORKSPACE_CHANGED,
10
+ TOPIC_WORKSPACE_CONNECTED,
11
+ workspaceService
12
+ } from "../core/filesys";
13
+ import { when } from "lit/directives/when.js";
14
+ import { subscribe } from "../core/events";
15
+ import { createRef, ref } from "lit/directives/ref.js";
16
+ import { HIDE_DOT_RESOURCE } from "../core/constants";
17
+
18
+ import { TreeNode, treeNodeComparator } from "../core/tree-utils";
19
+ import { activeSelectionSignal } from "../core/appstate";
20
+ import { confirmDialog } from "../dialogs";
21
+ import { editorRegistry } from "../core/editorregistry";
22
+ import { TOPIC_CONTRIBUTEIONS_CHANGED, type ContributionChangeEvent } from '../core/contributionregistry';
23
+ import { i18n } from '../core/i18n';
24
+
25
+ const t = i18n('filebrowser');
26
+
27
+ const WORKSPACE_CHANGED_DEBOUNCE_MS = 250;
28
+
29
+ @customElement('lyra-filebrowser')
30
+ export class LyraFileBrowser extends LyraPart {
31
+
32
+ @state()
33
+ private root?: TreeNode;
34
+ @state()
35
+ private fileEditorOptions: Array<{ editorId: string; title: string; icon?: string }> = [];
36
+ private workspaceDir?: Directory
37
+ private treeRef = createRef<HTMLElement>();
38
+ private loadingNodes = new Set<TreeNode>();
39
+ private workspaceChangedDebounceId: ReturnType<typeof setTimeout> | undefined;
40
+ private pendingWorkspaceDir: Directory | undefined;
41
+
42
+ protected doBeforeUI() {
43
+ this.initializeWorkspace();
44
+
45
+ subscribe(TOPIC_CONTRIBUTEIONS_CHANGED, (event: ContributionChangeEvent) => {
46
+ if (event.target === 'system.icons') {
47
+ this.requestUpdate();
48
+ }
49
+ });
50
+
51
+ this.subscribe(TOPIC_WORKSPACE_CHANGED, (workspaceDir: Directory | undefined) => this.onWorkspaceChanged(workspaceDir));
52
+ this.subscribe(TOPIC_WORKSPACE_CONNECTED, (workspaceDir: Directory | undefined) => this.onWorkspaceConnected(workspaceDir));
53
+ }
54
+
55
+ disconnectedCallback() {
56
+ if (this.workspaceChangedDebounceId !== undefined) {
57
+ clearTimeout(this.workspaceChangedDebounceId);
58
+ this.workspaceChangedDebounceId = undefined;
59
+ }
60
+ this.pendingWorkspaceDir = undefined;
61
+ super.disconnectedCallback();
62
+ }
63
+
64
+ protected firstUpdated(changedProperties: Map<string, any>) {
65
+ super.firstUpdated(changedProperties);
66
+ this.setupDragAndDrop();
67
+ }
68
+
69
+ protected updated(changedProperties: Map<string, any>) {
70
+ super.updated(changedProperties);
71
+ if (changedProperties.has('workspaceDir') && this.workspaceDir) {
72
+ this.setupDragAndDrop();
73
+ }
74
+ }
75
+
76
+ private async initializeWorkspace() {
77
+ const workspaceDir = await workspaceService.getWorkspace()
78
+ await this.loadWorkspace(workspaceDir ?? undefined)
79
+ }
80
+
81
+ protected renderToolbar() {
82
+ return html`
83
+ <lyra-command icon="folder-open" title="${t('CONNECT_WORKSPACE')}" dropdown="filebrowser.connections"></lyra-command>
84
+ <lyra-command cmd="refresh_resource" icon="repeat" title="${t('REFRESH_RESOURCE')}"></lyra-command>
85
+ <lyra-command cmd="touch" icon="plus" title="${t('CREATE_NEW')}" dropdown="filebrowser.create"></lyra-command>
86
+ `;
87
+ }
88
+
89
+ protected renderContextMenu() {
90
+ const selection = activeSelectionSignal.get()
91
+ const file = selection instanceof File ? selection : null
92
+ const hasOpenWith = file && this.fileEditorOptions.length > 0
93
+ return html`
94
+ <lyra-command cmd="open_editor" icon="folder-open">${t('OPEN')}</lyra-command>
95
+ ${hasOpenWith ? html`
96
+ <wa-dropdown-item>
97
+ <lyra-icon name="folder-open" slot="icon"></lyra-icon>
98
+ ${t('OPEN_WITH')}
99
+ ${this.fileEditorOptions.map(opt => html`
100
+ <lyra-command
101
+ slot="submenu"
102
+ cmd="open_editor"
103
+ icon="${opt.icon ?? 'file'}"
104
+ .params=${{ path: file!.getWorkspacePath(), editorId: opt.editorId }}>
105
+ ${opt.title}
106
+ </lyra-command>
107
+ `)}
108
+ </wa-dropdown-item>
109
+ ` : nothing}
110
+ <lyra-command cmd="touch" icon="plus" dropdown="filebrowser.create">${t('CREATE_NEW')}</lyra-command>
111
+ `;
112
+ }
113
+
114
+ onWorkspaceChanged(workspaceDir: Directory | undefined) {
115
+ this.pendingWorkspaceDir = workspaceDir;
116
+ if (this.workspaceChangedDebounceId !== undefined) {
117
+ clearTimeout(this.workspaceChangedDebounceId);
118
+ }
119
+ this.workspaceChangedDebounceId = setTimeout(() => {
120
+ this.workspaceChangedDebounceId = undefined;
121
+ const dir = this.pendingWorkspaceDir;
122
+ this.pendingWorkspaceDir = undefined;
123
+ if (dir) this.applyWorkspaceChange(dir);
124
+ else this.loadWorkspace(undefined, true);
125
+ }, WORKSPACE_CHANGED_DEBOUNCE_MS);
126
+ }
127
+
128
+ private async applyWorkspaceChange(workspaceDir: Directory) {
129
+ activeSelectionSignal.set(undefined);
130
+ await this.loadWorkspace(workspaceDir, true);
131
+ await this.syncTreeSelection();
132
+ }
133
+
134
+ async onWorkspaceConnected(workspaceDir: Directory | undefined) {
135
+ await this.loadWorkspace(workspaceDir, true);
136
+ }
137
+
138
+ async loadWorkspace(workspaceDir?: Directory, forceRefresh = false) {
139
+ this.workspaceDir = workspaceDir
140
+ if (!workspaceDir) {
141
+ this.root = undefined
142
+ } else {
143
+ this.root = await this.resourceToTreeNode(workspaceDir, true, forceRefresh)
144
+ }
145
+ }
146
+
147
+ private async syncTreeSelection() {
148
+ await this.updateComplete
149
+ const waTree = this.treeRef.value?.querySelector('wa-tree')
150
+ // @ts-ignore
151
+ const selectedItems = waTree?.selectedItems || []
152
+ if (selectedItems.length > 0) {
153
+ // @ts-ignore
154
+ activeSelectionSignal.set(selectedItems[0].model?.data)
155
+ }
156
+ }
157
+
158
+ async resourceToTreeNode(resource: Resource, loadChildren = false, forceRefreshChildren = false): Promise<TreeNode> {
159
+ const isFile = resource instanceof File;
160
+ const node: TreeNode = {
161
+ data: resource,
162
+ label: resource.getName(),
163
+ leaf: isFile,
164
+ children: []
165
+ };
166
+
167
+ if (resource instanceof Directory && !resource.getParent()) {
168
+ // Root workspace directory: attach workspace metadata (name) so the
169
+ // tree renderer can show a small backend tag without knowing about
170
+ // concrete backend types.
171
+ try {
172
+ const info = await workspaceService.getFolderInfoForDirectory(resource);
173
+ if (info?.backendName) {
174
+ (node as any).workspaceTag = info.backendName;
175
+ }
176
+ } catch (e) {
177
+ // Fail silently; tag is purely cosmetic.
178
+ console.warn('Failed to get workspace info for directory', e);
179
+ }
180
+ }
181
+
182
+ if (resource instanceof Directory && loadChildren) {
183
+ for (const childResource of await resource.listChildren(forceRefreshChildren)) {
184
+ if (HIDE_DOT_RESOURCE && childResource.getName().startsWith(".")) {
185
+ continue
186
+ }
187
+ const child = await this.resourceToTreeNode(childResource, true);
188
+ node.children.push(child);
189
+ }
190
+ node.children.sort(treeNodeComparator)
191
+ }
192
+
193
+ return node;
194
+ }
195
+
196
+ createTreeItems(node: TreeNode, expanded = false): TemplateResult {
197
+ if (!node) {
198
+ return html``
199
+ }
200
+
201
+ const isLazy = !node.leaf && node.children.length === 0;
202
+ const resource = node.data as Resource;
203
+ const isFile = resource instanceof File;
204
+ const icon = isFile
205
+ ? editorRegistry.getFileIcon(resource.getName())
206
+ : (node.icon || "folder-open");
207
+ const workspaceTag = (node as any).workspaceTag as string | undefined;
208
+
209
+ return html`
210
+ <wa-tree-item
211
+ draggable=${isFile}
212
+ @dragstart=${isFile ? this.nobubble((e: Event) => this.onDragStart(e as DragEvent, resource as File)) : undefined}
213
+ @dblclick=${this.nobubble(this.onFileDoubleClicked)}
214
+ @wa-lazy-load=${this.nobubble((e: Event) => this.onLazyLoad(e, node))}
215
+ .model=${node}
216
+ ?expanded=${expanded}
217
+ ?lazy=${isLazy}>
218
+ <span class="tree-label">
219
+ <wa-icon name=${icon} label="${node.leaf ? t('FILE') : t('FOLDER')}"></wa-icon>
220
+ <span class="tree-label-text">${node.label}</span>
221
+ ${!node.leaf && workspaceTag
222
+ ? html`<wa-badge appearance="outlined" variant="neutral" style="font-size: var(--wa-font-size-xs);">${workspaceTag}</wa-badge>`
223
+ : null}
224
+ </span>
225
+ ${node.children.map(child => this.createTreeItems(child, false))}
226
+ </wa-tree-item>`
227
+ }
228
+
229
+ private onDragStart(e: DragEvent, file: File) {
230
+ if (!e.dataTransfer) return;
231
+
232
+ const filePath = file.getWorkspacePath();
233
+ const fileName = file.getName();
234
+
235
+ e.dataTransfer.effectAllowed = 'copy';
236
+ e.dataTransfer.setData('text/plain', filePath);
237
+ e.dataTransfer.setData('application/x-workspace-file', filePath);
238
+ e.dataTransfer.setData('text/uri-list', filePath);
239
+
240
+ if (e.dataTransfer.setDragImage) {
241
+ const dragImage = document.createElement('div');
242
+ dragImage.textContent = fileName;
243
+ dragImage.style.position = 'absolute';
244
+ dragImage.style.top = '-1000px';
245
+ dragImage.style.padding = '4px 8px';
246
+ dragImage.style.background = 'var(--wa-color-neutral-10)';
247
+ dragImage.style.border = '1px solid var(--wa-color-neutral-30)';
248
+ dragImage.style.borderRadius = '4px';
249
+ document.body.appendChild(dragImage);
250
+ e.dataTransfer.setDragImage(dragImage, 0, 0);
251
+ setTimeout(() => document.body.removeChild(dragImage), 0);
252
+ }
253
+ }
254
+
255
+ private async onLazyLoad(event: Event, node: TreeNode) {
256
+ const resource = node.data as Resource;
257
+ if (resource instanceof Directory && node.children.length === 0) {
258
+ await this.loadNodeChildren(node, resource);
259
+ }
260
+ }
261
+
262
+ private async loadNodeChildren(node: TreeNode, resource: Directory) {
263
+ if (this.loadingNodes.has(node)) {
264
+ return;
265
+ }
266
+
267
+ this.loadingNodes.add(node);
268
+ try {
269
+ for (const childResource of await resource.listChildren(false)) {
270
+ if (HIDE_DOT_RESOURCE && childResource.getName().startsWith(".")) {
271
+ continue
272
+ }
273
+ const child = await this.resourceToTreeNode(childResource, false);
274
+ node.children.push(child);
275
+ }
276
+ node.children.sort(treeNodeComparator);
277
+ this.requestUpdate();
278
+ } catch (error) {
279
+ console.error('Failed to load directory children:', error);
280
+ } finally {
281
+ this.loadingNodes.delete(node);
282
+ }
283
+ }
284
+
285
+ async onFileDoubleClicked(event: Event) {
286
+ // @ts-ignore
287
+ const node: TreeNode = event.currentTarget.model
288
+ const resource = node.data as Resource
289
+ if (resource instanceof File) {
290
+ activeSelectionSignal.set(resource)
291
+ this.executeCommand("open_editor", {})
292
+ }
293
+ }
294
+
295
+ onSelectionChanged(event: Event) {
296
+ // @ts-ignore
297
+ const selection = event.detail.selection
298
+ if (selection && selection.length > 0) {
299
+ const node: TreeNode = selection[0].model
300
+ const data = node.data
301
+ activeSelectionSignal.set(data)
302
+ if (data instanceof File) {
303
+ this.fileEditorOptions = editorRegistry.getEditorOptionsForInput(data)
304
+ this.updateContextMenu()
305
+ } else {
306
+ this.fileEditorOptions = []
307
+ this.updateContextMenu()
308
+ }
309
+ } else {
310
+ activeSelectionSignal.set(undefined)
311
+ this.fileEditorOptions = []
312
+ this.updateContextMenu()
313
+ }
314
+ }
315
+
316
+ private currentDropTarget?: HTMLElement;
317
+
318
+ private setupDragAndDrop() {
319
+ const treeElement = this.treeRef.value;
320
+ if (!treeElement) return;
321
+
322
+ const dragOverHandler = (e: DragEvent) => {
323
+ if (!e.dataTransfer?.types.includes('Files')) return;
324
+
325
+ e.preventDefault();
326
+ e.dataTransfer.dropEffect = 'copy';
327
+
328
+ treeElement.classList.add('drag-over');
329
+
330
+ const target = e.target as HTMLElement;
331
+ const treeItem = target.closest('wa-tree-item') as HTMLElement;
332
+
333
+ if (treeItem && treeItem !== this.currentDropTarget) {
334
+ this.currentDropTarget?.classList.remove('drop-target');
335
+ this.currentDropTarget = treeItem;
336
+ treeItem.classList.add('drop-target');
337
+ }
338
+ };
339
+
340
+ const dragEnterHandler = (e: DragEvent) => {
341
+ if (!e.dataTransfer?.types.includes('Files')) return;
342
+
343
+ e.preventDefault();
344
+ treeElement.classList.add('drag-over');
345
+ };
346
+
347
+ const dragLeaveHandler = (e: DragEvent) => {
348
+ const rect = treeElement.getBoundingClientRect();
349
+ const x = e.clientX;
350
+ const y = e.clientY;
351
+
352
+ if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
353
+ treeElement.classList.remove('drag-over');
354
+ this.currentDropTarget?.classList.remove('drop-target');
355
+ this.currentDropTarget = undefined;
356
+ }
357
+ };
358
+
359
+ const dropHandler = async (e: DragEvent) => {
360
+ e.preventDefault();
361
+ treeElement.classList.remove('drag-over');
362
+ this.currentDropTarget?.classList.remove('drop-target');
363
+ this.currentDropTarget = undefined;
364
+
365
+ if (!e.dataTransfer || !this.workspaceDir) return;
366
+
367
+ const files = Array.from(e.dataTransfer.files);
368
+ if (files.length === 0) return;
369
+
370
+ const targetDir = await this.getDropTarget(e);
371
+ await this.handleFilesDrop(files, targetDir);
372
+ };
373
+
374
+ treeElement.removeEventListener('dragover', dragOverHandler as any);
375
+ treeElement.removeEventListener('dragenter', dragEnterHandler as any);
376
+ treeElement.removeEventListener('dragleave', dragLeaveHandler as any);
377
+ treeElement.removeEventListener('drop', dropHandler as any);
378
+
379
+ treeElement.addEventListener('dragover', dragOverHandler);
380
+ treeElement.addEventListener('dragenter', dragEnterHandler);
381
+ treeElement.addEventListener('dragleave', dragLeaveHandler);
382
+ treeElement.addEventListener('drop', dropHandler);
383
+ }
384
+
385
+ private async getDropTarget(e: DragEvent): Promise<Directory> {
386
+ const target = e.target as HTMLElement;
387
+ const treeItem = target.closest('wa-tree-item');
388
+
389
+ if (treeItem) {
390
+ const node: TreeNode = (treeItem as any).model;
391
+ const resource = node.data as Resource;
392
+
393
+ if (resource instanceof Directory) {
394
+ return resource;
395
+ }
396
+ const parent = resource.getParent();
397
+ if (parent) {
398
+ return parent;
399
+ }
400
+ }
401
+
402
+ return this.workspaceDir!;
403
+ }
404
+
405
+ private async handleFilesDrop(files: globalThis.File[], targetDir: Directory) {
406
+ const total = files.length;
407
+ let processed = 0;
408
+ let failed = 0;
409
+ let skipped = 0;
410
+
411
+ for (const file of files) {
412
+ try {
413
+ const targetPath = this.buildTargetPath(targetDir, file.name);
414
+
415
+ const existingFile = await this.workspaceDir!.getResource(targetPath);
416
+ if (existingFile) {
417
+ const overwrite = await confirmDialog(t('FILE_EXISTS_OVERWRITE', { fileName: file.name }));
418
+ if (!overwrite) {
419
+ skipped++;
420
+ continue;
421
+ }
422
+ }
423
+
424
+ const workspaceFile = await this.workspaceDir!.getResource(
425
+ targetPath,
426
+ { create: true }
427
+ ) as File;
428
+
429
+ await workspaceFile.saveContents(file);
430
+
431
+ processed++;
432
+ } catch (error) {
433
+ console.error(`Failed to upload ${file.name}:`, error);
434
+ failed++;
435
+ }
436
+ }
437
+
438
+ console.log(`Uploaded ${processed}/${total} files${skipped > 0 ? `, ${skipped} skipped` : ''}${failed > 0 ? `, ${failed} failed` : ''}`);
439
+
440
+ await this.loadWorkspace(this.workspaceDir);
441
+ }
442
+
443
+ private buildTargetPath(targetDir: Directory, fileName: string): string {
444
+ const dirPath = targetDir.getWorkspacePath();
445
+ return dirPath ? `${dirPath}/${fileName}` : fileName;
446
+ }
447
+
448
+ render() {
449
+ return html`
450
+ <div class="tree" ${ref(this.treeRef)} style="--drop-files-text: '${t('DROP_FILES_HERE')}'">
451
+ ${when(!this.workspaceDir, () => html`
452
+ <lyra-no-content message="${t('SELECT_WORKSPACE')}"></lyra-no-content>`, () => when(this.root, () => html`
453
+ <wa-tree @wa-selection-change=${this.nobubble(this.onSelectionChanged)}
454
+ style="--indent-guide-width: 1px;">
455
+ ${this.root!.children.map(child => this.createTreeItems(child, true))}
456
+ </wa-tree>`, () => html``))}
457
+ </div>
458
+ `
459
+ }
460
+
461
+ static styles = css`
462
+ :host {
463
+ }
464
+
465
+ .tree {
466
+ height: 100%;
467
+ position: relative;
468
+ transition: all 0.2s ease;
469
+ }
470
+
471
+ .tree.drag-over {
472
+ background-color: var(--wa-color-brand-fill-quiet);
473
+ outline: 2px dashed var(--wa-color-brand-border-normal);
474
+ outline-offset: -4px;
475
+ border-radius: var(--wa-border-radius-medium);
476
+ }
477
+
478
+ .tree.drag-over::before {
479
+ content: var(--drop-files-text);
480
+ position: absolute;
481
+ top: 50%;
482
+ left: 50%;
483
+ transform: translate(-50%, -50%);
484
+ background: var(--wa-color-brand-fill-loud);
485
+ color: var(--wa-color-brand-on-loud);
486
+ padding: var(--wa-spacing-large);
487
+ border-radius: var(--wa-border-radius-large);
488
+ font-weight: bold;
489
+ pointer-events: none;
490
+ z-index: 1000;
491
+ opacity: 0.3;
492
+ }
493
+
494
+ .tree-label {
495
+ display: inline-flex;
496
+ align-items: center;
497
+ gap: 0.4rem;
498
+ }
499
+
500
+ .tree-label-text {
501
+ white-space: nowrap;
502
+ }
503
+
504
+ wa-tree-item.drop-target {
505
+ background-color: var(--wa-color-brand-fill-loud);
506
+ color: var(--wa-color-brand-on-loud);
507
+ border-radius: var(--wa-border-radius-small);
508
+ outline: 2px solid var(--wa-color-brand-border-loud);
509
+ outline-offset: -2px;
510
+ }
511
+ `;
512
+ }
513
+
514
+ declare global {
515
+ interface HTMLElementTagNameMap {
516
+ 'lyra-filebrowser': LyraFileBrowser
517
+ }
518
+ }
@@ -0,0 +1,9 @@
1
+ import "./filebrowser"
2
+ import "./tasks"
3
+ import "./part-name"
4
+ import "./extensions"
5
+ import "./log-terminal"
6
+ import "./command"
7
+ import "./fastviews"
8
+ import "./language-selector"
9
+ import "./app-switcher"
@@ -0,0 +1,166 @@
1
+ import { html, render, css } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import { LyraElement } from "../parts/element";
4
+ import { currentLanguageSignal, languageContributionsSignal, SETTINGS_KEY_LANGUAGE } from "../core/i18n";
5
+ import { appSettings } from "../core/settingsservice";
6
+
7
+ function getDialogContainer(): HTMLElement {
8
+ let container = document.getElementById('global-dialog-container');
9
+ if (!container) {
10
+ container = document.createElement('div');
11
+ container.id = 'global-dialog-container';
12
+ document.body.appendChild(container);
13
+ }
14
+ return container;
15
+ }
16
+
17
+ const getLanguageName = (code: string): string => {
18
+ try {
19
+ return new Intl.DisplayNames([code], { type: 'language' }).of(code) || code.toUpperCase();
20
+ } catch {
21
+ return code.toUpperCase();
22
+ }
23
+ };
24
+
25
+ const getAvailableLanguages = (): string[] => {
26
+ const contributions = languageContributionsSignal.get();
27
+ const languages = new Set<string>();
28
+
29
+ for (const contribution of contributions) {
30
+ if (contribution.namespace) {
31
+ const contributionObj = contribution as any;
32
+ for (const key in contributionObj) {
33
+ if (key !== 'namespace' && key !== 'label' && key !== 'language' && key !== 'translations' && typeof contributionObj[key] === 'object') {
34
+ languages.add(key);
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ return Array.from(languages).sort();
41
+ };
42
+
43
+ export const showLanguageSelectorDialog = async (): Promise<void> => {
44
+ const availableLanguages = getAvailableLanguages();
45
+ const currentLanguage = currentLanguageSignal.get();
46
+
47
+ return new Promise((resolve) => {
48
+ const container = getDialogContainer();
49
+
50
+ let isResolved = false;
51
+
52
+ const handleClose = () => {
53
+ const dialog = container.querySelector('wa-dialog') as any;
54
+ if (dialog && !isResolved) {
55
+ dialog.open = false;
56
+ }
57
+ };
58
+
59
+ const handleAfterHide = () => {
60
+ if (isResolved) return;
61
+ isResolved = true;
62
+ render(html``, container);
63
+ resolve();
64
+ };
65
+
66
+ const selectLanguage = async (language: string) => {
67
+ await appSettings.set(SETTINGS_KEY_LANGUAGE, language);
68
+ handleClose();
69
+ };
70
+
71
+ const template = html`
72
+ <wa-dialog
73
+ label="Select Language"
74
+ open
75
+ light-dismiss
76
+ @wa-request-close=${handleClose}
77
+ @wa-after-hide=${handleAfterHide}>
78
+ <style>
79
+ .language-list {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 0.5rem;
83
+ padding: 1rem;
84
+ min-width: 300px;
85
+ max-height: 400px;
86
+ overflow-y: auto;
87
+ }
88
+
89
+ .language-item {
90
+ display: flex;
91
+ align-items: center;
92
+ padding: 0.75rem;
93
+ border-radius: var(--wa-border-radius-small);
94
+ cursor: pointer;
95
+ transition: background-color 0.2s;
96
+ }
97
+
98
+ .language-item:hover {
99
+ background-color: var(--wa-color-neutral-fill-quiet);
100
+ }
101
+
102
+ .language-item.active {
103
+ background-color: var(--wa-color-brand-fill-quiet);
104
+ font-weight: 600;
105
+ }
106
+
107
+ .language-code {
108
+ font-family: monospace;
109
+ margin-right: 0.75rem;
110
+ min-width: 3rem;
111
+ color: var(--wa-color-neutral-600);
112
+ }
113
+
114
+ .language-name {
115
+ flex: 1;
116
+ }
117
+ </style>
118
+
119
+ <div class="language-list">
120
+ ${availableLanguages.map(lang => html`
121
+ <div
122
+ class="language-item ${lang === currentLanguage ? 'active' : ''}"
123
+ @click=${() => selectLanguage(lang)}>
124
+ <span class="language-code">${lang.toUpperCase()}</span>
125
+ <span class="language-name">${getLanguageName(lang)}</span>
126
+ </div>
127
+ `)}
128
+ </div>
129
+ </wa-dialog>
130
+ `;
131
+
132
+ render(template, container);
133
+ });
134
+ };
135
+
136
+ @customElement('lyra-language-selector')
137
+ export class LyraLanguageSelector extends LyraElement {
138
+ static styles = css`
139
+ :host {
140
+ display: inline-block;
141
+ }
142
+ `;
143
+
144
+ protected render() {
145
+ const currentLanguage = currentLanguageSignal.get();
146
+ const languageName = getLanguageName(currentLanguage);
147
+ const buttonLabel = `${currentLanguage.toUpperCase()} ${languageName}`;
148
+
149
+ return html`
150
+ <wa-button
151
+ appearance="plain"
152
+ size="small"
153
+ title="Current language: ${languageName}"
154
+ @click=${() => showLanguageSelectorDialog()}>
155
+ ${buttonLabel}
156
+ </wa-button>
157
+ `;
158
+ }
159
+ }
160
+
161
+ declare global {
162
+ interface HTMLElementTagNameMap {
163
+ 'lyra-language-selector': LyraLanguageSelector;
164
+ }
165
+ }
166
+