Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added key binding functionality for any task. #10676

Closed
wants to merge 8 commits into from
Closed
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
2 changes: 1 addition & 1 deletion extensions/configuration-editing/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ export function activate(context) {
}

function registerKeybindingsCompletions(): vscode.Disposable {
const commands = vscode.commands.getCommands(true);

return vscode.languages.registerCompletionItemProvider({ pattern: '**/keybindings.json' }, {

provideCompletionItems(document, position, token) {
const commands = vscode.commands.getCommands(true); //This is here so updated commands can be fetched later after startup
const location = getLocation(document.getText(), document.offsetAt(position));
if (location.path[1] === 'command') {

Expand Down
7 changes: 7 additions & 0 deletions src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface IMenuRegistry {
getCommand(id: string): ICommandAction;
appendMenuItem(menu: MenuId, item: IMenuItem): IDisposable;
getMenuItems(loc: MenuId): IMenuItem[];
removeCommand(id: string);
}

export const MenuRegistry: IMenuRegistry = new class {
Expand All @@ -78,6 +79,12 @@ export const MenuRegistry: IMenuRegistry = new class {
return this.commands[id];
}

removeCommand(id: string) {
if (this.commands[id]) {
delete this.commands[id];
}
}

appendMenuItem(loc: MenuId, item: IMenuItem): IDisposable {
let array = this.menuItems[loc];
if (!array) {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/commands/common/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,4 @@ export const NullCommandService: ICommandService = {
executeCommand() {
return TPromise.as(undefined);
}
};
};
49 changes: 49 additions & 0 deletions src/vs/workbench/parts/tasks/common/taskSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,50 @@ import { TerminateResponse } from 'vs/base/common/processes';
import { IEventEmitter } from 'vs/base/common/eventEmitter';

import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as nls from 'vs/nls';

export interface IRegisteredTask {
id: string;
command: IDisposable;
}

export type ITaskIcon = string | { light: string; dark: string; };

export interface ITaskCommand {
id: string;
title: string;
category?: string;
icon?: ITaskIcon;
}

export function checkValidCommand(command: ITaskCommand): TaskError {
if (!command) {
return new TaskError(Severity.Error, nls.localize('nonempty', "expected non-empty value."), TaskErrors.ConfigValidationError);
}
if (typeof command.id !== 'string') {
return new TaskError(Severity.Error, nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id'), TaskErrors.ConfigValidationError);
}
if (typeof command.title !== 'string') {
return new TaskError(Severity.Error, nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'title'), TaskErrors.ConfigValidationError);
}
if (command.category && typeof command.category !== 'string') {
return new TaskError(Severity.Error, nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'category'), TaskErrors.ConfigValidationError);
}
return checkIconValid(command.icon);
}

function checkIconValid(icon: ITaskIcon): TaskError {
if (typeof icon === 'undefined') {
return;
}
if (typeof icon === 'string') {
return;
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
return;
}
return new TaskError(Severity.Error, nls.localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"), TaskErrors.ConfigValidationError);
}

export enum TaskErrors {
NotConfigured,
Expand Down Expand Up @@ -122,6 +166,11 @@ export interface TaskDescription {
* The problem watchers to use for this task
*/
problemMatchers?: ProblemMatcher[];

/**
* Command binding for task
*/
commandBinding?: ITaskCommand;
}

export interface CommandOptions {
Expand Down
10 changes: 10 additions & 0 deletions src/vs/workbench/parts/tasks/common/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ValidationStatus, ValidationState, ILogger, Parser } from 'vs/base/comm
import { Executable, ExecutableParser, Config as ProcessConfig } from 'vs/base/common/processes';

import { ProblemMatcher, Config as ProblemMatcherConfig, ProblemMatcherParser } from 'vs/platform/markers/common/problemMatcher';
import { ITaskCommand } from 'vs/workbench/parts/tasks/common/taskSystem';

export namespace Config {

Expand Down Expand Up @@ -69,6 +70,10 @@ export namespace Config {
*/
problemMatcher?: ProblemMatcherConfig.ProblemMatcherType;

/**
* Command binding for task
*/
commandBinding?: ITaskCommand;
}
}

Expand Down Expand Up @@ -149,6 +154,11 @@ export interface Task {
* output.
*/
problemMatcher: ProblemMatcher[];

/**
* Command binding for task
*/
commandBinding?: ITaskCommand;
}

export interface ParserSettings {
Expand Down
101 changes: 97 additions & 4 deletions src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import * as strings from 'vs/base/common/strings';

import { Registry } from 'vs/platform/platform';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { SyncActionDescriptor, MenuRegistry } from 'vs/platform/actions/common/actions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IEventService } from 'vs/platform/event/common/event';
import { IEditor } from 'vs/platform/editor/common/editor';
Expand Down Expand Up @@ -60,7 +60,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IOutputService, IOutputChannelRegistry, Extensions as OutputExt, IOutputChannel } from 'vs/workbench/parts/output/common/output';

import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskConfiguration, TaskDescription, TaskSystemEvents } from 'vs/workbench/parts/tasks/common/taskSystem';
import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskConfiguration, TaskDescription, TaskSystemEvents, checkValidCommand, IRegisteredTask } from 'vs/workbench/parts/tasks/common/taskSystem';
import { ITaskService, TaskServiceEvents } from 'vs/workbench/parts/tasks/common/taskService';
import { templates as taskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates';

Expand All @@ -71,6 +71,10 @@ import { ProcessRunnerDetector } from 'vs/workbench/parts/tasks/node/processRunn

import { IEnvironmentService } from 'vs/platform/environment/common/environment';

import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { join } from 'vs/base/common/paths';

let $ = Builder.$;

class AbstractTaskAction extends Action {
Expand Down Expand Up @@ -393,7 +397,6 @@ class RunTaskAction extends AbstractTaskAction {
}
}


class StatusBarItem implements IStatusbarItem {

private panelService: IPanelService;
Expand Down Expand Up @@ -622,6 +625,8 @@ class TaskService extends EventEmitter implements ITaskService {
private clearTaskSystemPromise: boolean;
private outputChannel: IOutputChannel;

private _registeredSpecificTasks: IRegisteredTask[];

private fileChangesListener: IDisposable;

constructor( @IModeService modeService: IModeService, @IConfigurationService configurationService: IConfigurationService,
Expand Down Expand Up @@ -654,6 +659,12 @@ class TaskService extends EventEmitter implements ITaskService {
this.taskSystemListeners = [];
this.clearTaskSystemPromise = false;
this.outputChannel = this.outputService.getChannel(TaskService.OutputChannelId);

this._registeredSpecificTasks = [];
this.addListener2(TaskServiceEvents.ConfigChanged, () => {
this.tasks(); //reload tasks for keyboard shortcuts task
});

this.configurationService.onDidUpdateConfiguration(() => {
this.emit(TaskServiceEvents.ConfigChanged);
if (this._taskSystem && this._taskSystem.isActiveSync()) {
Expand All @@ -679,6 +690,55 @@ class TaskService extends EventEmitter implements ITaskService {
}
}

private refreshTaskCommands(tasks: TaskDescription[], taskSystem: ITaskSystem): void {
//Remove any existing ones
this._registeredSpecificTasks.forEach((rt) => {
MenuRegistry.removeCommand(rt.id);
rt.command.dispose();
});
this._registeredSpecificTasks = []; //clear existing

const ids = new IdGenerator('task-cmd-icon-');

//Re-add them
tasks.forEach(t => {
if (t.commandBinding) {
let err = checkValidCommand(t.commandBinding);
if (err) {
this.messageService.show(err.severity, err.message);
return;
}

let { icon, category, title, id } = t.commandBinding;

if (CommandsRegistry.getCommand(id)) {
this.messageService.show(Severity.Error, nls.localize('TaskSystem.IdAlreadyExists', 'A command with the id "{0}" already exists. Please use a different id.', id));
return;
}

let iconClass: string;
if (icon) {
iconClass = ids.nextId();
if (typeof icon === 'string') {
const path = join(this.contextService.getWorkspace().resource.fsPath, icon);
Dom.createCSSRule(`.icon.${iconClass}`, `background-image: url("${path}")`);
} else {
const light = join(this.contextService.getWorkspace().resource.fsPath, icon.light);
const dark = join(this.contextService.getWorkspace().resource.fsPath, icon.dark);
Dom.createCSSRule(`.icon.${iconClass}`, `background-image: url("${light}")`);
Dom.createCSSRule(`.vs-dark .icon.${iconClass}, hc-black .icon.${iconClass}`, `background-image: url("${dark}")`);
}
}

MenuRegistry.addCommand({ id: id, title, category, iconClass });
let command = CommandsRegistry.registerCommand(id, () => {
taskSystem.run(t.id);
});
this._registeredSpecificTasks.push({ id: id, command: command });
}
});
}

private get taskSystemPromise(): TPromise<ITaskSystem> {
if (!this._taskSystemPromise) {
if (!this.contextService.getWorkspace()) {
Expand Down Expand Up @@ -742,11 +802,13 @@ class TaskService extends EventEmitter implements ITaskService {
this._taskSystemPromise = null;
throw new TaskError(Severity.Info, nls.localize('TaskSystem.noConfiguration', 'No task runner configured.'), TaskErrors.NotConfigured);
}
let refreshShortcuts = false;
let result: ITaskSystem = null;
if (config.buildSystem === 'service') {
result = new LanguageServiceTaskSystem(<LanguageServiceTaskConfiguration>config, this.telemetryService, this.modeService);
} else if (this.isRunnerConfig(config)) {
result = new ProcessRunnerSystem(<FileConfig.ExternalTaskRunnerConfiguration>config, this.markerService, this.modelService, this.telemetryService, this.outputService, this.configurationResolverService, TaskService.OutputChannelId, clearOutput);
refreshShortcuts = true;
}
if (result === null) {
this._taskSystemPromise = null;
Expand All @@ -755,7 +817,15 @@ class TaskService extends EventEmitter implements ITaskService {
this.taskSystemListeners.push(result.addListener2(TaskSystemEvents.Active, (event) => this.emit(TaskServiceEvents.Active, event)));
this.taskSystemListeners.push(result.addListener2(TaskSystemEvents.Inactive, (event) => this.emit(TaskServiceEvents.Inactive, event)));
this._taskSystem = result;
return result;
if (refreshShortcuts) {
return result.tasks().then(tasks => {
this.refreshTaskCommands(tasks, result);
return result;
});
}
else {
return result;
}
}, (err: any) => {
this.handleError(err);
return Promise.wrapError(err);
Expand Down Expand Up @@ -1355,6 +1425,29 @@ let schema: IJSONSchema =
'problemMatcher': {
'$ref': '#/definitions/problemMatcherType',
'description': nls.localize('JsonSchema.tasks.matchers', 'The problem matcher(s) to use. Can either be a string or a problem matcher definition or an array of strings and problem matchers.')
},
'commandBinding': {
'type': 'object',
'description': nls.localize('JsonSchema.tasks.commandBinding', 'Command binding to use in the menu or as a keyboard shortcut'),
'required': ['id', 'title'],
'properties': {
'id': {
'type': 'string',
'description': nls.localize('JsonSchema.tasks.commandBinding.id', 'ID to identify command')
},
'title': {
'type': 'string',
'description': nls.localize('JsonSchema.tasks.commandBinding.title', 'Title of command')
},
'category': {
'type': 'string',
'description': nls.localize('JsonSchema.tasks.commandBinding.category', 'Optional category of command')
},
'icon': {
'type': 'string',
'description': nls.localize('JsonSchema.tasks.commandBinding.icon', 'Optional icon file name of command')
},
}
}
},
'defaultSnippets': [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ export interface TaskDescription {
* output.
*/
problemMatcher?: ProblemMatcherConfig.ProblemMatcherType;

/**
* Command binding for task
*/
commandBinding?: TaskSystem.ITaskCommand;
}

/**
Expand Down Expand Up @@ -566,6 +571,9 @@ class ConfigurationParser {
if (problemMatchers) {
task.problemMatchers = problemMatchers;
}
if (externalTask.commandBinding) {
task.commandBinding = externalTask.commandBinding;
}
// ToDo@dirkb: this is very special for the tsc watch mode. We should find
// a exensible solution for this.
for (let matcher of task.problemMatchers) {
Expand Down