diff --git a/packages/console/src/browser/ansi-console-item.tsx b/packages/console/src/browser/ansi-console-item.tsx index b9940879d0c05..078323bf49145 100644 --- a/packages/console/src/browser/ansi-console-item.tsx +++ b/packages/console/src/browser/ansi-console-item.tsx @@ -15,9 +15,9 @@ ********************************************************************************/ import * as React from 'react'; -import { MessageType } from '@theia/core/lib/common'; import { ConsoleItem } from './console-session'; import { ansiToHtml } from 'anser'; +import { Severity } from '@theia/core/lib/common/severity'; export class AnsiConsoleItem implements ConsoleItem { @@ -25,7 +25,7 @@ export class AnsiConsoleItem implements ConsoleItem { constructor( public readonly content: string, - public readonly severity?: MessageType + public readonly severity?: Severity ) { this.htmlContent = ansiToHtml(this.content, { use_classes: true, diff --git a/packages/console/src/browser/console-content-widget.tsx b/packages/console/src/browser/console-content-widget.tsx index 32c8784d40307..414c66f145c74 100644 --- a/packages/console/src/browser/console-content-widget.tsx +++ b/packages/console/src/browser/console-content-widget.tsx @@ -16,11 +16,12 @@ import { Message } from '@phosphor/messaging'; import { interfaces, Container, injectable } from 'inversify'; -import { MenuPath, MessageType } from '@theia/core'; +import { MenuPath } from '@theia/core'; import { TreeProps } from '@theia/core/lib/browser/tree'; import { TreeSourceNode } from '@theia/core/lib/browser/source-tree'; import { SourceTreeWidget, TreeElementNode } from '@theia/core/lib/browser/source-tree'; import { ConsoleItem } from './console-session'; +import { Severity } from '@theia/core/lib/common/severity'; @injectable() export class ConsoleContentWidget extends SourceTreeWidget { @@ -73,16 +74,16 @@ export class ConsoleContentWidget extends SourceTreeWidget { return classNames; } protected toClassName(item: ConsoleItem): string | undefined { - if (item.severity === MessageType.Error) { + if (item.severity === Severity.Error) { return ConsoleItem.errorClassName; } - if (item.severity === MessageType.Warning) { + if (item.severity === Severity.Warning) { return ConsoleItem.warningClassName; } - if (item.severity === MessageType.Info) { + if (item.severity === Severity.Info) { return ConsoleItem.infoClassName; } - if (item.severity === MessageType.Log) { + if (item.severity === Severity.Log) { return ConsoleItem.logClassName; } return undefined; diff --git a/packages/console/src/browser/console-session.ts b/packages/console/src/browser/console-session.ts index ed4ef2a4fb42f..ec87166955b65 100644 --- a/packages/console/src/browser/console-session.ts +++ b/packages/console/src/browser/console-session.ts @@ -16,11 +16,12 @@ import { injectable } from 'inversify'; import { MaybePromise } from '@theia/core/lib/common/types'; -import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { TreeSource, TreeElement, CompositeTreeElement } from '@theia/core/lib/browser/source-tree'; +import { Emitter } from '@theia/core/lib/common/event'; +import { Severity } from '@theia/core/lib/common/severity'; export interface ConsoleItem extends TreeElement { - readonly severity?: MessageType + readonly severity?: Severity; } export namespace ConsoleItem { export const errorClassName = 'theia-console-error'; @@ -35,6 +36,24 @@ export interface CompositeConsoleItem extends ConsoleItem, CompositeTreeElement @injectable() export abstract class ConsoleSession extends TreeSource { + protected selectedSeverity?: Severity; + protected readonly selectionEmitter: Emitter = new Emitter(); + readonly onSelectionChange = this.selectionEmitter.event; + + get severity(): Severity | undefined { + return this.selectedSeverity; + } + + set severity(severity: Severity | undefined) { + if (severity === this.selectedSeverity) { + return; + } + + this.selectedSeverity = severity; + this.selectionEmitter.fire(undefined); + this.fireDidChange(); + } + abstract getElements(): MaybePromise>; abstract execute(value: string): MaybePromise; abstract clear(): MaybePromise; diff --git a/packages/core/src/common/severity.ts b/packages/core/src/common/severity.ts new file mode 100644 index 0000000000000..c93bd20ad35e2 --- /dev/null +++ b/packages/core/src/common/severity.ts @@ -0,0 +1,96 @@ +/******************************************************************************** + * Copyright (C) 2019 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DiagnosticSeverity } from 'vscode-languageserver-types'; + +export enum Severity { + Ignore = 0, + Error = 1, + Warning = 2, + Info = 3, + Log = 4 +} + +export namespace Severity { + const error = 'Errors'; + const warning = 'Warnings'; + const info = 'Info'; + const log = 'Log'; + const ignore = 'All'; + + export function fromValue(value: string | undefined): Severity { + value = value && value.toLowerCase(); + + if (!value) { + return Severity.Ignore; + } + if (['error', 'errors'].indexOf(value) !== -1) { + return Severity.Error; + } + if (['warn', 'warning', 'warnings'].indexOf(value) !== -1) { + return Severity.Warning; + } + if (value === 'info') { + return Severity.Info; + } + if (value === 'log') { + return Severity.Log; + } + + return Severity.Ignore; + } + + export function toDiagnosticSeverity(value: Severity): DiagnosticSeverity { + switch (value) { + case Severity.Ignore: + return DiagnosticSeverity.Hint; + case Severity.Info: + return DiagnosticSeverity.Information; + case Severity.Log: + return DiagnosticSeverity.Information; + case Severity.Warning: + return DiagnosticSeverity.Warning; + case Severity.Error: + return DiagnosticSeverity.Error; + default: + return DiagnosticSeverity.Error; + } + } + + export function toString(severity: Severity | undefined): string { + switch (severity) { + case Severity.Error: + return error; + case Severity.Warning: + return warning; + case Severity.Info: + return info; + case Severity.Log: + return log; + default: + return ignore; + } + } + + export function toArray(): string[] { + return [ignore, error, warning, info, log]; + } +} diff --git a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts index 56769d0985c0f..5e15a4a95f9e8 100644 --- a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts +++ b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts @@ -38,7 +38,7 @@ export class ElectronContextMenuRenderer implements ContextMenuRenderer { // native context menu stops the event loop, so there is no keyboard events this.context.resetAltPressed(); if (onHide) { - onHide(); + menu.once('menu-will-close', () => onHide()); } } diff --git a/packages/cpp/src/browser/cpp-task-provider.ts b/packages/cpp/src/browser/cpp-task-provider.ts index 094bea434b198..c983365734cdb 100644 --- a/packages/cpp/src/browser/cpp-task-provider.ts +++ b/packages/cpp/src/browser/cpp-task-provider.ts @@ -174,7 +174,16 @@ export class CppTaskProvider implements TaskContribution, TaskProvider, TaskReso source: 'cpp', properties: { required: ['label'], - all: ['label'] + all: ['label'], + schema: { + type: CPP_BUILD_TASK_TYPE_KEY, + required: ['label'], + properties: { + label: { + type: 'string' + } + } + } } }); } diff --git a/packages/debug/src/browser/console/debug-console-contribution.ts b/packages/debug/src/browser/console/debug-console-contribution.tsx similarity index 74% rename from packages/debug/src/browser/console/debug-console-contribution.ts rename to packages/debug/src/browser/console/debug-console-contribution.tsx index 253a6cf4cae2c..ba9c1ba6da066 100644 --- a/packages/debug/src/browser/console/debug-console-contribution.ts +++ b/packages/debug/src/browser/console/debug-console-contribution.tsx @@ -14,13 +14,15 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { interfaces, injectable } from 'inversify'; -import { AbstractViewContribution, bindViewContribution, WidgetFactory, Widget } from '@theia/core/lib/browser'; -import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service'; -import { ConsoleWidget, ConsoleOptions } from '@theia/console/lib/browser/console-widget'; -import { DebugConsoleSession } from './debug-console-session'; -import { Command, CommandRegistry } from '@theia/core/lib/common/command'; +import { ConsoleOptions, ConsoleWidget } from '@theia/console/lib/browser/console-widget'; +import { AbstractViewContribution, bindViewContribution, Widget, WidgetFactory } from '@theia/core/lib/browser'; +import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { Command, CommandRegistry } from '@theia/core/lib/common/command'; +import { Severity } from '@theia/core/lib/common/severity'; +import { inject, injectable, interfaces } from 'inversify'; +import * as React from 'react'; +import { DebugConsoleSession } from './debug-console-session'; export type InDebugReplContextKey = ContextKey; export const InDebugReplContextKey = Symbol('inDebugReplContextKey'); @@ -38,6 +40,9 @@ export namespace DebugConsoleCommands { @injectable() export class DebugConsoleContribution extends AbstractViewContribution implements TabBarToolbarContribution { + @inject(DebugConsoleSession) + protected debugConsoleSession: DebugConsoleSession; + constructor() { super({ widgetId: DebugConsoleContribution.options.id, @@ -61,7 +66,14 @@ export class DebugConsoleContribution extends AbstractViewContribution { + toolbarRegistry.registerItem({ + id: 'debug-console-severity', + render: widget => this.renderSeveritySelector(widget), + isVisible: widget => this.withWidget(widget, () => true), + onDidChange: this.debugConsoleSession.onSelectionChange + }); + toolbarRegistry.registerItem({ id: DebugConsoleCommands.CLEAR.id, command: DebugConsoleCommands.CLEAR.id, @@ -114,6 +126,25 @@ export class DebugConsoleContribution extends AbstractViewContribution severityElements.push()); + const selectedValue = Severity.toString(this.debugConsoleSession.severity || Severity.Ignore); + + return ; + } + + protected changeSeverity = (event: React.ChangeEvent) => { + this.debugConsoleSession.severity = Severity.fromValue(event.target.value); + } + protected withWidget(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: ConsoleWidget) => T): T | false { if (widget instanceof ConsoleWidget && widget.id === DebugConsoleContribution.options.id) { return fn(widget); diff --git a/packages/debug/src/browser/console/debug-console-items.tsx b/packages/debug/src/browser/console/debug-console-items.tsx index ee05afbe78285..cfaaf7047315e 100644 --- a/packages/debug/src/browser/console/debug-console-items.tsx +++ b/packages/debug/src/browser/console/debug-console-items.tsx @@ -16,10 +16,10 @@ import * as React from 'react'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { MessageType } from '@theia/core/lib/common'; import { SingleTextInputDialog } from '@theia/core/lib/browser'; import { ConsoleItem, CompositeConsoleItem } from '@theia/console/lib/browser/console-session'; import { DebugSession } from '../debug-session'; +import { Severity } from '@theia/core/lib/common/severity'; export class ExpressionContainer implements CompositeConsoleItem { @@ -104,7 +104,7 @@ export class ExpressionContainer implements CompositeConsoleItem { } } catch (e) { result.push({ - severity: MessageType.Error, + severity: Severity.Error, visible: !!e.message, render: () => e.message }); @@ -256,6 +256,7 @@ export namespace VirtualVariableItem { export class ExpressionItem extends ExpressionContainer { + severity?: Severity; static notAvailable = 'not available'; protected _value = ExpressionItem.notAvailable; @@ -299,14 +300,17 @@ export class ExpressionItem extends ExpressionContainer { this.namedVariables = body.namedVariables; this.indexedVariables = body.indexedVariables; this.elements = undefined; + this.severity = Severity.Log; } } catch (err) { this._value = err.message; this._available = false; + this.severity = Severity.Error; } } else { this._value = 'Please start a debug session to evaluate'; this._available = false; + this.severity = Severity.Error; } } diff --git a/packages/debug/src/browser/console/debug-console-session.ts b/packages/debug/src/browser/console/debug-console-session.ts index d3975f677c0b8..7972c307baa55 100644 --- a/packages/debug/src/browser/console/debug-console-session.ts +++ b/packages/debug/src/browser/console/debug-console-session.ts @@ -17,7 +17,6 @@ import throttle = require('lodash.throttle'); import { injectable, inject, postConstruct } from 'inversify'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; -import { MessageType } from '@theia/core/lib/common'; import { ConsoleSession, ConsoleItem } from '@theia/console/lib/browser/console-session'; import { AnsiConsoleItem } from '@theia/console/lib/browser/ansi-console-item'; import { DebugSession } from '../debug-session'; @@ -25,6 +24,7 @@ import { DebugSessionManager } from '../debug-session-manager'; import { Languages, CompletionItem, CompletionItemKind, Position, Range, TextEdit, Workspace, TextDocument, CompletionParams } from '@theia/languages/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { ExpressionContainer, ExpressionItem } from './debug-console-items'; +import { Severity } from '@theia/core/lib/common/severity'; @injectable() export class DebugConsoleSession extends ConsoleSession { @@ -83,7 +83,7 @@ export class DebugConsoleSession extends ConsoleSession { } getElements(): IterableIterator { - return this.items[Symbol.iterator](); + return this.items.filter(e => !this.severity || e.severity === this.severity)[Symbol.iterator](); } protected async completions({ textDocument: { uri }, position }: CompletionParams): Promise { @@ -142,12 +142,12 @@ export class DebugConsoleSession extends ConsoleSession { this.uncompletedItemContent = value; } - this.items.push(new AnsiConsoleItem(this.uncompletedItemContent, MessageType.Info)); + this.items.push(new AnsiConsoleItem(this.uncompletedItemContent, Severity.Info)); this.fireDidChange(); } appendLine(value: string): void { - this.items.push(new AnsiConsoleItem(value, MessageType.Info)); + this.items.push(new AnsiConsoleItem(value, Severity.Info)); this.fireDidChange(); } @@ -158,7 +158,7 @@ export class DebugConsoleSession extends ConsoleSession { console.debug(`telemetry/${event.body.output}`, event.body.data); return; } - const severity = category === 'stderr' ? MessageType.Error : event.body.category === 'console' ? MessageType.Warning : MessageType.Info; + const severity = category === 'stderr' ? Severity.Error : event.body.category === 'console' ? Severity.Warning : Severity.Info; if (variablesReference) { const items = await new ExpressionContainer({ session, variablesReference }).getElements(); this.items.push(...items); diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index b84e8f9180163..3b50e8ef6a2bb 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -42,6 +42,7 @@ import { DebugService } from '../common/debug-service'; import { DebugSchemaUpdater } from './debug-schema-updater'; import { DebugPreferences } from './debug-preferences'; import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { DebugSessionOptions } from './debug-session-options'; export namespace DebugMenus { export const DEBUG = [...MAIN_MENU_BAR, '6_debug']; @@ -512,10 +513,10 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi registerCommands(registry: CommandRegistry): void { super.registerCommands(registry); registry.registerCommand(DebugCommands.START, { - execute: () => this.start() + execute: (config?: DebugSessionOptions) => this.start(false, config) }); registry.registerCommand(DebugCommands.START_NO_DEBUG, { - execute: () => this.start(true) + execute: (config?: DebugSessionOptions) => this.start(true, config) }); registry.registerCommand(DebugCommands.STOP, { execute: () => this.manager.currentSession && this.manager.currentSession.terminate(), @@ -747,60 +748,61 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi isVisible: () => !!this.selectedVariable && this.selectedVariable.supportCopyAsExpression }); + // Debug context menu commands registry.registerCommand(DebugEditorContextCommands.ADD_BREAKPOINT, { - execute: () => this.editors.toggleBreakpoint(), - isEnabled: () => !this.editors.anyBreakpoint, - isVisible: () => !this.editors.anyBreakpoint + execute: position => this.isPosition(position) && this.editors.toggleBreakpoint(position), + isEnabled: position => this.isPosition(position) && !this.editors.anyBreakpoint(position), + isVisible: position => this.isPosition(position) && !this.editors.anyBreakpoint(position) }); registry.registerCommand(DebugEditorContextCommands.ADD_CONDITIONAL_BREAKPOINT, { - execute: () => this.editors.addBreakpoint('condition'), - isEnabled: () => !this.editors.anyBreakpoint, - isVisible: () => !this.editors.anyBreakpoint + execute: position => this.isPosition(position) && this.editors.addBreakpoint('condition', position), + isEnabled: position => this.isPosition(position) && !this.editors.anyBreakpoint(position), + isVisible: position => this.isPosition(position) && !this.editors.anyBreakpoint(position) }); registry.registerCommand(DebugEditorContextCommands.ADD_LOGPOINT, { - execute: () => this.editors.addBreakpoint('logMessage'), - isEnabled: () => !this.editors.anyBreakpoint, - isVisible: () => !this.editors.anyBreakpoint + execute: position => this.isPosition(position) && this.editors.addBreakpoint('logMessage', position), + isEnabled: position => this.isPosition(position) && !this.editors.anyBreakpoint(position), + isVisible: position => this.isPosition(position) && !this.editors.anyBreakpoint(position) }); registry.registerCommand(DebugEditorContextCommands.REMOVE_BREAKPOINT, { - execute: () => this.editors.toggleBreakpoint(), - isEnabled: () => !!this.editors.breakpoint, - isVisible: () => !!this.editors.breakpoint + execute: position => this.isPosition(position) && this.editors.toggleBreakpoint(position), + isEnabled: position => this.isPosition(position) && !!this.editors.getBreakpoint(position), + isVisible: position => this.isPosition(position) && !!this.editors.getBreakpoint(position) }); registry.registerCommand(DebugEditorContextCommands.EDIT_BREAKPOINT, { - execute: () => this.editors.editBreakpoint(), - isEnabled: () => !!this.editors.breakpoint, - isVisible: () => !!this.editors.breakpoint + execute: position => this.isPosition(position) && this.editors.editBreakpoint(position), + isEnabled: position => this.isPosition(position) && !!this.editors.getBreakpoint(position), + isVisible: position => this.isPosition(position) && !!this.editors.getBreakpoint(position) }); registry.registerCommand(DebugEditorContextCommands.ENABLE_BREAKPOINT, { - execute: () => this.editors.setBreakpointEnabled(true), - isEnabled: () => this.editors.breakpointEnabled === false, - isVisible: () => this.editors.breakpointEnabled === false + execute: position => this.isPosition(position) && this.editors.setBreakpointEnabled(position, true), + isEnabled: position => this.isPosition(position) && this.editors.getBreakpointEnabled(position) === false, + isVisible: position => this.isPosition(position) && this.editors.getBreakpointEnabled(position) === false }); registry.registerCommand(DebugEditorContextCommands.DISABLE_BREAKPOINT, { - execute: () => this.editors.setBreakpointEnabled(false), - isEnabled: () => !!this.editors.breakpointEnabled, - isVisible: () => !!this.editors.breakpointEnabled + execute: position => this.isPosition(position) && this.editors.setBreakpointEnabled(position, false), + isEnabled: position => this.isPosition(position) && !!this.editors.getBreakpointEnabled(position), + isVisible: position => this.isPosition(position) && !!this.editors.getBreakpointEnabled(position) }); registry.registerCommand(DebugEditorContextCommands.REMOVE_LOGPOINT, { - execute: () => this.editors.toggleBreakpoint(), - isEnabled: () => !!this.editors.logpoint, - isVisible: () => !!this.editors.logpoint + execute: position => this.isPosition(position) && this.editors.toggleBreakpoint(position), + isEnabled: position => this.isPosition(position) && !!this.editors.getLogpoint(position), + isVisible: position => this.isPosition(position) && !!this.editors.getLogpoint(position) }); registry.registerCommand(DebugEditorContextCommands.EDIT_LOGPOINT, { - execute: () => this.editors.editBreakpoint(), - isEnabled: () => !!this.editors.logpoint, - isVisible: () => !!this.editors.logpoint + execute: position => this.isPosition(position) && this.editors.editBreakpoint(position), + isEnabled: position => this.isPosition(position) && !!this.editors.getLogpoint(position), + isVisible: position => this.isPosition(position) && !!this.editors.getLogpoint(position) }); registry.registerCommand(DebugEditorContextCommands.ENABLE_LOGPOINT, { - execute: () => this.editors.setBreakpointEnabled(true), - isEnabled: () => this.editors.logpointEnabled === false, - isVisible: () => this.editors.logpointEnabled === false + execute: position => this.isPosition(position) && this.editors.setBreakpointEnabled(position, true), + isEnabled: position => this.isPosition(position) && this.editors.getLogpointEnabled(position) === false, + isVisible: position => this.isPosition(position) && this.editors.getLogpointEnabled(position) === false }); registry.registerCommand(DebugEditorContextCommands.DISABLE_LOGPOINT, { - execute: () => this.editors.setBreakpointEnabled(false), - isEnabled: () => !!this.editors.logpointEnabled, - isVisible: () => !!this.editors.logpointEnabled + execute: position => this.isPosition(position) && this.editors.setBreakpointEnabled(position, false), + isEnabled: position => this.isPosition(position) && !!this.editors.getLogpointEnabled(position), + isVisible: position => this.isPosition(position) && !!this.editors.getLogpointEnabled(position) }); registry.registerCommand(DebugBreakpointWidgetCommands.ACCEPT, { @@ -945,8 +947,13 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi return widget; } - async start(noDebug?: boolean): Promise { - let { current } = this.configurations; + async start(noDebug?: boolean, debugSessionOptions?: DebugSessionOptions): Promise { + let current = debugSessionOptions ? debugSessionOptions : this.configurations.current; + // If no configurations are currently present, create the `launch.json` and prompt users to select the config. + if (!current) { + await this.configurations.addConfiguration(); + return; + } if (current) { if (noDebug !== undefined) { current = { @@ -1009,4 +1016,7 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi return variables && variables.selectedElement instanceof DebugVariable && variables.selectedElement || undefined; } + protected isPosition(position: monaco.Position): boolean { + return (position instanceof monaco.Position); + } } diff --git a/packages/debug/src/browser/editor/debug-editor-model.ts b/packages/debug/src/browser/editor/debug-editor-model.ts index 090b2cbcd49e5..022090b5d64fe 100644 --- a/packages/debug/src/browser/editor/debug-editor-model.ts +++ b/packages/debug/src/browser/editor/debug-editor-model.ts @@ -240,20 +240,13 @@ export class DebugEditorModel implements Disposable { return breakpoints; } - protected _position: monaco.Position | undefined; get position(): monaco.Position { - return this._position || this.editor.getControl().getPosition()!; + return this.editor.getControl().getPosition()!; } - get breakpoint(): DebugBreakpoint | undefined { - return this.getBreakpoint(); - } - protected getBreakpoint(position: monaco.Position = this.position): DebugBreakpoint | undefined { + getBreakpoint(position: monaco.Position = this.position): DebugBreakpoint | undefined { return this.sessions.getBreakpoint(this.uri, position.lineNumber); } - toggleBreakpoint(): void { - this.doToggleBreakpoint(); - } - protected doToggleBreakpoint(position: monaco.Position = this.position): void { + toggleBreakpoint(position: monaco.Position = this.position): void { const breakpoint = this.getBreakpoint(position); if (breakpoint) { breakpoint.remove(); @@ -285,12 +278,16 @@ export class DebugEditorModel implements Disposable { protected handleMouseDown(event: monaco.editor.IEditorMouseEvent): void { if (event.target && event.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN) { if (event.event.rightButton) { - this._position = event.target.position!; - this.contextMenu.render(DebugEditorModel.CONTEXT_MENU, event.event.browserEvent, () => - setTimeout(() => this._position = undefined) - ); + this.editor.focus(); + setTimeout(() => { + this.contextMenu.render({ + menuPath: DebugEditorModel.CONTEXT_MENU, + anchor: event.event.browserEvent, + args: [event.target.position!] + }); + }); } else { - this.doToggleBreakpoint(event.target.position!); + this.toggleBreakpoint(event.target.position!); } } this.hintBreakpoint(event); diff --git a/packages/debug/src/browser/editor/debug-editor-service.ts b/packages/debug/src/browser/editor/debug-editor-service.ts index 9b4ea52ad4908..01fc451cc11ea 100644 --- a/packages/debug/src/browser/editor/debug-editor-service.ts +++ b/packages/debug/src/browser/editor/debug-editor-service.ts @@ -84,38 +84,38 @@ export class DebugEditorService { return uri && this.models.get(uri.toString()); } - get logpoint(): DebugBreakpoint | undefined { - const logpoint = this.anyBreakpoint; + getLogpoint(position: monaco.Position): DebugBreakpoint | undefined { + const logpoint = this.anyBreakpoint(position); return logpoint && logpoint.logMessage ? logpoint : undefined; } - get logpointEnabled(): boolean | undefined { - const { logpoint } = this; + getLogpointEnabled(position: monaco.Position): boolean | undefined { + const logpoint = this.getLogpoint(position); return logpoint && logpoint.enabled; } - get breakpoint(): DebugBreakpoint | undefined { - const breakpoint = this.anyBreakpoint; + getBreakpoint(position: monaco.Position): DebugBreakpoint | undefined { + const breakpoint = this.anyBreakpoint(position); return breakpoint && breakpoint.logMessage ? undefined : breakpoint; } - get breakpointEnabled(): boolean | undefined { - const { breakpoint } = this; + getBreakpointEnabled(position: monaco.Position): boolean | undefined { + const breakpoint = this.getBreakpoint(position); return breakpoint && breakpoint.enabled; } - get anyBreakpoint(): DebugBreakpoint | undefined { - return this.model && this.model.breakpoint; + anyBreakpoint(position?: monaco.Position): DebugBreakpoint | undefined { + return this.model && this.model.getBreakpoint(position); } - toggleBreakpoint(): void { + toggleBreakpoint(position?: monaco.Position): void { const { model } = this; if (model) { - model.toggleBreakpoint(); + model.toggleBreakpoint(position); } } - setBreakpointEnabled(enabled: boolean): void { - const { anyBreakpoint } = this; - if (anyBreakpoint) { - anyBreakpoint.setEnabled(enabled); + setBreakpointEnabled(position: monaco.Position, enabled: boolean): void { + const breakpoint = this.anyBreakpoint(position); + if (breakpoint) { + breakpoint.setEnabled(enabled); } } @@ -135,28 +135,31 @@ export class DebugEditorService { return false; } - addBreakpoint(context: DebugBreakpointWidget.Context): void { + addBreakpoint(context: DebugBreakpointWidget.Context, position?: monaco.Position): void { const { model } = this; if (model) { - const { breakpoint } = model; + position = position || model.position; + const breakpoint = model.getBreakpoint(position); if (breakpoint) { model.breakpointWidget.show({ breakpoint, context }); } else { model.breakpointWidget.show({ - position: model.position, + position, context }); } } } - editBreakpoint(): Promise; - editBreakpoint(breakpoint: DebugBreakpoint): Promise; - async editBreakpoint(breakpoint: DebugBreakpoint | undefined = this.anyBreakpoint): Promise { - if (breakpoint) { - await breakpoint.open(); - const model = this.models.get(breakpoint.uri.toString()); + async editBreakpoint(breakpointOrPosition?: DebugBreakpoint | monaco.Position): Promise { + if (breakpointOrPosition instanceof monaco.Position) { + breakpointOrPosition = this.anyBreakpoint(breakpointOrPosition); + } + + if (breakpointOrPosition) { + await breakpointOrPosition.open(); + const model = this.models.get(breakpointOrPosition.uri.toString()); if (model) { - model.breakpointWidget.show(breakpoint); + model.breakpointWidget.show(breakpointOrPosition); } } } diff --git a/packages/debug/src/browser/view/debug-configuration-widget.tsx b/packages/debug/src/browser/view/debug-configuration-widget.tsx index d6929324cf48e..157fe46c575c0 100644 --- a/packages/debug/src/browser/view/debug-configuration-widget.tsx +++ b/packages/debug/src/browser/view/debug-configuration-widget.tsx @@ -26,10 +26,15 @@ import { DebugSessionManager } from '../debug-session-manager'; import { DebugAction } from './debug-action'; import { DebugViewModel } from './debug-view-model'; import { DebugSessionOptions } from '../debug-session-options'; +import { DebugCommands } from '../debug-frontend-application-contribution'; +import { CommandRegistry } from '@theia/core/lib/common'; @injectable() export class DebugConfigurationWidget extends ReactWidget { + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + @inject(DebugViewModel) protected readonly viewModel: DebugViewModel; @@ -118,11 +123,7 @@ export class DebugConfigurationWidget extends ReactWidget { protected readonly start = () => { const configuration = this.manager.current; - if (configuration) { - this.sessionManager.start(configuration); - } else { - this.manager.addConfiguration(); - } + this.commandRegistry.executeCommand(DebugCommands.START.id, configuration); } protected readonly openConfiguration = () => this.manager.openConfiguration(); diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 034a896963745..6c906383d04e5 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -450,7 +450,8 @@ export class TheiaPluginScanner implements PluginScanner { source: pluginName, properties: { required: definitionContribution.required, - all: propertyKeys + all: propertyKeys, + schema: definitionContribution } }; } diff --git a/packages/task/src/browser/quick-open-task.ts b/packages/task/src/browser/quick-open-task.ts index 58eadbd558438..b5293a49b063d 100644 --- a/packages/task/src/browser/quick-open-task.ts +++ b/packages/task/src/browser/quick-open-task.ts @@ -23,8 +23,11 @@ import { TaskActionProvider } from './task-action-provider'; import { QuickOpenHandler, QuickOpenService, QuickOpenOptions } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; +import { FileSystem } from '@theia/filesystem/lib/common'; import { QuickOpenModel, QuickOpenItem, QuickOpenActionProvider, QuickOpenMode, QuickOpenGroupItem, QuickOpenGroupItemOptions } from '@theia/core/lib/common/quick-open-model'; +import { PreferenceService } from '@theia/core/lib/browser'; import { TaskNameResolver } from './task-name-resolver'; +import { TaskConfigurationManager } from './task-configuration-manager'; @injectable() export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { @@ -54,6 +57,15 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { @inject(TaskNameResolver) protected readonly taskNameResolver: TaskNameResolver; + @inject(FileSystem) + protected readonly fileSystem: FileSystem; + + @inject(TaskConfigurationManager) + protected readonly taskConfigurationManager: TaskConfigurationManager; + + @inject(PreferenceService) + protected readonly preferences: PreferenceService; + /** Initialize this quick open model with the tasks. */ async init(): Promise { const recentTasks = this.taskService.recentTasks; @@ -167,27 +179,79 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { const configuredTasks = await this.taskService.getConfiguredTasks(); const providedTasks = await this.taskService.getProvidedTasks(); - if (!configuredTasks.length && !providedTasks.length) { + // check if tasks.json exists. If not, display "Create tasks.json file from template" + // If tasks.json exists and empty, display 'Open tasks.json file' + let isFirstGroup = true; + const { filteredConfiguredTasks, filteredProvidedTasks } = this.getFilteredTasks([], configuredTasks, providedTasks); + const groupedTasks = this.getGroupedTasksByWorkspaceFolder([...filteredConfiguredTasks, ...filteredProvidedTasks]); + if (groupedTasks.has(undefined)) { + const configs = groupedTasks.get(undefined)!; + this.items.push( + ...configs.map(taskConfig => { + const item = new TaskConfigureQuickOpenItem( + taskConfig, + this.taskService, + this.taskNameResolver, + this.workspaceService, + isMulti, + { showBorder: false } + ); + item['taskDefinitionRegistry'] = this.taskDefinitionRegistry; + return item; + }) + ); + isFirstGroup = false; + } + + const rootUris = (await this.workspaceService.roots).map(rootStat => rootStat.uri); + for (const rootFolder of rootUris) { + const uri = new URI(rootFolder).withScheme('file'); + const folderName = uri.displayName; + if (groupedTasks.has(uri.toString())) { + const configs = groupedTasks.get(uri.toString())!; + this.items.push( + ...configs.map((taskConfig, index) => { + const item = new TaskConfigureQuickOpenItem( + taskConfig, + this.taskService, + this.taskNameResolver, + this.workspaceService, + isMulti, + { + groupLabel: index === 0 && isMulti ? folderName : '', + showBorder: !isFirstGroup && index === 0 + } + ); + item['taskDefinitionRegistry'] = this.taskDefinitionRegistry; + return item; + }) + ); + } else { + const { configUri } = this.preferences.resolve('tasks', [], uri.toString()); + const existTaskConfigFile = !!configUri; + this.items.push(new QuickOpenGroupItem({ + label: existTaskConfigFile ? 'Open tasks.json file' : 'Create tasks.json file from template', + run: (mode: QuickOpenMode): boolean => { + if (mode !== QuickOpenMode.OPEN) { + return false; + } + setTimeout(() => this.taskConfigurationManager.openConfiguration(uri.toString())); + return true; + }, + showBorder: !isFirstGroup, + groupLabel: isMulti ? folderName : '' + })); + } + isFirstGroup = false; + } + + if (this.items.length === 0) { this.items.push(new QuickOpenItem({ label: 'No tasks found', run: (_mode: QuickOpenMode): boolean => false })); } - const { filteredConfiguredTasks, filteredProvidedTasks } = this.getFilteredTasks([], configuredTasks, providedTasks); - this.items.push( - ...filteredConfiguredTasks.map((task, index) => { - const item = new TaskConfigureQuickOpenItem(task, this.taskService, this.taskNameResolver, this.workspaceService, isMulti); - item['taskDefinitionRegistry'] = this.taskDefinitionRegistry; - return item; - }), - ...filteredProvidedTasks.map((task, index) => { - const item = new TaskConfigureQuickOpenItem(task, this.taskService, this.taskNameResolver, this.workspaceService, isMulti); - item['taskDefinitionRegistry'] = this.taskDefinitionRegistry; - return item; - }), - ); - this.quickOpenService.open(this, { placeholder: 'Select a task to configure', fuzzyMatchLabel: true, @@ -234,6 +298,22 @@ export class QuickOpenTask implements QuickOpenModel, QuickOpenHandler { filteredRecentTasks, filteredConfiguredTasks, filteredProvidedTasks }; } + + private getGroupedTasksByWorkspaceFolder(tasks: TaskConfiguration[]): Map { + const grouped = new Map(); + for (const task of tasks) { + const folder = task._scope; + if (grouped.has(folder)) { + grouped.get(folder)!.push(task); + } else { + grouped.set(folder, [task]); + } + } + for (const taskConfigs of grouped.values()) { + taskConfigs.sort((t1, t2) => t1.label.localeCompare(t2.label)); + } + return grouped; + } } export class TaskRunQuickOpenItem extends QuickOpenGroupItem { @@ -324,9 +404,10 @@ export class TaskConfigureQuickOpenItem extends QuickOpenGroupItem { protected readonly taskService: TaskService, protected readonly taskNameResolver: TaskNameResolver, protected readonly workspaceService: WorkspaceService, - protected readonly isMulti: boolean + protected readonly isMulti: boolean, + protected readonly options: QuickOpenGroupItemOptions ) { - super(); + super(options); const stat = this.workspaceService.workspace; this.isMulti = stat ? !stat.isDirectory : false; } @@ -335,6 +416,10 @@ export class TaskConfigureQuickOpenItem extends QuickOpenGroupItem { return this.taskNameResolver.resolve(this.task); } + getGroupLabel(): string { + return this.options.groupLabel || ''; + } + getDescription(): string { if (!this.isMulti) { return ''; diff --git a/packages/task/src/browser/task-configuration-manager.ts b/packages/task/src/browser/task-configuration-manager.ts index 8d87bbf304811..d188421617d12 100644 --- a/packages/task/src/browser/task-configuration-manager.ts +++ b/packages/task/src/browser/task-configuration-manager.ts @@ -19,13 +19,14 @@ import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { Emitter, Event } from '@theia/core/lib/common/event'; import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; -import { PreferenceService } from '@theia/core/lib/browser'; +import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser'; import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { TaskConfigurationModel } from './task-configuration-model'; +import { TaskTemplateSelector } from './task-templates'; import { TaskCustomization, TaskConfiguration } from '../common/task-protocol'; import { WorkspaceVariableContribution } from '@theia/workspace/lib/browser/workspace-variable-contribution'; -import { FileSystem, FileSystemError } from '@theia/filesystem/lib/common'; +import { FileSystem, FileSystemError /*, FileStat */ } from '@theia/filesystem/lib/common'; import { FileChange, FileChangeType } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations'; @@ -53,6 +54,9 @@ export class TaskConfigurationManager { @inject(WorkspaceVariableContribution) protected readonly workspaceVariables: WorkspaceVariableContribution; + @inject(TaskTemplateSelector) + protected readonly taskTemplateSelector: TaskTemplateSelector; + protected readonly onDidChangeTaskConfigEmitter = new Emitter(); readonly onDidChangeTaskConfig: Event = this.onDidChangeTaskConfigEmitter.event; @@ -64,6 +68,9 @@ export class TaskConfigurationManager { this.updateModels(); } }); + this.workspaceService.onWorkspaceChanged(() => { + this.updateModels(); + }); } protected readonly models = new Map(); @@ -142,50 +149,54 @@ export class TaskConfigurationManager { } } - protected async doOpen(model: TaskConfigurationModel): Promise { + protected async doOpen(model: TaskConfigurationModel): Promise { let uri = model.uri; if (!uri) { uri = await this.doCreate(model); } - return this.editorManager.open(uri, { - mode: 'activate' - }); + if (uri) { + return this.editorManager.open(uri, { + mode: 'activate' + }); + } } - protected async doCreate(model: TaskConfigurationModel): Promise { - await this.preferences.set('tasks', {}); // create dummy tasks.json in the correct place - const { configUri } = this.preferences.resolve('tasks'); // get uri to write content to it - let uri: URI; - if (configUri && configUri.path.base === 'tasks.json') { - uri = configUri; - } else { // fallback - uri = new URI(model.workspaceFolderUri).resolve(`${this.preferenceConfigurations.getPaths()[0]}/tasks.json`); - } - const content = this.getInitialConfigurationContent(); - const fileStat = await this.filesystem.getFileStat(uri.toString()); - if (!fileStat) { - throw new Error(`file not found: ${uri.toString()}`); - } - try { - await this.filesystem.setContent(fileStat, content); - } catch (e) { - if (!FileSystemError.FileExists.is(e)) { - throw e; + protected async doCreate(model: TaskConfigurationModel): Promise { + const content = await this.getInitialConfigurationContent(); + if (content) { + await this.preferences.set('tasks', {}, PreferenceScope.Folder, model.workspaceFolderUri); // create dummy tasks.json in the correct place + const { configUri } = this.preferences.resolve('tasks', [], model.workspaceFolderUri); // get uri to write content to it + + let uri: URI; + if (configUri && configUri.path.base === 'tasks.json') { + uri = configUri; + } else { // fallback + uri = new URI(model.workspaceFolderUri).resolve(`${this.preferenceConfigurations.getPaths()[0]}/tasks.json`); + } + + const fileStat = await this.filesystem.getFileStat(uri.toString()); + if (!fileStat) { + throw new Error(`file not found: ${uri.toString()}`); + } + try { + this.filesystem.setContent(fileStat, content); + } catch (e) { + if (!FileSystemError.FileExists.is(e)) { + throw e; + } } + return uri; } - return uri; } - protected getInitialConfigurationContent(): string { - return `{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - "version": "2.0.0", - "tasks": ${JSON.stringify([], undefined, ' ').split('\n').map(line => ' ' + line).join('\n').trim()} -} -`; + protected async getInitialConfigurationContent(): Promise { + const selected = await this.quickPick.show(this.taskTemplateSelector.selectTemplates(), { + placeholder: 'Select a Task Template' + }); + if (selected) { + return selected.content; + } } - } export namespace TaskConfigurationManager { diff --git a/packages/task/src/browser/task-definition-registry.spec.ts b/packages/task/src/browser/task-definition-registry.spec.ts index de00d2021e3eb..06248e5e4ff06 100644 --- a/packages/task/src/browser/task-definition-registry.spec.ts +++ b/packages/task/src/browser/task-definition-registry.spec.ts @@ -26,7 +26,15 @@ describe('TaskDefinitionRegistry', () => { required: ['extensionType'], properties: { required: ['extensionType'], - all: ['extensionType', 'taskLabel'] + all: ['extensionType', 'taskLabel'], + schema: { + type: 'extA', + required: ['extensionType'], + properties: { + extensionType: {}, + taskLabel: {} + } + } } }; const definitonContributionB = { @@ -34,7 +42,16 @@ describe('TaskDefinitionRegistry', () => { source: 'extA', properties: { required: ['extensionType', 'taskLabel', 'taskDetailedLabel'], - all: ['extensionType', 'taskLabel', 'taskDetailedLabel'] + all: ['extensionType', 'taskLabel', 'taskDetailedLabel'], + schema: { + type: 'extA', + required: ['extensionType', 'taskLabel', 'taskDetailedLabel'], + properties: { + extensionType: {}, + taskLabel: {}, + taskDetailedLabel: {} + } + } } }; diff --git a/packages/task/src/browser/task-frontend-module.ts b/packages/task/src/browser/task-frontend-module.ts index e015e3090408f..96f71519e1285 100644 --- a/packages/task/src/browser/task-frontend-module.ts +++ b/packages/task/src/browser/task-frontend-module.ts @@ -38,6 +38,7 @@ import { bindTaskPreferences } from './task-preferences'; import '../../src/browser/style/index.css'; import './tasks-monaco-contribution'; import { TaskNameResolver } from './task-name-resolver'; +import { TaskTemplateSelector } from './task-templates'; export default new ContainerModule(bind => { bind(TaskFrontendContribution).toSelf().inSingletonScope(); @@ -73,6 +74,7 @@ export default new ContainerModule(bind => { bindContributionProvider(bind, TaskContribution); bind(TaskSchemaUpdater).toSelf().inSingletonScope(); bind(TaskNameResolver).toSelf().inSingletonScope(); + bind(TaskTemplateSelector).toSelf().inSingletonScope(); bindProcessTaskModule(bind); bindTaskPreferences(bind); diff --git a/packages/task/src/browser/task-problem-matcher-registry.ts b/packages/task/src/browser/task-problem-matcher-registry.ts index a884afcde36e6..7cd84f582aed2 100644 --- a/packages/task/src/browser/task-problem-matcher-registry.ts +++ b/packages/task/src/browser/task-problem-matcher-registry.ts @@ -23,10 +23,11 @@ import { inject, injectable, postConstruct } from 'inversify'; import { Event, Emitter } from '@theia/core/lib/common'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { - ApplyToKind, FileLocationKind, NamedProblemMatcher, Severity, + ApplyToKind, FileLocationKind, NamedProblemMatcher, ProblemPattern, ProblemMatcher, ProblemMatcherContribution, WatchingMatcher } from '../common'; import { ProblemPatternRegistry } from './task-problem-pattern-registry'; +import { Severity } from '@theia/core/lib/common/severity'; @injectable() export class ProblemMatcherRegistry { diff --git a/packages/task/src/browser/task-schema-updater.ts b/packages/task/src/browser/task-schema-updater.ts index 8600b7b2a3ca2..b65316f1200a8 100644 --- a/packages/task/src/browser/task-schema-updater.ts +++ b/packages/task/src/browser/task-schema-updater.ts @@ -75,9 +75,41 @@ export class TaskSchemaUpdater { return Array.from(allTypes.values()).sort(); } + private updateSchemasForRegisteredTasks(): void { + customizedDetectedTasks.length = 0; + const definitions = this.taskDefinitionRegistry.getAll(); + definitions.forEach(def => { + const customizedDetectedTask = { + type: 'object', + required: ['type'], + properties: {} + } as IJSONSchema; + const taskType = { + ...defaultTaskType, + enum: [def.taskType], + default: def.taskType, + description: 'The task type to customize' + }; + customizedDetectedTask.properties!.type = taskType; + def.properties.all.forEach(taskProp => { + if (!!def.properties.required.find(requiredProp => requiredProp === taskProp)) { // property is mandatory + customizedDetectedTask.required!.push(taskProp); + } + customizedDetectedTask.properties![taskProp] = { ...def.properties.schema.properties![taskProp] }; + }); + customizedDetectedTask.properties!.problemMatcher = problemMatcher; + customizedDetectedTask.properties!.options = commandOptionsSchema; + customizedDetectedTasks.push(customizedDetectedTask); + }); + + taskConfigurationSchema.oneOf!.length = 1; + taskConfigurationSchema.oneOf!.push(...customizedDetectedTasks); + } + /** Returns the task's JSON schema */ getTaskSchema(): IJSONSchema { return { + type: 'object', properties: { tasks: { type: 'array', @@ -103,11 +135,8 @@ export class TaskSchemaUpdater { this.update(); } - /** Gets the most up-to-date names of task types Theia supports from the registry and update the task schema */ private async updateSupportedTaskTypes(): Promise { - const allTypes = await this.getRegisteredTaskTypes(); - supportedTaskTypes.length = 0; - supportedTaskTypes.push(...allTypes); + this.updateSchemasForRegisteredTasks(); this.update(); } } @@ -160,80 +189,74 @@ const commandOptionsSchema: IJSONSchema = { }; const problemMatcherNames: string[] = []; -const supportedTaskTypes = ['shell', 'process']; // default types that Theia supports -const taskConfigurationSchema: IJSONSchema = { - $id: taskSchemaId, +const defaultTaskTypes = ['shell', 'process']; +const supportedTaskTypes = [...defaultTaskTypes]; +const taskLabel = { + type: 'string', + description: 'A unique string that identifies the task that is also used as task\'s user interface label' +}; +const defaultTaskType = { + type: 'string', + enum: supportedTaskTypes, + default: defaultTaskTypes[0], + description: 'Determines what type of process will be used to execute the task. Only shell types will have output shown on the user interface' +}; +const commandAndArgs = { + command: commandSchema, + args: commandArgSchema, + options: commandOptionsSchema +}; +const problemMatcher = { oneOf: [ { - allOf: [ - { - type: 'object', - required: ['type'], - properties: { - label: { - type: 'string', - description: 'A unique string that identifies the task that is also used as task\'s user interface label' - }, - type: { - type: 'string', - enum: supportedTaskTypes, - default: 'shell', - description: 'Determines what type of process will be used to execute the task. Only shell types will have output shown on the user interface' - }, - command: commandSchema, - args: commandArgSchema, - options: commandOptionsSchema, - windows: { - type: 'object', - description: 'Windows specific command configuration that overrides the command, args, and options', - properties: { - command: commandSchema, - args: commandArgSchema, - options: commandOptionsSchema - } - }, - osx: { - type: 'object', - description: 'MacOS specific command configuration that overrides the command, args, and options', - properties: { - command: commandSchema, - args: commandArgSchema, - options: commandOptionsSchema - } - }, - linux: { - type: 'object', - description: 'Linux specific command configuration that overrides the default command, args, and options', - properties: { - command: commandSchema, - args: commandArgSchema, - options: commandOptionsSchema - } - }, - problemMatcher: { - oneOf: [ - { - type: 'string', - description: 'Name of the problem matcher to parse the output of the task', - enum: problemMatcherNames - }, - { - type: 'object', - description: 'User defined problem matcher(s) to parse the output of the task', - }, - { - type: 'array', - description: 'Name(s) of the problem matcher(s) to parse the output of the task', - items: { - type: 'string', - enum: problemMatcherNames - } - } - ] - } - } - } - ] + type: 'string', + description: 'Name of the problem matcher to parse the output of the task', + enum: problemMatcherNames + }, + { + type: 'object', + description: 'User defined problem matcher(s) to parse the output of the task', + }, + { + type: 'array', + description: 'Name(s) of the problem matcher(s) to parse the output of the task', + items: { + type: 'string', + enum: problemMatcherNames + } } ] }; + +const processTaskConfigurationSchema: IJSONSchema = { + type: 'object', + required: ['type', 'label', 'command'], + properties: { + label: taskLabel, + type: defaultTaskType, + ...commandAndArgs, + windows: { + type: 'object', + description: 'Windows specific command configuration that overrides the command, args, and options', + properties: commandAndArgs + }, + osx: { + type: 'object', + description: 'MacOS specific command configuration that overrides the command, args, and options', + properties: commandAndArgs + }, + linux: { + type: 'object', + description: 'Linux specific command configuration that overrides the default command, args, and options', + properties: commandAndArgs + }, + problemMatcher + } +}; + +const customizedDetectedTasks: IJSONSchema[] = []; + +const taskConfigurationSchema: IJSONSchema = { + $id: taskSchemaId, + oneOf: [processTaskConfigurationSchema, ...customizedDetectedTasks] +}; diff --git a/packages/task/src/browser/task-templates.ts b/packages/task/src/browser/task-templates.ts new file mode 100644 index 0000000000000..06b3db420a3f3 --- /dev/null +++ b/packages/task/src/browser/task-templates.ts @@ -0,0 +1,168 @@ +/******************************************************************************** + * Copyright (C) 2019 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 WITH Classpath-exception-2.0 + ********************************************************************************/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { injectable } from 'inversify'; +import { QuickPickItem } from '@theia/core/lib/common/quick-pick-service'; + +/** The representation of a task template used in the auto-generation of `tasks.json` */ +export interface TaskTemplateEntry { + id: string; + label: string; + description: string; + sort?: string; // string used in the sorting. If `undefined` the label is used in sorting. + autoDetect: boolean; // not supported in Theia + content: string; +} + +const dotnetBuild: TaskTemplateEntry = { + id: 'dotnetCore', + label: '.NET Core', + sort: 'NET Core', + autoDetect: false, // not supported in Theia + description: 'Executes .NET Core build command', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "build",', + '\t\t\t"command": "dotnet",', + '\t\t\t"type": "shell",', + '\t\t\t"args": [', + '\t\t\t\t"build",', + '\t\t\t\t// Ask dotnet build to generate full paths for file names.', + '\t\t\t\t"/property:GenerateFullPaths=true",', + '\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel', + '\t\t\t\t"/consoleloggerparameters:NoSummary"', + '\t\t\t],', + '\t\t\t"group": "build",', + '\t\t\t"presentation": {', + '\t\t\t\t"reveal": "silent"', + '\t\t\t},', + '\t\t\t"problemMatcher": "$msCompile"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +const msbuild: TaskTemplateEntry = { + id: 'msbuild', + label: 'MSBuild', + autoDetect: false, // not supported in Theia + description: 'Executes the build target', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "build",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "msbuild",', + '\t\t\t"args": [', + '\t\t\t\t// Ask msbuild to generate full paths for file names.', + '\t\t\t\t"/property:GenerateFullPaths=true",', + '\t\t\t\t"/t:build",', + '\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel', + '\t\t\t\t"/consoleloggerparameters:NoSummary"', + '\t\t\t],', + '\t\t\t"group": "build",', + '\t\t\t"presentation": {', + '\t\t\t\t// Reveal the output only if unrecognized errors occur.', + '\t\t\t\t"reveal": "silent"', + '\t\t\t},', + '\t\t\t// Use the standard MS compiler pattern to detect errors, warnings and infos', + '\t\t\t"problemMatcher": "$msCompile"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +const maven: TaskTemplateEntry = { + id: 'maven', + label: 'maven', + sort: 'MVN', + autoDetect: false, // not supported in Theia + description: 'Executes common maven commands', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "verify",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "mvn -B verify",', + '\t\t\t"group": "build"', + '\t\t},', + '\t\t{', + '\t\t\t"label": "test",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "mvn -B test",', + '\t\t\t"group": "test"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +const command: TaskTemplateEntry = { + id: 'externalCommand', + label: 'Others', + autoDetect: false, // not supported in Theia + description: 'Example to run an arbitrary external command', + content: [ + '{', + '\t// See https://go.microsoft.com/fwlink/?LinkId=733558', + '\t// for the documentation about the tasks.json format', + '\t"version": "2.0.0",', + '\t"tasks": [', + '\t\t{', + '\t\t\t"label": "echo",', + '\t\t\t"type": "shell",', + '\t\t\t"command": "echo Hello"', + '\t\t}', + '\t]', + '}' + ].join('\n') +}; + +@injectable() +export class TaskTemplateSelector { + selectTemplates(): QuickPickItem[] { + const templates: TaskTemplateEntry[] = [ + dotnetBuild, msbuild, maven + ].sort((a, b) => + (a.sort || a.label).localeCompare(b.sort || b.label) + ); + templates.push(command); + return templates.map(t => ({ + label: t.label, + description: t.description, + value: t + })); + } +} diff --git a/packages/task/src/common/problem-matcher-protocol.ts b/packages/task/src/common/problem-matcher-protocol.ts index 85bc2de24fbc3..92d2ecc7dd236 100644 --- a/packages/task/src/common/problem-matcher-protocol.ts +++ b/packages/task/src/common/problem-matcher-protocol.ts @@ -21,7 +21,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; +import { Severity } from '@theia/core/lib/common/severity'; +import { Diagnostic } from 'vscode-languageserver-types'; import vscodeURI from 'vscode-uri/lib/umd'; import { ProblemPatternContribution, WatchingMatcherContribution } from './task-protocol'; @@ -66,56 +67,6 @@ export namespace FileLocationKind { } } -export enum Severity { - Ignore = 0, - Info = 1, - Warning = 2, - Error = 3 -} - -export namespace Severity { - - const _error = 'error'; - const _warning = 'warning'; - const _warn = 'warn'; - const _info = 'info'; - - // Parses 'error', 'warning', 'warn', 'info' in call casings and falls back to ignore. - export function fromValue(value: string | undefined): Severity { - if (!value) { - return Severity.Ignore; - } - - if (value.toLowerCase() === _error) { - return Severity.Error; - } - - if (value.toLowerCase() === _warning || value.toLowerCase() === _warn) { - return Severity.Warning; - } - - if (value.toLowerCase() === _info) { - return Severity.Info; - } - return Severity.Ignore; - } - - export function toDiagnosticSeverity(value: Severity): DiagnosticSeverity { - switch (value) { - case Severity.Ignore: - return DiagnosticSeverity.Hint; - case Severity.Info: - return DiagnosticSeverity.Information; - case Severity.Warning: - return DiagnosticSeverity.Warning; - case Severity.Error: - return DiagnosticSeverity.Error; - default: - return DiagnosticSeverity.Error; - } - } -} - export interface WatchingPattern { regexp: string; file?: number; diff --git a/packages/task/src/common/task-protocol.ts b/packages/task/src/common/task-protocol.ts index 0955f1d42a322..ac8d4cd2432e8 100644 --- a/packages/task/src/common/task-protocol.ts +++ b/packages/task/src/common/task-protocol.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; +import { IJSONSchema } from '@theia/core/lib/common/json-schema'; import { ProblemMatcher, ProblemMatch, WatchingPattern } from './problem-matcher-protocol'; export const taskPath = '/services/task'; @@ -133,6 +134,7 @@ export interface TaskDefinition { properties: { required: string[]; all: string[]; + schema: IJSONSchema; } } diff --git a/packages/task/src/node/task-abstract-line-matcher.ts b/packages/task/src/node/task-abstract-line-matcher.ts index 3cacfe030ef6a..55dc9f744ce37 100644 --- a/packages/task/src/node/task-abstract-line-matcher.ts +++ b/packages/task/src/node/task-abstract-line-matcher.ts @@ -23,10 +23,11 @@ import { isWindows } from '@theia/core/lib/common/os'; import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver-types'; import { FileLocationKind, ProblemMatcher, ProblemPattern, - ProblemMatch, ProblemMatchData, ProblemLocationKind, Severity + ProblemMatch, ProblemMatchData, ProblemLocationKind } from '../common/problem-matcher-protocol'; import URI from '@theia/core/lib/common/uri'; import vscodeURI from 'vscode-uri/lib/umd'; +import { Severity } from '@theia/core/lib/common/severity'; const endOfLine: string = isWindows ? '\r\n' : '\n'; diff --git a/packages/task/src/node/task-problem-collector.spec.ts b/packages/task/src/node/task-problem-collector.spec.ts index dd75d2f7c2f20..2931e23f93302 100644 --- a/packages/task/src/node/task-problem-collector.spec.ts +++ b/packages/task/src/node/task-problem-collector.spec.ts @@ -17,7 +17,8 @@ import { expect } from 'chai'; import { DiagnosticSeverity } from 'vscode-languageserver-types'; import { ProblemCollector } from './task-problem-collector'; -import { ApplyToKind, FileLocationKind, ProblemLocationKind, ProblemMatch, ProblemMatchData, ProblemMatcher, Severity } from '../common/problem-matcher-protocol'; +import { ApplyToKind, FileLocationKind, ProblemLocationKind, ProblemMatch, ProblemMatchData, ProblemMatcher } from '../common/problem-matcher-protocol'; +import { Severity } from '@theia/core/lib/common/severity'; const startStopMatcher1: ProblemMatcher = { owner: 'test1',