Skip to content

Commit 8721632

Browse files
Joery-MJoery
and
Joery
authored
feat: enable virtual scrolling in code preview (#133)
Co-authored-by: Joery <joery@supportprofessionals.nl>
1 parent 0864365 commit 8721632

File tree

4 files changed

+75
-40
lines changed

4 files changed

+75
-40
lines changed

src/client/components/DiffEditor.vue

+27-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script setup lang="ts">
2+
import type CodeMirror from 'codemirror'
23
import { Pane, Splitpanes } from 'splitpanes'
34
import { nextTick, onMounted, ref, toRefs, watchEffect } from 'vue'
4-
import { syncCmHorizontalScrolling, useCodeMirror } from '../logic/codemirror'
5+
import { syncEditorScrolls, syncScrollListeners, useCodeMirror } from '../logic/codemirror'
56
import { guessMode } from '../logic/utils'
67
import { useOptionsStore } from '../stores/options'
78
import { calculateDiffWithWorker } from '../worker/diff'
@@ -17,47 +18,54 @@ const options = useOptionsStore()
1718
1819
const { from, to } = toRefs(props)
1920
20-
const fromEl = ref<HTMLTextAreaElement>()
21-
const toEl = ref<HTMLTextAreaElement>()
21+
const fromEl = useTemplateRef('fromEl')
22+
const toEl = useTemplateRef('toEl')
23+
24+
let cm1: CodeMirror.Editor
25+
let cm2: CodeMirror.Editor
2226
2327
onMounted(() => {
24-
const cm1 = useCodeMirror(
28+
cm1 = useCodeMirror(
2529
fromEl,
2630
from,
2731
{
2832
mode: 'javascript',
2933
readOnly: true,
3034
lineNumbers: true,
31-
scrollbarStyle: 'null',
3235
},
3336
)
3437
35-
const cm2 = useCodeMirror(
38+
cm2 = useCodeMirror(
3639
toEl,
3740
to,
3841
{
3942
mode: 'javascript',
4043
readOnly: true,
4144
lineNumbers: true,
42-
scrollbarStyle: 'null',
4345
},
4446
)
4547
46-
syncCmHorizontalScrolling(cm1, cm2)
48+
syncScrollListeners(cm1, cm2)
4749
4850
watchEffect(() => {
4951
cm1.setOption('lineWrapping', options.view.lineWrapping)
5052
cm2.setOption('lineWrapping', options.view.lineWrapping)
5153
})
5254
53-
watchEffect(() => {
54-
// @ts-expect-error untyped
55-
cm1.display.wrapper.style.display = props.oneColumn ? 'none' : ''
55+
watchEffect(async () => {
56+
cm1.getWrapperElement().style.display = props.oneColumn ? 'none' : ''
57+
if (!props.oneColumn) {
58+
await nextTick()
59+
// Force sync to current scroll
60+
cm1.refresh()
61+
syncEditorScrolls(cm2, cm1)
62+
}
5663
})
5764
5865
watchEffect(async () => {
5966
const l = from.value
6067
const r = to.value
68+
const diffEnabled = props.diff
6169
6270
cm1.setOption('mode', guessMode(l))
6371
cm2.setOption('mode', guessMode(r))
@@ -75,7 +83,7 @@ onMounted(() => {
7583
for (let i = 0; i < cm2.lineCount() + 2; i++)
7684
cm2.removeLineClass(i, 'background', 'diff-added')
7785
78-
if (props.diff && from.value) {
86+
if (diffEnabled && from.value) {
7987
const changes = await calculateDiffWithWorker(l, r)
8088
8189
const addedLines = new Set()
@@ -124,6 +132,9 @@ const leftPanelSize = computed(() => {
124132
})
125133
126134
function onUpdate(size: number) {
135+
// Refresh sizes
136+
cm1?.refresh()
137+
cm2?.refresh()
127138
if (props.oneColumn)
128139
return
129140
options.view.panelSizeDiff = size
@@ -132,11 +143,11 @@ function onUpdate(size: number) {
132143

133144
<template>
134145
<Splitpanes @resize="onUpdate($event[0].size)">
135-
<Pane v-show="!oneColumn" min-size="10" :size="leftPanelSize" class="h-max min-h-screen" border="main r">
136-
<textarea ref="fromEl" v-text="from" />
146+
<Pane v-show="!oneColumn" min-size="10" :size="leftPanelSize" border="main r">
147+
<div ref="fromEl" class="h-inherit" />
137148
</Pane>
138-
<Pane min-size="10" :size="100 - leftPanelSize" class="h-max min-h-screen">
139-
<textarea ref="toEl" v-text="to" />
149+
<Pane min-size="10" :size="100 - leftPanelSize">
150+
<div ref="toEl" class="h-inherit" />
140151
</Pane>
141152
</Splitpanes>
142153
</template>

src/client/logic/codemirror.ts

+38-19
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,32 @@ import type { Ref, WritableComputedRef } from 'vue'
22
import CodeMirror from 'codemirror'
33
import { watch } from 'vue'
44

5-
import 'codemirror/mode/javascript/javascript'
5+
import 'codemirror/addon/dialog/dialog'
6+
import 'codemirror/addon/dialog/dialog.css'
7+
import 'codemirror/addon/display/placeholder'
8+
import 'codemirror/addon/search/jump-to-line'
9+
import 'codemirror/addon/search/search'
10+
import 'codemirror/lib/codemirror.css'
611
import 'codemirror/mode/css/css'
12+
import 'codemirror/mode/handlebars/handlebars'
13+
import 'codemirror/mode/htmlmixed/htmlmixed'
14+
import 'codemirror/mode/javascript/javascript'
715
import 'codemirror/mode/markdown/markdown'
8-
import 'codemirror/mode/xml/xml'
916
import 'codemirror/mode/pug/pug'
1017
import 'codemirror/mode/sass/sass'
1118
import 'codemirror/mode/vue/vue'
12-
import 'codemirror/mode/handlebars/handlebars'
13-
import 'codemirror/mode/htmlmixed/htmlmixed'
14-
import 'codemirror/addon/display/placeholder'
15-
import 'codemirror/lib/codemirror.css'
19+
import 'codemirror/mode/xml/xml'
1620

1721
export function useCodeMirror(
18-
textarea: Ref<HTMLTextAreaElement | null | undefined>,
22+
container: Ref<HTMLDivElement | null | undefined>,
1923
input: Ref<string> | WritableComputedRef<string>,
2024
options: CodeMirror.EditorConfiguration = {},
2125
) {
22-
const cm = CodeMirror.fromTextArea(
23-
textarea.value!,
24-
{
25-
theme: 'vars',
26-
...options,
27-
},
28-
)
26+
const cm = CodeMirror(container.value!, {
27+
theme: 'vars',
28+
value: input.value,
29+
...options,
30+
})
2931

3032
let skip = false
3133

@@ -45,6 +47,7 @@ export function useCodeMirror(
4547
const selections = cm.listSelections()
4648
cm.replaceRange(v, cm.posFromIndex(0), cm.posFromIndex(Number.POSITIVE_INFINITY))
4749
cm.setSelections(selections)
50+
cm.scrollTo(0, 0)
4851
}
4952
},
5053
{ immediate: true },
@@ -53,9 +56,21 @@ export function useCodeMirror(
5356
return cm
5457
}
5558

56-
export function syncCmHorizontalScrolling(
57-
cm1: CodeMirror.EditorFromTextArea,
58-
cm2: CodeMirror.EditorFromTextArea,
59+
export function syncEditorScrolls(primary: CodeMirror.Editor, target: CodeMirror.Editor) {
60+
const pInfo = primary.getScrollInfo()
61+
const tInfo = target.getScrollInfo()
62+
63+
// Map scroll range
64+
let x = ((tInfo.width - tInfo.clientWidth) / (pInfo.width - pInfo.clientWidth)) * pInfo.left
65+
let y = ((tInfo.height - tInfo.clientHeight) / (pInfo.height - pInfo.clientHeight)) * pInfo.top
66+
x = Number.isNaN(x) ? 0 : x
67+
y = Number.isNaN(y) ? 0 : y
68+
target.scrollTo(x, y)
69+
}
70+
71+
export function syncScrollListeners(
72+
cm1: CodeMirror.Editor,
73+
cm2: CodeMirror.Editor,
5974
) {
6075
let activeCm = 1
6176

@@ -68,10 +83,14 @@ export function syncCmHorizontalScrolling(
6883

6984
cm1.on('scroll', (editor) => {
7085
if (activeCm === 1)
71-
cm2.scrollTo(editor.getScrollInfo().left)
86+
syncEditorScrolls(editor, cm2)
7287
})
88+
// Scroll cursor into view no matter which view is active
89+
cm1.on('scrollCursorIntoView', editor => syncEditorScrolls(editor, cm2))
90+
7391
cm2.on('scroll', (editor) => {
7492
if (activeCm === 2)
75-
cm1.scrollTo(editor.getScrollInfo().left)
93+
syncEditorScrolls(editor, cm1)
7694
})
95+
cm2.on('scrollCursorIntoView', editor => syncEditorScrolls(editor, cm1))
7796
}

src/client/pages/index/module.vue

-1
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@ getHot().then((hot) => {
249249
:one-column="options.view.showOneColumn || !!currentTransform?.error"
250250
:diff="options.view.diff && !currentTransform?.error"
251251
:from="from" :to="to"
252-
h-unset
253252
/>
254253
</div>
255254
</Pane>

src/client/styles/main.css

+10-4
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@ html.dark {
1515
}
1616

1717
.CodeMirror {
18-
height: max-content !important;
18+
height: inherit !important;
1919
font-family: var(--cm-font-family) !important;
2020
font-size: 13px !important;
2121
}
2222

23-
.CodeMirror-scroll {
24-
overflow-y: hidden !important;
25-
overflow-x: auto !important;
23+
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
24+
background-color: var(--cm-background) !important;
2625
}
2726

27+
.CodeMirror-dialog {
28+
@apply !border-main !py-1;
29+
}
30+
31+
#CodeMirror-search-field {
32+
@apply !border-main !border !rounded !outline-none !border-solid !px-1.5 !py-0.25;
33+
}
2834

2935
/* Splitpanes */
3036
.splitpanes__pane {

0 commit comments

Comments
 (0)