Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix: restore toolbar default items
- properly restore the toolbar items in the storage provider from the default factory and ensure to write to the config file
- ensure the defaults are restored on startup in case there is no config file yet
- in case the config file was corrupted, allow the user to restore the default toolbar items via an action in the error message popup
- chore: remove unused injections in ToolbarCommandContribution

Fixes #15231
  • Loading branch information
ndoschek committed Jun 25, 2025
commit 1c75d781e010eca96cad717cabf09254583eb6e0
24 changes: 9 additions & 15 deletions packages/toolbar/src/browser/toolbar-command-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
bindContributionProvider,
CommandContribution,
CommandRegistry,
CommandService,
MenuContribution,
MenuModelRegistry,
} from '@theia/core';
Expand All @@ -30,11 +29,9 @@ import {
PreferenceContribution,
PreferenceScope,
PreferenceService,
QuickInputService,
Widget,
} from '@theia/core/lib/browser';
import { injectable, inject, interfaces, Container } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser';
import { ToolbarImpl } from './toolbar';
import { bindToolbarIconDialog } from './toolbar-icon-selector-dialog';
import {
Expand All @@ -57,14 +54,11 @@ import URI from '@theia/core/lib/common/uri';

@injectable()
export class ToolbarCommandContribution implements CommandContribution, KeybindingContribution, MenuContribution, JsonSchemaContribution {
@inject(ToolbarController) protected readonly model: ToolbarController;
@inject(QuickInputService) protected readonly quickInputService: QuickInputService;
@inject(ToolbarController) protected readonly controller: ToolbarController;
@inject(ToolbarCommandQuickInputService) protected toolbarCommandPickService: ToolbarCommandQuickInputService;
@inject(CommandService) protected readonly commandService: CommandService;
@inject(EditorManager) protected readonly editorManager: EditorManager;
@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
@inject(ToolbarController) protected readonly toolbarModel: ToolbarController;
@inject(JsonSchemaDataStore) protected readonly schemaStore: JsonSchemaDataStore;

protected readonly schemaURI = new URI(toolbarSchemaId);

registerSchemas(context: JsonSchemaRegisterContext): void {
Expand All @@ -77,10 +71,10 @@ export class ToolbarCommandContribution implements CommandContribution, Keybindi

registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ToolbarCommands.CUSTOMIZE_TOOLBAR, {
execute: () => this.model.openOrCreateJSONFile(true),
execute: () => this.controller.openOrCreateJSONFile(true),
});
registry.registerCommand(ToolbarCommands.RESET_TOOLBAR, {
execute: () => this.model.clearAll(),
execute: () => this.controller.restoreToolbarDefaults(),
});
registry.registerCommand(ToolbarCommands.TOGGLE_TOOLBAR, {
execute: () => {
Expand All @@ -90,26 +84,26 @@ export class ToolbarCommandContribution implements CommandContribution, Keybindi
});

registry.registerCommand(ToolbarCommands.REMOVE_COMMAND_FROM_TOOLBAR, {
execute: async (_widget, position: ToolbarItemPosition | undefined, id?: string) => position && this.model.removeItem(position, id),
execute: async (_widget, position: ToolbarItemPosition | undefined, id?: string) => position && this.controller.removeItem(position, id),
isVisible: (...args) => this.isToolbarWidget(args[0]),
});
registry.registerCommand(ToolbarCommands.INSERT_GROUP_LEFT, {
execute: async (_widget: Widget, position: ToolbarItemPosition | undefined) => position && this.model.insertGroup(position, 'left'),
execute: async (_widget: Widget, position: ToolbarItemPosition | undefined) => position && this.controller.insertGroup(position, 'left'),
isVisible: (widget: Widget, position: ToolbarItemPosition | undefined) => {
if (position) {
const { alignment, groupIndex, itemIndex } = position;
const owningGroupLength = this.toolbarModel.toolbarItems.items[alignment][groupIndex].length;
const owningGroupLength = this.controller.toolbarItems.items[alignment][groupIndex].length;
return this.isToolbarWidget(widget) && (owningGroupLength > 1) && (itemIndex > 0);
}
return false;
},
});
registry.registerCommand(ToolbarCommands.INSERT_GROUP_RIGHT, {
execute: async (_widget: Widget, position: ToolbarItemPosition | undefined) => position && this.model.insertGroup(position, 'right'),
execute: async (_widget: Widget, position: ToolbarItemPosition | undefined) => position && this.controller.insertGroup(position, 'right'),
isVisible: (widget: Widget, position: ToolbarItemPosition | undefined) => {
if (position) {
const { alignment, groupIndex, itemIndex } = position;
const owningGroupLength = this.toolbarModel.toolbarItems.items[alignment][groupIndex].length;
const owningGroupLength = this.controller.toolbarItems.items[alignment][groupIndex].length;
const isNotLastItem = itemIndex < (owningGroupLength - 1);
return this.isToolbarWidget(widget) && owningGroupLength > 1 && isNotLastItem;
}
Expand Down
61 changes: 33 additions & 28 deletions packages/toolbar/src/browser/toolbar-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { Command, CommandRegistry, ContributionProvider, Emitter, MaybePromise, MessageService } from '@theia/core';
import { Command, CommandRegistry, ContributionProvider, Emitter, MaybePromise, MessageService, nls } from '@theia/core';
import { KeybindingRegistry, Widget } from '@theia/core/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify';
import { ToolbarDefaultsFactory } from './toolbar-defaults';
import {
DeflatedToolbarTree,
ToolbarContribution,
Expand All @@ -31,13 +30,13 @@ import { ToolbarStorageProvider, TOOLBAR_BAD_JSON_ERROR_MESSAGE } from './toolba
import { ReactToolbarItemImpl, RenderedToolbarItemImpl, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar/tab-toolbar-item';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { LabelParser } from '@theia/core/lib/browser/label-parser';
import { ToolbarCommands } from './toolbar-constants';

@injectable()
export class ToolbarController {
@inject(ToolbarStorageProvider) protected readonly storageProvider: ToolbarStorageProvider;
@inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(ToolbarDefaultsFactory) protected readonly defaultsFactory: () => DeflatedToolbarTree;
@inject(CommandRegistry) commandRegistry: CommandRegistry;
@inject(ContextKeyService) contextKeyService: ContextKeyService;
@inject(KeybindingRegistry) keybindingRegistry: KeybindingRegistry;
Expand All @@ -64,30 +63,32 @@ export class ToolbarController {
this.toolbarModelDidUpdateEmitter.fire();
}

protected inflateItems(schema: DeflatedToolbarTree): ToolbarTreeSchema {
protected inflateItems(schema?: DeflatedToolbarTree): ToolbarTreeSchema {
const newTree: ToolbarTreeSchema = {
items: {
[ToolbarAlignment.LEFT]: [],
[ToolbarAlignment.CENTER]: [],
[ToolbarAlignment.RIGHT]: [],
},
};
for (const column of Object.keys(schema.items)) {
const currentColumn = schema.items[column as ToolbarAlignment];
for (const group of currentColumn) {
const newGroup: TabBarToolbarItem[] = [];
for (const item of group) {
if (item.group === 'contributed') {
const contribution = this.getContributionByID(item.id);
if (contribution) {
newGroup.push(new ReactToolbarItemImpl(this.commandRegistry, this.contextKeyService, contribution));
if (schema) {
for (const column of Object.keys(schema.items)) {
const currentColumn = schema.items[column as ToolbarAlignment];
for (const group of currentColumn) {
const newGroup: TabBarToolbarItem[] = [];
for (const item of group) {
if (item.group === 'contributed') {
const contribution = this.getContributionByID(item.id);
if (contribution) {
newGroup.push(new ReactToolbarItemImpl(this.commandRegistry, this.contextKeyService, contribution));
}
} else {
newGroup.push(new RenderedToolbarItemImpl(this.commandRegistry, this.contextKeyService, this.keybindingRegistry, this.labelParser, item));
}
} else {
newGroup.push(new RenderedToolbarItemImpl(this.commandRegistry, this.contextKeyService, this.keybindingRegistry, this.labelParser, item));
}
}
if (newGroup.length) {
newTree.items[column as ToolbarAlignment].push(newGroup);
if (newGroup.length) {
newTree.items[column as ToolbarAlignment].push(newGroup);
}
}
}
}
Expand All @@ -108,7 +109,7 @@ export class ToolbarController {
await this.storageProvider.ready;
this.toolbarItems = await this.resolveToolbarItems();
this.storageProvider.onToolbarItemsChanged(async () => {
this.toolbarItems = await this.resolveToolbarItems();
this.toolbarItems = await this.resolveToolbarItems(true);
});
this.ready.resolve();
this.widgetContributions.getContributions().forEach(contribution => {
Expand All @@ -118,17 +119,21 @@ export class ToolbarController {
});
}

protected async resolveToolbarItems(): Promise<ToolbarTreeSchema> {
protected async resolveToolbarItems(promptUserOnInvalidConfig = false): Promise<ToolbarTreeSchema> {
await this.storageProvider.ready;

if (this.storageProvider.toolbarItems) {
try {
return this.inflateItems(this.storageProvider.toolbarItems);
} catch (e) {
this.messageService.error(TOOLBAR_BAD_JSON_ERROR_MESSAGE);
if (!this.storageProvider.toolbarItems) {
let restoreDefaults = true;
if (promptUserOnInvalidConfig) {
const resetLabel = ToolbarCommands.RESET_TOOLBAR.label!;
const answer = await this.messageService.error(nls.localize('theia/toolbar/jsonError', TOOLBAR_BAD_JSON_ERROR_MESSAGE), resetLabel);
restoreDefaults = answer === resetLabel;
}
if (restoreDefaults) {
await this.restoreToolbarDefaults();
}
}
return this.inflateItems(this.defaultsFactory());
return this.inflateItems(this.storageProvider.toolbarItems);
}

async swapValues(
Expand All @@ -142,8 +147,8 @@ export class ToolbarController {
});
}

async clearAll(): Promise<boolean> {
return this.withBusy<boolean>(() => this.storageProvider.clearAll());
async restoreToolbarDefaults(): Promise<boolean> {
return this.withBusy<boolean>(() => this.storageProvider.restoreToolbarDefaults());
}

async openOrCreateJSONFile(doOpen = false): Promise<Widget | undefined> {
Expand Down
31 changes: 14 additions & 17 deletions packages/toolbar/src/browser/toolbar-storage-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ import {
} from './toolbar-interfaces';
import { UserToolbarURI } from './toolbar-constants';
import { isToolbarPreferences } from './toolbar-preference-schema';
import { ToolbarDefaultsFactory } from './toolbar-defaults';

export const TOOLBAR_BAD_JSON_ERROR_MESSAGE = 'There was an error reading your toolbar.json file. Please check if it is corrupt'
+ ' by right-clicking the toolbar and selecting "Customize Toolbar". You can also reset it to its defaults by selecting'
+ ' "Restore Toolbar Defaults"';

@injectable()
export class ToolbarStorageProvider implements Disposable {
@inject(FrontendApplicationStateService) protected readonly appState: FrontendApplicationStateService;
Expand All @@ -50,6 +52,7 @@ export class ToolbarStorageProvider implements Disposable {
@inject(MessageService) protected readonly messageService: MessageService;
@inject(LateInjector) protected lateInjector: <T>(id: interfaces.ServiceIdentifier<T>) => T;
@inject(UserToolbarURI) protected readonly USER_TOOLBAR_URI: URI;
@inject(ToolbarDefaultsFactory) protected readonly defaultsFactory: () => DeflatedToolbarTree;

get ready(): Promise<void> {
return this._ready.promise;
Expand All @@ -65,7 +68,12 @@ export class ToolbarStorageProvider implements Disposable {
protected toDispose = new DisposableCollection();
protected toolbarItemsUpdatedEmitter = new Emitter<void>();
readonly onToolbarItemsChanged = this.toolbarItemsUpdatedEmitter.event;
toolbarItems: DeflatedToolbarTree | undefined;

protected _toolbarItems: DeflatedToolbarTree | undefined;

get toolbarItems(): DeflatedToolbarTree | undefined {
return this._toolbarItems;
}

@postConstruct()
protected init(): void {
Expand Down Expand Up @@ -97,9 +105,9 @@ export class ToolbarStorageProvider implements Disposable {
try {
if (this.model.valid) {
const content = this.model.getText();
this.toolbarItems = this.parseContent(content);
this._toolbarItems = this.parseContent(content);
} else {
this.toolbarItems = undefined;
this._toolbarItems = undefined;
}
this.toolbarItemsUpdatedEmitter.fire();
} catch (e) {
Expand Down Expand Up @@ -253,20 +261,9 @@ export class ToolbarStorageProvider implements Disposable {
return undefined;
}

async clearAll(): Promise<boolean> {
if (this.model) {
const textModel = this.model.textEditorModel;
await this.monacoWorkspace.applyBackgroundEdit(this.model, [
{
range: textModel.getFullModelRange(),
// eslint-disable-next-line no-null/no-null
text: null,
forceMoveMarkers: false,
},
]);
}
this.toolbarItemsUpdatedEmitter.fire();
return true;
async restoreToolbarDefaults(): Promise<boolean> {
this._toolbarItems = this.defaultsFactory();
return this.writeToFile([], this._toolbarItems);
}

protected async writeToFile(path: jsoncParser.JSONPath, value: unknown, insertion = false): Promise<boolean> {
Expand Down
Loading