blob: b0e9aaf619d764c57448fa67ef706dba1eae10dc [file] [log] [blame] [raw]
// Copyright (c) 2019, 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 $ from 'jquery';
import {ga} from '../analytics.js';
import {Toggles} from '../widgets/toggles.js';
import {FontScale} from '../widgets/fontscale.js';
import {options} from '../options.js';
import {Alert} from '../widgets/alert.js';
import {LibsWidget} from '../widgets/libs-widget.js';
import {Filter as AnsiToHtml} from '../ansi-to-html.js';
import * as TimingWidget from '../widgets/timing-info-widget.js';
import {Settings, SiteSettings} from '../settings.js';
import * as utils from '../utils.js';
import * as LibUtils from '../lib-utils.js';
import {PaneRenaming} from '../widgets/pane-renaming.js';
import {CompilerService} from '../compiler-service.js';
import {Pane} from './pane.js';
import {Hub} from '../hub.js';
import {Container} from 'golden-layout';
import {PaneState} from './pane.interfaces.js';
import {ExecutorState} from './executor.interfaces.js';
import {CompilerInfo} from '../../types/compiler.interfaces.js';
import {Language} from '../../types/languages.interfaces.js';
import {LanguageLibs} from '../options.interfaces.js';
import {LLVMOptPipelineBackendOptions} from '../../types/compilation/llvm-opt-pipeline-output.interfaces.js';
import {PPOptions} from './pp-view.interfaces.js';
import {FiledataPair, CompilationResult} from '../../types/compilation/compilation.interfaces.js';
import {ResultLine} from '../../types/resultline/resultline.interfaces.js';
import {CompilationStatus as CompilerServiceCompilationStatus} from '../compiler-service.interfaces.js';
import {CompilerPicker} from '../widgets/compiler-picker.js';
import {GccDumpViewSelectedPass} from './gccdump-view.interfaces.js';
import {SourceAndFiles} from '../download-service.js';
const languages = options.languages;
type CompilationStatus = Omit<CompilerServiceCompilationStatus, 'compilerOut'> & {
didExecute?: boolean;
};
function makeAnsiToHtml(color?: string): AnsiToHtml {
return new AnsiToHtml({
fg: color ? color : '#333',
bg: '#f5f5f5',
stream: true,
escapeXML: true,
});
}
type ActiveTools = {
id: number;
args: string[];
stdin: string;
};
type CompilationRequestOptions = {
userArguments: string;
compilerOptions: {
executorRequest?: boolean;
skipAsm?: boolean;
producePp?: PPOptions | null;
produceAst?: boolean;
produceGccDump?: {
opened: boolean;
pass?: GccDumpViewSelectedPass;
treeDump?: boolean;
rtlDump?: boolean;
ipaDump?: boolean;
dumpFlags: any;
};
produceOptInfo?: boolean;
produceCfg?: boolean;
produceGnatDebugTree?: boolean;
produceGnatDebug?: boolean;
produceIr?: boolean;
produceLLVMOptPipeline?: LLVMOptPipelineBackendOptions | null;
produceDevice?: boolean;
produceRustMir?: boolean;
produceRustMacroExp?: boolean;
produceRustHir?: boolean;
produceHaskellCore?: boolean;
produceHaskellStg?: boolean;
produceHaskellCmm?: boolean;
cmakeArgs?: string;
customOutputFilename?: string;
};
executeParameters: {
args: string;
stdin: string;
};
filters: Record<string, boolean>;
tools: ActiveTools[];
libraries: CompileChildLibraries[];
};
type CompilationRequest = {
source: string;
compiler: string;
options: CompilationRequestOptions;
lang: string | null;
files: FiledataPair[];
bypassCache?: boolean;
};
type LangInfo = {
compiler: string;
options: string;
execArgs: string;
execStdin: string;
};
type CompileChildLibraries = {
id: string;
version: string;
};
export class Executor extends Pane<ExecutorState> {
private contentRoot: JQuery<HTMLElement>;
private readonly sourceEditorId: number | null;
private sourceTreeId: number | null;
private readonly id: number;
private deferCompiles: boolean;
private needsCompile: boolean;
private executionArguments: string;
private executionStdin: string;
private source: string;
private lastTimeTaken: number;
private pendingRequestSentAt: number;
private pendingCMakeRequestSentAt: number;
private nextRequest: CompilationRequest | null;
private nextCMakeRequest: CompilationRequest | null;
private options: string;
private lastResult: CompilationResult | null;
private alertSystem: Alert;
private readonly normalAnsiToHtml: AnsiToHtml;
private readonly errorAnsiToHtml: AnsiToHtml;
private fontScale: FontScale;
private compilerPicker: CompilerPicker;
private currentLangId: string;
private toggleWrapButton: Toggles;
private compileClearCache: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private outputContentRoot: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private executionStatusSection: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private compilerOutputSection: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private executionOutputSection: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private optionsField: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private execArgsField: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private execStdinField: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private prependOptions: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private fullCompilerName: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private fullTimingInfo: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private libsButton: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private compileTimeLabel: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private shortCompilerName: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private bottomBar: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private statusLabel: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private statusIcon: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]> | null;
private panelCompilation: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private panelArgs: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private panelStdin: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private wrapTitle: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private triggerCompilationButton: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private wrapButton: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private toggleCompilation: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private toggleArgs: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private toggleStdin: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private toggleCompilerOut: JQuery<HTMLElementTagNameMap[keyof HTMLElementTagNameMap]>;
private libsWidget?: LibsWidget;
private readonly infoByLang: Record<string, LangInfo | undefined>;
private compiler: CompilerInfo | null;
constructor(hub: Hub, container: Container, state: PaneState & ExecutorState) {
super(hub, container, state);
if (this.sourceTreeId) {
this.sourceEditorId = null;
} else {
this.sourceEditorId = state.source || 1;
}
this.id = state.id || this.hub.nextExecutorId();
this.contentRoot = this.domRoot.find('.content');
this.infoByLang = {};
this.deferCompiles = hub.deferred;
this.needsCompile = false;
this.source = '';
this.lastResult = {code: -1, timedOut: false, stdout: [], stderr: []};
this.lastTimeTaken = 0;
this.pendingRequestSentAt = 0;
this.pendingCMakeRequestSentAt = 0;
this.nextRequest = null;
this.nextCMakeRequest = null;
this.alertSystem = new Alert();
this.alertSystem.prefixMessage = 'Executor #' + this.id;
this.normalAnsiToHtml = makeAnsiToHtml();
this.errorAnsiToHtml = makeAnsiToHtml('red');
this.initButtons(state);
this.fontScale = new FontScale(this.domRoot, state, 'pre.content');
this.compilerPicker = new CompilerPicker(
this.domRoot,
this.hub,
this.currentLangId,
this.compiler ? this.compiler.id : '',
this.onCompilerChange.bind(this),
this.compilerIsVisible,
);
this.initLibraries(state);
this.initCallbacks();
// Handle initial settings
this.onSettingsChange(this.settings);
this.updateCompilerInfo();
this.saveState();
if (this.sourceTreeId) {
this.compile();
}
}
override initializeStateDependentProperties(state: PaneState & ExecutorState) {
this.sourceTreeId = state.tree ?? null;
this.settings = Settings.getStoredSettings();
this.initLangAndCompiler(state);
this.options = state.options || options.compileOptions[this.currentLangId];
this.executionArguments = state.execArgs || '';
this.executionStdin = state.execStdin || '';
this.paneRenaming = new PaneRenaming(this, state);
}
override getInitialHTML(): string {
return $('#executor').html();
}
compilerIsVisible(compiler: CompilerInfo): boolean {
return !!compiler.supportsExecute;
}
getEditorIdByFilename(filename: string): number | null {
if (this.sourceTreeId) {
const tree = this.hub.getTreeById(this.sourceTreeId);
if (tree) {
return tree.multifileService.getEditorIdByFilename(filename);
}
} else if (this.sourceEditorId) {
return this.sourceEditorId;
}
return null;
}
initLangAndCompiler(state: PaneState & ExecutorState): void {
const langId = state.lang ?? null;
const compilerId = state.compiler;
const result = this.hub.compilerService.processFromLangAndCompiler(langId, compilerId);
this.compiler = result?.compiler ?? null;
this.currentLangId = result?.langId ?? '';
this.updateLibraries();
}
close(): void {
this.eventHub.unsubscribe();
if (this.compilerPicker instanceof CompilerPicker) {
this.compilerPicker.close();
}
this.eventHub.emit('executorClose', this.id);
}
undefer(): void {
this.deferCompiles = false;
if (this.needsCompile) this.compile();
}
override resize(): void {
_.defer(self => {
let topBarHeight = utils.updateAndCalcTopBarHeight(self.domRoot, $(self.topBar[0]), self.hideable);
// We have some more elements that modify the topBarHeight
if (!self.panelCompilation.hasClass('d-none')) {
topBarHeight += self.panelCompilation.outerHeight(true);
}
if (!self.panelArgs.hasClass('d-none')) {
topBarHeight += self.panelArgs.outerHeight(true);
}
if (!self.panelStdin.hasClass('d-none')) {
topBarHeight += self.panelStdin.outerHeight(true);
}
const bottomBarHeight = self.bottomBar.outerHeight(true);
self.outputContentRoot.outerHeight(self.domRoot.height() - topBarHeight - bottomBarHeight);
}, this);
}
private errorResult(message: string): CompilationResult {
// @ts-expect-error: This is a valid CompilationResult
return {stdout: [], timedOut: false, code: -1, stderr: message};
}
compile(bypassCache?: boolean): void {
if (this.deferCompiles) {
this.needsCompile = true;
return;
}
this.needsCompile = false;
this.compileTimeLabel.text(' - Compiling...');
const options: CompilationRequestOptions = {
userArguments: this.options,
executeParameters: {
args: this.executionArguments,
stdin: this.executionStdin,
},
compilerOptions: {
executorRequest: true,
skipAsm: true,
},
filters: {execute: true},
tools: [],
libraries: [],
};
this.libsWidget?.getLibsInUse()?.forEach(item => {
options.libraries.push({
id: item.libId,
version: item.versionId,
});
});
if (this.sourceTreeId) {
this.compileFromTree(options, bypassCache);
} else {
this.compileFromEditorSource(options, bypassCache);
}
}
compileFromEditorSource(options: CompilationRequestOptions, bypassCache?: boolean): void {
if (!this.compiler?.supportsExecute) {
this.alertSystem.notify('This compiler (' + this.compiler?.name + ') does not support execution', {
group: 'execution',
});
return;
}
this.hub.compilerService.expandToFiles(this.source).then((sourceAndFiles: SourceAndFiles) => {
const request: CompilationRequest = {
source: sourceAndFiles.source || '',
compiler: this.compiler ? this.compiler.id : '',
options: options,
lang: this.currentLangId,
files: sourceAndFiles.files,
};
if (bypassCache) request.bypassCache = true;
if (!this.compiler) {
this.onCompileResponse(request, this.errorResult('<Please select a compiler>'), false);
} else {
this.sendCompile(request);
}
});
}
compileFromTree(options: CompilationRequestOptions, bypassCache?: boolean): void {
const tree = this.hub.getTreeById(this.sourceTreeId ?? -1);
if (!tree) {
this.sourceTreeId = null;
this.compileFromEditorSource(options, bypassCache);
return;
}
const request: CompilationRequest = {
source: tree.multifileService.getMainSource(),
compiler: this.compiler ? this.compiler.id : '',
options: options,
lang: this.currentLangId,
files: tree.multifileService.getFiles(),
};
const fetches: Promise<void>[] = [];
fetches.push(
this.hub.compilerService.expandToFiles(request.source).then((sourceAndFiles: SourceAndFiles) => {
request.source = sourceAndFiles.source;
request.files.push(...sourceAndFiles.files);
}),
);
const moreFiles: FiledataPair[] = [];
for (let i = 0; i < request.files.length; i++) {
const file = request.files[i];
fetches.push(
this.hub.compilerService.expandToFiles(file.contents).then((sourceAndFiles: SourceAndFiles) => {
file.contents = sourceAndFiles.source;
moreFiles.push(...sourceAndFiles.files);
}),
);
}
request.files.push(...moreFiles);
Promise.all(fetches).then(() => {
const treeState = tree.currentState();
const cmakeProject = tree.multifileService.isACMakeProject();
if (bypassCache) request.bypassCache = true;
if (!this.compiler) {
this.onCompileResponse(request, this.errorResult('<Please select a compiler>'), false);
} else if (cmakeProject && request.source === '') {
this.onCompileResponse(request, this.errorResult('<Please supply a CMakeLists.txt>'), false);
} else {
if (cmakeProject) {
request.options.compilerOptions.cmakeArgs = treeState.cmakeArgs;
request.options.compilerOptions.customOutputFilename = treeState.customOutputFilename;
this.sendCMakeCompile(request);
} else {
this.sendCompile(request);
}
}
});
}
sendCMakeCompile(request: CompilationRequest): void {
const onCompilerResponse = this.onCMakeResponse.bind(this);
if (this.pendingCMakeRequestSentAt) {
// If we have a request pending, then just store this request to do once the
// previous request completes.
this.nextCMakeRequest = request;
return;
}
// this.eventHub.emit('compiling', this.id, this.compiler);
// Display the spinner
this.handleCompilationStatus({code: 4});
this.pendingCMakeRequestSentAt = Date.now();
// After a short delay, give the user some indication that we're working on their
// compilation.
this.hub.compilerService
.submitCMake(request)
.then((x: any) => {
onCompilerResponse(request, x.result, x.localCacheHit);
})
.catch(x => {
let message = 'Unknown error';
if (_.isString(x)) {
message = x;
} else if (x) {
message = x.error || x.code || x.message || x;
}
onCompilerResponse(request, this.errorResult(message), false);
});
}
sendCompile(request: CompilationRequest): void {
const onCompilerResponse = this.onCompileResponse.bind(this);
if (this.pendingRequestSentAt) {
// If we have a request pending, then just store this request to do once the
// previous request completes.
this.nextRequest = request;
return;
}
// this.eventHub.emit('compiling', this.id, this.compiler);
// Display the spinner
this.handleCompilationStatus({code: 4});
this.pendingRequestSentAt = Date.now();
// After a short delay, give the user some indication that we're working on their
// compilation.
this.hub.compilerService
.submit(request)
.then((x: any) => {
onCompilerResponse(request, x.result, x.localCacheHit);
})
.catch(x => {
let message = 'Unknown error';
if (typeof x === 'string') {
message = x;
} else if (x) {
message = x.error || x.code || x.message || x;
}
onCompilerResponse(request, this.errorResult(message), false);
});
}
addCompilerOutputLine(
msg: string,
container: JQuery,
lineNum: number | undefined,
column: number | undefined,
addLineLinks: boolean,
filename: string | null,
): void {
const elem = $('<div/>').appendTo(container);
if (addLineLinks && lineNum) {
elem.html(
// @ts-expect-error: JQuery types are wrong
$('<span class="linked-compiler-output-line"></span>')
.html(msg)
.on('click', e => {
const editorId = this.getEditorIdByFilename(filename ?? '');
if (editorId) {
this.eventHub.emit(
'editorLinkLine',
editorId,
lineNum,
column ?? 0,
(column ?? 0) + 1,
true,
);
}
// do not bring user to the top of index.html
// http://stackoverflow.com/questions/3252730
e.preventDefault();
return false;
})
.on('mouseover', () => {
const editorId = this.getEditorIdByFilename(filename ?? '');
if (editorId) {
this.eventHub.emit(
'editorLinkLine',
editorId,
lineNum,
column ?? 0,
(column ?? 0) + 1,
false,
);
}
}),
);
} else {
elem.html(msg);
}
}
clearPreviousOutput(): void {
this.executionStatusSection.empty();
this.compilerOutputSection.empty();
this.executionOutputSection.empty();
}
handleOutput(
output: ResultLine[],
element: JQuery<HTMLElement>,
ansiParser: AnsiToHtml,
addLineLinks: boolean,
): JQuery<HTMLElement> {
const outElem = $('<pre class="card"></pre>').appendTo(element);
output.forEach(obj => {
if (obj.text === '') {
this.addCompilerOutputLine('<br/>', outElem, undefined, undefined, false, null);
} else {
const lineNumber = obj.tag ? obj.tag.line : obj.line;
const columnNumber = obj.tag ? obj.tag.column : -1;
const filename = obj.tag ? obj.tag.file : false;
this.addCompilerOutputLine(
ansiParser.toHtml(obj.text),
outElem,
lineNumber,
columnNumber,
addLineLinks,
filename || null,
);
}
});
return outElem;
}
getBuildStdoutFromResult(result: CompilationResult): ResultLine[] {
let arr: ResultLine[] = [];
if (result.buildResult) {
arr = arr.concat(result.buildResult.stdout);
}
if (result.buildsteps) {
result.buildsteps.forEach(step => {
arr = arr.concat(step.stdout);
});
}
return arr;
}
getBuildStderrFromResult(result: CompilationResult): ResultLine[] {
let arr: ResultLine[] = [];
if (result.buildResult) {
arr = arr.concat(result.buildResult.stderr);
}
if (result.buildsteps) {
result.buildsteps.forEach(step => {
arr = arr.concat(step.stderr);
});
}
return arr;
}
getExecutionStdoutfromResult(result: CompilationResult): ResultLine[] {
if (result.execResult && result.execResult.stdout !== undefined) {
return result.execResult.stdout;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return result.stdout || [];
}
getExecutionStderrfromResult(result: CompilationResult): ResultLine[] {
if (result.execResult) {
return result.execResult.stderr as ResultLine[];
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return result.stderr || [];
}
onCMakeResponse(request: CompilationRequest, result: CompilationResult, cached: boolean): void {
result.source = this.source;
this.lastResult = result;
const timeTaken = Math.max(0, Date.now() - this.pendingCMakeRequestSentAt);
this.lastTimeTaken = timeTaken;
const wasRealReply = this.pendingCMakeRequestSentAt > 0;
this.pendingCMakeRequestSentAt = 0;
this.handleCompileRequestAndResponse(request, result, cached, wasRealReply, timeTaken);
this.doNextCMakeRequest();
}
doNextCompileRequest(): void {
if (this.nextRequest) {
const next = this.nextRequest;
this.nextRequest = null;
this.sendCompile(next);
}
}
doNextCMakeRequest(): void {
if (this.nextCMakeRequest) {
const next = this.nextCMakeRequest;
this.nextCMakeRequest = null;
this.sendCMakeCompile(next);
}
}
handleCompileRequestAndResponse(
request: CompilationRequest,
result: CompilationResult,
cached: boolean,
wasRealReply: boolean,
timeTaken: number,
): void {
ga.proxy('send', {
hitType: 'event',
eventCategory: 'Compile',
eventAction: request.compiler,
eventLabel: request.options.userArguments,
eventValue: cached ? 1 : 0,
});
ga.proxy('send', {
hitType: 'timing',
timingCategory: 'Compile',
timingVar: request.compiler,
timingValue: timeTaken,
});
this.clearPreviousOutput();
const compileStdout = this.getBuildStdoutFromResult(result);
const compileStderr = this.getBuildStderrFromResult(result);
const execStdout = this.getExecutionStdoutfromResult(result);
const execStderr = this.getExecutionStderrfromResult(result);
let buildResultCode = 0;
if (result.buildResult) {
buildResultCode = result.buildResult.code;
} else if (result.buildsteps) {
result.buildsteps.forEach(step => {
buildResultCode = step.code;
});
}
if (!result.didExecute) {
this.executionStatusSection.append($('<div/>').text('Could not execute the program'));
this.executionStatusSection.append($('<div/>').text('Compiler returned: ' + buildResultCode));
}
// reset stream styles
this.normalAnsiToHtml.reset();
this.errorAnsiToHtml.reset();
if (compileStdout.length > 0) {
this.compilerOutputSection.append($('<div/>').text('Compiler stdout'));
this.handleOutput(compileStdout, this.compilerOutputSection, this.normalAnsiToHtml, true);
}
if (compileStderr.length > 0) {
this.compilerOutputSection.append($('<div/>').text('Compiler stderr'));
this.handleOutput(compileStderr, this.compilerOutputSection, this.errorAnsiToHtml, true);
}
if (result.didExecute) {
const exitCode = result.execResult ? result.execResult.code : result.code;
this.executionOutputSection.append($('<div/>').text('Program returned: ' + exitCode));
if (execStdout.length > 0) {
this.executionOutputSection.append($('<div/>').text('Program stdout'));
const outElem = this.handleOutput(
execStdout,
this.executionOutputSection,
this.normalAnsiToHtml,
false,
);
outElem.addClass('execution-stdout');
}
if (execStderr.length > 0) {
this.executionOutputSection.append($('<div/>').text('Program stderr'));
this.handleOutput(execStderr, this.executionOutputSection, this.normalAnsiToHtml, false);
}
}
this.handleCompilationStatus({code: 1, didExecute: result.didExecute});
let timeLabelText = '';
if (cached) {
timeLabelText = ' - cached';
} else if (wasRealReply) {
timeLabelText = ' - ' + timeTaken + 'ms';
}
this.compileTimeLabel.text(timeLabelText);
this.setCompilationOptionsPopover(result.buildResult ? result.buildResult.compilationOptions.join(' ') : '');
if (this.currentLangId)
this.eventHub.emit('executeResult', this.id, this.compiler, result, languages[this.currentLangId]);
}
onCompileResponse(request: CompilationRequest, result: CompilationResult, cached: boolean): void {
// Save which source produced this change. It should probably be saved earlier though
result.source = this.source;
this.lastResult = result;
const timeTaken = Math.max(0, Date.now() - this.pendingRequestSentAt);
this.lastTimeTaken = timeTaken;
const wasRealReply = this.pendingRequestSentAt > 0;
this.pendingRequestSentAt = 0;
this.handleCompileRequestAndResponse(request, result, cached, wasRealReply, timeTaken);
this.doNextCompileRequest();
}
resendResult(): boolean {
if (!$.isEmptyObject(this.lastResult)) {
// @ts-expect-error: 'executeResult' may accept only 4 arguments
this.eventHub.emit('executeResult', this.id, this.compiler, this.lastResult);
return true;
}
return false;
}
onResendExecutionResult(id: number): void {
if (id === this.id) {
this.resendResult();
}
}
onEditorChange(editor: number, source: string, langId: string, compilerId?: number): void {
if (this.sourceTreeId) {
const tree = this.hub.getTreeById(this.sourceTreeId);
if (tree) {
if (tree.multifileService.isEditorPartOfProject(editor)) {
if (this.settings.compileOnChange) {
this.compile();
return;
}
}
}
}
if (editor === this.sourceEditorId && langId === this.currentLangId && compilerId === undefined) {
this.source = source;
if (this.settings.compileOnChange) {
this.compile();
}
}
}
initButtons(state: PaneState & ExecutorState): void {
this.compileClearCache = this.domRoot.find('.clear-cache');
this.outputContentRoot = this.domRoot.find('pre.content');
this.executionStatusSection = this.outputContentRoot.find('.execution-status');
this.compilerOutputSection = this.outputContentRoot.find('.compiler-output');
this.executionOutputSection = this.outputContentRoot.find('.execution-output');
this.toggleWrapButton = new Toggles(this.domRoot.find('.options'), state as unknown as Record<string, boolean>);
this.optionsField = this.domRoot.find('.compilation-options');
this.execArgsField = this.domRoot.find('.execution-arguments');
this.execStdinField = this.domRoot.find('.execution-stdin');
this.prependOptions = this.domRoot.find('.prepend-options');
this.fullCompilerName = this.domRoot.find('.full-compiler-name');
this.fullTimingInfo = this.domRoot.find('.full-timing-info');
this.setCompilationOptionsPopover(this.compiler?.options ?? null);
this.compileTimeLabel = this.domRoot.find('.compile-time');
this.libsButton = this.domRoot.find('.btn.show-libs');
// Dismiss on any click that isn't either in the opening element, inside
// the popover or on any alert
$(document).on('mouseup', e => {
const target = $(e.target);
if (
!target.is(this.prependOptions) &&
// @ts-expect-error: JQuery types are wrong
this.prependOptions.has(target).length === 0 &&
target.closest('.popover').length === 0
)
this.prependOptions.popover('hide');
if (
!target.is(this.fullCompilerName) &&
// @ts-expect-error: JQuery types are wrong
this.fullCompilerName.has(target).length === 0 &&
target.closest('.popover').length === 0
)
this.fullCompilerName.popover('hide');
});
this.optionsField.val(this.options);
this.execArgsField.val(this.executionArguments);
this.execStdinField.val(this.executionStdin);
this.shortCompilerName = this.domRoot.find('.short-compiler-name');
this.setCompilerVersionPopover({version: '', fullVersion: ''}, '');
this.topBar = this.domRoot.find('.top-bar');
this.bottomBar = this.domRoot.find('.bottom-bar');
this.statusLabel = this.domRoot.find('.status-text');
this.hideable = this.domRoot.find('.hideable');
this.statusIcon = this.domRoot.find('.status-icon');
this.panelCompilation = this.domRoot.find('.panel-compilation');
this.panelArgs = this.domRoot.find('.panel-args');
this.panelStdin = this.domRoot.find('.panel-stdin');
this.wrapButton = this.domRoot.find('.wrap-lines');
this.wrapTitle = this.wrapButton.prop('title');
this.triggerCompilationButton = this.bottomBar.find('.trigger-compilation');
this.initToggleButtons(state);
}
initToggleButtons(state: PaneState & ExecutorState): void {
this.toggleCompilation = this.domRoot.find('.toggle-compilation');
this.toggleArgs = this.domRoot.find('.toggle-args');
this.toggleStdin = this.domRoot.find('.toggle-stdin');
this.toggleCompilerOut = this.domRoot.find('.toggle-compilerout');
if (!state.compilationPanelShown) {
this.hidePanel(this.toggleCompilation, this.panelCompilation);
}
if (state.argsPanelShown) {
this.showPanel(this.toggleArgs, this.panelArgs);
}
if (state.stdinPanelShown) {
this.showPanel(this.toggleStdin, this.panelStdin);
}
if (!state.compilerOutShown) {
this.hidePanel(this.toggleCompilerOut, this.compilerOutputSection);
}
if (state.wrap === true) {
this.contentRoot.addClass('wrap');
this.wrapButton.prop('title', '[ON] ' + this.wrapTitle);
} else {
this.contentRoot.removeClass('wrap');
this.wrapButton.prop('title', '[OFF] ' + this.wrapTitle);
}
}
onLibsChanged(): void {
this.saveState();
this.compile();
}
initLibraries(state: PaneState & ExecutorState): void {
this.libsWidget = new LibsWidget(
this.currentLangId,
this.compiler,
this.libsButton,
state,
this.onLibsChanged.bind(this),
LibUtils.getSupportedLibraries(
this.compiler ? this.compiler.libsArr : [],
this.currentLangId,
this.compiler?.remote ?? null,
),
);
}
onFontScale(): void {
this.saveState();
}
initListeners(): void {
// this.filters.on('change', _.bind(this.onFilterChange, this));
this.fontScale.on('change', this.onFontScale.bind(this));
this.paneRenaming.on('renamePane', this.saveState.bind(this));
this.toggleWrapButton.on('change', this.onToggleWrapChange.bind(this));
this.container.on('destroy', this.close, this);
this.container.on('resize', this.resize, this);
this.container.on('shown', this.resize, this);
this.container.on('open', () => {
this.eventHub.emit('executorOpen', this.id, this.sourceEditorId ?? false);
});
this.eventHub.on('editorChange', this.onEditorChange, this);
this.eventHub.on('editorClose', this.onEditorClose, this);
this.eventHub.on('settingsChange', this.onSettingsChange, this);
this.eventHub.on('requestCompilation', this.onRequestCompilation, this);
this.eventHub.on('resendExecution', this.onResendExecutionResult, this);
this.eventHub.on('resize', this.resize, this);
this.eventHub.on('findExecutors', this.sendExecutor, this);
this.eventHub.on('languageChange', this.onLanguageChange, this);
this.fullTimingInfo.off('click').on('click', () => {
TimingWidget.displayCompilationTiming(this.lastResult, this.lastTimeTaken);
});
}
showPanel(button: JQuery<HTMLElement>, panel: JQuery<HTMLElement>): void {
panel.removeClass('d-none');
button.addClass('active');
this.resize();
}
hidePanel(button: JQuery<HTMLElement>, panel: JQuery<HTMLElement>): void {
panel.addClass('d-none');
button.removeClass('active');
this.resize();
}
togglePanel(button: JQuery<HTMLElement>, panel: JQuery<HTMLElement>): void {
if (panel.hasClass('d-none')) {
this.showPanel(button, panel);
} else {
this.hidePanel(button, panel);
}
this.saveState();
}
initCallbacks(): void {
this.initListeners();
const optionsChange = _.debounce(e => {
this.onOptionsChange($(e.target).val() as string);
}, 800);
const execArgsChange = _.debounce(e => {
this.onExecArgsChange($(e.target).val() as string);
}, 800);
const execStdinChange = _.debounce(e => {
this.onExecStdinChange($(e.target).val() as string);
}, 800);
this.optionsField.on('change', optionsChange).on('keyup', optionsChange);
this.execArgsField.on('change', execArgsChange).on('keyup', execArgsChange);
this.execStdinField.on('change', execStdinChange).on('keyup', execStdinChange);
this.compileClearCache.on('click', () => {
this.hub.compilerService.cache.reset();
this.compile(true);
});
// Dismiss the popover on escape.
$(document).on('keyup.editable', e => {
if (e.which === 27) {
this.libsButton.popover('hide');
}
});
this.toggleCompilation.on('click', () => {
this.togglePanel(this.toggleCompilation, this.panelCompilation);
});
this.toggleArgs.on('click', () => {
this.togglePanel(this.toggleArgs, this.panelArgs);
});
this.toggleStdin.on('click', () => {
this.togglePanel(this.toggleStdin, this.panelStdin);
});
this.toggleCompilerOut.on('click', () => {
this.togglePanel(this.toggleCompilerOut, this.compilerOutputSection);
});
this.triggerCompilationButton.on('click', () => {
this.compile(true);
});
// Dismiss on any click that isn't either in the opening element, inside
// the popover or on any alert
$(document).on('click', e => {
const elem = this.libsButton;
const target = $(e.target);
// @ts-expect-error: JQuery types are again wrong
if (!target.is(elem) && elem.has(target).length === 0 && target.closest('.popover').length === 0) {
elem.popover('hide');
}
});
this.eventHub.on('initialised', this.undefer, this);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (MutationObserver !== undefined) {
new MutationObserver(_.bind(this.resize, this)).observe(this.execStdinField[0], {
attributes: true,
attributeFilter: ['style'],
});
}
}
shouldEmitExecutionOnFieldChange(): boolean {
return this.settings.executorCompileOnChange;
}
onOptionsChange(options: string): void {
this.options = options;
this.saveState();
if (this.shouldEmitExecutionOnFieldChange()) {
this.compile();
}
}
onExecArgsChange(args: string): void {
this.executionArguments = args;
this.saveState();
if (this.shouldEmitExecutionOnFieldChange()) {
this.compile();
}
}
onExecStdinChange(newStdin: string): void {
this.executionStdin = newStdin;
this.saveState();
if (this.shouldEmitExecutionOnFieldChange()) {
this.compile();
}
}
onRequestCompilation(editorId: number | boolean, treeId: number | boolean): void {
if (editorId === this.sourceEditorId || (treeId && treeId === this.sourceTreeId)) {
this.compile();
}
}
updateCompilerInfo(): void {
this.updateCompilerName();
if (this.compiler) {
if (this.compiler.notification) {
this.alertSystem.notify(this.compiler.notification, {
group: 'compilerwarning',
alertClass: 'notification-info',
dismissTime: 5000,
});
}
this.prependOptions.data('content', this.compiler.options);
}
this.sendExecutor();
}
updateCompilerUI(): void {
this.updateCompilerInfo();
// Resize in case the new compiler name is too big
this.resize();
}
onCompilerChange(value: string): void {
this.compiler = this.hub.compilerService.findCompiler(this.currentLangId, value);
this.updateLibraries();
this.saveState();
this.compile();
this.updateCompilerUI();
}
onToggleWrapChange(): void {
const state = this.currentState();
this.contentRoot.toggleClass('wrap', state.wrap);
this.wrapButton.prop('title', '[' + (state.wrap ? 'ON' : 'OFF') + '] ' + this.wrapTitle);
this.saveState();
}
sendExecutor(): void {
this.eventHub.emit(
'executor',
this.id,
this.compiler,
this.options,
this.sourceEditorId ?? false,
this.sourceTreeId ?? false,
);
}
onEditorClose(editor: number): void {
if (editor === this.sourceEditorId) {
// 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(function (self) {
self.container.close();
}, this);
}
}
currentState(): ExecutorState & PaneState {
const state: ExecutorState & PaneState = {
id: this.id,
compilerName: '',
compiler: this.compiler ? this.compiler.id : '',
source: this.sourceEditorId ?? undefined,
tree: this.sourceTreeId ?? undefined,
options: this.options,
execArgs: this.executionArguments,
execStdin: this.executionStdin,
libs: this.libsWidget?.get(),
lang: this.currentLangId,
compilationPanelShown: !this.panelCompilation.hasClass('d-none'),
compilerOutShown: !this.compilerOutputSection.hasClass('d-none'),
argsPanelShown: !this.panelArgs.hasClass('d-none'),
stdinPanelShown: !this.panelStdin.hasClass('d-none'),
wrap: this.toggleWrapButton.get().wrap,
};
this.paneRenaming.addState(state);
this.fontScale.addState(state);
return state;
}
saveState(): void {
this.container.setState(this.currentState());
}
getCompilerName(): string {
return this.compiler ? this.compiler.name : 'No compiler set';
}
getLanguageName(): string {
const lang = this.currentLangId ? (options.languages[this.currentLangId] as Language | undefined) : undefined;
return lang ? lang.name : '?';
}
getLinkHint(): string {
if (this.sourceTreeId) {
return 'Tree #' + this.sourceTreeId;
} else {
return 'Editor #' + this.sourceEditorId;
}
}
override getPaneName(): string {
const langName = this.getLanguageName();
const compName = this.getCompilerName();
return 'Executor ' + compName + ' (' + langName + ', ' + this.getLinkHint() + ')';
}
override updateTitle(): void {
const name = this.paneName ? this.paneName : this.getPaneName();
this.container.setTitle(_.escape(name));
}
updateCompilerName() {
this.updateTitle();
const compilerName = this.getCompilerName();
const compilerVersion = this.compiler?.version ?? '';
const compilerFullVersion = this.compiler?.fullVersion ?? compilerVersion;
const compilerNotification = this.compiler?.notification ?? '';
this.shortCompilerName.text(compilerName);
this.setCompilerVersionPopover(
{
version: compilerVersion,
fullVersion: compilerFullVersion,
},
compilerNotification,
);
}
setCompilationOptionsPopover(content: string | null) {
this.prependOptions.popover('dispose');
this.prependOptions.popover({
content: content || 'No options in use',
template:
'<div class="popover' +
(content ? ' compiler-options-popover' : '') +
'" role="tooltip"><div class="arrow"></div>' +
'<h3 class="popover-header"></h3><div class="popover-body"></div></div>',
});
}
setCompilerVersionPopover(version?: {fullVersion?: string; version: string}, notification?: string) {
this.fullCompilerName.popover('dispose');
// `notification` contains HTML from a config file, so is 'safe'.
// `version` comes from compiler output, so isn't, and is escaped.
const bodyContent = $('<div>');
const versionContent = $('<div>').html(_.escape(version?.version ?? ''));
bodyContent.append(versionContent);
if (version?.fullVersion) {
const hiddenSection = $('<div>');
const hiddenVersionText = $('<div>').html(_.escape(version.fullVersion)).hide();
const clickToExpandContent = $('<a>')
.attr('href', 'javascript:;')
.text('Toggle full version output')
.on('click', () => {
versionContent.toggle();
hiddenVersionText.toggle();
this.fullCompilerName.popover('update');
});
hiddenSection.append(hiddenVersionText).append(clickToExpandContent);
bodyContent.append(hiddenSection);
}
this.fullCompilerName.popover({
html: true,
title: notification
? ($.parseHTML('<span>Compiler Version: ' + notification + '</span>')[0] as any)
: 'Full compiler version',
content: bodyContent,
template:
'<div class="popover' +
(version ? ' compiler-options-popover' : '') +
'" role="tooltip">' +
'<div class="arrow"></div>' +
'<h3 class="popover-header"></h3><div class="popover-body"></div>' +
'</div>',
});
}
override onSettingsChange(newSettings: SiteSettings): void {
this.settings = _.clone(newSettings);
}
private ariaLabel(status: CompilationStatus): string {
// Compiling...
if (status.code === 4) return 'Compiling';
if (status.didExecute) {
return 'Program compiled & executed';
} else {
return 'Program could not be executed';
}
}
private color(status: CompilationStatus) {
// Compiling...
if (status.code === 4) return '#888888';
if (status.didExecute) return '#12BB12';
return '#FF1212';
}
handleCompilationStatus(status: CompilationStatus): void {
// We want to do some custom styles for the icon, so we don't pass it here and instead do it later
CompilerService.handleCompilationStatus(this.statusLabel, null, {compilerOut: 0, ...status});
if (this.statusIcon != null) {
this.statusIcon
.removeClass()
.addClass('status-icon fas')
.css('color', this.color(status))
.toggle(status.code !== 0)
.prop('aria-label', this.ariaLabel(status))
.prop('data-status', status.code)
.toggleClass('fa-spinner fa-spin', status.code === 4)
.toggleClass('fa-times-circle', status.code !== 4 && !status.didExecute)
.toggleClass('fa-check-circle', status.code !== 4 && status.didExecute);
}
}
updateLibraries(): void {
if (this.libsWidget) {
let filteredLibraries: LanguageLibs = {};
if (this.compiler) {
filteredLibraries = LibUtils.getSupportedLibraries(
this.compiler.libsArr,
this.currentLangId || '',
this.compiler.remote ?? null,
);
}
this.libsWidget.setNewLangId(this.currentLangId, this.compiler?.id ?? '', filteredLibraries);
}
}
onLanguageChange(editorId: number | boolean, newLangId: string): void {
if (this.sourceEditorId === editorId && this.currentLangId) {
const oldLangId = this.currentLangId;
this.currentLangId = newLangId;
// Store the current selected stuff to come back to it later in the same session (Not state stored!)
this.infoByLang[oldLangId] = {
compiler: this.compiler && this.compiler.id ? this.compiler.id : options.defaultCompiler[oldLangId],
options: this.options,
execArgs: this.executionArguments,
execStdin: this.executionStdin,
};
const info = this.infoByLang[this.currentLangId];
this.initLangAndCompiler({compilerName: '', id: 0, lang: newLangId, compiler: info?.compiler ?? ''});
this.updateCompilersSelector(info);
this.updateCompilerUI();
this.saveState();
}
}
getCurrentLangCompilers(): CompilerInfo[] {
const allCompilers: Record<string, CompilerInfo> | undefined = this.hub.compilerService.getCompilersForLang(
this.currentLangId,
);
if (!allCompilers) return [];
const hasAtLeastOneExecuteSupported = Object.values(allCompilers).some(compiler => {
return compiler.supportsExecute !== false;
});
if (!hasAtLeastOneExecuteSupported) {
this.compiler = null;
return [];
}
return Object.values(allCompilers).filter(compiler => {
return (
(compiler.hidden !== true && compiler.supportsExecute !== false) ||
(this.compiler && compiler.id === this.compiler.id)
);
});
}
updateCompilersSelector(info: LangInfo | undefined): void {
this.compilerPicker.update(this.currentLangId, this.compiler?.id ?? '');
this.options = info?.options || '';
this.optionsField.val(this.options);
this.executionArguments = info?.execArgs || '';
this.execArgsField.val(this.executionArguments);
this.executionStdin = info?.execStdin || '';
this.execStdinField.val(this.executionStdin);
}
getDefaultPaneName(): string {
return '';
}
onCompileResult(compilerId: number, compiler: CompilerInfo, result: CompilationResult): void {}
onCompiler(compilerId: number, compiler: CompilerInfo, options: string, editorId: number, treeId: number): void {}
registerOpeningAnalyticsEvent(): void {
ga.proxy('send', {
hitType: 'event',
eventCategory: 'OpenViewPane',
eventAction: 'Executor',
});
}
}