blob: c4a5e9c417b35aa51d5660ebe92b8848a4b20f8d [file] [log] [blame] [raw]
// Copyright (c) 2021, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import _ from 'underscore';
import {Container} from 'golden-layout';
import * as monaco from 'monaco-editor';
import {MonacoPaneState, PaneCompilerState, PaneState} from './pane.interfaces';
import {FontScale} from '../widgets/fontscale';
import {Settings, SiteSettings} from '../settings';
import * as utils from '../utils';
import {PaneRenaming} from '../widgets/pane-renaming';
import {EventHub} from '../event-hub';
import {Hub} from '../hub';
import {unwrap} from '../assert';
/**
* Basic container for a tool pane in Compiler Explorer.
*
* Type parameter S refers to a state interface for the pane
*/
export abstract class Pane<S> {
compilerInfo: PaneCompilerState;
container: Container;
domRoot: JQuery;
topBar: JQuery;
hideable: JQuery;
protected hub: Hub;
eventHub: EventHub;
isAwaitingInitialResults = false;
settings: SiteSettings;
paneName: string | undefined = undefined;
paneRenaming: PaneRenaming;
/**
* Base constructor for any pane. Performs common initialization tasks such
* as registering standard event listeners and lifecycle handlers.
*
* Overridable for implementors
*/
protected constructor(hub: Hub, container: Container, state: S & PaneState) {
this.container = container;
this.hub = hub;
this.eventHub = hub.createEventHub();
this.domRoot = container.getElement();
this.domRoot.html(this.getInitialHTML());
this.hideable = this.domRoot.find('.hideable');
this.initializeCompilerInfo(state);
this.topBar = this.domRoot.find('.top-bar');
this.paneRenaming = new PaneRenaming(this, state);
this.initializeDefaults();
this.initializeGlobalDependentProperties();
this.initializeStateDependentProperties(state);
this.registerDynamicElements(state);
this.registerButtons(state);
this.registerStandardCallbacks();
this.registerCallbacks();
this.registerOpeningAnalyticsEvent();
}
protected initializeCompilerInfo(state: Record<string, any>) {
this.compilerInfo = {
compilerId: state.id,
compilerName: state.compilerName,
editorId: state.editorid,
treeId: state.treeid,
};
}
/**
* Get the initial HTML layout for the pane's default content. A typical
* implementation looks like this:
*
* ```ts
* override getInitialHTML(): string {
* return $('#rustmir').html();
* }
* ```
*/
abstract getInitialHTML(): string;
/**
* Emit analytics event for opening the pane tab. Typical implementation
* looks like this:
*
* ```ts
* ga.proxy('send', {
* hitType: 'event',
* eventCategory: 'OpenViewPane',
* eventAction: 'RustMir',
* });
* ```
*/
abstract registerOpeningAnalyticsEvent(): void;
initializeDefaults(): void {}
initializeGlobalDependentProperties(): void {
this.settings = Settings.getStoredSettings();
}
initializeStateDependentProperties(state: S): void {}
/** Optional overridable code for initializing necessary elements before rest of registers **/
registerDynamicElements(state: S): void {}
/** Optionally overridable code for initializing pane buttons */
registerButtons(state: S): void {}
/** Optionally overridable code for initializing event callbacks */
registerCallbacks(): void {}
/**
* Handle user selected compiler change.
*
* This event is triggered when the user selects a different compiler in the
* compiler dropdown.
*
* Note that this event is also triggered when the changed compiler is not
* the one this view is attached to. Therefore, it is smart to check that
* the updated compiler is the one the view is attached to. This can be done
* with a simple check.
*
* ```ts
* if (this.compilerInfo.compilerId === compilerId) { ... }
* ```
*
* @param compilerId - Id of the compiler that had its version changed
* @param compiler - The updated compiler object
* @param options - User commandline args
* @param editorId - The editor id the updated compiler is attached to
*/
abstract onCompiler(compilerId: number, compiler: unknown, options: string, editorId: number, treeId: number): void;
/**
* Handle compilation result.
*
* This event is triggered when a code compilation was triggered.
*
* Note that this event is triggered for *any* compilation, even when the
* compilation was done for a source/compiler this view is not attached to.
* Therefore, it is smart to check that the updated compiler is the one the
* view is attached to. This can be done with a simple check.
*
* ```ts
* if (this.compilerInfo.compilerId === compilerId) { ... }
* ```
*
* @param compilerId - Id of the compiler that had a compilation
* @param compiler - The compiler object
* @param result - The entire HTTP request response
*/
abstract onCompileResult(compilerId: number, compiler: unknown, result: unknown): void;
/**
* Perform any clean-up events when the pane is closed.
*
* This is typically used to emit an analytics event for closing the pane,
* unsubscribing from the event hub and disposing the monaco editor.
*/
abstract close(): void;
/** Initialize standard lifecycle hooks */
protected registerStandardCallbacks(): void {
this.paneRenaming.on('renamePane', this.updateState.bind(this));
this.container.on('destroy', this.close.bind(this));
this.container.on('resize', this.resize.bind(this));
this.eventHub.on('compileResult', this.onCompileResult.bind(this));
this.eventHub.on('compiler', this.onCompiler.bind(this));
this.eventHub.on('compilerClose', this.onCompilerClose.bind(this));
this.eventHub.on('settingsChange', this.onSettingsChange.bind(this));
this.eventHub.on('shown', this.resize.bind(this));
this.eventHub.on('resize', this.resize.bind(this));
}
/**
* Produce a default name for the pane. Typical implementation
* looks like this:
*
* ```ts
* return 'Rust MIR Viewer';
* ```
*/
abstract getDefaultPaneName(): string;
/** Generate "(Editor #1, Compiler #1)" tag */
protected getPaneTag() {
const {compilerName, editorId, treeId, compilerId} = this.compilerInfo;
if (editorId) {
return `${compilerName} (Editor #${editorId}, Compiler #${compilerId})`;
} else {
return `${compilerName} (Tree #${treeId}, Compiler #${compilerId})`;
}
}
/** Get name for the pane */
protected getPaneName() {
return this.paneName ?? this.getDefaultPaneName() + ' ' + this.getPaneTag();
}
/** Update the pane's title, called when the pane name or compiler info changes */
protected updateTitle() {
this.container.setTitle(_.escape(this.getPaneName()));
}
/** Close the pane if the compiler this pane was attached to closes */
protected onCompilerClose(compilerId: number) {
if (this.compilerInfo.compilerId === compilerId) {
_.defer(() => this.container.close());
}
}
protected onSettingsChange(settings: SiteSettings) {
this.settings = settings;
}
getCurrentState(): PaneState {
const state = {
id: this.compilerInfo.compilerId,
compilerName: this.compilerInfo.compilerName,
editorid: this.compilerInfo.editorId,
treeid: this.compilerInfo.treeId,
};
this.paneRenaming.addState(state);
return state;
}
updateState() {
this.container.setState(this.getCurrentState());
}
abstract resize(): void;
}
/**
* Basic container for a tool pane with a monaco editor in Compiler Explorer.
*
* Type parameter E indicates which monaco editor kind this pane hosts. Common
* values are monaco.editor.IDiffEditor and monaco.ICodeEditor
*
* Type parameter S refers to a state interface for the pane
*/
export abstract class MonacoPane<E extends monaco.editor.IEditor, S> extends Pane<S> {
editor: E;
selection: monaco.Selection | undefined = undefined;
fontScale: FontScale;
protected constructor(hub: any /* Hub */, container: Container, state: S & MonacoPaneState) {
super(hub, container, state);
this.selection = state.selection;
this.registerEditorActions();
}
override registerButtons(state: S): void {
const editorRoot = this.domRoot.find('.monaco-placeholder')[0];
this.editor = this.createEditor(editorRoot);
this.fontScale = new FontScale(this.domRoot, state, this.editor);
}
override getCurrentState(): MonacoPaneState {
const parent = super.getCurrentState();
const state: MonacoPaneState = {
selection: this.selection,
...parent,
};
this.fontScale.addState(state);
return state;
}
resize() {
_.defer(() => {
const topBarHeight = utils.updateAndCalcTopBarHeight(this.domRoot, this.topBar, this.hideable);
this.editor.layout({
width: unwrap(this.domRoot.width()),
height: unwrap(this.domRoot.height()) - topBarHeight,
});
});
}
/**
* Initialize the monaco editor instance. Typical implementation for looks
* like this:
*
* ```ts
* return monaco.editor.create(editorRoot, extendConfig({
* // goodies
* }));
* ```
*/
abstract createEditor(editorRoot: HTMLElement): E;
protected override onSettingsChange(settings: SiteSettings) {
super.onSettingsChange(settings);
this.editor.updateOptions({
contextmenu: settings.useCustomContextMenu,
minimap: {
enabled: settings.showMinimap,
},
fontFamily: settings.editorsFFont,
fontLigatures: settings.editorsFLigatures,
});
}
protected onDidChangeCursorSelection(event: monaco.editor.ICursorSelectionChangedEvent) {
if (this.isAwaitingInitialResults) {
this.selection = event.selection;
this.updateState();
}
}
/** Initialize standard lifecycle hooks */
protected override registerStandardCallbacks(): void {
super.registerStandardCallbacks();
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.fontScale) this.fontScale.on('change', this.updateState.bind(this));
this.eventHub.on('broadcastFontScale', (scale: number) => {
this.fontScale.setScale(scale);
this.updateState();
});
}
/**
* Optionally overridable code for initializing monaco actions on the
* editor instance
*/
registerEditorActions(): void {}
}