@theia/toolbar 1.45.0 → 1.46.0-next.72

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 (53) hide show
  1. package/README.md +32 -32
  2. package/lib/browser/abstract-toolbar-contribution.d.ts +16 -16
  3. package/lib/browser/abstract-toolbar-contribution.js +68 -68
  4. package/lib/browser/application-shell-with-toolbar-override.d.ts +15 -15
  5. package/lib/browser/application-shell-with-toolbar-override.js +101 -101
  6. package/lib/browser/codicons.d.ts +1 -1
  7. package/lib/browser/codicons.js +20 -20
  8. package/lib/browser/font-awesome-icons.d.ts +1 -1
  9. package/lib/browser/font-awesome-icons.js +20 -20
  10. package/lib/browser/package.spec.js +18 -18
  11. package/lib/browser/toolbar-command-contribution.d.ts +25 -25
  12. package/lib/browser/toolbar-command-contribution.js +211 -211
  13. package/lib/browser/toolbar-command-quick-input-service.d.ts +19 -19
  14. package/lib/browser/toolbar-command-quick-input-service.js +112 -112
  15. package/lib/browser/toolbar-constants.d.ts +23 -23
  16. package/lib/browser/toolbar-constants.js +75 -75
  17. package/lib/browser/toolbar-controller.d.ts +34 -34
  18. package/lib/browser/toolbar-controller.js +186 -186
  19. package/lib/browser/toolbar-defaults.d.ts +3 -3
  20. package/lib/browser/toolbar-defaults.js +60 -60
  21. package/lib/browser/toolbar-frontend-module.d.ts +4 -4
  22. package/lib/browser/toolbar-frontend-module.js +25 -25
  23. package/lib/browser/toolbar-icon-selector-dialog.d.ts +65 -65
  24. package/lib/browser/toolbar-icon-selector-dialog.js +235 -235
  25. package/lib/browser/toolbar-interfaces.d.ts +45 -45
  26. package/lib/browser/toolbar-interfaces.js +42 -42
  27. package/lib/browser/toolbar-preference-contribution.d.ts +9 -9
  28. package/lib/browser/toolbar-preference-contribution.js +34 -34
  29. package/lib/browser/toolbar-preference-schema.d.ts +5 -5
  30. package/lib/browser/toolbar-preference-schema.js +73 -73
  31. package/lib/browser/toolbar-storage-provider.d.ts +47 -47
  32. package/lib/browser/toolbar-storage-provider.js +357 -357
  33. package/lib/browser/toolbar.d.ts +56 -56
  34. package/lib/browser/toolbar.js +380 -380
  35. package/package.json +11 -11
  36. package/src/browser/abstract-toolbar-contribution.tsx +53 -53
  37. package/src/browser/application-shell-with-toolbar-override.ts +98 -98
  38. package/src/browser/codicons.ts +18 -18
  39. package/src/browser/font-awesome-icons.ts +18 -18
  40. package/src/browser/package.spec.ts +19 -19
  41. package/src/browser/style/toolbar.css +255 -255
  42. package/src/browser/toolbar-command-contribution.ts +211 -211
  43. package/src/browser/toolbar-command-quick-input-service.ts +86 -86
  44. package/src/browser/toolbar-constants.ts +79 -79
  45. package/src/browser/toolbar-controller.ts +185 -185
  46. package/src/browser/toolbar-defaults.ts +58 -58
  47. package/src/browser/toolbar-frontend-module.ts +30 -30
  48. package/src/browser/toolbar-icon-selector-dialog.tsx +296 -296
  49. package/src/browser/toolbar-interfaces.ts +76 -76
  50. package/src/browser/toolbar-preference-contribution.ts +38 -38
  51. package/src/browser/toolbar-preference-schema.ts +75 -75
  52. package/src/browser/toolbar-storage-provider.ts +352 -352
  53. package/src/browser/toolbar.tsx +424 -424
@@ -1,352 +1,352 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2022 Ericsson and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import * as jsoncParser from 'jsonc-parser';
18
- import { Command, deepClone, Disposable, DisposableCollection, Emitter, MessageService, nls } from '@theia/core';
19
- import { injectable, postConstruct, inject, interfaces } from '@theia/core/shared/inversify';
20
- import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
21
- import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
22
- import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
23
- import * as monaco from '@theia/monaco-editor-core';
24
- import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
25
- import { EditorManager } from '@theia/editor/lib/browser';
26
- import { Widget } from '@theia/core/lib/browser';
27
- import { FileService } from '@theia/filesystem/lib/browser/file-service';
28
- import { Deferred } from '@theia/core/lib/common/promise-util';
29
- import URI from '@theia/core/lib/common/uri';
30
- import {
31
- DeflatedToolbarTree,
32
- ToolbarTreeSchema,
33
- ToolbarItem,
34
- ToolbarItemDeflated,
35
- ToolbarAlignment,
36
- ToolbarItemPosition,
37
- LateInjector,
38
- } from './toolbar-interfaces';
39
- import { UserToolbarURI } from './toolbar-constants';
40
- import { isToolbarPreferences } from './toolbar-preference-schema';
41
-
42
- export const TOOLBAR_BAD_JSON_ERROR_MESSAGE = 'There was an error reading your toolbar.json file. Please check if it is corrupt'
43
- + ' by right-clicking the toolbar and selecting "Customize Toolbar". You can also reset it to its defaults by selecting'
44
- + ' "Restore Toolbar Defaults"';
45
- @injectable()
46
- export class ToolbarStorageProvider implements Disposable {
47
- @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
48
- @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService;
49
- @inject(FileService) protected readonly fileService: FileService;
50
- @inject(MessageService) protected readonly messageService: MessageService;
51
- @inject(LateInjector) protected lateInjector: <T>(id: interfaces.ServiceIdentifier<T>) => T;
52
- @inject(UserToolbarURI) protected readonly USER_TOOLBAR_URI: URI;
53
-
54
- get ready(): Promise<void> {
55
- return this._ready.promise;
56
- }
57
-
58
- protected readonly _ready = new Deferred<void>();
59
-
60
- // Injecting this directly causes a circular dependency, so we're using a custom utility
61
- // to inject this after the application has started up
62
- protected monacoWorkspace: MonacoWorkspace;
63
- protected editorManager: EditorManager;
64
- protected model: MonacoEditorModel | undefined;
65
- protected toDispose = new DisposableCollection();
66
- protected toolbarItemsUpdatedEmitter = new Emitter<void>();
67
- readonly onToolbarItemsChanged = this.toolbarItemsUpdatedEmitter.event;
68
- toolbarItems: DeflatedToolbarTree | undefined;
69
-
70
- @postConstruct()
71
- protected init(): void {
72
- this.doInit();
73
- }
74
-
75
- protected async doInit(): Promise<void> {
76
- const reference = await this.textModelService.createModelReference(this.USER_TOOLBAR_URI);
77
- this.model = reference.object;
78
- this.toDispose.push(reference);
79
- this.toDispose.push(Disposable.create(() => this.model = undefined));
80
- this.readConfiguration();
81
- if (this.model) {
82
- this.toDispose.push(this.model.onDidChangeContent(() => this.readConfiguration()));
83
- this.toDispose.push(this.model.onDirtyChanged(() => this.readConfiguration()));
84
- this.toDispose.push(this.model.onDidChangeValid(() => this.readConfiguration()));
85
- }
86
- this.toDispose.push(this.toolbarItemsUpdatedEmitter);
87
- await this.appState.reachedState('ready');
88
- this.monacoWorkspace = this.lateInjector(MonacoWorkspace);
89
- this.editorManager = this.lateInjector(EditorManager);
90
- this._ready.resolve();
91
- }
92
-
93
- protected readConfiguration(): void {
94
- if (!this.model || this.model.dirty) {
95
- return;
96
- }
97
- try {
98
- if (this.model.valid) {
99
- const content = this.model.getText();
100
- this.toolbarItems = this.parseContent(content);
101
- } else {
102
- this.toolbarItems = undefined;
103
- }
104
- this.toolbarItemsUpdatedEmitter.fire();
105
- } catch (e) {
106
- console.error(`Failed to load toolbar config from '${this.USER_TOOLBAR_URI}'.`, e);
107
- }
108
- }
109
-
110
- async removeItem(position: ToolbarItemPosition): Promise<boolean> {
111
- if (this.toolbarItems) {
112
- const { alignment, groupIndex, itemIndex } = position;
113
- const modifiedConfiguration = deepClone(this.toolbarItems);
114
- modifiedConfiguration.items[alignment][groupIndex].splice(itemIndex, 1);
115
- const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
116
- return this.writeToFile([], sanitizedConfiguration);
117
- }
118
- return false;
119
- }
120
-
121
- async addItem(command: Command, alignment: ToolbarAlignment): Promise<boolean> {
122
- if (this.toolbarItems) {
123
- const itemFromCommand: ToolbarItem = {
124
- id: command.id,
125
- command: command.id,
126
- icon: command.iconClass,
127
- };
128
- const groupIndex = this.toolbarItems?.items[alignment].length;
129
- if (groupIndex) {
130
- const lastItemIndex = this.toolbarItems?.items[alignment][groupIndex - 1].length;
131
- const modifiedConfiguration = deepClone(this.toolbarItems);
132
- modifiedConfiguration.items[alignment][groupIndex - 1].push(itemFromCommand);
133
- return !!lastItemIndex && this.writeToFile([], modifiedConfiguration);
134
- }
135
- return this.addItemToEmptyColumn(itemFromCommand, alignment);
136
- }
137
- return false;
138
- }
139
-
140
- async swapValues(
141
- oldPosition: ToolbarItemPosition,
142
- newPosition: ToolbarItemPosition,
143
- direction: 'location-left' | 'location-right',
144
- ): Promise<boolean> {
145
- if (this.toolbarItems) {
146
- const { alignment, groupIndex, itemIndex } = oldPosition;
147
- const draggedItem = this.toolbarItems?.items[alignment][groupIndex][itemIndex];
148
- const newItemIndex = direction === 'location-right' ? newPosition.itemIndex + 1 : newPosition.itemIndex;
149
- const modifiedConfiguration = deepClone(this.toolbarItems);
150
- if (newPosition.alignment === oldPosition.alignment && newPosition.groupIndex === oldPosition.groupIndex) {
151
- modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
152
- if (newPosition.itemIndex > oldPosition.itemIndex) {
153
- modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
154
- } else {
155
- modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex + 1, 1);
156
- }
157
- } else {
158
- modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
159
- modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
160
- }
161
- const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
162
- return this.writeToFile([], sanitizedConfiguration);
163
- }
164
- return false;
165
- }
166
-
167
- async addItemToEmptyColumn(item: ToolbarItemDeflated, alignment: ToolbarAlignment): Promise<boolean> {
168
- if (this.toolbarItems) {
169
- const modifiedConfiguration = deepClone(this.toolbarItems);
170
- modifiedConfiguration.items[alignment].push([item]);
171
- return this.writeToFile([], modifiedConfiguration);
172
- }
173
- return false;
174
- }
175
-
176
- async moveItemToEmptySpace(
177
- oldPosition: ToolbarItemPosition,
178
- newAlignment: ToolbarAlignment,
179
- centerPosition?: 'left' | 'right',
180
- ): Promise<boolean> {
181
- const { alignment: oldAlignment, itemIndex: oldItemIndex } = oldPosition;
182
- let oldGroupIndex = oldPosition.groupIndex;
183
- if (this.toolbarItems) {
184
- const draggedItem = this.toolbarItems.items[oldAlignment][oldGroupIndex][oldItemIndex];
185
- const newGroupIndex = this.toolbarItems.items[oldAlignment].length;
186
- const modifiedConfiguration = deepClone(this.toolbarItems);
187
- if (newAlignment === ToolbarAlignment.LEFT) {
188
- modifiedConfiguration.items[newAlignment].push([draggedItem]);
189
- } else if (newAlignment === ToolbarAlignment.CENTER) {
190
- if (centerPosition === 'left') {
191
- modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
192
- if (newAlignment === oldAlignment) {
193
- oldGroupIndex = oldGroupIndex + 1;
194
- }
195
- } else if (centerPosition === 'right') {
196
- modifiedConfiguration.items[newAlignment].splice(newGroupIndex + 1, 0, [draggedItem]);
197
- }
198
- } else if (newAlignment === ToolbarAlignment.RIGHT) {
199
- modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
200
- if (newAlignment === oldAlignment) {
201
- oldGroupIndex = oldGroupIndex + 1;
202
- }
203
- }
204
- modifiedConfiguration.items[oldAlignment][oldGroupIndex].splice(oldItemIndex, 1);
205
- const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
206
- return this.writeToFile([], sanitizedConfiguration);
207
- }
208
- return false;
209
- }
210
-
211
- async insertGroup(position: ToolbarItemPosition, insertDirection: 'left' | 'right'): Promise<boolean> {
212
- if (this.toolbarItems) {
213
- const { alignment, groupIndex, itemIndex } = position;
214
- const modifiedConfiguration = deepClone(this.toolbarItems);
215
- const originalColumn = modifiedConfiguration.items[alignment];
216
- if (originalColumn) {
217
- const existingGroup = originalColumn[groupIndex];
218
- const existingGroupLength = existingGroup.length;
219
- let poppedGroup: ToolbarItemDeflated[] = [];
220
- let numItemsToRemove: number;
221
- if (insertDirection === 'left' && itemIndex !== 0) {
222
- numItemsToRemove = existingGroupLength - itemIndex;
223
- poppedGroup = existingGroup.splice(itemIndex, numItemsToRemove);
224
- originalColumn.splice(groupIndex, 1, existingGroup, poppedGroup);
225
- } else if (insertDirection === 'right' && itemIndex !== existingGroupLength - 1) {
226
- numItemsToRemove = itemIndex + 1;
227
- poppedGroup = existingGroup.splice(0, numItemsToRemove);
228
- originalColumn.splice(groupIndex, 1, poppedGroup, existingGroup);
229
- }
230
- const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
231
- return this.writeToFile([], sanitizedConfiguration);
232
- }
233
- }
234
- return false;
235
- }
236
-
237
- protected removeEmptyGroupsFromToolbar(
238
- toolbarItems: DeflatedToolbarTree | undefined,
239
- ): DeflatedToolbarTree | undefined {
240
- if (toolbarItems) {
241
- const modifiedConfiguration = deepClone(toolbarItems);
242
- const columns = [ToolbarAlignment.LEFT, ToolbarAlignment.CENTER, ToolbarAlignment.RIGHT];
243
- columns.forEach(column => {
244
- const groups = toolbarItems.items[column];
245
- groups.forEach((group, index) => {
246
- if (group.length === 0) {
247
- modifiedConfiguration.items[column].splice(index, 1);
248
- }
249
- });
250
- });
251
- return modifiedConfiguration;
252
- }
253
- return undefined;
254
- }
255
-
256
- async clearAll(): Promise<boolean> {
257
- if (this.model) {
258
- const textModel = this.model.textEditorModel;
259
- await this.monacoWorkspace.applyBackgroundEdit(this.model, [
260
- {
261
- range: textModel.getFullModelRange(),
262
- // eslint-disable-next-line no-null/no-null
263
- text: null,
264
- forceMoveMarkers: false,
265
- },
266
- ]);
267
- }
268
- this.toolbarItemsUpdatedEmitter.fire();
269
- return true;
270
- }
271
-
272
- protected async writeToFile(path: jsoncParser.JSONPath, value: unknown, insertion = false): Promise<boolean> {
273
- if (this.model) {
274
- try {
275
- const content = this.model.getText().trim();
276
- const textModel = this.model.textEditorModel;
277
- const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
278
- const { insertSpaces, tabSize, defaultEOL } = textModel.getOptions();
279
- for (const edit of jsoncParser.modify(content, path, value, {
280
- isArrayInsertion: insertion,
281
- formattingOptions: {
282
- insertSpaces,
283
- tabSize,
284
- eol: defaultEOL === monaco.editor.DefaultEndOfLine.LF ? '\n' : '\r\n',
285
- },
286
- })) {
287
- const start = textModel.getPositionAt(edit.offset);
288
- const end = textModel.getPositionAt(edit.offset + edit.length);
289
- editOperations.push({
290
- range: monaco.Range.fromPositions(start, end),
291
- // eslint-disable-next-line no-null/no-null
292
- text: edit.content || null,
293
- forceMoveMarkers: false,
294
- });
295
- }
296
- await this.monacoWorkspace.applyBackgroundEdit(this.model, editOperations, false);
297
- await this.model.save();
298
- return true;
299
- } catch (e) {
300
- const message = nls.localize('theia/toolbar/failedUpdate', "Failed to update the value of '{0}' in '{1}'.", path.join('.'), this.USER_TOOLBAR_URI.path.toString());
301
- this.messageService.error(nls.localize('theia/toolbar/jsonError', TOOLBAR_BAD_JSON_ERROR_MESSAGE));
302
- console.error(`${message}`, e);
303
- return false;
304
- }
305
- }
306
- return false;
307
- }
308
-
309
- protected parseContent(fileContent: string): DeflatedToolbarTree | undefined {
310
- const rawConfig = this.parse(fileContent);
311
- if (!isToolbarPreferences(rawConfig)) {
312
- return undefined;
313
- }
314
- return rawConfig;
315
- }
316
-
317
- protected parse(fileContent: string): DeflatedToolbarTree | undefined {
318
- let strippedContent = fileContent.trim();
319
- if (!strippedContent) {
320
- return undefined;
321
- }
322
- strippedContent = jsoncParser.stripComments(strippedContent);
323
- return jsoncParser.parse(strippedContent);
324
- }
325
-
326
- async openOrCreateJSONFile(state: ToolbarTreeSchema, doOpen = false): Promise<Widget | undefined> {
327
- const fileExists = await this.fileService.exists(this.USER_TOOLBAR_URI);
328
- let doWriteStateToFile = false;
329
- if (fileExists) {
330
- const fileContent = await this.fileService.read(this.USER_TOOLBAR_URI);
331
- if (fileContent.value.trim() === '') {
332
- doWriteStateToFile = true;
333
- }
334
- } else {
335
- await this.fileService.create(this.USER_TOOLBAR_URI);
336
- doWriteStateToFile = true;
337
- }
338
- if (doWriteStateToFile) {
339
- await this.writeToFile([], state);
340
- }
341
- this.readConfiguration();
342
- if (doOpen) {
343
- const widget = await this.editorManager.open(this.USER_TOOLBAR_URI);
344
- return widget;
345
- }
346
- return undefined;
347
- }
348
-
349
- dispose(): void {
350
- this.toDispose.dispose();
351
- }
352
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2022 Ericsson and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import * as jsoncParser from 'jsonc-parser';
18
+ import { Command, deepClone, Disposable, DisposableCollection, Emitter, MessageService, nls } from '@theia/core';
19
+ import { injectable, postConstruct, inject, interfaces } from '@theia/core/shared/inversify';
20
+ import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
21
+ import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
22
+ import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
23
+ import * as monaco from '@theia/monaco-editor-core';
24
+ import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
25
+ import { EditorManager } from '@theia/editor/lib/browser';
26
+ import { Widget } from '@theia/core/lib/browser';
27
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
28
+ import { Deferred } from '@theia/core/lib/common/promise-util';
29
+ import URI from '@theia/core/lib/common/uri';
30
+ import {
31
+ DeflatedToolbarTree,
32
+ ToolbarTreeSchema,
33
+ ToolbarItem,
34
+ ToolbarItemDeflated,
35
+ ToolbarAlignment,
36
+ ToolbarItemPosition,
37
+ LateInjector,
38
+ } from './toolbar-interfaces';
39
+ import { UserToolbarURI } from './toolbar-constants';
40
+ import { isToolbarPreferences } from './toolbar-preference-schema';
41
+
42
+ export const TOOLBAR_BAD_JSON_ERROR_MESSAGE = 'There was an error reading your toolbar.json file. Please check if it is corrupt'
43
+ + ' by right-clicking the toolbar and selecting "Customize Toolbar". You can also reset it to its defaults by selecting'
44
+ + ' "Restore Toolbar Defaults"';
45
+ @injectable()
46
+ export class ToolbarStorageProvider implements Disposable {
47
+ @inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
48
+ @inject(MonacoTextModelService) protected readonly textModelService: MonacoTextModelService;
49
+ @inject(FileService) protected readonly fileService: FileService;
50
+ @inject(MessageService) protected readonly messageService: MessageService;
51
+ @inject(LateInjector) protected lateInjector: <T>(id: interfaces.ServiceIdentifier<T>) => T;
52
+ @inject(UserToolbarURI) protected readonly USER_TOOLBAR_URI: URI;
53
+
54
+ get ready(): Promise<void> {
55
+ return this._ready.promise;
56
+ }
57
+
58
+ protected readonly _ready = new Deferred<void>();
59
+
60
+ // Injecting this directly causes a circular dependency, so we're using a custom utility
61
+ // to inject this after the application has started up
62
+ protected monacoWorkspace: MonacoWorkspace;
63
+ protected editorManager: EditorManager;
64
+ protected model: MonacoEditorModel | undefined;
65
+ protected toDispose = new DisposableCollection();
66
+ protected toolbarItemsUpdatedEmitter = new Emitter<void>();
67
+ readonly onToolbarItemsChanged = this.toolbarItemsUpdatedEmitter.event;
68
+ toolbarItems: DeflatedToolbarTree | undefined;
69
+
70
+ @postConstruct()
71
+ protected init(): void {
72
+ this.doInit();
73
+ }
74
+
75
+ protected async doInit(): Promise<void> {
76
+ const reference = await this.textModelService.createModelReference(this.USER_TOOLBAR_URI);
77
+ this.model = reference.object;
78
+ this.toDispose.push(reference);
79
+ this.toDispose.push(Disposable.create(() => this.model = undefined));
80
+ this.readConfiguration();
81
+ if (this.model) {
82
+ this.toDispose.push(this.model.onDidChangeContent(() => this.readConfiguration()));
83
+ this.toDispose.push(this.model.onDirtyChanged(() => this.readConfiguration()));
84
+ this.toDispose.push(this.model.onDidChangeValid(() => this.readConfiguration()));
85
+ }
86
+ this.toDispose.push(this.toolbarItemsUpdatedEmitter);
87
+ await this.appState.reachedState('ready');
88
+ this.monacoWorkspace = this.lateInjector(MonacoWorkspace);
89
+ this.editorManager = this.lateInjector(EditorManager);
90
+ this._ready.resolve();
91
+ }
92
+
93
+ protected readConfiguration(): void {
94
+ if (!this.model || this.model.dirty) {
95
+ return;
96
+ }
97
+ try {
98
+ if (this.model.valid) {
99
+ const content = this.model.getText();
100
+ this.toolbarItems = this.parseContent(content);
101
+ } else {
102
+ this.toolbarItems = undefined;
103
+ }
104
+ this.toolbarItemsUpdatedEmitter.fire();
105
+ } catch (e) {
106
+ console.error(`Failed to load toolbar config from '${this.USER_TOOLBAR_URI}'.`, e);
107
+ }
108
+ }
109
+
110
+ async removeItem(position: ToolbarItemPosition): Promise<boolean> {
111
+ if (this.toolbarItems) {
112
+ const { alignment, groupIndex, itemIndex } = position;
113
+ const modifiedConfiguration = deepClone(this.toolbarItems);
114
+ modifiedConfiguration.items[alignment][groupIndex].splice(itemIndex, 1);
115
+ const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
116
+ return this.writeToFile([], sanitizedConfiguration);
117
+ }
118
+ return false;
119
+ }
120
+
121
+ async addItem(command: Command, alignment: ToolbarAlignment): Promise<boolean> {
122
+ if (this.toolbarItems) {
123
+ const itemFromCommand: ToolbarItem = {
124
+ id: command.id,
125
+ command: command.id,
126
+ icon: command.iconClass,
127
+ };
128
+ const groupIndex = this.toolbarItems?.items[alignment].length;
129
+ if (groupIndex) {
130
+ const lastItemIndex = this.toolbarItems?.items[alignment][groupIndex - 1].length;
131
+ const modifiedConfiguration = deepClone(this.toolbarItems);
132
+ modifiedConfiguration.items[alignment][groupIndex - 1].push(itemFromCommand);
133
+ return !!lastItemIndex && this.writeToFile([], modifiedConfiguration);
134
+ }
135
+ return this.addItemToEmptyColumn(itemFromCommand, alignment);
136
+ }
137
+ return false;
138
+ }
139
+
140
+ async swapValues(
141
+ oldPosition: ToolbarItemPosition,
142
+ newPosition: ToolbarItemPosition,
143
+ direction: 'location-left' | 'location-right',
144
+ ): Promise<boolean> {
145
+ if (this.toolbarItems) {
146
+ const { alignment, groupIndex, itemIndex } = oldPosition;
147
+ const draggedItem = this.toolbarItems?.items[alignment][groupIndex][itemIndex];
148
+ const newItemIndex = direction === 'location-right' ? newPosition.itemIndex + 1 : newPosition.itemIndex;
149
+ const modifiedConfiguration = deepClone(this.toolbarItems);
150
+ if (newPosition.alignment === oldPosition.alignment && newPosition.groupIndex === oldPosition.groupIndex) {
151
+ modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
152
+ if (newPosition.itemIndex > oldPosition.itemIndex) {
153
+ modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
154
+ } else {
155
+ modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex + 1, 1);
156
+ }
157
+ } else {
158
+ modifiedConfiguration.items[oldPosition.alignment][oldPosition.groupIndex].splice(oldPosition.itemIndex, 1);
159
+ modifiedConfiguration.items[newPosition.alignment][newPosition.groupIndex].splice(newItemIndex, 0, draggedItem);
160
+ }
161
+ const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
162
+ return this.writeToFile([], sanitizedConfiguration);
163
+ }
164
+ return false;
165
+ }
166
+
167
+ async addItemToEmptyColumn(item: ToolbarItemDeflated, alignment: ToolbarAlignment): Promise<boolean> {
168
+ if (this.toolbarItems) {
169
+ const modifiedConfiguration = deepClone(this.toolbarItems);
170
+ modifiedConfiguration.items[alignment].push([item]);
171
+ return this.writeToFile([], modifiedConfiguration);
172
+ }
173
+ return false;
174
+ }
175
+
176
+ async moveItemToEmptySpace(
177
+ oldPosition: ToolbarItemPosition,
178
+ newAlignment: ToolbarAlignment,
179
+ centerPosition?: 'left' | 'right',
180
+ ): Promise<boolean> {
181
+ const { alignment: oldAlignment, itemIndex: oldItemIndex } = oldPosition;
182
+ let oldGroupIndex = oldPosition.groupIndex;
183
+ if (this.toolbarItems) {
184
+ const draggedItem = this.toolbarItems.items[oldAlignment][oldGroupIndex][oldItemIndex];
185
+ const newGroupIndex = this.toolbarItems.items[oldAlignment].length;
186
+ const modifiedConfiguration = deepClone(this.toolbarItems);
187
+ if (newAlignment === ToolbarAlignment.LEFT) {
188
+ modifiedConfiguration.items[newAlignment].push([draggedItem]);
189
+ } else if (newAlignment === ToolbarAlignment.CENTER) {
190
+ if (centerPosition === 'left') {
191
+ modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
192
+ if (newAlignment === oldAlignment) {
193
+ oldGroupIndex = oldGroupIndex + 1;
194
+ }
195
+ } else if (centerPosition === 'right') {
196
+ modifiedConfiguration.items[newAlignment].splice(newGroupIndex + 1, 0, [draggedItem]);
197
+ }
198
+ } else if (newAlignment === ToolbarAlignment.RIGHT) {
199
+ modifiedConfiguration.items[newAlignment].unshift([draggedItem]);
200
+ if (newAlignment === oldAlignment) {
201
+ oldGroupIndex = oldGroupIndex + 1;
202
+ }
203
+ }
204
+ modifiedConfiguration.items[oldAlignment][oldGroupIndex].splice(oldItemIndex, 1);
205
+ const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
206
+ return this.writeToFile([], sanitizedConfiguration);
207
+ }
208
+ return false;
209
+ }
210
+
211
+ async insertGroup(position: ToolbarItemPosition, insertDirection: 'left' | 'right'): Promise<boolean> {
212
+ if (this.toolbarItems) {
213
+ const { alignment, groupIndex, itemIndex } = position;
214
+ const modifiedConfiguration = deepClone(this.toolbarItems);
215
+ const originalColumn = modifiedConfiguration.items[alignment];
216
+ if (originalColumn) {
217
+ const existingGroup = originalColumn[groupIndex];
218
+ const existingGroupLength = existingGroup.length;
219
+ let poppedGroup: ToolbarItemDeflated[] = [];
220
+ let numItemsToRemove: number;
221
+ if (insertDirection === 'left' && itemIndex !== 0) {
222
+ numItemsToRemove = existingGroupLength - itemIndex;
223
+ poppedGroup = existingGroup.splice(itemIndex, numItemsToRemove);
224
+ originalColumn.splice(groupIndex, 1, existingGroup, poppedGroup);
225
+ } else if (insertDirection === 'right' && itemIndex !== existingGroupLength - 1) {
226
+ numItemsToRemove = itemIndex + 1;
227
+ poppedGroup = existingGroup.splice(0, numItemsToRemove);
228
+ originalColumn.splice(groupIndex, 1, poppedGroup, existingGroup);
229
+ }
230
+ const sanitizedConfiguration = this.removeEmptyGroupsFromToolbar(modifiedConfiguration);
231
+ return this.writeToFile([], sanitizedConfiguration);
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+
237
+ protected removeEmptyGroupsFromToolbar(
238
+ toolbarItems: DeflatedToolbarTree | undefined,
239
+ ): DeflatedToolbarTree | undefined {
240
+ if (toolbarItems) {
241
+ const modifiedConfiguration = deepClone(toolbarItems);
242
+ const columns = [ToolbarAlignment.LEFT, ToolbarAlignment.CENTER, ToolbarAlignment.RIGHT];
243
+ columns.forEach(column => {
244
+ const groups = toolbarItems.items[column];
245
+ groups.forEach((group, index) => {
246
+ if (group.length === 0) {
247
+ modifiedConfiguration.items[column].splice(index, 1);
248
+ }
249
+ });
250
+ });
251
+ return modifiedConfiguration;
252
+ }
253
+ return undefined;
254
+ }
255
+
256
+ async clearAll(): Promise<boolean> {
257
+ if (this.model) {
258
+ const textModel = this.model.textEditorModel;
259
+ await this.monacoWorkspace.applyBackgroundEdit(this.model, [
260
+ {
261
+ range: textModel.getFullModelRange(),
262
+ // eslint-disable-next-line no-null/no-null
263
+ text: null,
264
+ forceMoveMarkers: false,
265
+ },
266
+ ]);
267
+ }
268
+ this.toolbarItemsUpdatedEmitter.fire();
269
+ return true;
270
+ }
271
+
272
+ protected async writeToFile(path: jsoncParser.JSONPath, value: unknown, insertion = false): Promise<boolean> {
273
+ if (this.model) {
274
+ try {
275
+ const content = this.model.getText().trim();
276
+ const textModel = this.model.textEditorModel;
277
+ const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
278
+ const { insertSpaces, tabSize, defaultEOL } = textModel.getOptions();
279
+ for (const edit of jsoncParser.modify(content, path, value, {
280
+ isArrayInsertion: insertion,
281
+ formattingOptions: {
282
+ insertSpaces,
283
+ tabSize,
284
+ eol: defaultEOL === monaco.editor.DefaultEndOfLine.LF ? '\n' : '\r\n',
285
+ },
286
+ })) {
287
+ const start = textModel.getPositionAt(edit.offset);
288
+ const end = textModel.getPositionAt(edit.offset + edit.length);
289
+ editOperations.push({
290
+ range: monaco.Range.fromPositions(start, end),
291
+ // eslint-disable-next-line no-null/no-null
292
+ text: edit.content || null,
293
+ forceMoveMarkers: false,
294
+ });
295
+ }
296
+ await this.monacoWorkspace.applyBackgroundEdit(this.model, editOperations, false);
297
+ await this.model.save();
298
+ return true;
299
+ } catch (e) {
300
+ const message = nls.localize('theia/toolbar/failedUpdate', "Failed to update the value of '{0}' in '{1}'.", path.join('.'), this.USER_TOOLBAR_URI.path.toString());
301
+ this.messageService.error(nls.localize('theia/toolbar/jsonError', TOOLBAR_BAD_JSON_ERROR_MESSAGE));
302
+ console.error(`${message}`, e);
303
+ return false;
304
+ }
305
+ }
306
+ return false;
307
+ }
308
+
309
+ protected parseContent(fileContent: string): DeflatedToolbarTree | undefined {
310
+ const rawConfig = this.parse(fileContent);
311
+ if (!isToolbarPreferences(rawConfig)) {
312
+ return undefined;
313
+ }
314
+ return rawConfig;
315
+ }
316
+
317
+ protected parse(fileContent: string): DeflatedToolbarTree | undefined {
318
+ let strippedContent = fileContent.trim();
319
+ if (!strippedContent) {
320
+ return undefined;
321
+ }
322
+ strippedContent = jsoncParser.stripComments(strippedContent);
323
+ return jsoncParser.parse(strippedContent);
324
+ }
325
+
326
+ async openOrCreateJSONFile(state: ToolbarTreeSchema, doOpen = false): Promise<Widget | undefined> {
327
+ const fileExists = await this.fileService.exists(this.USER_TOOLBAR_URI);
328
+ let doWriteStateToFile = false;
329
+ if (fileExists) {
330
+ const fileContent = await this.fileService.read(this.USER_TOOLBAR_URI);
331
+ if (fileContent.value.trim() === '') {
332
+ doWriteStateToFile = true;
333
+ }
334
+ } else {
335
+ await this.fileService.create(this.USER_TOOLBAR_URI);
336
+ doWriteStateToFile = true;
337
+ }
338
+ if (doWriteStateToFile) {
339
+ await this.writeToFile([], state);
340
+ }
341
+ this.readConfiguration();
342
+ if (doOpen) {
343
+ const widget = await this.editorManager.open(this.USER_TOOLBAR_URI);
344
+ return widget;
345
+ }
346
+ return undefined;
347
+ }
348
+
349
+ dispose(): void {
350
+ this.toDispose.dispose();
351
+ }
352
+ }