From 5591cade759a50d44c8d598a420d66f101cf11bc Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 9 Apr 2020 17:00:07 +0200 Subject: [PATCH] GH-7347: Added scroll-lock to the Output view. Closes #7347. Closes #7008. Signed-off-by: Akos Kitta --- .../sample-output-channel-with-severity.ts | 4 + package.json | 2 +- packages/languages/compile.tsconfig.json | 3 - packages/languages/package.json | 3 +- packages/languages/src/browser/window-impl.ts | 43 +- .../monaco/src/browser/monaco-context-menu.ts | 11 +- .../monaco/src/browser/monaco-editor-model.ts | 41 ++ .../src/browser/monaco-editor-provider.ts | 64 ++- .../src/browser/monaco-frontend-module.ts | 10 +- .../src/browser/monaco-text-model-service.ts | 12 +- packages/output/compile.tsconfig.json | 6 + packages/output/package.json | 6 +- .../output/src/browser/output-context-menu.ts | 34 ++ .../output/src/browser/output-contribution.ts | 172 ++++-- .../output/src/browser/output-editor-model.ts | 52 ++ .../browser/output-editor-options-provider.ts | 70 +++ .../src/browser/output-frontend-module.ts | 33 +- .../output/src/browser/output-resource.ts | 58 ++ .../browser/output-toolbar-contribution.tsx | 60 ++- packages/output/src/browser/output-widget.tsx | 265 +++++++--- packages/output/src/browser/style/output.css | 56 +- packages/output/src/common/output-channel.ts | 500 ++++++++++++++---- packages/output/src/common/output-uri.ts | 41 ++ 23 files changed, 1195 insertions(+), 351 deletions(-) create mode 100644 packages/output/src/browser/output-context-menu.ts create mode 100644 packages/output/src/browser/output-editor-model.ts create mode 100644 packages/output/src/browser/output-editor-options-provider.ts create mode 100644 packages/output/src/browser/output-resource.ts create mode 100644 packages/output/src/common/output-uri.ts 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 d4cf01e9dea97..a16e793acc918 100644 --- a/packages/languages/package.json +++ b/packages/languages/package.json @@ -6,7 +6,6 @@ "@theia/application-package": "^1.1.0", "@theia/core": "^1.1.0", "@theia/monaco-editor-core": "^0.19.3", - "@theia/output": "^1.1.0", "@theia/process": "^1.1.0", "@theia/workspace": "^1.1.0", "@types/uuid": "^7.0.3", @@ -51,4 +50,4 @@ "nyc": { "extends": "../../configs/nyc.json" } -} \ No newline at end of file +} 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-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts index 062757637de4a..7cca61f58bccb 100644 --- a/packages/monaco/src/browser/monaco-editor-model.ts +++ b/packages/monaco/src/browser/monaco-editor-model.ts @@ -14,12 +14,15 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import { inject, injectable, named } from 'inversify'; import { Position } from 'vscode-languageserver-types'; import { TextDocumentSaveReason, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from 'monaco-languageclient'; +import { MaybePromise } from '@theia/core/lib/common'; import { TextEditorDocument } from '@theia/editor/lib/browser'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { Emitter, Event } from '@theia/core/lib/common/event'; +import { ContributionProvider, Prioritizeable } from '@theia/core'; import { CancellationTokenSource, CancellationToken } from '@theia/core/lib/common/cancellation'; import { Resource, ResourceError, ResourceVersion } from '@theia/core/lib/common/resource'; import { Range } from 'vscode-languageserver-types'; @@ -42,6 +45,44 @@ export interface MonacoModelContentChangedEvent { readonly contentChanges: TextDocumentContentChangeEvent[]; } +export const MonacoEditorModelFactoryHandler = Symbol('MonacoEditorModelFactoryHandler'); +export interface MonacoEditorModelFactoryHandler { + + canHandle(resource: Resource): MaybePromise; + + createModel( + resource: Resource, + m2p: MonacoToProtocolConverter, + p2m: ProtocolToMonacoConverter, + options?: { encoding?: string | undefined } + ): MaybePromise; + +} + +@injectable() +export class MonacoEditorModelFactory { + + @inject(ContributionProvider) + @named(MonacoEditorModelFactoryHandler) + protected readonly contributions: ContributionProvider; + + @inject(MonacoToProtocolConverter) + protected readonly m2p: MonacoToProtocolConverter; + + @inject(ProtocolToMonacoConverter) + protected readonly p2m: ProtocolToMonacoConverter; + + async createModel(resource: Resource, options?: { encoding?: string | undefined }): Promise { + const contributions = this.contributions.getContributions(); + const handler = (await Prioritizeable.prioritizeAll(contributions, c => c.canHandle(resource))).map(({ value }) => value).shift(); + if (handler) { + return handler.createModel(resource, this.m2p, this.p2m, options); + } + return new MonacoEditorModel(resource, this.m2p, this.p2m, options); + } + +} + export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { autoSave: 'on' | 'off' = 'on'; diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index a99a4f870f897..5a33bee72a849 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -18,10 +18,10 @@ 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'; +import { MonacoCommandServiceFactory, MonacoCommandService } from './monaco-command-service'; import { MonacoContextMenuService } from './monaco-context-menu'; import { MonacoDiffEditor } from './monaco-diff-editor'; import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory'; @@ -35,11 +35,22 @@ 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, MaybePromise, ContributionProvider, Prioritizeable } 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 MonacoEditorOptionsProvider = Symbol('MonacoEditorOptionsProvider'); +export interface MonacoEditorOptionsProvider { + canHandle(model: MonacoEditorModel): MaybePromise; + create(model: MonacoEditorModel, defaultOptions: MonacoEditor.IOptions): MonacoEditor.IOptions; +} +export const MonacoOverrideServicesProvider = Symbol('MonacoOverrideServicesProvider'); +export interface MonacoOverrideServicesProvider { + canHandle(uri: URI): MaybePromise; + create(uri: URI): MaybePromise>; +} + @injectable() export class MonacoEditorProvider { @@ -55,6 +66,14 @@ export class MonacoEditorProvider { @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(ContributionProvider) + @named(MonacoEditorOptionsProvider) + protected readonly editorOptionsProvider: ContributionProvider; + + @inject(ContributionProvider) + @named(MonacoOverrideServicesProvider) + protected readonly overrideServicesProvider: ContributionProvider; + private isWindowsBackend: boolean = false; protected _current: MonacoEditor | undefined; @@ -123,20 +142,19 @@ 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 getOverrideServices(uri: URI): Promise { const commandService = this.commandServiceFactory(); const contextKeyService = this.contextKeyService.createScoped(); const { codeEditorService, textModelService, contextMenuService } = this; const IWorkspaceEditService = this.bulkEditService; - 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({ + let override: IEditorOverrideServices = { codeEditorService, textModelService, contextMenuService, @@ -144,14 +162,33 @@ export class MonacoEditorProvider { IWorkspaceEditService, contextKeyService, openerService - }, toDispose); + }; + const contributions = this.overrideServicesProvider.getContributions(); + const prioritized = (await Prioritizeable.prioritizeAll(contributions, c => c.canHandle(uri))).map(({ value }) => value); + for (const contribution of prioritized.reverse()) { + const refinedOverride = await contribution.create(uri); + override = { ...override, ...refinedOverride }; + } + return override; + } + + protected async doCreateEditor(uri: URI, factory: (override: IEditorOverrideServices, toDispose: DisposableCollection) => Promise): Promise { + const override = await this.getOverrideServices(uri); + const commandService = override.commandService; + const toDispose = new DisposableCollection(); + if (commandService instanceof MonacoCommandService) { + toDispose.push(commandService); + } + const editor = await factory({ ...override }, toDispose); editor.onDispose(() => toDispose.dispose()); this.suppressMonacoKeybindingListener(editor); this.injectKeybindingResolver(editor); const standaloneCommandService = new monaco.services.StandaloneCommandService(editor.instantiationService); - commandService.setDelegate(standaloneCommandService); + if (commandService instanceof MonacoCommandService) { + commandService.setDelegate(standaloneCommandService); + } toDispose.push(this.installQuickOpenService(editor)); toDispose.push(this.installReferencesController(editor)); @@ -244,7 +281,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 contributions = this.editorOptionsProvider.getContributions(); + const optionsProvider = (await Prioritizeable.prioritizeAll(contributions, c => c.canHandle(model))).map(({ value }) => value).shift(); + const defaultOptions = this.createMonacoEditorOptions(model); + const options = optionsProvider ? optionsProvider.create(model, defaultOptions) : defaultOptions; const editor = new MonacoEditor(uri, model, document.createElement('div'), this.services, options, override); toDispose.push(this.editorPreferences.onPreferenceChanged(event => { if (event.affects(uri.toString(), model.languageId)) { @@ -471,7 +511,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..95c81bda75a80 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, MonacoEditorOptionsProvider, MonacoOverrideServicesProvider } from './monaco-editor-provider'; import { MonacoEditorMenuContribution } from './monaco-menu'; import { MonacoEditorCommandHandlers } from './monaco-command'; import { MonacoKeybindingContribution } from './monaco-keybinding'; @@ -63,6 +63,8 @@ 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 { MonacoEditorModelFactory, MonacoEditorModelFactoryHandler } from './monaco-editor-model'; +import { bindContributionProvider } from '@theia/core'; decorate(injectable(), MonacoToProtocolConverter); decorate(injectable(), ProtocolToMonacoConverter); @@ -97,10 +99,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { ).inSingletonScope(); bind(MonacoBulkEditService).toSelf().inSingletonScope(); bind(MonacoEditorService).toSelf().inSingletonScope(); + bind(MonacoEditorModelFactory).toSelf().inSingletonScope(); + bindContributionProvider(bind, MonacoEditorModelFactoryHandler); bind(MonacoTextModelService).toSelf().inSingletonScope(); bind(MonacoContextMenuService).toSelf().inSingletonScope(); bind(MonacoEditorServices).toSelf().inSingletonScope(); bind(MonacoEditorProvider).toSelf().inSingletonScope(); + bindContributionProvider(bind, MonacoOverrideServicesProvider); + bindContributionProvider(bind, MonacoEditorOptionsProvider); bind(MonacoCommandService).toSelf().inTransientScope(); bind(MonacoCommandServiceFactory).toAutoFactory(MonacoCommandService); bind(TextEditorProvider).toProvider(context => @@ -151,7 +157,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..f3ff1053d17ad 100644 --- a/packages/monaco/src/browser/monaco-text-model-service.ts +++ b/packages/monaco/src/browser/monaco-text-model-service.ts @@ -15,11 +15,10 @@ ********************************************************************************/ import { inject, injectable } from 'inversify'; -import { MonacoToProtocolConverter, ProtocolToMonacoConverter } from 'monaco-languageclient'; import URI from '@theia/core/lib/common/uri'; import { ResourceProvider, ReferenceCollection, Event } from '@theia/core'; import { EditorPreferences, EditorPreferenceChange } from '@theia/editor/lib/browser'; -import { MonacoEditorModel } from './monaco-editor-model'; +import { MonacoEditorModel, MonacoEditorModelFactory } from './monaco-editor-model'; @injectable() export class MonacoTextModelService implements monaco.editor.ITextModelService { @@ -34,11 +33,8 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { @inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences; - @inject(MonacoToProtocolConverter) - protected readonly m2p: MonacoToProtocolConverter; - - @inject(ProtocolToMonacoConverter) - protected readonly p2m: ProtocolToMonacoConverter; + @inject(MonacoEditorModelFactory) + protected modelFactory: MonacoEditorModelFactory; get models(): MonacoEditorModel[] { return this._models.values(); @@ -59,7 +55,7 @@ export class MonacoTextModelService implements monaco.editor.ITextModelService { 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.modelFactory.createModel(resource, { encoding: this.editorPreferences.get('files.encoding') })).load(); this.updateModel(model); model.textEditorModel.onDidChangeLanguage(() => this.updateModel(model)); const disposable = this.editorPreferences.onPreferenceChanged(change => this.updateModel(model, change)); 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 a97c0a78aaae3..89b82b4446c0e 100644 --- a/packages/output/package.json +++ b/packages/output/package.json @@ -3,7 +3,11 @@ "version": "1.1.0", "description": "Theia - Output Extension", "dependencies": { - "@theia/core": "^1.1.0" + "@theia/core": "^1.1.0", + "@theia/editor": "^1.1.0", + "@theia/monaco": "^1.1.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..3b7a6c368402f 100644 --- a/packages/output/src/browser/output-contribution.ts +++ b/packages/output/src/browser/output-contribution.ts @@ -14,100 +14,168 @@ * 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()) }); - commands.registerCommand(OutputCommands.SELECT_ALL, { - isEnabled: () => this.outputIsActiveContext.isEnabled(), - isVisible: () => this.outputIsActiveContext.isEnabled(), - execute: widget => this.withWidget(widget, outputWidget => outputWidget.selectAll()) + 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()) + }); + 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); - } - return false; + open(uri: URI, options?: OpenerOptions): Promise { + return new Promise((resolve, reject) => { + if (!OutputUri.is(uri)) { + reject(new Error(`Expected '${OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`)); + return; + } + this.openView(options).then(widget => { + widget.setInput(OutputUri.channelName(uri)); + resolve(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-model.ts b/packages/output/src/browser/output-editor-model.ts new file mode 100644 index 0000000000000..530ae803f32f8 --- /dev/null +++ b/packages/output/src/browser/output-editor-model.ts @@ -0,0 +1,52 @@ +/******************************************************************************** + * 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 { MonacoToProtocolConverter, ProtocolToMonacoConverter } from 'monaco-languageclient'; +import { Resource } from '@theia/core/lib/common/resource'; +import { MaybePromise } from '@theia/core/lib/common/types'; +import { MonacoEditorModelFactoryHandler, MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { OutputUri } from '../common/output-uri'; + +@injectable() +export class OutputEditorModelFactoryHandler implements MonacoEditorModelFactoryHandler { + + canHandle(resource: Resource): MaybePromise { + return OutputUri.is(resource.uri) ? 1 : 0; + } + + async createModel( + resource: Resource, + m2p: MonacoToProtocolConverter, + p2m: ProtocolToMonacoConverter): + Promise { + + return new OutputEditorModel(resource, m2p, p2m); + } + +} + +export class OutputEditorModel extends MonacoEditorModel { + + get readOnly(): boolean { + return true; + } + + protected setDirty(dirty: boolean): void { + // NOOP + } + +} diff --git a/packages/output/src/browser/output-editor-options-provider.ts b/packages/output/src/browser/output-editor-options-provider.ts new file mode 100644 index 0000000000000..6c8148b152642 --- /dev/null +++ b/packages/output/src/browser/output-editor-options-provider.ts @@ -0,0 +1,70 @@ +/******************************************************************************** + * 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 { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { MonacoContextMenuService } from '@theia/monaco/lib/browser/monaco-context-menu'; +import { MonacoEditorOptionsProvider, MonacoOverrideServicesProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { OutputUri } from '../common/output-uri'; +import { OutputContextMenuService } from './output-context-menu'; + +@injectable() +export class OutputEditorProvider implements MonacoEditorOptionsProvider { + + canHandle(model: MonacoEditorModel): number { + return OutputUri.is(model.uri) ? 1 : 0; + } + + create(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' + }; + } + +} + +@injectable() +export class OutputOverrideServicesProvider implements MonacoOverrideServicesProvider { + + @inject(OutputContextMenuService) + protected readonly contextMenuService: MonacoContextMenuService; + + canHandle(uri: URI): number { + return OutputUri.is(uri) ? 1 : 0; + } + + create(uri: URI): { contextMenuService: MonacoContextMenuService } { + return { + contextMenuService: this.contextMenuService + }; + } +} diff --git a/packages/output/src/browser/output-frontend-module.ts b/packages/output/src/browser/output-frontend-module.ts index fdf9fdfb36489..39a31192afa93 100644 --- a/packages/output/src/browser/output-frontend-module.ts +++ b/packages/output/src/browser/output-frontend-module.ts @@ -16,26 +16,39 @@ 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 { MonacoEditorModelFactoryHandler } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { OutputEditorModelFactoryHandler } from './output-editor-model'; +import { OutputEditorProvider as OutputEditorOptionsProvider, OutputOverrideServicesProvider } from './output-editor-options-provider'; +import { MonacoEditorOptionsProvider, MonacoOverrideServicesProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { OutputContextMenuService } from './output-context-menu'; -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(MonacoEditorOptionsProvider).to(OutputEditorOptionsProvider).inSingletonScope(); + bind(MonacoEditorModelFactoryHandler).to(OutputEditorModelFactoryHandler).inSingletonScope(); + bind(OutputContextMenuService).toSelf().inSingletonScope(); + bind(MonacoOverrideServicesProvider).to(OutputOverrideServicesProvider).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..98126e960efc8 --- /dev/null +++ b/packages/output/src/browser/output-resource.ts @@ -0,0 +1,58 @@ +/******************************************************************************** + * 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'; + +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 editorModel: Deferred) { + this.editorModel.promise.then(({ textEditorModel: textModel }) => { + this._textModel = textModel; + this.toDispose.push(this._textModel); + 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 model = await this.editorModel.promise; + return model.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