-
-
Notifications
You must be signed in to change notification settings - Fork 335
fix: wrap code viewer lines by computing width #2070
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
base: main
Are you sure you want to change the base?
Changes from all commits
da6f15a
6955710
9e08913
a6e9ca5
f62fefe
81c79a9
a201a36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,16 +11,38 @@ const emit = defineEmits<{ | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const codeRef = useTemplateRef('codeRef') | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Generate line numbers array | ||||||||||||||||||||||||||||||||||||||||||||||
| const lineNumbers = computed(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| return Array.from({ length: props.lines }, (_, i) => i + 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
| // Using this so we can track the height of each line, and therefore compute digit sidebar | ||||||||||||||||||||||||||||||||||||||||||||||
| const lineMultipliers = ref<number[]>([]) | ||||||||||||||||||||||||||||||||||||||||||||||
| const LINE_HEIGHT_PX = 24 // also used in css | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function updateLineMultipliers() { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!codeRef.value) return | ||||||||||||||||||||||||||||||||||||||||||||||
| const lines = Array.from(codeRef.value.querySelectorAll('code > .line')) | ||||||||||||||||||||||||||||||||||||||||||||||
| lineMultipliers.value = lines.map(line => | ||||||||||||||||||||||||||||||||||||||||||||||
| Math.max(1, Math.round(parseFloat(getComputedStyle(line).height) / LINE_HEIGHT_PX)), | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Used for CSS calculation of line number column width | ||||||||||||||||||||||||||||||||||||||||||||||
| const lineDigits = computed(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| return String(props.lines).length | ||||||||||||||||||||||||||||||||||||||||||||||
| watch( | ||||||||||||||||||||||||||||||||||||||||||||||
| () => props.html, | ||||||||||||||||||||||||||||||||||||||||||||||
| () => nextTick(updateLineMultipliers), | ||||||||||||||||||||||||||||||||||||||||||||||
| { immediate: true }, | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| useResizeObserver(codeRef, updateLineMultipliers) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Line numbers ++ blank rows for the wrapped lines | ||||||||||||||||||||||||||||||||||||||||||||||
| const displayLines = computed(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const result: (number | null)[] = [] | ||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < props.lines; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||
| result.push(i + 1) | ||||||||||||||||||||||||||||||||||||||||||||||
| const extra = (lineMultipliers.value[i] ?? 1) - 1 | ||||||||||||||||||||||||||||||||||||||||||||||
| for (let j = 0; j < extra; j++) result.push(null) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const lineDigits = computed(() => String(props.lines).length) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Check if a line is selected | ||||||||||||||||||||||||||||||||||||||||||||||
| function isLineSelected(lineNum: number): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (!props.selectedLines) return false | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -86,36 +108,37 @@ watch( | |||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div class="code-viewer flex min-h-full max-w-full"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div class="code-viewer flex min-h-full max-w-full" :style="{ '--line-digits': lineDigits }"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Line numbers column --> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||
| class="line-numbers shrink-0 bg-bg-subtle border-ie border-solid border-border text-end select-none relative" | ||||||||||||||||||||||||||||||||||||||||||||||
| :style="{ '--line-digits': lineDigits }" | ||||||||||||||||||||||||||||||||||||||||||||||
| aria-hidden="true" | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <!-- This needs to be a native <a> element, because `LinkBase` (or specifically `NuxtLink`) does not seem to work when trying to prevent default behavior (jumping to the anchor) --> | ||||||||||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||||||||||
| v-for="lineNum in lineNumbers" | ||||||||||||||||||||||||||||||||||||||||||||||
| :id="`L${lineNum}`" | ||||||||||||||||||||||||||||||||||||||||||||||
| :key="lineNum" | ||||||||||||||||||||||||||||||||||||||||||||||
| :href="`#L${lineNum}`" | ||||||||||||||||||||||||||||||||||||||||||||||
| tabindex="-1" | ||||||||||||||||||||||||||||||||||||||||||||||
| class="line-number block px-3 py-0 font-mono text-sm leading-6 cursor-pointer transition-colors no-underline" | ||||||||||||||||||||||||||||||||||||||||||||||
| :class="[ | ||||||||||||||||||||||||||||||||||||||||||||||
| isLineSelected(lineNum) | ||||||||||||||||||||||||||||||||||||||||||||||
| ? 'bg-yellow-500/20 text-fg' | ||||||||||||||||||||||||||||||||||||||||||||||
| : 'text-fg-subtle hover:text-fg-muted', | ||||||||||||||||||||||||||||||||||||||||||||||
| ]" | ||||||||||||||||||||||||||||||||||||||||||||||
| @click.prevent="onLineClick(lineNum, $event)" | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| {{ lineNum }} | ||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||
| <template v-for="(lineNum, idx) in displayLines" :key="idx"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||||||||||
| v-if="lineNum !== null" | ||||||||||||||||||||||||||||||||||||||||||||||
| :id="`L${lineNum}`" | ||||||||||||||||||||||||||||||||||||||||||||||
| :href="`#L${lineNum}`" | ||||||||||||||||||||||||||||||||||||||||||||||
| tabindex="-1" | ||||||||||||||||||||||||||||||||||||||||||||||
| class="line-number block px-3 py-0 font-mono text-sm leading-6 cursor-pointer transition-colors no-underline" | ||||||||||||||||||||||||||||||||||||||||||||||
| :class="[ | ||||||||||||||||||||||||||||||||||||||||||||||
| isLineSelected(lineNum) | ||||||||||||||||||||||||||||||||||||||||||||||
| ? 'bg-yellow-500/20 text-fg' | ||||||||||||||||||||||||||||||||||||||||||||||
| : 'text-fg-subtle hover:text-fg-muted', | ||||||||||||||||||||||||||||||||||||||||||||||
| ]" | ||||||||||||||||||||||||||||||||||||||||||||||
| @click.prevent="onLineClick(lineNum, $event)" | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| {{ lineNum }} | ||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||
| <span v-else class="block px-3 leading-6"> </span> | ||||||||||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Code content --> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div class="code-content flex-1 overflow-x-auto min-w-0"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div class="code-content"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <!-- eslint-disable vue/no-v-html -- HTML is generated server-side by Shiki --> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div ref="codeRef" class="code-lines min-w-full w-fit" v-html="html" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| <div ref="codeRef" class="code-lines" v-html="html" /> | ||||||||||||||||||||||||||||||||||||||||||||||
| <!-- eslint-enable vue/no-v-html --> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -124,46 +147,56 @@ watch( | |||||||||||||||||||||||||||||||||||||||||||||
| <style scoped> | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-viewer { | ||||||||||||||||||||||||||||||||||||||||||||||
| font-size: 14px; | ||||||||||||||||||||||||||||||||||||||||||||||
| /* 1ch per digit + 1.5rem (px-3 * 2) padding */ | ||||||||||||||||||||||||||||||||||||||||||||||
| --line-numbers-width: calc(var(--line-digits) * 1ch + 1.5rem); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| .line-numbers { | ||||||||||||||||||||||||||||||||||||||||||||||
| /* 1ch per digit + 1.5rem (px-3 * 2) padding */ | ||||||||||||||||||||||||||||||||||||||||||||||
| min-width: calc(var(--line-digits) * 1ch + 1.5rem); | ||||||||||||||||||||||||||||||||||||||||||||||
| min-width: var(--line-numbers-width); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| .code-content :deep(pre) { | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content { | ||||||||||||||||||||||||||||||||||||||||||||||
| flex: 1; | ||||||||||||||||||||||||||||||||||||||||||||||
| min-width: 0; | ||||||||||||||||||||||||||||||||||||||||||||||
| max-width: calc(100% - var(--line-numbers-width)); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| .code-content:deep(pre) { | ||||||||||||||||||||||||||||||||||||||||||||||
| margin: 0; | ||||||||||||||||||||||||||||||||||||||||||||||
| padding: 0; | ||||||||||||||||||||||||||||||||||||||||||||||
| background: transparent !important; | ||||||||||||||||||||||||||||||||||||||||||||||
| overflow: visible; | ||||||||||||||||||||||||||||||||||||||||||||||
| max-width: 100%; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| .code-content :deep(code) { | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content:deep(code) { | ||||||||||||||||||||||||||||||||||||||||||||||
| display: block; | ||||||||||||||||||||||||||||||||||||||||||||||
| padding: 0 1rem; | ||||||||||||||||||||||||||||||||||||||||||||||
| background: transparent !important; | ||||||||||||||||||||||||||||||||||||||||||||||
| max-width: 100%; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| .code-content :deep(.line) { | ||||||||||||||||||||||||||||||||||||||||||||||
| display: block; | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content:deep(.line) { | ||||||||||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||||||||||
| flex-wrap: wrap; | ||||||||||||||||||||||||||||||||||||||||||||||
| /* Ensure consistent height matching line numbers */ | ||||||||||||||||||||||||||||||||||||||||||||||
| line-height: 24px; | ||||||||||||||||||||||||||||||||||||||||||||||
| min-height: 24px; | ||||||||||||||||||||||||||||||||||||||||||||||
| max-height: 24px; | ||||||||||||||||||||||||||||||||||||||||||||||
| white-space: pre; | ||||||||||||||||||||||||||||||||||||||||||||||
| line-height: calc(v-bind(LINE_HEIGHT_PX) * 1px); | ||||||||||||||||||||||||||||||||||||||||||||||
| min-height: calc(v-bind(LINE_HEIGHT_PX) * 1px); | ||||||||||||||||||||||||||||||||||||||||||||||
| white-space: pre-wrap; | ||||||||||||||||||||||||||||||||||||||||||||||
| overflow: hidden; | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+179
to
186
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Long unbroken tokens can still overflow in wrap mode. Line 185 uses 🩹 Proposed fix .code-content:deep(.line) {
display: flex;
flex-wrap: wrap;
/* Ensure consistent height matching line numbers */
line-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
min-height: calc(v-bind(LINE_HEIGHT_PX) * 1px);
white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
overflow: hidden;
transition: background-color 0.1s;
max-width: 100%;
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
| transition: background-color 0.1s; | ||||||||||||||||||||||||||||||||||||||||||||||
| max-width: 100%; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /* Highlighted lines in code content - extend full width with negative margin */ | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content :deep(.line.highlighted) { | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content:deep(.line.highlighted) { | ||||||||||||||||||||||||||||||||||||||||||||||
| @apply bg-yellow-500/20; | ||||||||||||||||||||||||||||||||||||||||||||||
| margin: 0 -1rem; | ||||||||||||||||||||||||||||||||||||||||||||||
| padding: 0 1rem; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| /* Clickable import links */ | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content :deep(.import-link) { | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content:deep(.import-link) { | ||||||||||||||||||||||||||||||||||||||||||||||
| color: inherit; | ||||||||||||||||||||||||||||||||||||||||||||||
| text-decoration: underline; | ||||||||||||||||||||||||||||||||||||||||||||||
| text-decoration-style: dotted; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -175,7 +208,7 @@ watch( | |||||||||||||||||||||||||||||||||||||||||||||
| cursor: pointer; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| .code-content :deep(.import-link:hover) { | ||||||||||||||||||||||||||||||||||||||||||||||
| .code-content:deep(.import-link:hover) { | ||||||||||||||||||||||||||||||||||||||||||||||
| text-decoration-style: solid; | ||||||||||||||||||||||||||||||||||||||||||||||
| text-decoration-color: #9ecbff; /* syntax.str - light blue */ | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throttle resize-driven line measurement to avoid jank on large files.
Line 31 currently triggers a full recomputation, and Lines 20-23 read computed style for every rendered code line each time. On large files this can become expensive during resize and cause visible lag.
⚡ Proposed fix
Also applies to: 31-31