Skip to content

Commit 1de61c5

Browse files
committed
Switch diff viewers to single viewer
Render the diff viewers for a submission as if it was a single IDE, in which you can select the submission to diff against. We move the button interface out of the tabs and create functions to update the state of individual enditors.
1 parent b70be33 commit 1de61c5

File tree

5 files changed

+244
-168
lines changed

5 files changed

+244
-168
lines changed

webapp/public/js/domjudge.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,3 +1290,170 @@ function initScoreboardSubmissions() {
12901290
});
12911291
});
12921292
}
1293+
1294+
const editors = [];
1295+
function initDiffEditor(editorId) {
1296+
const wrapper = $(`#${editorId}-wrapper`);
1297+
1298+
// TODO: store and restore tag preference in local storage.
1299+
const initialSelect = "";
1300+
const select = wrapper.find(".diff-select");
1301+
select[0].selectedIndex = 0;
1302+
1303+
const initialDiffMode = getDiffMode();
1304+
const radios = wrapper.find(`.diff-mode > input[type='radio']`);
1305+
radios.each((_, radio) => {
1306+
radio.checked = radio.value === initialDiffMode
1307+
});
1308+
1309+
const download = wrapper.find(".download")[0];
1310+
const edit = wrapper.find(".edit")[0];
1311+
const updateTabRank = (rank) => {
1312+
if (rank) {
1313+
let url = new URL(download.href);
1314+
url.searchParams.set("fetch", rank);
1315+
download.href = url;
1316+
1317+
url = new URL(edit.href);
1318+
url.searchParams.set("rank", rank);
1319+
edit.href = url;
1320+
} else {
1321+
download.href = "#";
1322+
edit.href = "#";
1323+
}
1324+
};
1325+
wrapper.find(".nav").on('show.bs.tab', (e) => {
1326+
updateTabRank(e.target.dataset.rank);
1327+
})
1328+
1329+
const editor = {
1330+
'getDiffMode': () => {
1331+
for (let radio of radios) {
1332+
if (radio.checked) {
1333+
return radio.value;
1334+
}
1335+
}
1336+
},
1337+
'getDiffSelection': () => {
1338+
let s = select[0];
1339+
return s.options[s.selectedIndex].value;
1340+
},
1341+
'updateIcon': (rank, icon) => {
1342+
const element = wrapper.find(".nav-link[data-rank]")[rank].querySelector('.fa-fw');
1343+
element.className = 'fas fa-fw fa-' + icon;
1344+
},
1345+
'onDiffModeChange': (f) => {
1346+
radios.change((e) => {
1347+
const diffMode = e.target.value;
1348+
f(diffMode);
1349+
});
1350+
},
1351+
'onDiffSelectChange': (f) => {
1352+
select.change((e) => {
1353+
const submitId = e.target.value;
1354+
const noDiff = submitId === "";
1355+
f(submitId, noDiff);
1356+
});
1357+
}
1358+
};
1359+
editors[editorId] = editor;
1360+
1361+
const updateMode = (diffMode) => {
1362+
setDiffMode(diffMode);
1363+
};
1364+
updateMode(initialDiffMode);
1365+
editor.onDiffModeChange(updateMode);
1366+
1367+
const updateSelect = (submitId, noDiff) => {
1368+
radios.each((_, radio) => {
1369+
radio.disabled = noDiff;
1370+
});
1371+
// TODO: add tab panes for deleted source files.
1372+
};
1373+
updateSelect("", true);
1374+
editor.onDiffSelectChange(updateSelect);
1375+
}
1376+
1377+
function initDiffEditorTab(editorId, diffId, rank, models, modifiedModel) {
1378+
const empty = monaco.editor.getModel(monaco.Uri.file("empty")) ?? monaco.editor.createModel("", undefined, monaco.Uri.file("empty"));
1379+
1380+
const diffEditor = monaco.editor.createDiffEditor(
1381+
document.getElementById(diffId), {
1382+
scrollbar: {
1383+
alwaysConsumeMouseWheel: false,
1384+
vertical: 'auto',
1385+
horizontal: 'auto'
1386+
},
1387+
scrollBeyondLastLine: false,
1388+
automaticLayout: true,
1389+
readOnly: true,
1390+
theme: getCurrentEditorTheme(),
1391+
});
1392+
1393+
const updateSelect = (submitId, noDiff) => {
1394+
if (!noDiff) {
1395+
const model = models[submitId];
1396+
if (model === undefined) {
1397+
models[submitId] = {'model': empty};
1398+
} else if (model !== undefined && !model['model']) {
1399+
// TODO: show source code instead of diff to empty file?
1400+
model['model'] = monaco.editor.createModel(model['source'], undefined, monaco.Uri.file("test/" + submitId + "/" + model['filename']));
1401+
}
1402+
}
1403+
1404+
diffEditor.updateOptions({
1405+
renderOverviewRuler: !noDiff,
1406+
});
1407+
if (noDiff) {
1408+
diffEditor.updateOptions({
1409+
renderSideBySide: false,
1410+
});
1411+
} else {
1412+
// Reset the diff mode to the currently selected mode.
1413+
updateMode(editors[editorId].getDiffMode())
1414+
}
1415+
// TODO: handle single-file submission case with renamed file.
1416+
const oldViewState = diffEditor.saveViewState();
1417+
diffEditor.setModel({
1418+
original: noDiff ? modifiedModel : models[submitId]['model'],
1419+
modified: modifiedModel,
1420+
});
1421+
diffEditor.restoreViewState(oldViewState);
1422+
1423+
diffEditor.getOriginalEditor().updateOptions({
1424+
lineNumbers: !noDiff,
1425+
});
1426+
diffEditor.getModifiedEditor().updateOptions({
1427+
minimap: {
1428+
enabled: noDiff,
1429+
},
1430+
})
1431+
};
1432+
editors[editorId].onDiffSelectChange(updateSelect);
1433+
updateSelect("", true);
1434+
1435+
const updateIcon = () => {
1436+
const noDiff = editors[editorId].getDiffSelection() === "";
1437+
if (noDiff) {
1438+
editors[editorId].updateIcon(rank, 'file');
1439+
return;
1440+
}
1441+
1442+
const lineChanges = diffEditor.getLineChanges();
1443+
if (diffEditor.getModel().original == empty) {
1444+
editors[editorId].updateIcon(rank, 'file-circle-plus');
1445+
} else if (lineChanges !== null && lineChanges.length > 0) {
1446+
editors[editorId].updateIcon(rank, 'file-circle-exclamation');
1447+
} else {
1448+
editors[editorId].updateIcon(rank, 'file-circle-check');
1449+
}
1450+
}
1451+
diffEditor.onDidUpdateDiff(updateIcon);
1452+
1453+
const updateMode = (diffMode) => {
1454+
diffEditor.updateOptions({
1455+
renderSideBySide: diffMode === 'side-by-side',
1456+
});
1457+
};
1458+
editors[editorId].onDiffModeChange(updateMode);
1459+
}

webapp/src/Controller/Jury/SubmissionController.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -909,10 +909,6 @@ public function sourceAction(
909909
$otherFiles[$submitId][] = $f;
910910
}
911911

912-
foreach ($otherFiles as $s => $v) {
913-
$otherFiles[$s] = $this->determineFileChanged($files, $v);
914-
}
915-
916912
return $this->render('jury/submission_source.html.twig', [
917913
'submission' => $submission,
918914
'files' => $files,

webapp/src/Twig/TwigExtension.php

Lines changed: 30 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Symfony\Component\Routing\RouterInterface;
3535
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
3636
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
37+
use Symfony\Component\Serializer\SerializerInterface;
3738
use Twig\Environment;
3839
use Twig\Extension\AbstractExtension;
3940
use Twig\Extension\GlobalsInterface;
@@ -57,6 +58,7 @@ public function __construct(
5758
protected readonly TokenStorageInterface $tokenStorage,
5859
protected readonly AuthorizationCheckerInterface $authorizationChecker,
5960
protected readonly RouterInterface $router,
61+
protected readonly SerializerInterface $serializer,
6062
#[Autowire('%kernel.project_dir%')]
6163
protected readonly string $projectDir,
6264
protected array $renderedSources = []
@@ -177,7 +179,6 @@ public function getGlobals(): array
177179
'hc-black' => ['name' => 'High contrast (dark)'],
178180
],
179181
'diff_modes' => [
180-
'no-diff' => ["name" => "No diff"],
181182
'side-by-side' => ["name" => "Side-by-side"],
182183
'inline' => ["name" => "Inline"],
183184
],
@@ -954,71 +955,44 @@ public function getMonacoModel(SubmissionFile $file): string
954955
);
955956
}
956957

957-
public function showDiff(string $id, SubmissionFile $newFile, SubmissionFile $oldFile): string
958+
/** @param array<int, SubmissionFile[]> $otherFiles */
959+
public function showDiff(string $editorId, string $diffId, SubmissionFile $newFile, array $otherFiles): string
958960
{
959961
$editor = <<<HTML
960-
<div class="editor" id="__EDITOR__"></div>
962+
<div class="editor" id="$diffId"></div>
961963
<script>
962964
$(function() {
963-
require(['vs/editor/editor.main'], function () {
964-
const originalModel = %s
965-
const modifiedModel = %s
966-
967-
const initialDiffMode = getDiffMode();
968-
const radios = $("#diffselect-__EDITOR__ > input[name='__EDITOR__-mode']");
969-
radios.each((_, radio) => {
970-
$(radio).prop('checked', radio.value === initialDiffMode);
971-
});
972-
973-
const diffEditor = monaco.editor.createDiffEditor(
974-
document.getElementById("__EDITOR__"), {
975-
scrollbar: {
976-
alwaysConsumeMouseWheel: false,
977-
vertical: 'auto',
978-
horizontal: 'auto'
979-
},
980-
scrollBeyondLastLine: false,
981-
automaticLayout: true,
982-
readOnly: true,
983-
theme: getCurrentEditorTheme(),
984-
});
985-
986-
const updateMode = (diffMode) => {
987-
setDiffMode(diffMode);
988-
const noDiff = diffMode === 'no-diff';
989-
diffEditor.updateOptions({
990-
renderOverviewRuler: !noDiff,
991-
renderSideBySide: diffMode === 'side-by-side',
992-
});
993-
994-
const oldViewState = diffEditor.saveViewState();
995-
diffEditor.setModel({
996-
original: noDiff ? modifiedModel : originalModel,
997-
modified: modifiedModel,
998-
});
999-
diffEditor.restoreViewState(oldViewState);
1000-
1001-
diffEditor.getOriginalEditor().updateOptions({
1002-
lineNumbers: !noDiff,
1003-
});
1004-
diffEditor.getModifiedEditor().updateOptions({
1005-
minimap: {
1006-
enabled: noDiff,
1007-
},
1008-
})
1009-
};
1010-
radios.change((e) => {
1011-
updateMode(e.target.value);
1012-
});
1013-
updateMode(initialDiffMode);
965+
const editorId = '%s';
966+
const diffId = '%s';
967+
const rank = %d;
968+
const models = %s;
969+
require(['vs/editor/editor.main'], () => {
970+
const modifiedModel = %s;
971+
initDiffEditorTab(editorId, diffId, rank, models, modifiedModel)
1014972
});
1015973
});
1016974
</script>
1017975
HTML;
1018976

977+
$others = [];
978+
foreach ($otherFiles as $submissionId => $files) {
979+
foreach ($files as $f) {
980+
if ($f->getFilename() == $newFile->getFilename()) {
981+
// TODO: add `tag` containing `previous` / `original`
982+
$others[$submissionId] = [
983+
'filename' => $f->getFilename(),
984+
'source' => $f->getSourcecode(),
985+
];
986+
}
987+
}
988+
}
989+
1019990
return sprintf(
1020-
str_replace('__EDITOR__', $id, $editor),
1021-
$this->getMonacoModel($oldFile),
991+
$editor,
992+
$editorId,
993+
$diffId,
994+
$newFile->getRank(),
995+
$this->serializer->serialize($others, 'json'),
1022996
$this->getMonacoModel($newFile),
1023997
);
1024998
}

0 commit comments

Comments
 (0)