// 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.js'; import {sortedList, HistoryEntry, EditorSource} from '../history.js'; import {editor} from 'monaco-editor'; import IStandaloneDiffEditor = editor.IStandaloneDiffEditor; import ITextModel = editor.ITextModel; import {unwrap} from '../assert.js'; 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', }); } }