deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './keymaps-frontend-module';
export * from './keymaps-service';
export * from './keymaps-frontend-contribution';

View File

@@ -0,0 +1,94 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { nls, CommandRegistry, deepClone } from '@theia/core/lib/common';
import { JsonSchemaContribution, JsonSchemaDataStore, JsonSchemaRegisterContext } from '@theia/core/lib/browser/json-schema-store';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { IJSONSchema } from '@theia/core/lib/common/json-schema';
@injectable()
export class KeybindingSchemaUpdater implements JsonSchemaContribution {
protected readonly uri = new URI(keybindingSchemaId);
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
@inject(JsonSchemaDataStore) protected readonly schemaStore: JsonSchemaDataStore;
@postConstruct()
protected init(): void {
this.updateSchema();
this.commandRegistry.onCommandsChanged(() => this.updateSchema());
}
registerSchemas(store: JsonSchemaRegisterContext): void {
store.registerSchema({
fileMatch: ['keybindings.json', 'keymaps.json'],
url: this.uri.toString(),
});
}
protected updateSchema(): void {
const schema = deepClone(keybindingSchema);
const enumValues = schema.items.allOf[0].properties!.command.anyOf[1].enum!;
const enumDescriptions = schema.items.allOf[0].properties!.command.anyOf[1].enumDescriptions!;
for (const command of this.commandRegistry.getAllCommands()) {
if (command.handlers.length > 0 && !command.id.startsWith('_')) {
enumValues.push(command.id);
enumDescriptions.push(command.label ?? '');
}
}
this.schemaStore.setSchema(this.uri, schema);
}
}
const keybindingSchemaId = 'vscode://schemas/keybindings';
export const keybindingSchema = {
$id: keybindingSchemaId,
type: 'array',
title: 'Keybinding Configuration File',
default: [],
definitions: {
key: { type: 'string', description: nls.localizeByDefault('Key or key sequence (separated by space)') },
},
items: {
type: 'object',
defaultSnippets: [{ body: { key: '$1', command: '$2', when: '$3' } }],
allOf: [
{
required: ['command'],
properties: {
command: {
anyOf: [{ type: 'string' }, { enum: <string[]>[], enumDescriptions: <string[]>[] }], description: nls.localizeByDefault('Name of the command to execute')
},
when: { type: 'string', description: nls.localizeByDefault('Condition when the key is active.') },
args: { description: nls.localizeByDefault('Arguments to pass to the command to execute.') },
context: {
type: 'string',
description: nls.localizeByDefault('Condition when the key is active.'),
deprecationMessage: nls.localize('theia/keybinding-schema-updater/deprecation', 'Use `when` clause instead.')
}
}
},
{
anyOf: [
{ required: ['key'], properties: { key: { $ref: '#/definitions/key' }, } },
{ required: ['keybinding'], properties: { keybinding: { $ref: '#/definitions/key' } } }
]
}
]
},
allowComments: true,
allowTrailingCommas: true,
} as const satisfies IJSONSchema;

View File

@@ -0,0 +1,954 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import React = require('@theia/core/shared/react');
import debounce = require('@theia/core/shared/lodash.debounce');
import * as fuzzy from '@theia/core/shared/fuzzy';
import { injectable, inject, postConstruct, unmanaged } from '@theia/core/shared/inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { CommandRegistry, Command } from '@theia/core/lib/common/command';
import { Keybinding } from '@theia/core/lib/common/keybinding';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import {
KeybindingRegistry, SingleTextInputDialog, KeySequence, ConfirmDialog, Message, KeybindingScope,
SingleTextInputDialogProps, Key, ScopedKeybinding, codicon, StatefulWidget, Widget, ContextMenuRenderer, SELECTED_CLASS
} from '@theia/core/lib/browser';
import { KeymapsService } from './keymaps-service';
import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';
import { DisposableCollection, isOSX, isObject } from '@theia/core';
import { nls } from '@theia/core/lib/common/nls';
/**
* Representation of a keybinding item for the view.
*/
export interface KeybindingItem {
command: Command
keybinding?: ScopedKeybinding
/** human-readable labels can contain highlighting */
labels: {
id: RenderableLabel;
command: RenderableLabel;
keybinding: RenderableLabel;
context: RenderableLabel;
source: RenderableLabel;
}
visible?: boolean;
}
export namespace KeybindingItem {
export function is(arg: unknown): arg is KeybindingItem {
return isObject(arg) && 'command' in arg && 'labels' in arg;
}
export function keybinding(item: KeybindingItem): Keybinding {
return item.keybinding ?? {
command: item.command.id,
keybinding: ''
};
}
}
export interface RenderableLabel {
readonly value: string;
segments?: RenderableStringSegment[];
}
export interface RenderableStringSegment {
value: string;
match: boolean;
key?: boolean;
}
/**
* Representation of an individual table cell.
*/
export interface CellData {
/**
* The cell value.
*/
value: string,
/**
* Indicates if a cell's value is currently highlighted.
*/
highlighted: boolean,
}
@injectable()
export class KeybindingWidget extends ReactWidget implements StatefulWidget {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(KeybindingRegistry)
protected readonly keybindingRegistry: KeybindingRegistry;
@inject(KeymapsService)
protected readonly keymapsService: KeymapsService;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
static readonly ID = 'keybindings.view.widget';
static readonly LABEL = nls.localizeByDefault('Keyboard Shortcuts');
static readonly CONTEXT_MENU = ['keybinding-context-menu'];
static readonly COPY_MENU = [...KeybindingWidget.CONTEXT_MENU, 'a_copy'];
static readonly EDIT_MENU = [...KeybindingWidget.CONTEXT_MENU, 'b_edit'];
static readonly ADD_MENU = [...KeybindingWidget.CONTEXT_MENU, 'c_add'];
static readonly REMOVE_MENU = [...KeybindingWidget.CONTEXT_MENU, 'd_remove'];
static readonly SHOW_MENU = [...KeybindingWidget.CONTEXT_MENU, 'e_show'];
/**
* The list of all available keybindings.
*/
protected items: KeybindingItem[] = [];
/**
* The current user search query.
*/
protected query: string = '';
/**
* The regular expression used to extract values between fuzzy results.
*/
protected readonly regexp = /<match>(.*?)<\/match>/g;
/**
* The regular expression used to extract values between the keybinding separator.
*/
protected readonly keybindingSeparator = /<match>\+<\/match>/g;
/**
* The fuzzy search options.
* The `pre` and `post` options are used to wrap fuzzy matches.
*/
protected readonly fuzzyOptions = {
pre: '<match>',
post: '</match>',
};
protected readonly onDidUpdateEmitter = new Emitter<void>();
readonly onDidUpdate: Event<void> = this.onDidUpdateEmitter.event;
protected readonly onRenderCallbacks = new DisposableCollection();
protected onRender = () => this.onRenderCallbacks.dispose();
/**
* Search keybindings.
*/
protected readonly searchKeybindings: () => void = debounce(() => this.doSearchKeybindings(), 50);
constructor(@unmanaged() options?: Widget.IOptions) {
super(options);
this.onRender = this.onRender.bind(this);
}
/**
* Initialize the widget.
*/
@postConstruct()
protected init(): void {
this.id = KeybindingWidget.ID;
this.title.label = KeybindingWidget.LABEL;
this.title.caption = KeybindingWidget.LABEL;
this.title.iconClass = codicon('three-bars');
this.title.closable = true;
this.updateItemsAndRerender();
// Listen to changes made in the `keymaps.json` and update the view accordingly.
if (this.keymapsService.onDidChangeKeymaps) {
this.toDispose.push(this.keymapsService.onDidChangeKeymaps(() => {
this.items = this.getItems();
this.doSearchKeybindings();
}));
}
this.toDispose.push(this.keybindingRegistry.onKeybindingsChanged(this.updateItemsAndRerender));
}
protected updateItemsAndRerender = debounce(() => {
this.items = this.getItems();
this.update();
if (this.hasSearch()) {
this.doSearchKeybindings();
}
}, 100, { leading: false, trailing: true });
/**
* Determine if there currently is a search term.
* @returns `true` if a search term is present.
*/
hasSearch(): boolean {
return !!this.query.length;
}
/**
* Clear the search and reset the view.
*/
clearSearch(): void {
const search = this.findSearchField();
if (search) {
search.value = '';
this.query = '';
this.doSearchKeybindings();
}
}
/**
* Show keybinding items with the same key sequence as the given item.
* @param item the keybinding item
*/
showSameKeybindings(item: KeybindingItem): void {
const keybinding = item.keybinding;
if (keybinding) {
const search = this.findSearchField();
if (search) {
const query = `"${this.keybindingRegistry.acceleratorFor(keybinding, '+', true).join(' ')}"`;
search.value = query;
this.query = query;
this.doSearchKeybindings();
}
}
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.focusInputField();
}
/**
* Perform a search based on the user's search query.
*/
protected doSearchKeybindings(): void {
this.onDidUpdateEmitter.fire(undefined);
const searchField = this.findSearchField();
this.query = searchField ? searchField.value.trim().toLocaleLowerCase() : '';
let query = this.query;
const startsWithQuote = query.startsWith('"');
const endsWithQuote = query.endsWith('"');
const matchKeybindingOnly = startsWithQuote && endsWithQuote;
if (startsWithQuote) {
query = query.slice(1);
}
if (endsWithQuote) {
query = query.slice(0, -1);
}
const queryItems = query.split(/[+\s]/);
this.items.forEach(item => {
let matched = !this.query;
if (!matchKeybindingOnly) {
matched = this.formatAndMatchCommand(item) || matched;
}
matched = this.formatAndMatchKeybinding(item, queryItems, matchKeybindingOnly) || matched;
if (!matchKeybindingOnly) {
matched = this.formatAndMatchContext(item) || matched;
matched = this.formatAndMatchSource(item) || matched;
}
item.visible = matched;
});
this.update();
}
protected formatAndMatchCommand(item: KeybindingItem): boolean {
item.labels.command = this.toRenderableLabel(item.labels.command.value);
return Boolean(item.labels.command.segments);
}
protected formatAndMatchKeybinding(item: KeybindingItem, queryItems: string[], exactMatch?: boolean): boolean {
if (item.keybinding) {
const unmatchedTerms = queryItems.filter(Boolean);
const segments = this.keybindingRegistry.resolveKeybinding(item.keybinding).reduce<RenderableStringSegment[]>((collection, code, codeIndex) => {
if (codeIndex !== 0) {
// Two non-breaking spaces.
collection.push({ value: '\u00a0\u00a0', match: false, key: false });
}
const displayChunks = this.keybindingRegistry.componentsForKeyCode(code);
const matchChunks = isOSX ? this.keybindingRegistry.componentsForKeyCode(code, true) : displayChunks;
displayChunks.forEach((chunk, chunkIndex) => {
if (chunkIndex !== 0) {
collection.push({ value: '+', match: false, key: false });
}
const indexOfTerm = unmatchedTerms.indexOf(matchChunks[chunkIndex].toLocaleLowerCase());
const chunkMatches = indexOfTerm > -1;
if (chunkMatches) { unmatchedTerms.splice(indexOfTerm, 1); }
collection.push({ value: chunk, match: chunkMatches, key: true });
});
return collection;
}, []);
item.labels.keybinding = { value: item.labels.keybinding.value, segments };
if (unmatchedTerms.length) {
return false;
}
if (exactMatch) {
return !segments.some(segment => segment.key && !segment.match);
}
return true;
}
item.labels.keybinding = { value: '' };
return false;
}
protected formatAndMatchContext(item: KeybindingItem): boolean {
item.labels.context = this.toRenderableLabel(item.labels.context.value);
return Boolean(item.labels.context.segments);
}
protected formatAndMatchSource(item: KeybindingItem): boolean {
item.labels.source = this.toRenderableLabel(item.labels.source.value);
return Boolean(item.labels.source.segments);
}
protected toRenderableLabel(label: string, query: string = this.query): RenderableLabel {
if (label && query) {
const fuzzyMatch = fuzzy.match(query, label, this.fuzzyOptions);
if (fuzzyMatch) {
return {
value: label,
segments: fuzzyMatch.rendered.split(this.fuzzyOptions.pre).reduce<RenderableStringSegment[]>((collection, segment) => {
const [maybeMatch, notMatch] = segment.split(this.fuzzyOptions.post);
if (notMatch === undefined) {
collection.push({ value: maybeMatch, match: false });
} else {
collection.push({ value: maybeMatch, match: true }, { value: notMatch, match: false });
}
return collection;
}, [])
};
}
}
return { value: label };
}
/**
* Get the search input if available.
* @returns the search input if available.
*/
protected findSearchField(): HTMLInputElement | null {
return document.getElementById('search-kb') as HTMLInputElement;
}
/**
* Set the focus the search input field if available.
*/
protected focusInputField(): void {
const input = document.getElementById('search-kb');
if (input) {
(input as HTMLInputElement).focus();
(input as HTMLInputElement).select();
}
}
/**
* Render the view.
*/
protected render(): React.ReactNode {
return <div id='kb-main-container'>
{this.renderSearch()}
{(this.items.length > 0) ? this.renderTable() : this.renderMessage()}
</div>;
}
/**
* Render the search container with the search input.
*/
protected renderSearch(): React.ReactNode {
return <div>
<div className='search-kb-container'>
<input
id='search-kb'
ref={this.onRender}
className={`theia-input${(this.items.length > 0) ? '' : ' no-kb'}`}
type='text'
spellCheck={false}
placeholder={nls.localizeByDefault('Type to search in keybindings')}
autoComplete='off'
onKeyUp={this.searchKeybindings}
/>
</div>
</div>;
}
/**
* Render the warning message when no search results are found.
*/
protected renderMessage(): React.ReactNode {
return <AlertMessage
type='WARNING'
header='No results found!'
/>;
}
/**
* Render the keybindings table.
*/
protected renderTable(): React.ReactNode {
return <div id='kb-table-container'>
<div className='kb'>
<table>
<thead>
<tr>
<th className='th-action'></th>
<th className='th-label'>{nls.localizeByDefault('Command')}</th>
<th className='th-keybinding'>{nls.localizeByDefault('Keybinding')}</th>
<th className='th-context'>{nls.localizeByDefault('When')}</th>
<th className='th-source'>{nls.localizeByDefault('Source')}</th>
</tr>
</thead>
<tbody>
{this.renderRows()}
</tbody>
</table>
</div>
</div>;
}
/**
* Render the table rows.
*/
protected renderRows(): React.ReactNode {
return <React.Fragment>
{this.items.map((item, index) => item.visible !== false && this.renderRow(item, index))}
</React.Fragment>;
}
protected renderRow(item: KeybindingItem, index: number): React.ReactNode {
const { command, keybinding } = item;
// TODO get rid of array functions in event handlers
return <tr className='kb-item-row' key={index} onDoubleClick={event => this.handleItemDoubleClick(item, index, event)}
onClick={event => this.handleItemClick(item, index, event)}
onContextMenu={event => this.handleItemContextMenu(item, index, event)}>
<td className='kb-actions'>
{this.renderActions(item)}
</td>
<td className='kb-label' title={this.getCommandLabel(command)}>
{this.renderMatchedData(item.labels.command)}
</td>
<td title={this.getKeybindingLabel(keybinding)} className='kb-keybinding monaco-keybinding'>
{this.renderKeybinding(item)}
</td>
<td className='kb-context' title={this.getContextLabel(keybinding)}>
<code>{this.renderMatchedData(item.labels.context)}</code>
</td>
<td className='kb-source' title={this.getScopeLabel(keybinding)}>
<code className='td-source'>{this.renderMatchedData(item.labels.source)}</code>
</td>
</tr>;
}
protected handleItemClick(item: KeybindingItem, index: number, event: React.MouseEvent<HTMLElement>): void {
event.preventDefault();
this.selectItem(item, index, event.currentTarget);
}
protected handleItemDoubleClick(item: KeybindingItem, index: number, event: React.MouseEvent<HTMLElement>): void {
event.preventDefault();
this.selectItem(item, index, event.currentTarget);
this.editKeybinding(item);
}
protected handleItemContextMenu(item: KeybindingItem, index: number, event: React.MouseEvent<HTMLElement>): void {
event.preventDefault();
this.selectItem(item, index, event.currentTarget);
this.contextMenuRenderer.render({
menuPath: KeybindingWidget.CONTEXT_MENU,
anchor: event.nativeEvent,
args: [item, this],
context: event.currentTarget
});
}
protected selectItem(item: KeybindingItem, index: number, element: HTMLElement): void {
if (!element.classList.contains(SELECTED_CLASS)) {
const selected = element.parentElement?.getElementsByClassName(SELECTED_CLASS)[0];
if (selected) {
selected.classList.remove(SELECTED_CLASS);
}
element.classList.add(SELECTED_CLASS);
}
}
/**
* Render the actions container with action icons.
* @param item the keybinding item for the row.
*/
protected renderActions(item: KeybindingItem): React.ReactNode {
return <span className='kb-actions-icons'>{this.renderEdit(item)}{this.renderReset(item)}</span>;
}
/**
* Render the edit action used to update a keybinding.
* @param item the keybinding item for the row.
*/
protected renderEdit(item: KeybindingItem): React.ReactNode {
return <a title='Edit Keybinding' href='#' onClick={e => {
e.preventDefault();
this.editKeybinding(item);
}}><i className={`${codicon('edit', true)} kb-action-item`}></i></a>;
}
/**
* Render the reset action to reset the custom keybinding.
* Only visible if a keybinding has a `user` scope.
* @param item the keybinding item for the row.
*/
protected renderReset(item: KeybindingItem): React.ReactNode {
return this.canResetKeybinding(item)
? <a title='Reset Keybinding' href='#' onClick={e => {
e.preventDefault();
this.resetKeybinding(item);
}}><i className={`${codicon('discard', true)} kb-action-item`}></i></a> : '';
}
/**
* Render the keybinding.
* @param keybinding the keybinding value.
*/
protected renderKeybinding(keybinding: KeybindingItem): React.ReactNode {
if (!keybinding.keybinding) {
return undefined;
}
if (keybinding.labels.keybinding.segments) {
return keybinding.labels.keybinding.segments.map((segment, index) => {
if (segment.key) {
return <span key={index} className='monaco-keybinding-key'>
<span className={`${segment.match ? 'fuzzy-match' : ''}`}>{segment.value}</span>
</span>;
} else {
return <span key={index} className='monaco-keybinding-separator'>
{segment.value}
</span>;
}
});
}
console.warn('Unexpectedly encountered a keybinding without segment divisions');
return keybinding.labels.keybinding.value;
}
/**
* Get the list of keybinding items.
*
* @returns the list of keybinding items.
*/
protected getItems(): KeybindingItem[] {
// Sort the commands alphabetically.
const commands = this.commandRegistry.commands;
const items: KeybindingItem[] = [];
// Build the keybinding items.
for (let i = 0; i < commands.length; i++) {
const command = commands[i];
// Skip internal commands prefixed by `_`.
if (command.id.startsWith('_')) {
continue;
}
const keybindings = this.keybindingRegistry.getKeybindingsForCommand(command.id);
keybindings.forEach(keybinding => {
const item = this.createKeybindingItem(command, keybinding);
items.push(item);
});
// we might not have any keybindings for the command
if (keybindings.length < 1) {
const item = this.createKeybindingItem(command);
items.push(item);
}
}
return this.sortKeybindings(items);
}
protected createKeybindingItem(command: Command, keybinding?: ScopedKeybinding): KeybindingItem {
const item = {
command,
keybinding,
labels: {
id: { value: command.id },
command: { value: this.getCommandLabel(command) },
keybinding: { value: this.getKeybindingLabel(keybinding) || '' },
context: { value: this.getContextLabel(keybinding) || '' },
source: { value: this.getScopeLabel(keybinding) || '' }
}
};
this.formatAndMatchCommand(item);
this.formatAndMatchKeybinding(item, []);
this.formatAndMatchContext(item);
this.formatAndMatchSource(item);
return item;
}
/**
* @returns the input array, sorted.
* The sort priority is as follows: items with keybindings before those without, then alphabetical by command.
*/
protected sortKeybindings(bindings: KeybindingItem[]): KeybindingItem[] {
return bindings.sort((a, b) => {
if (a.keybinding && !b.keybinding) {
return -1;
}
if (b.keybinding && !a.keybinding) {
return 1;
}
return this.compareItem(a.command, b.command);
});
}
/**
* Get the human-readable label for a given command.
* @param command the command.
*
* @returns a human-readable label for the given command.
*/
protected getCommandLabel(command: Command): string {
if (command.label) {
// Prefix the command label with the category if it exists, else return the simple label.
return command.category ? `${command.category}: ${command.label}` : command.label;
}
return command.id;
}
protected getKeybindingLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
return keybinding && keybinding.keybinding;
}
protected getContextLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
return keybinding ? keybinding.context || keybinding.when : undefined;
}
protected getScopeLabel(keybinding: ScopedKeybinding | undefined): string | undefined {
let scope = keybinding && keybinding.scope;
if (scope !== undefined) {
if (scope < KeybindingScope.USER) {
scope = KeybindingScope.DEFAULT;
}
return KeybindingScope[scope].toLocaleLowerCase();
}
return undefined;
}
/**
* Compare two commands.
* - Commands with a label should be prioritized and alphabetically sorted.
* - Commands without a label (id) should be placed at the bottom.
* @param a the first command.
* @param b the second command.
*
* @returns an integer indicating whether `a` comes before, after or is equivalent to `b`.
* - returns `-1` if `a` occurs before `b`.
* - returns `1` if `a` occurs after `b`.
* - returns `0` if they are equivalent.
*/
protected compareItem(a: Command, b: Command): number {
const labelA = this.getCommandLabel(a);
const labelB = this.getCommandLabel(b);
if (labelA === a.id && labelB === b.id) {
return labelA.toLowerCase().localeCompare(labelB.toLowerCase());
}
if (labelA === a.id) {
return 1;
}
if (labelB === b.id) {
return -1;
}
return labelA.toLowerCase().localeCompare(labelB.toLowerCase());
}
/**
* Prompt users to update the keybinding for the given command.
* @param item the keybinding item.
*/
editKeybinding(item: KeybindingItem): void {
const command = item.command.id;
const oldKeybinding = item.keybinding;
const dialog = new EditKeybindingDialog({
title: nls.localize('theia/keymaps/editKeybindingTitle', 'Edit Keybinding for {0}', item.labels.command.value),
maxWidth: 400,
initialValue: oldKeybinding?.keybinding,
validate: newKeybinding => this.validateKeybinding(command, oldKeybinding?.keybinding, newKeybinding),
}, this.keymapsService, item, this.canResetKeybinding(item));
dialog.open().then(async keybinding => {
if (keybinding && keybinding !== oldKeybinding?.keybinding) {
await this.keymapsService.setKeybinding({
...oldKeybinding,
command,
keybinding
}, oldKeybinding);
}
});
}
/**
* Prompt users to update when expression for the given keybinding.
* @param item the keybinding item
*/
editWhenExpression(item: KeybindingItem): void {
const keybinding = item.keybinding;
if (!keybinding) {
return;
}
const dialog = new SingleTextInputDialog({
title: nls.localize('theia/keymaps/editWhenExpressionTitle', 'Edit When Expression for {0}', item.labels.command.value),
maxWidth: 400,
initialValue: keybinding.when
});
dialog.open().then(async when => {
if (when === undefined) {
return; // cancelled by the user
}
if (when !== (keybinding.when ?? '')) {
if (when === '') {
when = undefined;
}
await this.keymapsService.setKeybinding({
...keybinding,
when
}, keybinding);
}
});
}
/**
* Prompt users to add a keybinding for the given command.
* @param item the keybinding item
*/
addKeybinding(item: KeybindingItem): void {
const command = item.command.id;
const dialog = new SingleTextInputDialog({
title: nls.localize('theia/keymaps/addKeybindingTitle', 'Add Keybinding for {0}', item.labels.command.value),
maxWidth: 400,
validate: newKeybinding => this.validateKeybinding(command, undefined, newKeybinding),
});
dialog.open().then(async keybinding => {
if (keybinding) {
await this.keymapsService.setKeybinding({
...item.keybinding,
command,
keybinding
}, undefined);
}
});
}
/**
* Prompt users for confirmation before resetting.
* @param command the command label.
*
* @returns a Promise which resolves to `true` if a user accepts resetting.
*/
protected async confirmResetKeybinding(item: KeybindingItem): Promise<boolean> {
const message = document.createElement('div');
const question = document.createElement('p');
question.textContent = nls.localize('theia/keymaps/resetKeybindingConfirmation', 'Do you really want to reset this keybinding to its default value?');
message.append(question);
const info = document.createElement('p');
info.textContent = nls.localize('theia/keymaps/resetMultipleKeybindingsWarning', 'If multiple keybindings exist for this command, all of them will be reset.');
message.append(info);
const dialog = new ConfirmDialog({
title: nls.localize('theia/keymaps/resetKeybindingTitle', 'Reset keybinding for {0}', this.getCommandLabel(item.command)),
msg: message
});
return !!await dialog.open();
}
/**
* Reset the keybinding to its default value.
* @param item the keybinding item.
*/
async resetKeybinding(item: KeybindingItem): Promise<void> {
const confirmed = await this.confirmResetKeybinding(item);
if (confirmed) {
this.keymapsService.removeKeybinding(item.command.id);
}
}
/**
* Whether the keybinding can be reset to its default value.
* @param item the keybinding item
*/
canResetKeybinding(item: KeybindingItem): boolean {
return item.keybinding?.scope === KeybindingScope.USER || this.keymapsService.hasKeybinding('-' + item.command.id);
}
/**
* Validate the provided keybinding value against its previous value.
* @param command the command label.
* @param oldKeybinding the old keybinding value.
* @param keybinding the new keybinding value.
*
* @returns the end user message to display.
*/
protected validateKeybinding(command: string, oldKeybinding: string | undefined, keybinding: string): string {
if (!keybinding) {
return nls.localize('theia/keymaps/requiredKeybindingValidation', 'keybinding value is required');
}
try {
const binding = { command, keybinding };
KeySequence.parse(keybinding);
if (oldKeybinding === keybinding) {
return ''; // if old and new keybindings match, quietly reject update
}
if (this.keybindingRegistry.containsKeybindingInScope(binding)) {
return nls.localize('theia/keymaps/keybindingCollidesValidation', 'keybinding currently collides');
}
return '';
} catch (error) {
return error;
}
}
/**
* Build the cell data with highlights if applicable.
* @param raw the raw cell value.
*
* @returns the list of cell data.
*/
protected buildCellData(raw: string): CellData[] {
const data: CellData[] = [];
if (this.query === '') {
return data;
}
let following = raw;
let leading;
let result;
const regexp = new RegExp(this.regexp);
while (result = regexp.exec(raw)) {
const splitLeftIndex = following.indexOf(result[0]);
const splitRightIndex = splitLeftIndex + result[0].length;
leading = following.slice(0, splitLeftIndex);
following = following.slice(splitRightIndex);
if (leading) {
data.push({ value: leading, highlighted: false });
}
data.push({ value: result[1], highlighted: true });
}
if (following) {
data.push({ value: following, highlighted: false });
}
return data;
}
/**
* Render the fuzzy representation of a matched result.
* @param property one of the `KeybindingItem` properties.
*/
protected renderMatchedData(property: RenderableLabel): React.ReactNode {
if (property.segments) {
return <>
{
property.segments.map((segment, index) => segment.match
? <span key={index} className='fuzzy-match'>{segment.value}</span>
: <span key={index}>{segment.value}</span>)
}
</>;
}
return property.value;
}
storeState(): object | undefined {
return { query: this.query };
}
restoreState(oldState: { query: string }): void {
if (typeof oldState?.query === 'string') {
this.onRenderCallbacks.push({
dispose: () => {
const searchField = this.findSearchField();
if (searchField) {
searchField.value = oldState.query;
this.searchKeybindings();
}
}
});
}
}
}
/**
* Dialog used to edit keybindings, and reset custom keybindings.
*/
class EditKeybindingDialog extends SingleTextInputDialog {
/**
* The keybinding item in question.
*/
protected item: KeybindingItem;
/**
* HTMLButtonElement used to reset custom keybindings.
* Custom keybindings have a `User` scope (exist in `keymaps.json`).
*/
protected resetButton: HTMLButtonElement | undefined;
constructor(
@inject(SingleTextInputDialogProps) props: SingleTextInputDialogProps,
@inject(KeymapsService) protected readonly keymapsService: KeymapsService,
item: KeybindingItem,
canReset: boolean
) {
super(props);
this.item = item;
// Add the `Reset` button if the command currently has a custom keybinding.
if (canReset) {
this.appendResetButton();
}
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
if (this.resetButton) {
this.addResetAction(this.resetButton, 'click');
}
}
/**
* Add `Reset` action used to reset a custom keybinding, and close the dialog.
* @param element the HTML element in question.
* @param additionalEventTypes additional event types.
*/
protected addResetAction<K extends keyof HTMLElementEventMap>(element: HTMLElement, ...additionalEventTypes: K[]): void {
this.addKeyListener(element, Key.ENTER, () => {
this.reset();
this.close();
}, ...additionalEventTypes);
}
/**
* Create the `Reset` button, and append it to the dialog.
*
* @returns the `Reset` button.
*/
protected appendResetButton(): HTMLButtonElement {
// Create the `Reset` button.
const resetButtonTitle = nls.localizeByDefault('Reset');
this.resetButton = this.createButton(resetButtonTitle);
// Add the `Reset` button to the dialog control panel, before the `Accept` button.
this.controlPanel.insertBefore(this.resetButton, this.acceptButton!);
this.resetButton.title = nls.localizeByDefault('Reset Keybinding');
this.resetButton.classList.add('secondary');
return this.resetButton;
}
/**
* Perform keybinding reset.
*/
protected reset(): void {
this.keymapsService.removeKeybinding(this.item.command.id);
}
}

View File

@@ -0,0 +1,295 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import {
CommandContribution,
Command,
CommandRegistry,
MenuContribution,
MenuModelRegistry
} from '@theia/core/lib/common';
import { AbstractViewContribution, codicon, Widget, CommonCommands, CommonMenus } from '@theia/core/lib/browser';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { KeymapsService } from './keymaps-service';
import { Keybinding } from '@theia/core/lib/common/keybinding';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { KeybindingItem, KeybindingWidget } from './keybindings-widget';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { nls } from '@theia/core/lib/common/nls';
export namespace KeymapsCommands {
export const OPEN_KEYMAPS = Command.toDefaultLocalizedCommand({
id: 'keymaps:open',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Open Keyboard Shortcuts',
});
export const OPEN_KEYMAPS_JSON = Command.toDefaultLocalizedCommand({
id: 'keymaps:openJson',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Open Keyboard Shortcuts (JSON)',
});
export const OPEN_KEYMAPS_JSON_TOOLBAR: Command = {
id: 'keymaps:openJson.toolbar',
iconClass: codicon('json')
};
export const CLEAR_KEYBINDINGS_SEARCH: Command = {
id: 'keymaps.clearSearch',
iconClass: codicon('clear-all')
};
export const COPY_KEYBINDING = Command.toLocalizedCommand({
id: 'keymaps:keybinding.copy',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Copy Keybinding'
}, 'theia/keymaps/keybinding/copy', CommonCommands.PREFERENCES_CATEGORY_KEY);
export const COPY_COMMAND_ID = Command.toLocalizedCommand({
id: 'keymaps:keybinding.copyCommandId',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Copy Keybinding Command ID'
}, 'theia/keymaps/keybinding/copyCommandId', CommonCommands.PREFERENCES_CATEGORY_KEY);
export const COPY_COMMAND_TITLE = Command.toLocalizedCommand({
id: 'keymaps:keybinding.copyCommandTitle',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Copy Keybinding Command Title'
}, 'theia/keymaps/keybinding/copyCommandTitle', CommonCommands.PREFERENCES_CATEGORY_KEY);
export const EDIT_KEYBINDING = Command.toLocalizedCommand({
id: 'keymaps:keybinding.edit',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Edit Keybinding...'
}, 'theia/keymaps/keybinding/edit', CommonCommands.PREFERENCES_CATEGORY_KEY);
export const EDIT_WHEN_EXPRESSION = Command.toLocalizedCommand({
id: 'keymaps:keybinding.editWhenExpression',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Edit Keybinding When Expression...'
}, 'theia/keymaps/keybinding/editWhenExpression', CommonCommands.PREFERENCES_CATEGORY_KEY);
export const ADD_KEYBINDING = Command.toDefaultLocalizedCommand({
id: 'keymaps:keybinding.add',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Add Keybinding...'
});
export const REMOVE_KEYBINDING = Command.toDefaultLocalizedCommand({
id: 'keymaps:keybinding.remove',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Remove Keybinding'
});
export const RESET_KEYBINDING = Command.toDefaultLocalizedCommand({
id: 'keymaps:keybinding.reset',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Reset Keybinding'
});
export const SHOW_SAME = Command.toDefaultLocalizedCommand({
id: 'keymaps:keybinding.showSame',
category: CommonCommands.PREFERENCES_CATEGORY,
label: 'Show Same Keybindings'
});
}
@injectable()
export class KeymapsFrontendContribution extends AbstractViewContribution<KeybindingWidget> implements CommandContribution, MenuContribution, TabBarToolbarContribution {
@inject(KeymapsService)
protected readonly keymaps: KeymapsService;
@inject(ClipboardService)
protected readonly clipboard: ClipboardService;
constructor() {
super({
widgetId: KeybindingWidget.ID,
widgetName: KeybindingWidget.LABEL,
defaultWidgetOptions: {
area: 'main'
},
});
}
override registerCommands(commands: CommandRegistry): void {
commands.registerCommand(KeymapsCommands.OPEN_KEYMAPS, {
isEnabled: () => true,
execute: () => this.openView({ activate: true })
});
commands.registerCommand(KeymapsCommands.OPEN_KEYMAPS_JSON, {
isEnabled: () => true,
execute: () => this.keymaps.open()
});
commands.registerCommand(KeymapsCommands.OPEN_KEYMAPS_JSON_TOOLBAR, {
isEnabled: w => this.withWidget(w, () => true),
isVisible: w => this.withWidget(w, () => true),
execute: w => this.withWidget(w, widget => this.keymaps.open(widget)),
});
commands.registerCommand(KeymapsCommands.CLEAR_KEYBINDINGS_SEARCH, {
isEnabled: w => this.withWidget(w, widget => widget.hasSearch()),
isVisible: w => this.withWidget(w, () => true),
execute: w => this.withWidget(w, widget => widget.clearSearch()),
});
commands.registerCommand(KeymapsCommands.COPY_KEYBINDING, {
isEnabled: (...args) => this.withItem(() => true, ...args),
isVisible: (...args) => this.withItem(() => true, ...args),
execute: (...args) => this.withItem(item => this.clipboard.writeText(
JSON.stringify(Keybinding.apiObjectify(KeybindingItem.keybinding(item)), undefined, ' ')
), ...args)
});
commands.registerCommand(KeymapsCommands.COPY_COMMAND_ID, {
isEnabled: (...args) => this.withItem(() => true, ...args),
isVisible: (...args) => this.withItem(() => true, ...args),
execute: (...args) => this.withItem(item => this.clipboard.writeText(item.command.id), ...args)
});
commands.registerCommand(KeymapsCommands.COPY_COMMAND_TITLE, {
isEnabled: (...args) => this.withItem(item => !!item.command.label, ...args),
isVisible: (...args) => this.withItem(() => true, ...args),
execute: (...args) => this.withItem(item => this.clipboard.writeText(item.command.label!), ...args)
});
commands.registerCommand(KeymapsCommands.EDIT_KEYBINDING, {
isEnabled: (...args) => this.withWidgetItem(() => true, ...args),
isVisible: (...args) => this.withWidgetItem(() => true, ...args),
execute: (...args) => this.withWidgetItem((item, widget) => widget.editKeybinding(item), ...args)
});
commands.registerCommand(KeymapsCommands.EDIT_WHEN_EXPRESSION, {
isEnabled: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
isVisible: (...args) => this.withWidgetItem(() => true, ...args),
execute: (...args) => this.withWidgetItem((item, widget) => widget.editWhenExpression(item), ...args)
});
commands.registerCommand(KeymapsCommands.ADD_KEYBINDING, {
isEnabled: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
isVisible: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
execute: (...args) => this.withWidgetItem((item, widget) => widget.addKeybinding(item), ...args)
});
commands.registerCommand(KeymapsCommands.REMOVE_KEYBINDING, {
isEnabled: (...args) => this.withItem(item => !!item.keybinding, ...args),
isVisible: (...args) => this.withItem(() => true, ...args),
execute: (...args) => this.withItem(item => this.keymaps.unsetKeybinding(item.keybinding!), ...args)
});
commands.registerCommand(KeymapsCommands.RESET_KEYBINDING, {
isEnabled: (...args) => this.withWidgetItem((item, widget) => widget.canResetKeybinding(item), ...args),
isVisible: (...args) => this.withWidgetItem(() => true, ...args),
execute: (...args) => this.withWidgetItem((item, widget) => widget.resetKeybinding(item), ...args)
});
commands.registerCommand(KeymapsCommands.SHOW_SAME, {
isEnabled: (...args) => this.withWidgetItem(item => !!item.keybinding, ...args),
isVisible: (...args) => this.withWidgetItem(() => true, ...args),
execute: (...args) => this.withWidgetItem((item, widget) => widget.showSameKeybindings(item), ...args)
});
}
override registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(CommonMenus.FILE_SETTINGS_SUBMENU_OPEN, {
commandId: KeymapsCommands.OPEN_KEYMAPS.id,
label: nls.localizeByDefault('Keyboard Shortcuts'),
order: 'a20'
});
menus.registerMenuAction(CommonMenus.MANAGE_SETTINGS, {
commandId: KeymapsCommands.OPEN_KEYMAPS.id,
label: nls.localizeByDefault('Keyboard Shortcuts'),
order: 'a30'
});
menus.registerMenuAction(KeybindingWidget.COPY_MENU, {
commandId: KeymapsCommands.COPY_KEYBINDING.id,
label: nls.localizeByDefault('Copy'),
order: 'a'
});
menus.registerMenuAction(KeybindingWidget.COPY_MENU, {
commandId: KeymapsCommands.COPY_COMMAND_ID.id,
label: nls.localizeByDefault('Copy Command ID'),
order: 'b'
});
menus.registerMenuAction(KeybindingWidget.COPY_MENU, {
commandId: KeymapsCommands.COPY_COMMAND_TITLE.id,
label: nls.localizeByDefault('Copy Command Title'),
order: 'c'
});
menus.registerMenuAction(KeybindingWidget.EDIT_MENU, {
commandId: KeymapsCommands.EDIT_KEYBINDING.id,
label: nls.localize('theia/keymaps/editKeybinding', 'Edit Keybinding...'),
order: 'a'
});
menus.registerMenuAction(KeybindingWidget.EDIT_MENU, {
commandId: KeymapsCommands.EDIT_WHEN_EXPRESSION.id,
label: nls.localize('theia/keymaps/editWhenExpression', 'Edit When Expression...'),
order: 'b'
});
menus.registerMenuAction(KeybindingWidget.ADD_MENU, {
commandId: KeymapsCommands.ADD_KEYBINDING.id,
label: nls.localizeByDefault('Add Keybinding...'),
order: 'a'
});
menus.registerMenuAction(KeybindingWidget.REMOVE_MENU, {
commandId: KeymapsCommands.REMOVE_KEYBINDING.id,
label: nls.localizeByDefault('Remove Keybinding'),
order: 'a'
});
menus.registerMenuAction(KeybindingWidget.REMOVE_MENU, {
commandId: KeymapsCommands.RESET_KEYBINDING.id,
label: nls.localizeByDefault('Reset Keybinding'),
order: 'b'
});
menus.registerMenuAction(KeybindingWidget.SHOW_MENU, {
commandId: KeymapsCommands.SHOW_SAME.id,
label: nls.localizeByDefault('Show Same Keybindings'),
order: 'a'
});
}
override registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: KeymapsCommands.OPEN_KEYMAPS.id,
keybinding: 'ctrl+alt+,'
});
}
async registerToolbarItems(toolbar: TabBarToolbarRegistry): Promise<void> {
const widget = await this.widget;
const onDidChange = widget.onDidUpdate;
toolbar.registerItem({
id: KeymapsCommands.OPEN_KEYMAPS_JSON_TOOLBAR.id,
command: KeymapsCommands.OPEN_KEYMAPS_JSON_TOOLBAR.id,
tooltip: nls.localizeByDefault('Open Keyboard Shortcuts (JSON)'),
priority: 0,
});
toolbar.registerItem({
id: KeymapsCommands.CLEAR_KEYBINDINGS_SEARCH.id,
command: KeymapsCommands.CLEAR_KEYBINDINGS_SEARCH.id,
tooltip: nls.localizeByDefault('Clear Keybindings Search Input'),
priority: 1,
onDidChange,
});
}
/**
* Determine if the current widget is the keybindings widget.
*/
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: KeybindingWidget) => T): T | false {
if (widget instanceof KeybindingWidget && widget.id === KeybindingWidget.ID) {
return fn(widget);
}
return false;
}
protected withItem<T>(fn: (item: KeybindingItem, ...rest: unknown[]) => T, ...args: unknown[]): T | false {
const [item] = args;
if (KeybindingItem.is(item)) {
return fn(item, args.slice(1));
}
return false;
}
protected withWidgetItem<T>(fn: (item: KeybindingItem, widget: KeybindingWidget, ...rest: unknown[]) => T, ...args: unknown[]): T | false {
const [item, widget] = args;
if (widget instanceof KeybindingWidget && widget.id === KeybindingWidget.ID && KeybindingItem.is(item)) {
return fn(item, widget, args.slice(2));
}
return false;
}
}

View File

@@ -0,0 +1,45 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import './keymaps-monaco-contribution';
import '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { KeymapsService } from './keymaps-service';
import { KeymapsFrontendContribution } from './keymaps-frontend-contribution';
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { KeybindingContribution } from '@theia/core/lib/browser/keybinding';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { noopWidgetStatusBarContribution, WidgetFactory, WidgetStatusBarContribution } from '@theia/core/lib/browser';
import { KeybindingWidget } from './keybindings-widget';
import { KeybindingSchemaUpdater } from './keybinding-schema-updater';
import { JsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
export default new ContainerModule(bind => {
bind(KeymapsService).toSelf().inSingletonScope();
bind(KeymapsFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(KeymapsFrontendContribution);
bind(KeybindingContribution).toService(KeymapsFrontendContribution);
bind(MenuContribution).toService(KeymapsFrontendContribution);
bind(KeybindingWidget).toSelf();
bind(TabBarToolbarContribution).toService(KeymapsFrontendContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: KeybindingWidget.ID,
createWidget: () => context.container.get<KeybindingWidget>(KeybindingWidget),
})).inSingletonScope();
bind(KeybindingSchemaUpdater).toSelf().inSingletonScope();
bind(JsonSchemaContribution).toService(KeybindingSchemaUpdater);
bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(KeybindingWidget));
});

View File

@@ -0,0 +1,26 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as monaco from '@theia/monaco-editor-core';
monaco.languages.register({
id: 'jsonc',
'aliases': [
'JSON with Comments'
],
'filenames': [
'keymaps.json'
]
});

View File

@@ -0,0 +1,214 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { OpenerService, open, WidgetOpenerOptions, Widget } from '@theia/core/lib/browser';
import { KeybindingRegistry, KeybindingScope, ScopedKeybinding } from '@theia/core/lib/browser/keybinding';
import { Keybinding, RawKeybinding } from '@theia/core/lib/common/keybinding';
import { UserStorageUri } from '@theia/userstorage/lib/browser';
import * as jsoncparser from 'jsonc-parser';
import { Emitter } from '@theia/core/lib/common/event';
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { Deferred } from '@theia/core/lib/common/promise-util';
import URI from '@theia/core/lib/common/uri';
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
import { MessageService } from '@theia/core/lib/common/message-service';
import { MonacoJSONCEditor } from '@theia/preferences/lib/browser/monaco-jsonc-editor';
@injectable()
export class KeymapsService {
@inject(MonacoWorkspace)
protected readonly workspace: MonacoWorkspace;
@inject(MonacoTextModelService)
protected readonly textModelService: MonacoTextModelService;
@inject(KeybindingRegistry)
protected readonly keybindingRegistry: KeybindingRegistry;
@inject(OpenerService)
protected readonly opener: OpenerService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(MonacoJSONCEditor)
protected readonly jsoncEditor: MonacoJSONCEditor;
protected readonly changeKeymapEmitter = new Emitter<void>();
readonly onDidChangeKeymaps = this.changeKeymapEmitter.event;
protected model: MonacoEditorModel | undefined;
protected readonly deferredModel = new Deferred<MonacoEditorModel>();
/**
* Initialize the keybinding service.
*/
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
const reference = await this.textModelService.createModelReference(UserStorageUri.resolve('keymaps.json'));
this.model = reference.object;
this.deferredModel.resolve(this.model);
this.reconcile();
this.model.onDidChangeContent(() => this.reconcile());
this.model.onDirtyChanged(() => this.reconcile());
this.model.onDidChangeValid(() => this.reconcile());
this.keybindingRegistry.onKeybindingsChanged(() => this.changeKeymapEmitter.fire(undefined));
}
/**
* Reconcile all the keybindings, registering them to the registry.
*/
protected reconcile(): void {
const model = this.model;
if (!model || model.dirty) {
return;
}
try {
const keybindings: Keybinding[] = [];
if (model.valid) {
const content = model.getText();
const json = jsoncparser.parse(content, undefined, { disallowComments: false });
if (Array.isArray(json)) {
for (const value of json) {
if (Keybinding.is(value)) {
keybindings.push(value);
} else if (RawKeybinding.is(value)) {
keybindings.push(Keybinding.apiObjectify(value));
}
}
}
}
this.keybindingRegistry.setKeymap(KeybindingScope.USER, keybindings);
} catch (e) {
console.error(`Failed to load keymaps from '${model.uri}'.`, e);
}
}
/**
* Open the keybindings widget.
* @param ref the optional reference for opening the widget.
*/
async open(ref?: Widget): Promise<void> {
const model = await this.deferredModel.promise;
const options: WidgetOpenerOptions = {
widgetOptions: ref ? { area: 'main', mode: 'split-right', ref } : { area: 'main' },
mode: 'activate'
};
if (!model.valid) {
await model.save();
}
await open(this.opener, new URI(model.uri), options);
}
/**
* Set the keybinding in the JSON.
* @param newKeybinding the new JSON keybinding
* @param oldKeybinding the old JSON keybinding
*/
async setKeybinding(newKeybinding: Keybinding, oldKeybinding: ScopedKeybinding | undefined): Promise<void> {
return this.updateKeymap(() => {
const keybindings: Keybinding[] = [...this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER)];
if (!oldKeybinding) {
Keybinding.addKeybinding(keybindings, newKeybinding);
return keybindings;
} else if (oldKeybinding.scope === KeybindingScope.DEFAULT) {
Keybinding.addKeybinding(keybindings, newKeybinding);
const disabledBinding = {
...oldKeybinding,
command: '-' + oldKeybinding.command
};
Keybinding.addKeybinding(keybindings, disabledBinding);
return keybindings;
} else if (Keybinding.replaceKeybinding(keybindings, oldKeybinding, newKeybinding)) {
return keybindings;
}
});
}
/**
* Unset the given keybinding in the JSON.
* If the given keybinding has a default scope, it will be disabled in the JSON.
* Otherwise, it will be removed from the JSON.
* @param keybinding the keybinding to unset
*/
unsetKeybinding(keybinding: ScopedKeybinding): Promise<void> {
return this.updateKeymap(() => {
const keybindings = this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER);
if (keybinding.scope === KeybindingScope.DEFAULT) {
const result: Keybinding[] = [...keybindings];
const disabledBinding = {
...keybinding,
command: '-' + keybinding.command
};
Keybinding.addKeybinding(result, disabledBinding);
return result;
} else {
const filtered = keybindings.filter(a => !Keybinding.equals(a, keybinding, false, true));
if (filtered.length !== keybindings.length) {
return filtered;
}
}
});
}
/**
* Whether there is a keybinding with the given command id in the JSON.
* @param commandId the keybinding command id
*/
hasKeybinding(commandId: string): boolean {
const keybindings = this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER);
return keybindings.some(a => a.command === commandId);
}
/**
* Remove the keybindings with the given command id from the JSON.
* This includes disabled keybindings.
* @param commandId the keybinding command id.
*/
removeKeybinding(commandId: string): Promise<void> {
return this.updateKeymap(() => {
const keybindings = this.keybindingRegistry.getKeybindingsByScope(KeybindingScope.USER);
const removedCommand = '-' + commandId;
const filtered = keybindings.filter(a => a.command !== commandId && a.command !== removedCommand);
if (filtered.length !== keybindings.length) {
return filtered;
}
});
}
protected async updateKeymap(op: () => Keybinding[] | void): Promise<void> {
const model = await this.deferredModel.promise;
try {
const keybindings = op();
if (keybindings && this.model) {
await this.jsoncEditor.setValue(this.model, [], keybindings.map(binding => Keybinding.apiObjectify(binding)));
}
} catch (e) {
const message = `Failed to update a keymap in '${model.uri}'.`;
this.messageService.error(`${message} Please check if it is corrupted.`);
console.error(`${message}`, e);
}
}
}

View File

@@ -0,0 +1,182 @@
/********************************************************************************
* Copyright (C) 2018 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
#kb-main-container {
display: flex;
flex-direction: column;
height: 100%;
}
#kb-table-container {
flex: 1;
overflow: auto;
user-select: none;
}
.fuzzy-match {
font-weight: 600;
color: var(--theia-list-highlightForeground);
}
.kb-actions {
text-align: center;
vertical-align: middle;
}
.kb-action-item {
visibility: hidden;
}
.kb table {
border-spacing: 0;
border-collapse: separate;
background-color: var(--theia-editor-background);
width: 100%;
table-layout: fixed;
}
.kb table tr {
min-height: var(--theia-icon-size);
}
.th-action,
.th-keybinding,
.kb-actions,
.kb-keybinding {
min-height: 18px;
overflow: hidden;
vertical-align: middle;
white-space: nowrap;
}
.th-action,
.kb-actions {
padding: 2px 0px 5px 0px;
}
.th-keybinding,
.kb-keybinding {
padding: 2px 10px 5px 10px;
}
.th-label,
.th-source,
.th-context,
.th-keybinding,
.kb-label,
.kb-source,
.kb-context {
padding: 2px 10px 5px 10px;
min-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.kb table th {
font-size: var(--theia-ui-font-size1);
}
.kb table td code {
font-size: 90%;
}
.td-source {
text-transform: lowercase;
}
.kb table tr:nth-child(odd) {
background-color: rgba(130, 130, 130, 0.04);
}
.kb table tbody tr:hover {
background-color: var(--theia-list-hoverBackground);
color: var(--theia-list-hoverForeground);
}
.kb table tbody tr.theia-mod-selected {
background-color: var(--theia-list-inactiveSelectionBackground);
color: var(--theia-list-inactiveSelectionForeground);
}
.kb table tbody tr:hover .kb-action-item,
.kb table tbody tr.theia-mod-selected .kb-action-item {
visibility: visible;
color: var(--theia-icon-foreground);
text-decoration: none;
}
.kb table th {
word-break: keep-all;
padding-bottom: 5px;
padding-top: 5px;
text-align: left;
vertical-align: middle;
position: sticky;
top: 0;
background-color: var(--theia-editorWidget-background);
text-transform: capitalize;
}
.kb table .th-action {
width: 4%;
}
.kb table .th-label {
width: 25%;
}
.kb table .th-keybinding {
width: 20%;
}
.kb table .th-source {
width: 10%;
}
.kb table .th-context {
width: 25%;
}
.no-kb {
border: 1px solid var(--theia-editorWarning-foreground);
}
#search-kb {
height: 25px;
flex: 1;
}
.vs #search-kb {
border: 1px solid #ddd;
}
.search-kb-container {
padding: 10px;
display: flex;
}
.kb-item-row td a,
.kb-item-row td a:active,
.kb-item-row td a:focus {
outline: 0;
border: none;
}
.kb-actions-icons {
display: block;
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('keymaps package', () => {
it('support code coverage statistics', () => true);
});