diff --git a/examples/api-samples/src/browser/output/sample-output-channel-with-severity.ts b/examples/api-samples/src/browser/output/sample-output-channel-with-severity.ts index e8e72101199d9..82e4d31ee3b80 100644 --- a/examples/api-samples/src/browser/output/sample-output-channel-with-severity.ts +++ b/examples/api-samples/src/browser/output/sample-output-channel-with-severity.ts @@ -28,6 +28,10 @@ export class SampleOutputChannelWithSeverity channel.appendLine('hello info2', OutputChannelSeverity.Info); channel.appendLine('hello error', OutputChannelSeverity.Error); channel.appendLine('hello warning', OutputChannelSeverity.Warning); + channel.append('inlineInfo1 '); + channel.append('inlineWarning ', OutputChannelSeverity.Warning); + channel.append('inlineError ', OutputChannelSeverity.Error); + channel.append('inlineInfo2', OutputChannelSeverity.Info); } } export const bindSampleOutputChannelWithSeverity = (bind: interfaces.Bind) => { diff --git a/package.json b/package.json index 02662438ff204..660ae466676b8 100644 --- a/package.json +++ b/package.json @@ -146,4 +146,4 @@ "vscode-eslint": "https://open-vsx.org/api/dbaeumer/vscode-eslint/2.1.1/file/dbaeumer.vscode-eslint-2.1.1.vsix", "vscode-references-view": "https://open-vsx.org/api/ms-vscode/references-view/0.0.47/file/ms-vscode.references-view-0.0.47.vsix" } -} \ No newline at end of file +} diff --git a/packages/languages/compile.tsconfig.json b/packages/languages/compile.tsconfig.json index e8fc9b805ddcb..64ab05427af1b 100644 --- a/packages/languages/compile.tsconfig.json +++ b/packages/languages/compile.tsconfig.json @@ -15,9 +15,6 @@ { "path": "../core/compile.tsconfig.json" }, - { - "path": "../output/compile.tsconfig.json" - }, { "path": "../process/compile.tsconfig.json" }, diff --git a/packages/languages/package.json b/packages/languages/package.json index 779ebab3b152f..363fa12337692 100644 --- a/packages/languages/package.json +++ b/packages/languages/package.json @@ -6,7 +6,6 @@ "@theia/application-package": "^1.2.0", "@theia/core": "^1.2.0", "@theia/monaco-editor-core": "^0.19.3", - "@theia/output": "^1.2.0", "@theia/process": "^1.2.0", "@theia/workspace": "^1.2.0", "@types/uuid": "^7.0.3", diff --git a/packages/languages/src/browser/window-impl.ts b/packages/languages/src/browser/window-impl.ts index 6d257d8fc019c..8c9a1a14de783 100644 --- a/packages/languages/src/browser/window-impl.ts +++ b/packages/languages/src/browser/window-impl.ts @@ -15,17 +15,22 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; -import { MessageService } from '@theia/core/lib/common'; +import { MessageService, CommandRegistry } from '@theia/core/lib/common'; import { Window, OutputChannel, MessageActionItem, MessageType } from 'monaco-languageclient/lib/services'; -import { OutputChannelManager } from '@theia/output/lib/common/output-channel'; -import { OutputContribution } from '@theia/output/lib/browser/output-contribution'; @injectable() export class WindowImpl implements Window { + private canAccessOutput: boolean | undefined; + protected static readonly NOOP_CHANNEL: OutputChannel = { + append: () => { }, + appendLine: () => { }, + dispose: () => { }, + show: () => { } + }; + @inject(MessageService) protected readonly messageService: MessageService; - @inject(OutputChannelManager) protected readonly outputChannelManager: OutputChannelManager; - @inject(OutputContribution) protected readonly outputContribution: OutputContribution; + @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; showMessage(type: MessageType, message: string, ...actions: T[]): Thenable { const originalActions = new Map((actions || []).map(action => [action.title, action] as [string, T])); @@ -52,22 +57,20 @@ export class WindowImpl implements Window { } createOutputChannel(name: string): OutputChannel { - const outputChannel = this.outputChannelManager.getChannel(name); + // Note: alternatively, we could add `@theia/output` as a `devDependency` and check, for instance, + // the manager for the output channels can be injected or not with `@optional()` but this approach has the same effect. + // The `@theia/languages` extension will be removed anyway: https://github.com/eclipse-theia/theia/issues/7100 + if (this.canAccessOutput === undefined) { + this.canAccessOutput = !!this.commandRegistry.getCommand('output:append'); + } + if (!this.canAccessOutput) { + return WindowImpl.NOOP_CHANNEL; + } return { - append: outputChannel.append.bind(outputChannel), - appendLine: outputChannel.appendLine.bind(outputChannel), - show: async (preserveFocus?: boolean) => { - const options = Object.assign({ - preserveFocus: false, - }, { preserveFocus }); - const activate = !options.preserveFocus; - const reveal = options.preserveFocus; - await this.outputContribution.openView({ activate, reveal }); - outputChannel.setVisibility(true); - }, - dispose: () => { - this.outputChannelManager.deleteChannel(outputChannel.name); - } + append: text => this.commandRegistry.executeCommand('output:append', { name, text }), + appendLine: text => this.commandRegistry.executeCommand('output:appendLine', { name, text }), + dispose: () => this.commandRegistry.executeCommand('output:dispose', { name }), + show: (preserveFocus: boolean = false) => this.commandRegistry.executeCommand('output:show', { name, options: { preserveFocus } }) }; } } diff --git a/packages/monaco/src/browser/monaco-context-menu.ts b/packages/monaco/src/browser/monaco-context-menu.ts index 123b8a8d0f843..674bd9d114715 100644 --- a/packages/monaco/src/browser/monaco-context-menu.ts +++ b/packages/monaco/src/browser/monaco-context-menu.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import { injectable, inject } from 'inversify'; +import { MenuPath } from '@theia/core/lib/common/menu'; import { EDITOR_CONTEXT_MENU } from '@theia/editor/lib/browser'; import { ContextMenuRenderer, toAnchor } from '@theia/core/lib/browser'; import IContextMenuService = monaco.editor.IContextMenuService; @@ -35,7 +36,11 @@ export class MonacoContextMenuService implements IContextMenuService { // Actions for editor context menu come as 'MenuItemAction' items // In case of 'Quick Fix' actions come as 'CodeActionAction' items if (actions.length > 0 && actions[0] instanceof monaco.actions.MenuItemAction) { - this.contextMenuRenderer.render(EDITOR_CONTEXT_MENU, anchor, () => delegate.onHide(false)); + this.contextMenuRenderer.render({ + menuPath: this.menuPath(), + anchor, + onHide: () => delegate.onHide(false) + }); } else { const commands = new CommandRegistry(); const menu = new Menu({ @@ -61,4 +66,8 @@ export class MonacoContextMenuService implements IContextMenuService { } } + protected menuPath(): MenuPath { + return EDITOR_CONTEXT_MENU; + } + } diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index a99a4f870f897..a5e29ff0bbae3 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -18,7 +18,7 @@ import URI from '@theia/core/lib/common/uri'; import { EditorPreferenceChange, EditorPreferences, TextEditor, DiffNavigator } from '@theia/editor/lib/browser'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; import { DisposableCollection, deepClone, Disposable, } from '@theia/core/lib/common'; import { MonacoToProtocolConverter, ProtocolToMonacoConverter, TextDocumentSaveReason } from 'monaco-languageclient'; import { MonacoCommandServiceFactory } from './monaco-command-service'; @@ -35,14 +35,24 @@ import { MonacoBulkEditService } from './monaco-bulk-edit-service'; import IEditorOverrideServices = monaco.editor.IEditorOverrideServices; import { ApplicationServer } from '@theia/core/lib/common/application-protocol'; -import { OS } from '@theia/core'; +import { OS, ContributionProvider } from '@theia/core'; import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions } from '@theia/core/lib/browser'; import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding'; import { HttpOpenHandlerOptions } from '@theia/core/lib/browser/http-open-handler'; +export const MonacoEditorFactory = Symbol('MonacoEditorFactory'); +export interface MonacoEditorFactory { + readonly scheme: string; + create(model: MonacoEditorModel, defaultOptions: MonacoEditor.IOptions, defaultOverrides: IEditorOverrideServices): MonacoEditor; +} + @injectable() export class MonacoEditorProvider { + @inject(ContributionProvider) + @named(MonacoEditorFactory) + protected readonly factories: ContributionProvider; + @inject(MonacoBulkEditService) protected readonly bulkEditService: MonacoBulkEditService; @@ -123,10 +133,10 @@ export class MonacoEditorProvider { async get(uri: URI): Promise { await this.editorPreferences.ready; - return this.doCreateEditor((override, toDispose) => this.createEditor(uri, override, toDispose)); + return this.doCreateEditor(uri, (override, toDispose) => this.createEditor(uri, override, toDispose)); } - protected async doCreateEditor(factory: (override: IEditorOverrideServices, toDispose: DisposableCollection) => Promise): Promise { + protected async doCreateEditor(uri: URI, factory: (override: IEditorOverrideServices, toDispose: DisposableCollection) => Promise): Promise { const commandService = this.commandServiceFactory(); const contextKeyService = this.contextKeyService.createScoped(); const { codeEditorService, textModelService, contextMenuService } = this; @@ -134,7 +144,7 @@ export class MonacoEditorProvider { const toDispose = new DisposableCollection(commandService); const openerService = new monaco.services.OpenerService(codeEditorService, commandService); openerService.registerOpener({ - open: (uri, options) => this.interceptOpen(uri, options) + open: (u, options) => this.interceptOpen(u, options) }); const editor = await factory({ codeEditorService, @@ -245,7 +255,10 @@ export class MonacoEditorProvider { protected async createMonacoEditor(uri: URI, override: IEditorOverrideServices, toDispose: DisposableCollection): Promise { const model = await this.getModel(uri, toDispose); const options = this.createMonacoEditorOptions(model); - const editor = new MonacoEditor(uri, model, document.createElement('div'), this.services, options, override); + const factory = this.factories.getContributions().find(({ scheme }) => uri.scheme === scheme); + const editor = factory + ? factory.create(model, options, override) + : new MonacoEditor(uri, model, document.createElement('div'), this.services, options, override); toDispose.push(this.editorPreferences.onPreferenceChanged(event => { if (event.affects(uri.toString(), model.languageId)) { this.updateMonacoEditorOptions(editor, event); @@ -471,7 +484,7 @@ export class MonacoEditorProvider { } async createInline(uri: URI, node: HTMLElement, options?: MonacoEditor.IOptions): Promise { - return this.doCreateEditor(async (override, toDispose) => { + return this.doCreateEditor(uri, async (override, toDispose) => { override.contextMenuService = { showContextMenu: () => {/* no-op*/ } }; diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index f10de89d8299d..9b7586213d5cd 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -30,7 +30,7 @@ import { Languages, Workspace } from '@theia/languages/lib/browser'; import { TextEditorProvider, DiffNavigatorProvider } from '@theia/editor/lib/browser'; import { StrictEditorTextFocusContext } from '@theia/editor/lib/browser/editor-keybinding-contexts'; import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from 'monaco-languageclient'; -import { MonacoEditorProvider } from './monaco-editor-provider'; +import { MonacoEditorProvider, MonacoEditorFactory } from './monaco-editor-provider'; import { MonacoEditorMenuContribution } from './monaco-menu'; import { MonacoEditorCommandHandlers } from './monaco-command'; import { MonacoKeybindingContribution } from './monaco-keybinding'; @@ -38,7 +38,7 @@ import { MonacoLanguages } from './monaco-languages'; import { MonacoWorkspace } from './monaco-workspace'; import { MonacoConfigurations } from './monaco-configurations'; import { MonacoEditorService } from './monaco-editor-service'; -import { MonacoTextModelService } from './monaco-text-model-service'; +import { MonacoTextModelService, MonacoEditorModelFactory } from './monaco-text-model-service'; import { MonacoContextMenuService } from './monaco-context-menu'; import { MonacoOutlineContribution } from './monaco-outline-contribution'; import { MonacoStatusBarContribution } from './monaco-status-bar-contribution'; @@ -63,6 +63,7 @@ import { MonacoEditorServices } from './monaco-editor'; import { MonacoColorRegistry } from './monaco-color-registry'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; import { MonacoThemingService } from './monaco-theming-service'; +import { bindContributionProvider } from '@theia/core'; decorate(injectable(), MonacoToProtocolConverter); decorate(injectable(), ProtocolToMonacoConverter); @@ -101,6 +102,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(MonacoContextMenuService).toSelf().inSingletonScope(); bind(MonacoEditorServices).toSelf().inSingletonScope(); bind(MonacoEditorProvider).toSelf().inSingletonScope(); + bindContributionProvider(bind, MonacoEditorFactory); + bindContributionProvider(bind, MonacoEditorModelFactory); bind(MonacoCommandService).toSelf().inTransientScope(); bind(MonacoCommandServiceFactory).toAutoFactory(MonacoCommandService); bind(TextEditorProvider).toProvider(context => @@ -151,7 +154,7 @@ export function createMonacoConfigurationService(container: interfaces.Container _configuration.getValue = (section, overrides, workspace) => { const overrideIdentifier = overrides && 'overrideIdentifier' in overrides && overrides['overrideIdentifier'] as string || undefined; - const resourceUri = overrides && 'resource' in overrides && overrides['resource'].toString(); + const resourceUri = overrides && 'resource' in overrides && !!overrides['resource'] && overrides['resource'].toString(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const proxy = createPreferenceProxy<{ [key: string]: any }>(preferences, preferenceSchemaProvider.getCombinedSchema(), { resourceUri, overrideIdentifier, style: 'both' diff --git a/packages/monaco/src/browser/monaco-text-model-service.ts b/packages/monaco/src/browser/monaco-text-model-service.ts index 5f7d1c0e6e965..51567982be07b 100644 --- a/packages/monaco/src/browser/monaco-text-model-service.ts +++ b/packages/monaco/src/browser/monaco-text-model-service.ts @@ -14,12 +14,26 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from 'monaco-languageclient'; import URI from '@theia/core/lib/common/uri'; -import { ResourceProvider, ReferenceCollection, Event } from '@theia/core'; +import { ResourceProvider, ReferenceCollection, Event, MaybePromise, Resource, ContributionProvider } from '@theia/core'; import { EditorPreferences, EditorPreferenceChange } from '@theia/editor/lib/browser'; import { MonacoEditorModel } from './monaco-editor-model'; +import IReference = monaco.editor.IReference; +export { IReference }; + +export const MonacoEditorModelFactory = Symbol('MonacoEditorModelFactory'); +export interface MonacoEditorModelFactory { + + readonly scheme: string; + + createModel( + resource: Resource, + options?: { encoding?: string | undefined } + ): MaybePromise; + +} @injectable() export class MonacoTextModelService implements monaco.editor.ITextModelService { @@ -40,6 +54,10 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { @inject(ProtocolToMonacoConverter) protected readonly p2m: ProtocolToMonacoConverter; + @inject(ContributionProvider) + @named(MonacoEditorModelFactory) + protected readonly factories: ContributionProvider; + get models(): MonacoEditorModel[] { return this._models.values(); } @@ -52,14 +70,14 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { return this._models.onDidCreate; } - createModelReference(raw: monaco.Uri | URI): Promise> { + createModelReference(raw: monaco.Uri | URI): Promise> { return this._models.acquire(raw.toString()); } protected async loadModel(uri: URI): Promise { await this.editorPreferences.ready; const resource = await this.resourceProvider(uri); - const model = await (new MonacoEditorModel(resource, this.m2p, this.p2m, { encoding: this.editorPreferences.get('files.encoding') }).load()); + const model = await (await this.createModel(resource)).load(); this.updateModel(model); model.textEditorModel.onDidChangeLanguage(() => this.updateModel(model)); const disposable = this.editorPreferences.onPreferenceChanged(change => this.updateModel(model, change)); @@ -67,6 +85,12 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { return model; } + protected createModel(resource: Resource): MaybePromise { + const options = { encoding: this.editorPreferences.get('files.encoding') }; + const factory = this.factories.getContributions().find(({ scheme }) => resource.uri.scheme === scheme); + return factory ? factory.createModel(resource, options) : new MonacoEditorModel(resource, this.m2p, this.p2m, options); + } + protected readonly modelOptions: { [name: string]: (keyof monaco.editor.ITextModelUpdateOptions | undefined) } = { 'editor.tabSize': 'tabSize', 'editor.insertSpaces': 'insertSpaces' diff --git a/packages/output/compile.tsconfig.json b/packages/output/compile.tsconfig.json index 4f0b13d382c8f..a32429d641d65 100644 --- a/packages/output/compile.tsconfig.json +++ b/packages/output/compile.tsconfig.json @@ -11,6 +11,12 @@ "references": [ { "path": "../core/compile.tsconfig.json" + }, + { + "path": "../monaco/compile.tsconfig.json" + }, + { + "path": "../editor/compile.tsconfig.json" } ] } diff --git a/packages/output/package.json b/packages/output/package.json index c137993bf7d9b..5edea4d2858d4 100644 --- a/packages/output/package.json +++ b/packages/output/package.json @@ -3,7 +3,11 @@ "version": "1.2.0", "description": "Theia - Output Extension", "dependencies": { - "@theia/core": "^1.2.0" + "@theia/core": "^1.2.0", + "@theia/editor": "^1.2.0", + "@theia/monaco": "^1.2.0", + "@types/p-queue": "^2.3.1", + "p-queue": "^2.4.2" }, "publishConfig": { "access": "public" diff --git a/packages/output/src/browser/output-context-menu.ts b/packages/output/src/browser/output-context-menu.ts new file mode 100644 index 0000000000000..c0b76af0c8353 --- /dev/null +++ b/packages/output/src/browser/output-context-menu.ts @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (C) 2020 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 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { injectable } from 'inversify'; +import { MenuPath } from '@theia/core/lib/common'; +import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu'; + +export namespace OutputContextMenu { + export const MENU_PATH: MenuPath = ['output_context_menu']; + export const TEXT_EDIT_GROUP = [...MENU_PATH, '0_text_edit_group']; + export const COMMAND_GROUP = [...MENU_PATH, '1_command_group']; + export const WIDGET_GROUP = [...MENU_PATH, '2_widget_group']; +} + +@injectable() +export class OutputContextMenuService extends MonacoContextMenuService { + + protected menuPath(): MenuPath { + return OutputContextMenu.MENU_PATH; + } + +} diff --git a/packages/output/src/browser/output-contribution.ts b/packages/output/src/browser/output-contribution.ts index 2303d9a0ba17c..43d03f78f38c3 100644 --- a/packages/output/src/browser/output-contribution.ts +++ b/packages/output/src/browser/output-contribution.ts @@ -14,100 +14,164 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { Widget } from '@theia/core/lib/browser/widgets/widget'; +import { MaybePromise } from '@theia/core/lib/common/types'; +import { CommonCommands, quickCommand, OpenHandler, OpenerOptions } from '@theia/core/lib/browser'; +import { Command, CommandRegistry, MenuModelRegistry } from '@theia/core/lib/common'; import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; -import { Widget, KeybindingRegistry, KeybindingContext, ApplicationShell } from '@theia/core/lib/browser'; -import { OUTPUT_WIDGET_KIND, OutputWidget } from './output-widget'; -import { Command, CommandRegistry } from '@theia/core/lib/common'; +import { OutputWidget } from './output-widget'; +import { OutputContextMenu } from './output-context-menu'; +import { OutputUri } from '../common/output-uri'; export namespace OutputCommands { const OUTPUT_CATEGORY = 'Output'; - export const CLEAR_OUTPUT_TOOLBAR: Command = { - id: 'output:clear', + /* #region VS Code `OutputChannel` API */ + // Based on: https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/vscode.d.ts#L4692-L4745 + + export const APPEND: Command = { + id: 'output:append' + }; + + export const APPEND_LINE: Command = { + id: 'output:appendLine' + }; + + export const CLEAR: Command = { + id: 'output:clear' + }; + + export const SHOW: Command = { + id: 'output:show' + }; + + export const HIDE: Command = { + id: 'output:hide' + }; + + export const DISPOSE: Command = { + id: 'output:dispose' + }; + + /* #endregion VS Code `OutputChannel` API */ + + export const CLEAR__WIDGET: Command = { + id: 'output:widget:clear', category: OUTPUT_CATEGORY, - label: 'Clear Output', iconClass: 'clear-all' }; - export const SELECT_ALL: Command = { - id: 'output:selectAll', + export const LOCK__WIDGET: Command = { + id: 'output:widget:lock', category: OUTPUT_CATEGORY, - label: 'Select All' + iconClass: 'fa fa-unlock' }; -} - -/** - * Enabled when the `Output` widget is the `activeWidget` in the shell. - */ -@injectable() -export class OutputWidgetIsActiveContext implements KeybindingContext { + export const UNLOCK__WIDGET: Command = { + id: 'output:widget:unlock', + category: OUTPUT_CATEGORY, + iconClass: 'fa fa-lock' + }; - static readonly ID = 'output:isActive'; + export const CLEAR__QUICK_PICK: Command = { + id: 'output:pick-clear', + label: 'Clear Output Channel...', + category: OUTPUT_CATEGORY + }; - @inject(ApplicationShell) - protected readonly shell: ApplicationShell; + export const SHOW__QUICK_PICK: Command = { + id: 'output:pick-show', + label: 'Show Output Channel...', + category: OUTPUT_CATEGORY + }; - readonly id = OutputWidgetIsActiveContext.ID; + export const HIDE__QUICK_PICK: Command = { + id: 'output:pick-hide', + label: 'Hide Output Channel...', + category: OUTPUT_CATEGORY + }; - isEnabled(): boolean { - return this.shell.activeWidget instanceof OutputWidget; - } + export const DISPOSE__QUICK_PICK: Command = { + id: 'output:pick-dispose', + label: 'Close Output Channel...', + category: OUTPUT_CATEGORY + }; } @injectable() -export class OutputContribution extends AbstractViewContribution { +export class OutputContribution extends AbstractViewContribution implements OpenHandler { - @inject(OutputWidgetIsActiveContext) - protected readonly outputIsActiveContext: OutputWidgetIsActiveContext; + readonly id: string = `${OutputWidget.ID}-opener`; constructor() { super({ - widgetId: OUTPUT_WIDGET_KIND, + widgetId: OutputWidget.ID, widgetName: 'Output', defaultWidgetOptions: { area: 'bottom' }, toggleCommandId: 'output:toggle', - toggleKeybinding: 'ctrlcmd+shift+u' + toggleKeybinding: 'CtrlCmd+Shift+U' }); } - registerCommands(commands: CommandRegistry): void { - super.registerCommands(commands); - commands.registerCommand(OutputCommands.CLEAR_OUTPUT_TOOLBAR, { - isEnabled: widget => this.withWidget(widget, () => true), - isVisible: widget => this.withWidget(widget, () => true), - execute: widget => this.withWidget(widget, outputWidget => this.clear(outputWidget)) + registerCommands(registry: CommandRegistry): void { + super.registerCommands(registry); + registry.registerCommand(OutputCommands.CLEAR__WIDGET, { + isEnabled: () => this.withWidget(), + isVisible: () => this.withWidget(), + execute: () => this.widget.then(widget => widget.clear()) + }); + registry.registerCommand(OutputCommands.LOCK__WIDGET, { + isEnabled: widget => this.withWidget(widget, output => !output.isLocked), + isVisible: widget => this.withWidget(widget, output => !output.isLocked), + execute: () => this.widget.then(widget => widget.lock()) }); - commands.registerCommand(OutputCommands.SELECT_ALL, { - isEnabled: () => this.outputIsActiveContext.isEnabled(), - isVisible: () => this.outputIsActiveContext.isEnabled(), - execute: widget => this.withWidget(widget, outputWidget => outputWidget.selectAll()) + registry.registerCommand(OutputCommands.UNLOCK__WIDGET, { + isEnabled: widget => this.withWidget(widget, output => output.isLocked), + isVisible: widget => this.withWidget(widget, output => output.isLocked), + execute: () => this.widget.then(widget => widget.unlock()) }); } - registerKeybindings(registry: KeybindingRegistry): void { - super.registerKeybindings(registry); - registry.registerKeybindings({ - command: OutputCommands.SELECT_ALL.id, - keybinding: 'CtrlCmd+A', - context: OutputWidgetIsActiveContext.ID + registerMenus(registry: MenuModelRegistry): void { + super.registerMenus(registry); + registry.registerMenuAction(OutputContextMenu.TEXT_EDIT_GROUP, { + commandId: CommonCommands.COPY.id + }); + registry.registerMenuAction(OutputContextMenu.COMMAND_GROUP, { + commandId: quickCommand.id, + label: 'Find Command...' + }); + registry.registerMenuAction(OutputContextMenu.WIDGET_GROUP, { + commandId: OutputCommands.CLEAR__WIDGET.id, + label: 'Clear Output' }); } - protected async clear(widget: OutputWidget): Promise { - widget.clear(); + canHandle(uri: URI): MaybePromise { + return OutputUri.is(uri) ? 200 : 0; } - protected withWidget(widget: Widget | undefined = this.tryGetWidget(), cb: (problems: OutputWidget) => T): T | false { - if (widget instanceof OutputWidget && widget.id === OUTPUT_WIDGET_KIND) { - return cb(widget); + async open(uri: URI, options?: OpenerOptions): Promise { + if (!OutputUri.is(uri)) { + throw new Error(`Expected '${OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`); } - return false; + const widget = await this.openView(options); + widget.setInput(OutputUri.channelName(uri)); + return widget; + } + + protected withWidget( + widget: Widget | undefined = this.tryGetWidget(), + predicate: (output: OutputWidget) => boolean = () => true + ): boolean | false { + + return widget instanceof OutputWidget ? predicate(widget) : false; } } diff --git a/packages/output/src/browser/output-editor-factory.ts b/packages/output/src/browser/output-editor-factory.ts new file mode 100644 index 0000000000000..27c3ab0723a7e --- /dev/null +++ b/packages/output/src/browser/output-editor-factory.ts @@ -0,0 +1,72 @@ +/******************************************************************************** + * Copyright (C) 2020 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { MonacoEditorFactory } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu'; +import { MonacoEditor, MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; +import { OutputUri } from '../common/output-uri'; +import { OutputContextMenuService } from './output-context-menu'; + +@injectable() +export class OutputEditorFactory implements MonacoEditorFactory { + + @inject(MonacoEditorServices) + protected readonly services: MonacoEditorServices; + + @inject(OutputContextMenuService) + protected readonly contextMenuService: MonacoContextMenuService; + + readonly scheme = OutputUri.SCHEME; + + create(model: MonacoEditorModel, defaultsOptions: MonacoEditor.IOptions, defaultOverrides: monaco.editor.IEditorOverrideServices): MonacoEditor { + const uri = new URI(model.uri); + const options = this.createOptions(model, defaultsOptions); + const overrides = this.createOverrides(model, defaultOverrides); + return new MonacoEditor(uri, model, document.createElement('div'), this.services, options, overrides); + } + + protected createOptions(model: MonacoEditorModel, defaultOptions: MonacoEditor.IOptions): MonacoEditor.IOptions { + return { + ...defaultOptions, + overviewRulerLanes: 3, + lineNumbersMinChars: 3, + fixedOverflowWidgets: true, + wordWrap: 'off', + lineNumbers: 'off', + glyphMargin: false, + lineDecorationsWidth: 20, + rulers: [], + folding: false, + scrollBeyondLastLine: false, + readOnly: true, + renderLineHighlight: 'none', + minimap: { enabled: false }, + matchBrackets: 'never' + }; + } + + protected createOverrides(model: MonacoEditorModel, defaultOverrides: monaco.editor.IEditorOverrideServices): monaco.editor.IEditorOverrideServices { + const contextMenuService = this.contextMenuService; + return { + ...defaultOverrides, + contextMenuService + }; + } + +} diff --git a/packages/output/src/browser/output-editor-model-factory.ts b/packages/output/src/browser/output-editor-model-factory.ts new file mode 100644 index 0000000000000..7bfe9f3ffc6cd --- /dev/null +++ b/packages/output/src/browser/output-editor-model-factory.ts @@ -0,0 +1,54 @@ +/******************************************************************************** + * Copyright (C) 2020 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from 'monaco-languageclient'; +import { Resource } from '@theia/core/lib/common/resource'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { OutputUri } from '../common/output-uri'; +import { MonacoEditorModelFactory } from '@theia/monaco/lib/browser/monaco-text-model-service'; + +@injectable() +export class OutputEditorModelFactory implements MonacoEditorModelFactory { + + @inject(MonacoToProtocolConverter) + protected readonly m2p: MonacoToProtocolConverter; + + @inject(ProtocolToMonacoConverter) + protected readonly p2m: ProtocolToMonacoConverter; + + readonly scheme = OutputUri.SCHEME; + + createModel( + resource: Resource, + options?: { encoding?: string | undefined } + ): MonacoEditorModel { + return new OutputEditorModel(resource, this.m2p, this.p2m, options); + } + +} + +export class OutputEditorModel extends MonacoEditorModel { + + get readOnly(): boolean { + return true; + } + + protected setDirty(dirty: boolean): void { + // NOOP + } + +} diff --git a/packages/output/src/browser/output-frontend-module.ts b/packages/output/src/browser/output-frontend-module.ts index fdf9fdfb36489..7fb480811e3dd 100644 --- a/packages/output/src/browser/output-frontend-module.ts +++ b/packages/output/src/browser/output-frontend-module.ts @@ -16,26 +16,38 @@ import { ContainerModule } from 'inversify'; import { OutputWidget, OUTPUT_WIDGET_KIND } from './output-widget'; -import { WidgetFactory, bindViewContribution, KeybindingContext } from '@theia/core/lib/browser'; -import { OutputContribution, OutputWidgetIsActiveContext } from './output-contribution'; -import { OutputToolbarContribution } from './output-toolbar-contribution'; +import { CommandContribution } from '@theia/core/lib/common/command'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { ResourceResolver } from '@theia/core/lib/common'; +import { WidgetFactory, bindViewContribution, OpenHandler } from '@theia/core/lib/browser'; import { OutputChannelManager } from '../common/output-channel'; import { bindOutputPreferences } from '../common/output-preferences'; -import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { OutputToolbarContribution } from './output-toolbar-contribution'; +import { OutputContribution } from './output-contribution'; +import { MonacoEditorFactory } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { OutputContextMenuService } from './output-context-menu'; +import { OutputEditorFactory } from './output-editor-factory'; +import { MonacoEditorModelFactory } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { OutputEditorModelFactory } from './output-editor-model-factory'; -export default new ContainerModule((bind, unbind, isBound, rebind) => { - bindOutputPreferences(bind); - bind(OutputWidget).toSelf(); +export default new ContainerModule(bind => { bind(OutputChannelManager).toSelf().inSingletonScope(); + bind(CommandContribution).toService(OutputChannelManager); + bind(ResourceResolver).toService(OutputChannelManager); + bind(MonacoEditorFactory).to(OutputEditorFactory).inSingletonScope(); + bind(MonacoEditorModelFactory).to(OutputEditorModelFactory).inSingletonScope(); + bind(OutputContextMenuService).toSelf().inSingletonScope(); + bindOutputPreferences(bind); + + bind(OutputWidget).toSelf(); bind(WidgetFactory).toDynamicValue(context => ({ id: OUTPUT_WIDGET_KIND, createWidget: () => context.container.get(OutputWidget) })); - bindViewContribution(bind, OutputContribution); - bind(OutputWidgetIsActiveContext).toSelf().inSingletonScope(); - bind(KeybindingContext).toService(OutputWidgetIsActiveContext); + bind(OpenHandler).to(OutputContribution).inSingletonScope(); + bind(OutputToolbarContribution).toSelf().inSingletonScope(); bind(TabBarToolbarContribution).toService(OutputToolbarContribution); }); diff --git a/packages/output/src/browser/output-resource.ts b/packages/output/src/browser/output-resource.ts new file mode 100644 index 0000000000000..bb4e4ea491ed9 --- /dev/null +++ b/packages/output/src/browser/output-resource.ts @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (C) 2020 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import URI from '@theia/core/lib/common/uri'; +import { Event, Resource, ResourceReadOptions, DisposableCollection, Emitter } from '@theia/core/lib/common'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { IReference } from '@theia/monaco/lib/browser/monaco-text-model-service'; + +export class OutputResource implements Resource { + + protected _textModel: monaco.editor.ITextModel | undefined; + protected onDidChangeContentsEmitter = new Emitter(); + protected toDispose = new DisposableCollection( + this.onDidChangeContentsEmitter + ); + + constructor(readonly uri: URI, readonly editorModelRef: Deferred>) { + this.editorModelRef.promise.then(modelRef => { + if (this.toDispose.disposed) { + modelRef.dispose(); + return; + } + const textModel = modelRef.object.textEditorModel; + this._textModel = textModel; + this.toDispose.push(modelRef); + this.toDispose.push(this._textModel!.onDidChangeContent(() => this.onDidChangeContentsEmitter.fire())); + }); + } + + get textModel(): monaco.editor.ITextModel | undefined { + return this._textModel; + } + + get onDidChangeContents(): Event { + return this.onDidChangeContentsEmitter.event; + } + + async readContents(options?: ResourceReadOptions): Promise { + if (this._textModel) { + const modelRef = await this.editorModelRef.promise; + return modelRef.object.textEditorModel.getValue(); + } + return ''; + } + + dispose(): void { + this.toDispose.dispose(); + } + +} diff --git a/packages/output/src/browser/output-toolbar-contribution.tsx b/packages/output/src/browser/output-toolbar-contribution.tsx index 45a2cfd8b4197..849515314303e 100644 --- a/packages/output/src/browser/output-toolbar-contribution.tsx +++ b/packages/output/src/browser/output-toolbar-contribution.tsx @@ -14,12 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable } from 'inversify'; +import * as React from 'react'; +import { inject, injectable, postConstruct } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { OutputWidget } from './output-widget'; +import { OutputCommands, OutputContribution } from './output-contribution'; import { OutputChannelManager } from '../common/output-channel'; -import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; -import { OutputCommands } from './output-contribution'; -import * as React from 'react'; @injectable() export class OutputToolbarContribution implements TabBarToolbarContribution { @@ -27,20 +28,55 @@ export class OutputToolbarContribution implements TabBarToolbarContribution { @inject(OutputChannelManager) protected readonly outputChannelManager: OutputChannelManager; + @inject(OutputContribution) + protected readonly outputContribution: OutputContribution; + + protected readonly onOutputWidgetStateChangedEmitter = new Emitter(); + protected readonly onOutputWidgetStateChanged = this.onOutputWidgetStateChangedEmitter.event; + + protected readonly onChannelsChangedEmitter = new Emitter(); + protected readonly onChannelsChanged = this.onChannelsChangedEmitter.event; + + @postConstruct() + protected init(): void { + this.outputContribution.widget.then(widget => { + widget.onStateChanged(() => this.onOutputWidgetStateChangedEmitter.fire()); + }); + const fireChannelsChanged = () => this.onChannelsChangedEmitter.fire(); + this.outputChannelManager.onSelectedChannelChanged(fireChannelsChanged); + this.outputChannelManager.onChannelAdded(fireChannelsChanged); + this.outputChannelManager.onChannelDeleted(fireChannelsChanged); + this.outputChannelManager.onChannelWasShown(fireChannelsChanged); + this.outputChannelManager.onChannelWasHidden(fireChannelsChanged); + } + async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise { toolbarRegistry.registerItem({ id: 'channels', render: () => this.renderChannelSelector(), - isVisible: widget => (widget instanceof OutputWidget), - onDidChange: this.outputChannelManager.onListOrSelectionChange + isVisible: widget => widget instanceof OutputWidget, + onDidChange: this.onChannelsChanged }); - toolbarRegistry.registerItem({ - id: OutputCommands.CLEAR_OUTPUT_TOOLBAR.id, - command: OutputCommands.CLEAR_OUTPUT_TOOLBAR.id, - tooltip: 'Clear Output', + id: OutputCommands.CLEAR__WIDGET.id, + command: OutputCommands.CLEAR__WIDGET.id, + tooltip: OutputCommands.CLEAR__WIDGET.label, priority: 1, }); + toolbarRegistry.registerItem({ + id: OutputCommands.LOCK__WIDGET.id, + command: OutputCommands.LOCK__WIDGET.id, + tooltip: 'Turn Auto Scrolling Off', + onDidChange: this.onOutputWidgetStateChanged, + priority: 2 + }); + toolbarRegistry.registerItem({ + id: OutputCommands.UNLOCK__WIDGET.id, + command: OutputCommands.UNLOCK__WIDGET.id, + tooltip: 'Turn Auto Scrolling On', + onDidChange: this.onOutputWidgetStateChanged, + priority: 2 + }); } protected readonly NONE = ''; @@ -55,8 +91,8 @@ export class OutputToolbarContribution implements TabBarToolbarContribution { } return