Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Character precise matches #1695

Merged
merged 9 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading