mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
397 lines
15 KiB
TypeScript
397 lines
15 KiB
TypeScript
// Copyright (c) 2022, 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 $ from 'jquery';
|
|
import * as monaco from 'monaco-editor';
|
|
import _ from 'underscore';
|
|
import {escapeHTML} from '../../shared/common-utils.js';
|
|
import {CompilationResult} from '../../types/compilation/compilation.interfaces.js';
|
|
import {CompilerInfo} from '../../types/compiler.interfaces.js';
|
|
import {Fix} from '../../types/resultline/resultline.interfaces.js';
|
|
import * as AnsiToHtml from '../ansi-to-html.js';
|
|
import {Hub} from '../hub.js';
|
|
import {updateAndCalcTopBarHeight} from '../utils.js';
|
|
import {FontScale} from '../widgets/fontscale.js';
|
|
import {Toggles} from '../widgets/toggles.js';
|
|
import {OutputState} from './output.interfaces.js';
|
|
import {PaneState} from './pane.interfaces.js';
|
|
import {Pane} from './pane.js';
|
|
|
|
function makeAnsiToHtml(color?: string) {
|
|
return new AnsiToHtml.Filter({
|
|
fg: color ? color : '#333',
|
|
bg: '#f5f5f5',
|
|
stream: true,
|
|
escapeXML: true,
|
|
});
|
|
}
|
|
|
|
export class Output extends Pane<OutputState> {
|
|
contentRoot: JQuery<HTMLElement>;
|
|
optionsToolbar: JQuery<HTMLElement>;
|
|
fontScale: FontScale;
|
|
wrapButton: JQuery<HTMLElement>;
|
|
selectAllButton: JQuery;
|
|
normalAnsiToHtml: AnsiToHtml.Filter;
|
|
errorAnsiToHtml: AnsiToHtml.Filter;
|
|
wrapTitle: string;
|
|
options: Toggles;
|
|
|
|
clickCallback: (e: JQuery.ClickEvent) => void;
|
|
|
|
keydownCallback: (e: JQuery.KeyDownEvent) => void;
|
|
|
|
private isOutputCurrentSelection = false;
|
|
|
|
constructor(hub: Hub, container: Container, state: OutputState & PaneState) {
|
|
// canonicalize state
|
|
if ((state as any).compiler) state.id = (state as any).compiler;
|
|
if ((state as any).editor) state.editorid = (state as any).editor;
|
|
if ((state as any).tree) state.treeid = (state as any).tree;
|
|
super(hub, container, state);
|
|
this.hub = hub;
|
|
this.contentRoot = this.domRoot.find('.content');
|
|
this.optionsToolbar = this.domRoot.find('.options-toolbar');
|
|
this.fontScale = new FontScale(this.domRoot, state, '.content');
|
|
this.fontScale.on('change', this.updateState.bind(this));
|
|
this.normalAnsiToHtml = makeAnsiToHtml();
|
|
this.errorAnsiToHtml = makeAnsiToHtml('var(--terminal-red)');
|
|
this.eventHub.emit('outputOpened', this.compilerInfo.compilerId);
|
|
this.eventHub.on('printrequest', this.sendPrintData, this);
|
|
this.onOptionsChange();
|
|
}
|
|
|
|
private onClickCallback(e: JQuery.ClickEvent) {
|
|
this.isOutputCurrentSelection = this.contentRoot[0].contains(e.target);
|
|
}
|
|
|
|
private onKeydownCallback(e: JQuery.KeyDownEvent) {
|
|
if (this.isOutputCurrentSelection && e.ctrlKey && e.key === 'a') {
|
|
e.preventDefault();
|
|
this.selectAll();
|
|
}
|
|
}
|
|
|
|
private selectAll() {
|
|
const range = document.createRange();
|
|
range.selectNode(this.contentRoot[0]);
|
|
const selection = window.getSelection();
|
|
if (selection !== null) {
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
}
|
|
}
|
|
|
|
override getInitialHTML(): string {
|
|
return $('#compiler-output').html();
|
|
}
|
|
|
|
override registerButtons(state: OutputState & PaneState) {
|
|
this.wrapButton = this.domRoot.find('.wrap-lines');
|
|
this.selectAllButton = this.domRoot.find('.select-all');
|
|
this.wrapTitle = this.wrapButton.prop('title');
|
|
// TODO: Would be nice to be able to get rid of this cast
|
|
this.options = new Toggles(this.domRoot.find('.options'), state as unknown as Record<string, boolean>);
|
|
}
|
|
|
|
override registerCallbacks() {
|
|
this.options.on('change', this.onOptionsChange.bind(this));
|
|
this.eventHub.on('compiling', this.onCompiling, this);
|
|
this.selectAllButton.on('click', this.onSelectAllButton.bind(this));
|
|
|
|
this.clickCallback = e => {
|
|
this.onClickCallback(e);
|
|
};
|
|
|
|
this.keydownCallback = e => {
|
|
this.onKeydownCallback(e);
|
|
};
|
|
|
|
$(document).on('click', this.clickCallback);
|
|
// domRoot is not sufficient here
|
|
$(document).on('keydown', this.keydownCallback);
|
|
}
|
|
|
|
onOptionsChange() {
|
|
const options = this.getEffectiveOptions();
|
|
this.contentRoot.toggleClass('wrap', options.wrap);
|
|
this.wrapButton.prop('title', '[' + (options.wrap ? 'ON' : 'OFF') + '] ' + this.wrapTitle);
|
|
this.updateState();
|
|
}
|
|
|
|
getEffectiveOptions() {
|
|
return this.options.get();
|
|
}
|
|
|
|
override resize() {
|
|
const rootHeight = this.domRoot.height();
|
|
const toolbarHeight = updateAndCalcTopBarHeight(this.domRoot, this.optionsToolbar, this.hideable);
|
|
if (rootHeight && toolbarHeight) {
|
|
this.contentRoot.height(rootHeight - toolbarHeight - 5);
|
|
}
|
|
}
|
|
|
|
override getCurrentState() {
|
|
const parent = super.getCurrentState();
|
|
const state = {
|
|
...this.getEffectiveOptions(),
|
|
...parent,
|
|
};
|
|
this.fontScale.addState(state);
|
|
return state as any;
|
|
}
|
|
|
|
addOutputLines(result: CompilationResult) {
|
|
// When MS upgrade to a server version later than Nov 11, 2022 (the merge of PR #4278)
|
|
// the `undefined` check can be removed.
|
|
const stdout = (result.stdout as any) !== undefined ? result.stdout : [];
|
|
const stderr = result.stderr;
|
|
for (const obj of stdout.concat(stderr)) {
|
|
const lineNumber = obj.tag ? obj.tag.line : obj.line;
|
|
const columnNumber = obj.tag ? obj.tag.column : -1;
|
|
if (obj.text === '') {
|
|
this.add('<br/>');
|
|
} else {
|
|
this.add(
|
|
this.normalAnsiToHtml.toHtml(obj.text),
|
|
lineNumber,
|
|
columnNumber,
|
|
obj.tag?.file,
|
|
obj.tag?.fixes,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
onCompiling(compilerId: number) {
|
|
if (this.compilerInfo.compilerId === compilerId) {
|
|
this.setCompileStatus(true);
|
|
}
|
|
}
|
|
|
|
override onCompileResult(compilerId: number, compiler: CompilerInfo, result: CompilationResult) {
|
|
if (compilerId !== this.compilerInfo.compilerId) return;
|
|
this.compilerInfo.compilerName = compiler.name;
|
|
|
|
this.contentRoot.empty();
|
|
|
|
// reset stream colors
|
|
this.normalAnsiToHtml.reset();
|
|
this.errorAnsiToHtml.reset();
|
|
|
|
if (result.buildsteps) {
|
|
for (const step of result.buildsteps) {
|
|
this.add('Step ' + step.step + ' returned: ' + step.code);
|
|
this.addOutputLines(step);
|
|
}
|
|
} else {
|
|
this.addOutputLines(result);
|
|
if (!result.execResult) {
|
|
this.add('Compiler returned: ' + result.code);
|
|
} else {
|
|
this.add('ASM generation compiler returned: ' + result.code);
|
|
if (result.execResult.buildResult) {
|
|
this.addOutputLines(result.execResult.buildResult);
|
|
this.add('Execution build compiler returned: ' + result.execResult.buildResult.code);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (result.execResult) {
|
|
if (result.execResult.didExecute || result.didExecute) {
|
|
this.add('Program returned: ' + result.execResult.code);
|
|
}
|
|
|
|
if (result.execResult.stderr.length || result.execResult.stdout.length) {
|
|
for (const obj of result.execResult.stderr) {
|
|
// Conserve empty lines as they are discarded by ansiToHtml
|
|
if (obj.text === '') {
|
|
this.programOutput('<br/>');
|
|
} else {
|
|
this.programOutput(this.errorAnsiToHtml.toHtml(obj.text), 'var(--terminal-red)');
|
|
}
|
|
}
|
|
|
|
for (const obj of result.execResult.stdout) {
|
|
// Conserve empty lines as they are discarded by ansiToHtml
|
|
if (obj.text === '') {
|
|
this.programOutput('<br/>');
|
|
} else {
|
|
this.programOutput(this.normalAnsiToHtml.toHtml(obj.text));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.setCompileStatus(false);
|
|
this.updateTitle();
|
|
}
|
|
|
|
override onCompiler(
|
|
compilerId: number,
|
|
compiler: CompilerInfo | null,
|
|
options: string,
|
|
editorId: number,
|
|
treeId: number,
|
|
) {}
|
|
|
|
programOutput(msg: string, color?: string) {
|
|
const elem = $('<div/>').appendTo(this.contentRoot).html(msg).addClass('program-exec-output');
|
|
|
|
if (color) elem.css('color', color);
|
|
}
|
|
|
|
getEditorIdByFilename(filename: string) {
|
|
if (this.compilerInfo.treeId) {
|
|
const tree = this.hub.getTreeById(this.compilerInfo.treeId);
|
|
if (tree) {
|
|
return tree.multifileService.getEditorIdByFilename(filename);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
emitEditorLinkLine(lineNum: number, column: number, filename: string, goto: boolean) {
|
|
if (this.compilerInfo.editorId) {
|
|
this.eventHub.emit('editorLinkLine', this.compilerInfo.editorId, lineNum, column, column + 1, goto);
|
|
} else if (filename) {
|
|
const editorId = this.getEditorIdByFilename(filename);
|
|
if (editorId) {
|
|
this.eventHub.emit('editorLinkLine', editorId, lineNum, column, column + 1, goto);
|
|
}
|
|
}
|
|
}
|
|
|
|
emitEditorApplyQuickfix(filename: string, range: monaco.IRange, text: string | null): void {
|
|
if (this.compilerInfo.editorId) {
|
|
this.eventHub.emit('editorApplyQuickfix', this.compilerInfo.editorId, range, text);
|
|
} else if (filename) {
|
|
const editorId = this.getEditorIdByFilename(filename);
|
|
if (editorId) {
|
|
this.eventHub.emit('editorApplyQuickfix', editorId, range, text);
|
|
}
|
|
}
|
|
}
|
|
|
|
add(msg: string, lineNum?: number, column?: number, filename?: string, fixes?: Fix[]) {
|
|
const elem = $('<div/>').appendTo(this.contentRoot);
|
|
if (lineNum && column && filename) {
|
|
elem.empty();
|
|
const span = $('<span class="linked-compiler-output-line"></span>')
|
|
.html(msg)
|
|
.on('click', e => {
|
|
this.emitEditorLinkLine(lineNum, column, filename, true);
|
|
// do not bring user to the top of index.html
|
|
// http://stackoverflow.com/questions/3252730
|
|
e.preventDefault();
|
|
return false;
|
|
})
|
|
.on('mouseover', () => {
|
|
this.emitEditorLinkLine(lineNum, column, filename, false);
|
|
})
|
|
.on('click', '.diagnostic-url', e => {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
if (fixes) {
|
|
this.addQuickfixHandlers(span, msg, fixes, filename);
|
|
}
|
|
|
|
span.appendTo(elem);
|
|
} else {
|
|
elem.html(msg);
|
|
}
|
|
}
|
|
|
|
private addQuickfixHandlers(span: JQuery<HTMLElement>, msg: string, fixes: Fix[], filename: string): void {
|
|
if (fixes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
span.attr('title', fixes[0].title).addClass('quickfix-action');
|
|
for (const fix of fixes) {
|
|
span.on('click', e => {
|
|
for (const {text, line, endline, column, endcolumn} of fix.edits) {
|
|
if (line && endline && column && endcolumn) {
|
|
this.emitEditorApplyQuickfix(
|
|
filename,
|
|
{
|
|
startLineNumber: line,
|
|
startColumn: column,
|
|
endLineNumber: endline,
|
|
endColumn: endcolumn,
|
|
},
|
|
text,
|
|
);
|
|
}
|
|
}
|
|
$(e.delegateTarget).replaceWith($('<span></span>').html(msg));
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
|
|
override getDefaultPaneName() {
|
|
return `Output of ${this.compilerInfo.compilerName}`;
|
|
}
|
|
|
|
override getPaneTag() {
|
|
return `(Compiler #${this.compilerInfo.compilerId})`;
|
|
}
|
|
|
|
override onCompilerClose(id: number) {
|
|
if (id === this.compilerInfo.compilerId) {
|
|
// We can't immediately close as an outer loop somewhere in GoldenLayout is iterating over
|
|
// the hierarchy. We can't modify while it's being iterated over.
|
|
this.close();
|
|
_.defer(() => {
|
|
this.container.close();
|
|
});
|
|
}
|
|
}
|
|
|
|
override close() {
|
|
this.eventHub.emit('outputClosed', this.compilerInfo.compilerId);
|
|
this.eventHub.unsubscribe();
|
|
$(document).off('click', this.clickCallback);
|
|
$(document).off('keydown', this.keydownCallback);
|
|
}
|
|
|
|
setCompileStatus(isCompiling: boolean) {
|
|
this.contentRoot.toggleClass('compiling', isCompiling);
|
|
}
|
|
|
|
private onSelectAllButton(unused: JQuery.ClickEvent) {
|
|
this.selectAll();
|
|
}
|
|
|
|
protected sendPrintData() {
|
|
this.eventHub.emit(
|
|
'printdata',
|
|
|
|
`<h1>Output Pane: ${escapeHTML(this.getPaneName())}</h1>` + `<code>${this.contentRoot.html()}</code>`,
|
|
);
|
|
}
|
|
}
|