Skip to content

fix: wrap code viewer lines by computing width#2070

Draft
nosthrillz wants to merge 8 commits intonpmx-dev:mainfrom
nosthrillz:fix/2027-code-wrapping
Draft

fix: wrap code viewer lines by computing width#2070
nosthrillz wants to merge 8 commits intonpmx-dev:mainfrom
nosthrillz:fix/2027-code-wrapping

Conversation

@nosthrillz
Copy link

@nosthrillz nosthrillz commented Mar 13, 2026

🔗 Linked issue

Attempts to resolve 2027

🧭 Context

The current approach has overflow-x-auto, but wrapping might be better.
It's unclear whether that's preferred, so making the PR to allow a preview.

📚 Description

We actually can compute available screen width, so we can avoid overflow, and instead set the width and use flex-wrap combined with pre-wrap to get the desired effect.

Potential points of contention / concerns

I didn't visually test the highlighting, I'm yet unsure what the implications would be :)

@vercel
Copy link

vercel bot commented Mar 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 15, 2026 5:41pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 15, 2026 5:41pm
npmx-lunaria Ignored Ignored Mar 15, 2026 5:41pm

Request Review

@codecov
Copy link

codecov bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 84.21053% with 6 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Code/Viewer.vue 82.85% 4 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Reworks the Code Viewer to support wrapped lines by tracking per-line heights with a reactive lineMultipliers array, computing displayLines (real lines plus null placeholders), and updating multipliers via a watcher on html and a ResizeObserver. Template and CSS were adjusted: code-content and line elements now support wrapping (pre-wrap, flex wrap), highlighted lines span full width, gutter width is computed via a --line-digits CSS variable, and anchor navigation logic was realigned to the updated DOM. Separately, the package-code page adds layout scaffolding: a .main-content wrapper, fixed-width .file-tree sidebar and a responsive .file-viewer.

Possibly related PRs

Suggested labels

front

Suggested reviewers

  • ghostdevv
  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly relates to the changeset, explaining the motivation to replace overflow-x-auto with computed-width wrapping using flex-wrap and pre-wrap.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db7d3812-0584-449a-ac8f-5830e35c5103

📥 Commits

Reviewing files that changed from the base of the PR and between 9e8c805 and da6f15a.

📒 Files selected for processing (2)
  • app/components/Code/Viewer.vue
  • app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue (1)

588-603: @screen usage now correct; minor responsiveness consideration remains.

The @screen lg directive is correctly placed at top-level (not nested), addressing the previous review feedback. The switch from 100vw to 100% is also appropriate.

However, .file-viewer width applies at all breakpoints, whilst .file-tree is only visible on md+ screens. On mobile, this subtracts sidebar space for a hidden element. The flex-1 class should compensate via flex-grow, but consider scoping the width rule to md+ for clarity:

Option: Scope width rules to md+ breakpoint
 .file-tree {
   width: var(--sidebar-space);
 }
+@screen md {
+  .file-viewer {
+    width: calc(100% - var(--sidebar-space));
+  }
+}
-.file-viewer {
-  width: calc(100% - var(--sidebar-space));
-}

Alternatively, since .file-viewer already has flex-1 min-w-0 in the template, the explicit width may be unnecessary if flexbox sizing is sufficient.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d420e8f0-1328-42ad-968d-7926a66f68b3

📥 Commits

Reviewing files that changed from the base of the PR and between da6f15a and a6e9ca5.

📒 Files selected for processing (2)
  • app/components/Code/Viewer.vue
  • app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue

@gameroman
Copy link
Contributor

@nosthrillz nosthrillz marked this pull request as draft March 14, 2026 17:26
@nosthrillz
Copy link
Author

Switching to Draft because the line numbers need to be recomputed. As attempted in https://github.com/npmx-dev/npmx.dev/pull/2028/changes as well

@WilcoSp
Copy link
Contributor

WilcoSp commented Mar 14, 2026

the wrapping is very readable and atm I also only see the issue of the line numbers & the highlighting included with it.

a file that may be good to check would be in nuxt the /dist/index.mjs file because it has large imports

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/components/Code/Viewer.vue (1)

30-30: Consider debouncing the resize observer callback.

updateLineMultipliers queries getComputedStyle for every line element. For large files (as mentioned in PR comments regarding Nuxt's /dist/index.mjs), this could cause performance issues during continuous resize events.

♻️ Suggested debounce implementation
+import { useDebounceFn } from '@vueuse/core'
+
+const debouncedUpdateLineMultipliers = useDebounceFn(updateLineMultipliers, 100)
+
-useResizeObserver(codeRef, updateLineMultipliers)
+useResizeObserver(codeRef, debouncedUpdateLineMultipliers)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59176b83-d3f1-482d-a169-c55c5dd5d568

📥 Commits

Reviewing files that changed from the base of the PR and between a6e9ca5 and f62fefe.

📒 Files selected for processing (1)
  • app/components/Code/Viewer.vue

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e43e32c-4ae1-4c98-9eb3-4e11d0ded3ee

📥 Commits

Reviewing files that changed from the base of the PR and between f62fefe and 81c79a9.

📒 Files selected for processing (2)
  • app/utils/chart-data-buckets.ts
  • test/unit/app/utils/chart-data-buckets.spec.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/components/Code/Viewer.vue (1)

26-30: Consider consolidating duplicate props.html watchers.

Both watchers react to the same source and schedule nextTick work. Merging them into a single watcher would simplify ordering and reduce reactive overhead.

♻️ Proposed refactor
-watch(
-  () => props.html,
-  () => nextTick(updateLineMultipliers),
-  { immediate: true },
-)
 useResizeObserver(codeRef, updateLineMultipliers)
@@
-watch(
-  () => props.html,
-  () => {
-    nextTick(handleImportLinkNavigate)
-  },
-  { immediate: true },
-)
+watch(
+  () => props.html,
+  () => {
+    nextTick(() => {
+      updateLineMultipliers()
+      handleImportLinkNavigate()
+    })
+  },
+  { immediate: true },
+)

Also applies to: 101-107


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cad5e82e-58c5-4e74-b233-5ed5eeac11fd

📥 Commits

Reviewing files that changed from the base of the PR and between 81c79a9 and a201a36.

📒 Files selected for processing (1)
  • app/components/Code/Viewer.vue

Comment on lines +18 to +23
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)),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
+const scheduleLineMultiplierUpdate = useDebounceFn(() => {
+  updateLineMultipliers()
+}, 16)
+
 watch(
   () => props.html,
-  () => nextTick(updateLineMultipliers),
+  () => nextTick(scheduleLineMultiplierUpdate),
   { immediate: true },
 )
-useResizeObserver(codeRef, updateLineMultipliers)
+useResizeObserver(codeRef, scheduleLineMultiplierUpdate)

Also applies to: 31-31

Comment on lines +179 to 186
.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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Long unbroken tokens can still overflow in wrap mode.

Line 185 uses white-space: pre-wrap, which does not hard-break long strings without natural breakpoints (e.g. minified blobs, hashes, base64). This can reintroduce horizontal overflow despite the wrapping objective.

🩹 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.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;
.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;

@nosthrillz nosthrillz marked this pull request as draft March 15, 2026 07:02
@nosthrillz
Copy link
Author

nosthrillz commented Mar 15, 2026

After spending hours on this, I've hit a roadblock. This is a non-trivial issue, even though it seemed like "bite-sized" initially.

Been getting massive performance hits on large files due to recomputing line heights for 10^3 .. 10^5 nodes if you count in the highlighting.

What I tried:

  • measuring lines with JS (getComputedStyle / offsetHeight). But this blocks the main thread for large files like the nuxt dist example. (There's a reason GH hides it, eh?)
  • v-html line numbers + multiplier spacers (still measuring). Can lead to as bad as 120s hang INP score for the nuxt file.
  • chunks of HTML + infinite scroll. Aside from the hydration mismatches that I didn't continue investigating, the UX is a bit unfortunate as it no longer allows you to easily jump to a certain line, or simply scroll ahead multiple pages at a time.
  • Non-blocking expansion in a window-pane mode. Worked out, but still 16s INP because we're parsing 10^4+ DOM nodes
  • CSS counters + truncation at 1000 lines. Kinda like GitHub. The problem here is that going to line 1001 on load will block again, until the full page is shown. Could potentially make it a hybrid approach.

The jist of it is that, to wrap the 8500 Shiki-highlighted lines, the browser needs a lot of time counting line heights, and reconciling, because we need the window resize, so it's not a one-off

Going forward:

  1. GitHub-style show chunk buttons. Show lines X-Y, with "Load lines above" / "Load lines below" buttons at edges and a "Show all" nuclear button. At least the users would be aware that clicking load all will be consuming. Problem is that we would have to give up on anchor tags to lines
  2. Infinite scroll (both directions, chunked). Works, but not ideal UX :)
  3. Actuall virtual scroller like twitter. There's virtual-scroller solving for exactly that. We'd show a virtual window pane for the nodes. It would be smooth AF. The big problem is, that it's impossible to "jump to line" or "find on page". Not to mention it would have to be CSR.

IMHO I think the GitHub style option fits the UX the closest.

Please advise

Comment on lines +37 to +43
const rangeEndDate = parseIsoDate(rangeEndIso)

// Align from last day with actual data (npm has 1-2 day delay, today is incomplete)
const lastNonZero = sorted.findLast(d => d.value > 0)
const pickerEnd = parseIsoDate(rangeEndIso)
const effectiveEnd = lastNonZero ? parseIsoDate(lastNonZero.day) : pickerEnd
const rangeEndDate = effectiveEnd.getTime() < pickerEnd.getTime() ? effectiveEnd : pickerEnd

// Group into 7-day buckets from END backwards
const buckets = new Map<number, number>()

for (const item of sorted) {
const offset = Math.floor((rangeEndDate.getTime() - parseIsoDate(item.day).getTime()) / DAY_MS)
const itemDate = parseIsoDate(item.day)
const offset = Math.floor((rangeEndDate.getTime() - itemDate.getTime()) / DAY_MS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes intended in this PR?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not, a mistake on my part for merging latest instead of rebasing :)

I will have to remove them. Don't think this PR will end up being merged to be honest, given the above conversation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add word wrap to code viewer

4 participants