Skip to content

Commit 60927d8

Browse files
committed
Implement jumping to problem
1 parent b0ad058 commit 60927d8

4 files changed

Lines changed: 183 additions & 12 deletions

File tree

src/snapshots-app/client/bundles/components/submission/tabs/timeline/BasicFileViewer.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ import Editor from "@monaco-editor/react";
44

55
import "./FileViewer.css";
66

7-
function BasicFileViewer({ code, language, lightMode }) {
7+
function BasicFileViewer({ code, language, lightMode, editorRef }) {
8+
89

910
return (
1011
<>
1112
<Editor
13+
onMount={(editor) => {
14+
editorRef.current = editor;
15+
}}
1216
defaultLanguage={language}
1317
defaultValue={code}
1418
theme={lightMode ? "light" : "vs-dark"}

src/snapshots-app/client/bundles/components/submission/tabs/timeline/DiffViewer.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef } from "react";
1+
import React from "react";
22

33
import Dialog from "@mui/material/Dialog";
44
import DialogContent from "@mui/material/DialogContent";
@@ -17,12 +17,12 @@ import "@git-diff-view/react/styles/diff-view.css";
1717
function DiffViewer({
1818
open,
1919
onClose,
20+
editorRef,
2021
prevFileContents,
2122
currentFileContents,
2223
lightMode = false,
2324
renderSideBySide = false,
2425
}) {
25-
const editorRef = useRef(null);
2626

2727
const onDiffEditorMount = (editor, monaco) => {
2828
editorRef.current = editor;

src/snapshots-app/client/bundles/components/submission/tabs/timeline/Graphs.jsx

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
66
import { Tooltip } from "@mui/material";
77
import Typography from "@mui/material/Typography";
88
import Box from "@mui/material/Box";
9+
import { Link } from '@mui/material';
10+
import { List, ListItem } from '@mui/material';
11+
912

1013
function LinearProgressWithLabel(props) {
1114
return (
@@ -27,7 +30,68 @@ function LinearProgressWithLabel(props) {
2730
);
2831
}
2932

30-
function AssignmentProblems({ history, allProblemDisplayNames, numSolved }) {
33+
const ProblemJumpLink = ({ lineNumber, editorRef, label }) => {
34+
const handleJump = (event) => {
35+
// Prevent default link behavior if necessary
36+
event.preventDefault();
37+
38+
const editorInstance = editorRef.current;
39+
if (!editorInstance) return;
40+
41+
const targetEditor = editorInstance.getModifiedEditor
42+
? editorInstance.getModifiedEditor()
43+
: editorInstance;
44+
45+
targetEditor.revealLineInCenter(lineNumber);
46+
targetEditor.setPosition({ lineNumber: lineNumber, column: 1 });
47+
targetEditor.focus();
48+
};
49+
50+
return (
51+
<Link
52+
component="button"
53+
variant="body2"
54+
onClick={handleJump}
55+
sx={{
56+
textAlign: 'left',
57+
verticalAlign: 'baseline',
58+
textDecoration: 'none',
59+
'&:hover': {
60+
textDecoration: 'underline',
61+
},
62+
cursor: 'pointer',
63+
color: 'primary.main',
64+
fontWeight: 500,
65+
}}
66+
>
67+
{label || `Line ${lineNumber}`}
68+
</Link>
69+
);
70+
};
71+
72+
73+
function AssignmentProblems({
74+
history,
75+
allProblemDisplayNames,
76+
numSolved,
77+
editorRef,
78+
problemLines,
79+
}) {
80+
function goToLine(lineNumber) {
81+
const editor = editorRef.current;
82+
if (!editor) return;
83+
84+
// 1. Check if it's a Diff Editor (has getModifiedEditor method)
85+
// 2. Otherwise treat as a standard editor
86+
const targetEditor = editor.getModifiedEditor
87+
? editor.getModifiedEditor()
88+
: editor;
89+
90+
targetEditor.revealLineInCenter(lineNumber);
91+
targetEditor.setPosition({ lineNumber: lineNumber, column: 1 });
92+
targetEditor.focus();
93+
}
94+
3195
function getIcon(problemDisplayName) {
3296
const problemData = history.find(
3397
(p) => p.display_name === problemDisplayName,
@@ -51,11 +115,61 @@ function AssignmentProblems({ history, allProblemDisplayNames, numSolved }) {
51115
return (numSolved / allProblemDisplayNames.length) * 100;
52116
}
53117

54-
const problems = allProblemDisplayNames.map((problemDisplayName) => (
55-
<div>
56-
{getIcon(problemDisplayName)} {problemDisplayName}
57-
</div>
58-
));
118+
119+
// TODO span styling and improve accessibility?
120+
const problems = allProblemDisplayNames.map((problemDisplayName) => {
121+
const lines = problemLines[problemDisplayName];
122+
123+
if (problemLines[problemDisplayName].length === 0) {
124+
return (
125+
<Box key={problemDisplayName} sx={{ display: 'flex', alignItems: 'center', my: 0.5 }}>
126+
{getIcon(problemDisplayName)}
127+
<Typography variant="body2" sx={{ ml: 1, color: 'text.disabled' }}>
128+
{problemDisplayName} (Not found)
129+
</Typography>
130+
</Box>
131+
);
132+
} else if (problemLines[problemDisplayName].length === 1) {
133+
return (
134+
<Box key={problemDisplayName} sx={{ display: 'flex', alignItems: 'center', my: 0.5 }}>
135+
{getIcon(problemDisplayName)}
136+
<Box sx={{ ml: 1 }}>
137+
<ProblemJumpLink
138+
lineNumber={lines[0]}
139+
editorRef={editorRef}
140+
label={problemDisplayName}
141+
/>
142+
</Box>
143+
</Box>
144+
);
145+
} else {
146+
return (
147+
<Box key={problemDisplayName} sx={{ my: 1 }}>
148+
<Box sx={{ display: 'flex', alignItems: 'center' }}>
149+
{getIcon(problemDisplayName)}
150+
<Typography variant="body2" sx={{ ml: 1 }}>
151+
{problemDisplayName}
152+
</Typography>
153+
</Box>
154+
<Box sx={{ pl: 4, display: 'flex', flexDirection: 'column', flexWrap: 'wrap' }}>
155+
156+
<List sx={{ listStyleType: 'disc', pl: '1rem' }}>
157+
{lines.map((lineNumber) => (
158+
<ListItem disablePadding sx={{ display: 'list-item' }}>
159+
<ProblemJumpLink
160+
key={`${problemDisplayName}-${lineNumber}`}
161+
lineNumber={lineNumber}
162+
editorRef={editorRef}
163+
label={`Line ${lineNumber}`}
164+
/>
165+
</ListItem>
166+
))}
167+
</List>
168+
</Box>
169+
</Box>
170+
);
171+
}
172+
});
59173

60174
return (
61175
<div style={{ paddingTop: "1rem", paddingBottom: "1rem" }}>
@@ -71,6 +185,8 @@ function Graphs({
71185
currBackupHistory,
72186
allProblemDisplayNames,
73187
selectedBackup,
188+
problemLines,
189+
editorRef,
74190
}) {
75191
return (
76192
<div>
@@ -79,6 +195,8 @@ function Graphs({
79195
history={currBackupHistory}
80196
allProblemDisplayNames={allProblemDisplayNames}
81197
numSolved={numQuestionsSolved[selectedBackup]}
198+
problemLines={problemLines}
199+
editorRef={editorRef}
82200
/>
83201
</div>
84202
);

src/snapshots-app/client/bundles/components/submission/tabs/timeline/TimelineTab.jsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { useRef } from "react";
22
import { styled } from "@mui/material/styles";
33
import Toolbar from "@mui/material/Toolbar";
44
import Box from "@mui/material/Box";
@@ -91,6 +91,8 @@ function TimelineTab() {
9191
const routeParams = useParams();
9292
const navigate = useNavigate();
9393

94+
const editorRef = useRef(null);
95+
9496
// Fetch backups
9597
React.useEffect(() => {
9698
fetch(
@@ -413,6 +415,43 @@ function TimelineTab() {
413415
return null;
414416
}
415417

418+
function goToLine(lineNumber) {
419+
if (editorRef.current) {
420+
// Centers the line in the viewport
421+
editorRef.current.revealLineInCenter(lineNumber);
422+
423+
// Moves the cursor to that line
424+
editorRef.current.setPosition({ lineNumber: lineNumber, column: 1 });
425+
426+
// Focuses the editor
427+
editorRef.current.focus();
428+
}
429+
}
430+
431+
function getProblemLines(code, problemNames) {
432+
const result = {};
433+
problemNames.forEach((name) => {
434+
result[name] = [];
435+
});
436+
437+
const lines = code.split("\n");
438+
439+
lines.forEach((lineText, index) => {
440+
const trimmedLine = lineText.trim();
441+
442+
if (trimmedLine.startsWith("# BEGIN ")) {
443+
const problemName = trimmedLine.replace("# BEGIN ", "").trim();
444+
445+
// If this name is in our target list, add the line number (1-indexed)
446+
if (result.hasOwnProperty(problemName)) {
447+
result[problemName].push(index + 1);
448+
}
449+
}
450+
});
451+
452+
return result;
453+
}
454+
416455
return (
417456
<Box sx={{ display: "flex", flexDirection: "column", minHeight: "100vh" }}>
418457
{backups.length === 0 ? (
@@ -534,15 +573,22 @@ function TimelineTab() {
534573
{code === "" ? (
535574
<CircularProgress />
536575
) : (
537-
<div style={{ paddingLeft: "1rem", paddingRight: "1rem", height: "100%" }}>
576+
<div
577+
style={{
578+
paddingLeft: "1rem",
579+
paddingRight: "1rem",
580+
height: "100%",
581+
}}
582+
>
538583
{selectedBackup === 0 ||
539584
code === "" ||
540585
prevFileContents === "" ||
541586
prevFileContents === code ? (
542587
<BasicFileViewer
543588
code={code}
544-
language={"python"}
589+
language={getLanguage(file)}
545590
lightMode={lightMode}
591+
editorRef={editorRef}
546592
/>
547593
) : (
548594
<DiffViewer
@@ -553,6 +599,7 @@ function TimelineTab() {
553599
selectedFile={file}
554600
prevFileContents={prevFileContents}
555601
lightMode={lightMode}
602+
editorRef={editorRef}
556603
/>
557604
)}
558605
</div>
@@ -581,6 +628,8 @@ function TimelineTab() {
581628
currBackupHistory={backups[selectedBackup].history}
582629
allProblemDisplayNames={allProblemDisplayNames}
583630
selectedBackup={selectedBackup}
631+
problemLines={getProblemLines(code, allProblemDisplayNames)}
632+
editorRef={editorRef}
584633
/>
585634
)}
586635
</RightSidebar>

0 commit comments

Comments
 (0)