diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java index 914d58672..28445ab36 100644 --- a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java +++ b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java @@ -98,17 +98,30 @@ private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Mat List tokensFirst = comparison.firstSubmission().getTokenList().subList(match.startOfFirst(), match.endOfFirst() + 1); List tokensSecond = comparison.secondSubmission().getTokenList().subList(match.startOfSecond(), match.endOfSecond() + 1); - Comparator lineComparator = Comparator.comparingInt(Token::getLine); + Comparator lineComparator = Comparator.comparingInt(Token::getLine).thenComparingInt(Token::getColumn); Token startOfFirst = tokensFirst.stream().min(lineComparator).orElseThrow(); Token endOfFirst = tokensFirst.stream().max(lineComparator).orElseThrow(); Token startOfSecond = tokensSecond.stream().min(lineComparator).orElseThrow(); Token endOfSecond = tokensSecond.stream().max(lineComparator).orElseThrow(); - return new Match( - FilePathUtil.getRelativeSubmissionPath(startOfFirst.getFile(), comparison.firstSubmission(), submissionToIdFunction).toString(), - FilePathUtil.getRelativeSubmissionPath(startOfSecond.getFile(), comparison.secondSubmission(), submissionToIdFunction).toString(), - startOfFirst.getLine(), endOfFirst.getLine(), startOfSecond.getLine(), endOfSecond.getLine(), match.length()); + String firstFileName = FilePathUtil.getRelativeSubmissionPath(startOfFirst.getFile(), comparison.firstSubmission(), submissionToIdFunction) + .toString(); + String secondFileName = FilePathUtil.getRelativeSubmissionPath(startOfSecond.getFile(), comparison.secondSubmission(), submissionToIdFunction) + .toString(); + + int startLineFirst = startOfFirst.getLine(); + int startColumnFirst = startOfFirst.getColumn(); + int endLineFirst = endOfFirst.getLine(); + int endColumnFirst = endOfFirst.getColumn() + endOfFirst.getLength() - 1; + + int startLineSecond = startOfSecond.getLine(); + int startColumnSecond = startOfSecond.getColumn(); + int endLineSecond = endOfSecond.getLine(); + int endColumnSecond = endOfSecond.getColumn() + endOfSecond.getLength() - 1; + + return new Match(firstFileName, secondFileName, startLineFirst, startColumnFirst, endLineFirst, endColumnFirst, startLineSecond, + startColumnSecond, endLineSecond, endColumnSecond, match.length()); } } diff --git a/core/src/main/java/de/jplag/reporting/reportobject/model/Match.java b/core/src/main/java/de/jplag/reporting/reportobject/model/Match.java index 8af13af20..5ef5d728c 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/model/Match.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/model/Match.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; public record Match(@JsonProperty("file1") String firstFileName, @JsonProperty("file2") String secondFileName, - @JsonProperty("start1") int startInFirst, @JsonProperty("end1") int endInFirst, @JsonProperty("start2") int startInSecond, - @JsonProperty("end2") int endInSecond, @JsonProperty("tokens") int tokens) { + @JsonProperty("start1") int startInFirst, @JsonProperty("start1_col") int startColumnInFirst, @JsonProperty("end1") int endInFirst, + @JsonProperty("end1_col") int endColumnInFirst, @JsonProperty("start2") int startInSecond, + @JsonProperty("start2_col") int startColumnInSecond, @JsonProperty("end2") int endInSecond, @JsonProperty("end2_col") int endColumnInSecond, + @JsonProperty("tokens") int tokens) { } diff --git a/report-viewer/src/components/fileDisplaying/CodeLine.vue b/report-viewer/src/components/fileDisplaying/CodeLine.vue new file mode 100644 index 000000000..6d2b2ab99 --- /dev/null +++ b/report-viewer/src/components/fileDisplaying/CodeLine.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/report-viewer/src/components/fileDisplaying/CodePanel.vue b/report-viewer/src/components/fileDisplaying/CodePanel.vue index 506936f51..75010c1ea 100644 --- a/report-viewer/src/components/fileDisplaying/CodePanel.vue +++ b/report-viewer/src/components/fileDisplaying/CodePanel.vue @@ -22,39 +22,31 @@
- +
+ {{ index + 1 }} +
- - - - - - -
{{ index + 1 }}
+ ref="lineRefs" + :line="line.line" + :lineNumber="index + 1" + :matches="line.matches" + @matchSelected="(match) => matchSelected(match)" + /> +
Empty File @@ -68,12 +60,12 @@ import type { MatchInSingleFile } from '@/model/MatchInSingleFile' import { ref, nextTick, type PropType, computed, type Ref } from 'vue' import Interactable from '../InteractableComponent.vue' -import type { Match } from '@/model/Match' import type { SubmissionFile } from '@/model/File' import { highlight } from '@/utils/CodeHighlighter' import type { Language } from '@/model/Language' -import { getMatchColor } from '@/utils/ColorUtils' import ToolTipComponent from '../ToolTipComponent.vue' +import CodeLine from './CodeLine.vue' +import type { Match } from '@/model/Match' const props = defineProps({ /** @@ -99,24 +91,22 @@ const props = defineProps({ } }) -const emit = defineEmits(['lineSelected']) +const emit = defineEmits(['matchSelected']) const collapsed = ref(true) -const lineRefs = ref([]) +const lineRefs = ref<(typeof CodeLine)[]>([]) -const codeLines: Ref<{ line: string; match: null | Match }[]> = computed(() => +const codeLines: Ref<{ line: string; matches: MatchInSingleFile[] }[]> = computed(() => highlight(props.file.data, props.highlightLanguage).map((line, index) => { return { line, - match: props.matches?.find((m) => m.start <= index + 1 && index + 1 <= m.end)?.match ?? null + matches: props.matches?.filter((m) => m.start <= index + 1 && index + 1 <= m.end) ?? [] } }) ) -function lineSelected(lineIndex: number) { - if (codeLines.value[lineIndex].match !== null) { - emit('lineSelected', codeLines.value[lineIndex].match) - } +function matchSelected(match: Match) { + emit('matchSelected', match) } /** @@ -126,7 +116,7 @@ function lineSelected(lineIndex: number) { function scrollTo(lineNumber: number) { collapsed.value = false nextTick(function () { - lineRefs.value[lineNumber - 1].scrollIntoView({ block: 'center' }) + lineRefs.value[lineNumber - 1].scrollTo() }) } @@ -154,16 +144,3 @@ function getFileDisplayName(file: SubmissionFile): string { : file.fileName } - - diff --git a/report-viewer/src/components/fileDisplaying/FilesContainer.vue b/report-viewer/src/components/fileDisplaying/FilesContainer.vue index 58c394e4e..1e9b2365e 100644 --- a/report-viewer/src/components/fileDisplaying/FilesContainer.vue +++ b/report-viewer/src/components/fileDisplaying/FilesContainer.vue @@ -28,7 +28,7 @@ !matches.get(file.fileName) ? [] : (matches.get(file.fileName) as MatchInSingleFile[]) " :highlight-language="highlightLanguage" - @line-selected="(match) => $emit('lineSelected', match)" + @match-selected="(match) => $emit('matchSelected', match)" class="mt-1 first:mt-0" /> @@ -83,7 +83,7 @@ const props = defineProps({ } }) -defineEmits(['lineSelected']) +defineEmits(['matchSelected']) const codePanels: Ref<(typeof CodePanel)[]> = ref([]) diff --git a/report-viewer/src/model/Match.ts b/report-viewer/src/model/Match.ts index 21a597af0..e03451371 100644 --- a/report-viewer/src/model/Match.ts +++ b/report-viewer/src/model/Match.ts @@ -13,9 +13,13 @@ export interface Match { firstFile: string secondFile: string startInFirst: number + startColumnInFirst: number endInFirst: number + endColumnInFirst: number startInSecond: number + startColumnInSecond: number endInSecond: number + endColumnInSecond: number tokens: number colorIndex?: number } diff --git a/report-viewer/src/model/MatchInSingleFile.ts b/report-viewer/src/model/MatchInSingleFile.ts index bcaae43c0..e959d47e0 100644 --- a/report-viewer/src/model/MatchInSingleFile.ts +++ b/report-viewer/src/model/MatchInSingleFile.ts @@ -40,4 +40,20 @@ export class MatchInSingleFile { return this._match.endInSecond } } + + get startColumn(): number { + if (this._index === 1) { + return this._match.startColumnInFirst + } else { + return this._match.startColumnInSecond + } + } + + get endColumn(): number { + if (this._index === 1) { + return this._match.endColumnInFirst + } else { + return this._match.endColumnInSecond + } + } } diff --git a/report-viewer/src/model/factories/ComparisonFactory.ts b/report-viewer/src/model/factories/ComparisonFactory.ts index d6257e93e..98097f605 100644 --- a/report-viewer/src/model/factories/ComparisonFactory.ts +++ b/report-viewer/src/model/factories/ComparisonFactory.ts @@ -123,9 +123,13 @@ export class ComparisonFactory extends BaseFactory { firstFile: slash(match.file1 as string), secondFile: slash(match.file2 as string), startInFirst: match.start1 as number, + startColumnInFirst: ((match['start1_col'] as number) || 1) - 1, endInFirst: match.end1 as number, + endColumnInFirst: ((match['end1_col'] as number) || Infinity) - 1, startInSecond: match.start2 as number, + startColumnInSecond: ((match['start2_col'] as number) || 1) - 1, endInSecond: match.end2 as number, + endColumnInSecond: ((match['end2_col'] as number) || Infinity) - 1, tokens: match.tokens as number } } diff --git a/report-viewer/src/views/ComparisonView.vue b/report-viewer/src/views/ComparisonView.vue index 58380799a..b901b8d4a 100644 --- a/report-viewer/src/views/ComparisonView.vue +++ b/report-viewer/src/views/ComparisonView.vue @@ -85,7 +85,7 @@ :matches="comparison.matchesInFirstSubmission" :file-owner-display-name="store().getDisplayName(comparison.firstSubmissionId)" :highlight-language="language" - @line-selected="showMatchInSecond" + @match-selected="showMatchInSecond" class="max-h-0 min-h-full flex-1 overflow-hidden print:max-h-none print:overflow-y-visible" />