blob: e566458aaac4f8fe5b00c1500fec63ae451aa111 [file] [log] [blame] [raw]
// 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 $ from 'jquery';
import {pluck} from 'underscore';
import {ga} from '../analytics';
import {sortedList, HistoryEntry, EditorSource} from '../history';
import {editor} from 'monaco-editor';
import IStandaloneDiffEditor = editor.IStandaloneDiffEditor;
import ITextModel = editor.ITextModel;
import {unwrap} from '../assert';
export class HistoryDiffState {
public model: ITextModel;
private result: EditorSource[];
constructor(model: ITextModel) {
this.model = model;
this.result = [];
}
update(result: HistoryEntry) {
this.result = result.sources;
this.refresh();
return true;
}
private refresh() {
const output = this.result;
const content = output.map(val => `/****** ${val.lang} ******/\n${val.source}`).join('\n');
this.model.setValue(content);
}
}
type Entry = {dt: number; name: string; load: () => void};
export class HistoryWidget {
private modal: JQuery | null;
private diffEditor: IStandaloneDiffEditor | null;
private lhs: HistoryDiffState | null;
private rhs: HistoryDiffState | null;
private currentList: HistoryEntry[];
private onLoadCallback: (data: HistoryEntry) => void;
constructor() {
this.modal = null;
this.diffEditor = null;
this.lhs = null;
this.rhs = null;
this.currentList = [];
this.onLoadCallback = () => {};
}
private initializeIfNeeded() {
if (this.modal === null) {
this.modal = $('#history');
const placeholder = this.modal.find('.monaco-placeholder');
this.diffEditor = editor.createDiffEditor(placeholder[0], {
fontFamily: 'Consolas, "Liberation Mono", Courier, monospace',
scrollBeyondLastLine: true,
readOnly: true,
// language: 'c++',
minimap: {
enabled: true,
},
});
this.lhs = new HistoryDiffState(editor.createModel('', 'c++'));
this.rhs = new HistoryDiffState(editor.createModel('', 'c++'));
this.diffEditor.setModel({original: this.lhs.model, modified: this.rhs.model});
this.modal.find('.inline-diff-checkbox').on('click', event => {
const inline = $(event.target).prop('checked');
this.diffEditor?.updateOptions({
renderSideBySide: !inline,
});
this.resizeLayout();
});
}
}
private static getLanguagesFromHistoryEntry(entry: HistoryEntry) {
return pluck(entry.sources, 'lang');
}
private populateFromLocalStorage() {
this.currentList = sortedList();
this.populate(
unwrap(this.modal).find('.historiccode'),
this.currentList.map((data): Entry => {
const dt = new Date(data.dt).toString();
const languages = HistoryWidget.getLanguagesFromHistoryEntry(data).join(', ');
return {
dt: data.dt,
name: `${dt.replace(/\s\(.*\)/, '')} (${languages})`,
load: () => {
this.onLoad(data);
this.modal?.modal('hide');
},
};
})
);
}
private hideRadiosAndSetDiff() {
const root = unwrap(this.modal).find('.historiccode');
const items = root.find('li:not(.template)');
let foundbase = false;
let foundcomp = false;
for (const elem of items) {
const li = $(elem);
const dt = li.data('dt');
const base = li.find('.base');
const comp = li.find('.comp');
let baseShouldBeVisible = true;
let compShouldBeVisible = true;
if (comp.prop('checked')) {
foundcomp = true;
baseShouldBeVisible = false;
const itemRight = this.currentList.find(item => item.dt === dt);
if (itemRight) {
unwrap(this.rhs).update(itemRight);
}
} else if (base.prop('checked')) {
foundbase = true;
const itemLeft = this.currentList.find(item => item.dt === dt);
if (itemLeft) {
unwrap(this.lhs).update(itemLeft);
}
}
if (foundbase && foundcomp) {
compShouldBeVisible = false;
} else if (!foundbase && !foundcomp) {
baseShouldBeVisible = false;
}
if (compShouldBeVisible) {
comp.css('visibility', '');
} else {
comp.css('visibility', 'hidden');
}
if (baseShouldBeVisible) {
base.css('visibility', '');
} else {
base.css('visibility', 'hidden');
}
}
}
private populate(root: JQuery, list: Entry[]) {
root.find('li:not(.template)').remove();
const template = root.find('.template');
let baseMarked = false;
let compMarked = false;
for (const elem of list) {
const li = template.clone().removeClass('template').appendTo(root);
li.data('dt', elem.dt);
const base = li.find('.base');
const comp = li.find('.comp');
if (!compMarked) {
comp.prop('checked', 'checked');
compMarked = true;
} else if (!baseMarked) {
base.prop('checked', 'checked');
baseMarked = true;
}
base.on('click', () => this.hideRadiosAndSetDiff());
comp.on('click', () => this.hideRadiosAndSetDiff());
li.find('a').text(elem.name).on('click', elem.load);
}
this.hideRadiosAndSetDiff();
}
private resizeLayout() {
const tabcontent = unwrap(this.modal).find('div.tab-content');
this.diffEditor?.layout({
width: unwrap(tabcontent.width()),
height: unwrap(tabcontent.height()) - 20,
});
}
private onLoad(data: HistoryEntry) {
this.onLoadCallback(data);
}
run(onLoad: (data: HistoryEntry) => void) {
this.initializeIfNeeded();
this.populateFromLocalStorage();
this.onLoadCallback = onLoad;
// It can't tell that we initialize modal on initializeIfNeeded, so it sticks to the possibility of it being null
unwrap(this.modal).on('shown.bs.modal', () => this.resizeLayout());
unwrap(this.modal).modal();
ga.proxy('send', {
hitType: 'event',
eventCategory: 'OpenModalPane',
eventAction: 'History',
});
}
}