Files
compiler-explorer/static/panes/executor.ts
Jeremy Rifkin 55dc09017e Compiler Picker UI Improvements (#4849)
Alrighty, round 2!

This PR makes the compiler picker dropdown taller (max height is to the
bottom of the screen)

![image](https://user-images.githubusercontent.com/51220084/224517409-17706ba6-fe85-444a-ad3b-f244f66eea5b.png)

I've added a toolbar button for opening the popout as well as a button
when the dropdown is open:


![image](https://user-images.githubusercontent.com/51220084/224517304-6478fca5-7bc1-4f87-b2ae-e9091d7d48bc.png)

![image](https://user-images.githubusercontent.com/51220084/224517317-83468805-1e9d-4bc6-8a4f-8786325f1e7b.png)

Then the modal interface allows text searching, filtering by compiler
category, and filtering by compiler instruction set. In the future I'd
like to replace the instructionSet property with an architecture
property so the filtering here can be a little better.


![image](https://user-images.githubusercontent.com/51220084/224517348-9cf40866-c5af-4274-84ba-21eb640ab2ea.png)

Demo of the category filtering:


![image](https://user-images.githubusercontent.com/51220084/224517529-5370b3f9-9ad3-4340-838a-b2d0a47c7467.png)

The text filtering highlights results the same as tomselect does:


![image](https://user-images.githubusercontent.com/51220084/224517490-d0dfa6d0-9b61-4be4-8cc7-ca4a4729a532.png)

I moved the favorites permanently to their own column so compilers
aren't jarringly shifted around the screen when favoriting /
unfavoriting.

Also added media queries so the modal looks ok on smaller screens:


![image](https://user-images.githubusercontent.com/51220084/224517563-bdf409c1-7e3a-4c73-812b-f02179b55799.png)


![image](https://user-images.githubusercontent.com/51220084/224517574-07fa2c8b-67d2-496d-9174-b1343e22bd62.png)

I think the only other two changes from the first PR are now the active
compiler is highlighted like it is in the tomselect dropdown and I
figured out how to reliably focus the search bar on modal open. (Oh, and
now clicking a compiler actually does something!)

---------

Co-authored-by: Matt Godbolt <matt@godbolt.org>
2023-03-12 17:07:44 -04:00

1379 lines
52 KiB
TypeScript

// 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.destroy();
}
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.clear();
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',
});
}
}