Skip to content

Commit

Permalink
Merge pull request #1695 from jplag/report-viewer/match-in-line
Browse files Browse the repository at this point in the history
Character precise matches
  • Loading branch information
tsaglam authored Apr 19, 2024
2 parents 26b5494 + 0f3fb76 commit 8f0daec
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,30 @@ private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Mat
List<Token> tokensFirst = comparison.firstSubmission().getTokenList().subList(match.startOfFirst(), match.endOfFirst() + 1);
List<Token> tokensSecond = comparison.secondSubmission().getTokenList().subList(match.startOfSecond(), match.endOfSecond() + 1);

Comparator<? super Token> lineComparator = Comparator.comparingInt(Token::getLine);
Comparator<? super Token> 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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
}
175 changes: 175 additions & 0 deletions report-viewer/src/components/fileDisplaying/CodeLine.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<template>
<div
class="col-span-1 col-start-2 row-span-1 flex w-full cursor-default"
:class="{ 'cursor-pointer': matches.length > 0 }"
:style="{
gridRowStart: lineNumber
}"
ref="lineRef"
>
<div
v-for="(part, index) in textParts"
:key="index"
class="print-excact h-full last:flex-1"
@click="matchSelected(part.match)"
:style="{
background:
part.match != undefined
? getMatchColor(0.3, part.match.match.colorIndex)
: 'hsla(0, 0%, 0%, 0)'
}"
>
<pre
v-html="part.line"
class="code-font print-excact break-child !bg-transparent print:whitespace-pre-wrap"
></pre>
</div>
</div>
</template>

<script setup lang="ts">
import type { MatchInSingleFile } from '@/model/MatchInSingleFile'
import { getMatchColor } from '@/utils/ColorUtils'
import { ref } from 'vue'
const props = defineProps({
lineNumber: {
type: Number,
required: true
},
line: {
type: String,
required: true
},
matches: {
type: Array<MatchInSingleFile>,
required: true
}
})
const emit = defineEmits(['matchSelected'])
function matchSelected(match?: MatchInSingleFile) {
if (match) {
emit('matchSelected', match.match)
}
}
const lineRef = ref<HTMLElement | null>(null)
function scrollTo() {
if (lineRef.value) {
lineRef.value.scrollIntoView({ block: 'center' })
}
}
defineExpose({ scrollTo })
interface TextPart {
line: string
match?: MatchInSingleFile
}
let lineIndex = ref(0)
let colIndex = ref(0)
function computeTextParts() {
if (props.matches.length == 0) {
return [{ line: props.line }]
}
const sortedMatches = Array.from(props.matches)
.sort((a, b) => a.startColumn - b.startColumn)
.sort((a, b) => a.start - b.start)
let lineParts: {
start: number
end: number
match?: MatchInSingleFile
}[] = []
if (sortedMatches[0].start == props.lineNumber && sortedMatches[0].startColumn > 0) {
const end = sortedMatches[0].startColumn - 1
lineParts.push({ start: 0, end: end })
}
const start = sortedMatches[0].start == props.lineNumber ? sortedMatches[0].startColumn : 0
const end =
sortedMatches[0].end == props.lineNumber ? sortedMatches[0].endColumn : props.line.length
lineParts.push({ start: start, end: end, match: sortedMatches[0] })
let matchIndex = 1
while (matchIndex < sortedMatches.length) {
const match = sortedMatches[matchIndex]
const prevMatchPart = lineParts[matchIndex - 1]
if (prevMatchPart.end + 1 < match.startColumn) {
const end = match.startColumn - 1
lineParts.push({ start: prevMatchPart.end + 1, end: end })
}
const end = match.end == props.lineNumber ? match.endColumn : props.line.length
lineParts.push({ start: match.startColumn, end: end, match })
matchIndex++
}
if (lineParts[lineParts.length - 1].end < props.line.length) {
lineParts.push({ start: lineParts[lineParts.length - 1].end + 1, end: props.line.length })
}
let textParts: TextPart[] = []
lineIndex.value = 0
colIndex.value = 0
for (const matchPart of lineParts) {
const line = getNextLinePartTillColumn(matchPart.end)
textParts.push({ line, match: matchPart.match })
}
return textParts
}
const textParts = computeTextParts()
function getNextLinePartTillColumn(endCol: number) {
let part = ''
while (colIndex.value <= endCol && lineIndex.value < props.line.length) {
// spans from highlighting do not count as characters in the code
if (props.line[lineIndex.value] == '<') {
while (props.line[lineIndex.value] != '>') {
part += props.line[lineIndex.value]
lineIndex.value++
}
part += props.line[lineIndex.value]
lineIndex.value++
} else if (props.line[lineIndex.value] == '\t') {
// display tabs properly
part += ' '
lineIndex.value++
colIndex.value += 8
} else if (props.line[lineIndex.value] == '&') {
// html escape characters for e.g. <,>,&
while (props.line[lineIndex.value] != ';') {
part += props.line[lineIndex.value]
lineIndex.value++
}
lineIndex.value++
colIndex.value++
} else {
part += props.line[lineIndex.value]
lineIndex.value++
colIndex.value++
}
}
return part
}
</script>

<style scoped>
.code-font {
font-family: 'JetBrains Mono NL', monospace !important;
}
@media print {
.break-child *,
.break-child {
word-break: break-word;
}
}
</style>
81 changes: 29 additions & 52 deletions report-viewer/src/components/fileDisplaying/CodePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,31 @@

<div class="mx-1 overflow-x-auto print:!mx-0 print:overflow-x-hidden">
<div class="print:display-initial w-fit min-w-full !text-xs" :class="{ hidden: collapsed }">
<table
<div
v-if="file.data.trim() !== ''"
class="w-full print:table-auto"
:aria-describedby="`Content of file ${file.fileName}`"
class="grid w-full grid-cols-[auto_1fr] gap-x-2 print:table-auto"
>
<div
v-for="(_, index) in codeLines"
:key="index"
class="col-span-1 col-start-1 row-span-1 text-right"
:style="{
gridRowStart: index + 1
}"
>
{{ index + 1 }}
</div>
<!-- One row in table per code line -->
<tr
<CodeLine
v-for="(line, index) in codeLines"
:key="index"
class="w-full cursor-default"
:class="{ 'cursor-pointer': line.match !== null }"
@click="lineSelected(index)"
>
<!-- Line number -->
<td class="float-right pr-3">{{ index + 1 }}</td>
<!-- Code line -->
<td
class="print-excact w-full"
:style="{
background:
line.match !== null
? getMatchColor(0.3, line.match.colorIndex)
: 'hsla(0, 0%, 0%, 0)'
}"
>
<pre
v-html="line.line"
class="code-font print-excact break-child !bg-transparent print:whitespace-pre-wrap"
ref="lineRefs"
></pre>
</td>
</tr>
</table>
ref="lineRefs"
:line="line.line"
:lineNumber="index + 1"
:matches="line.matches"
@matchSelected="(match) => matchSelected(match)"
/>
</div>

<div v-else class="flex flex-col items-start overflow-x-auto">
<i>Empty File</i>
Expand All @@ -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({
/**
Expand All @@ -99,24 +91,22 @@ const props = defineProps({
}
})

const emit = defineEmits(['lineSelected'])
const emit = defineEmits(['matchSelected'])

const collapsed = ref(true)
const lineRefs = ref<HTMLElement[]>([])
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)
}

/**
Expand All @@ -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()
})
}

Expand Down Expand Up @@ -154,16 +144,3 @@ function getFileDisplayName(file: SubmissionFile): string {
: file.fileName
}
</script>

<style scoped>
.code-font {
font-family: 'JetBrains Mono NL', monospace !important;
}

@media print {
.break-child *,
.break-child {
word-break: break-word;
}
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
</VueDraggableNext>
Expand Down Expand Up @@ -83,7 +83,7 @@ const props = defineProps({
}
})

defineEmits(['lineSelected'])
defineEmits(['matchSelected'])

const codePanels: Ref<(typeof CodePanel)[]> = ref([])

Expand Down
4 changes: 4 additions & 0 deletions report-viewer/src/model/Match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit 8f0daec

Please sign in to comment.