mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 07:04:04 -05:00
- Removes `rootDirs` so all imports will be relative in the frontend - Updates (and unifies) imports to be `../types/...` etc instead of relying on "types" being in the rootDir for the frontend. - Fixes one type that was being picked up from `lib` in the frontend. - Adds a precommit hook to check in future Paves the way to writing _unit_ tests for the frontend for the subset of the frontend code we can import from `node` (which might be a lot of it!)
398 lines
14 KiB
TypeScript
398 lines
14 KiB
TypeScript
// 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 {Container} from 'golden-layout';
|
|
import * as monaco from 'monaco-editor';
|
|
import _ from 'underscore';
|
|
|
|
import {MonacoPaneState, PaneCompilerState, PaneState} from './pane.interfaces.js';
|
|
|
|
import {Settings, SiteSettings} from '../settings.js';
|
|
import * as utils from '../utils.js';
|
|
import {FontScale} from '../widgets/fontscale.js';
|
|
|
|
import {escapeHTML} from '../../shared/common-utils.js';
|
|
import {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
|
|
import {CompilerInfo} from '../../types/compiler.interfaces.js';
|
|
import {unwrap} from '../assert.js';
|
|
import {EventHub} from '../event-hub.js';
|
|
import {Hub} from '../hub.js';
|
|
import {PaneRenaming} from '../widgets/pane-renaming.js';
|
|
|
|
/**
|
|
* 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, hub);
|
|
|
|
this.initializeDefaults();
|
|
this.initializeGlobalDependentProperties();
|
|
this.initializeStateDependentProperties(state);
|
|
|
|
this.registerDynamicElements(state);
|
|
|
|
this.registerButtons(state);
|
|
this.registerStandardCallbacks();
|
|
this.registerCallbacks();
|
|
}
|
|
|
|
protected initializeCompilerInfo(state: PaneState) {
|
|
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;
|
|
|
|
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: CompilerInfo | null,
|
|
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: CompilerInfo, result: CompilationResult): void;
|
|
|
|
/**
|
|
* Perform any clean-up events when the pane is closed.
|
|
*
|
|
* This is typically used to unsubscribe from the event hub and dispose the monaco editor.
|
|
*/
|
|
abstract close(): void;
|
|
|
|
/** Initialize standard lifecycle hooks */
|
|
protected registerStandardCallbacks(): void {
|
|
this.eventHub.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})`;
|
|
}
|
|
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(escapeHTML(this.getPaneName()));
|
|
}
|
|
|
|
/** Close the pane if the compiler this pane was attached to closes */
|
|
protected onCompilerClose(compilerId: number) {
|
|
if (this.compilerInfo.compilerId === compilerId) {
|
|
_.defer(() => {
|
|
// Check if container is still valid before attempting to close
|
|
if (this.container?.parent && this.container.layoutManager?.isInitialised) {
|
|
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;
|
|
editorDecorations: monaco.editor.IEditorDecorationsCollection;
|
|
selection: monaco.Selection | undefined = undefined;
|
|
fontScale: FontScale;
|
|
|
|
protected constructor(hub: 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.createEditor(editorRoot);
|
|
this.editorDecorations = this.editor.createDecorationsCollection();
|
|
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): void;
|
|
|
|
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();
|
|
|
|
if (this.fontScale) this.fontScale.on('change', this.updateState.bind(this));
|
|
this.eventHub.on('broadcastFontScale', (scale: number) => {
|
|
this.fontScale.setScale(scale);
|
|
this.updateState();
|
|
});
|
|
this.eventHub.on('printrequest', this.sendPrintData, this);
|
|
}
|
|
|
|
/**
|
|
* Optionally overridable code for initializing monaco actions on the
|
|
* editor instance
|
|
*/
|
|
registerEditorActions(): void {}
|
|
|
|
/**
|
|
* Utility function to check if this is a code editor or something else (like a diff editor)
|
|
*/
|
|
protected isStandaloneEditor(editor: monaco.editor.IEditor): editor is monaco.editor.IStandaloneCodeEditor {
|
|
return editor.getEditorType() === 'vs.editor.ICodeEditor';
|
|
}
|
|
/**
|
|
* Get the name of the pane to be displayed in the print view
|
|
*/
|
|
abstract getPrintName(): string;
|
|
|
|
/**
|
|
* Send any printable content to the print view when requested
|
|
*/
|
|
protected sendPrintData() {
|
|
const editor = this.editor;
|
|
if (this.isStandaloneEditor(editor)) {
|
|
const model = (editor as monaco.editor.IStandaloneCodeEditor).getModel();
|
|
if (model) {
|
|
const lines = [...new Array(model.getLineCount()).keys()].map(i =>
|
|
monaco.editor.colorizeModelLine(model, i + 1),
|
|
);
|
|
const extra = this.getExtraPrintData();
|
|
this.eventHub.emit(
|
|
'printdata',
|
|
`<h1>${this.getPrintName()}: ${escapeHTML(this.getPaneName())}</h1>` +
|
|
(extra ?? '') +
|
|
`<code>${lines.join('<br/>\n')}</code>`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provide additional info to be included below the header in the default sendPrintData
|
|
*/
|
|
protected getExtraPrintData(): string | undefined {
|
|
return undefined;
|
|
}
|
|
}
|