+
diff --git a/script.js b/script.js
index 95874a2..50128a1 100644
--- a/script.js
+++ b/script.js
@@ -80,6 +80,7 @@ document.addEventListener("DOMContentLoaded", function () {
const githubImportCancelBtn = document.getElementById("github-import-cancel");
const githubImportSubmitBtn = document.getElementById("github-import-submit");
const editorHighlightLayer = document.getElementById("editor-highlight-layer");
+ const lineNumbers = document.getElementById("line-numbers");
const clearFormattingModal = document.getElementById("clear-formatting-modal");
const clearFormattingConfirm = document.getElementById("clear-formatting-confirm");
const clearFormattingCancel = document.getElementById("clear-formatting-cancel");
@@ -229,7 +230,7 @@ document.addEventListener("DOMContentLoaded", function () {
const markedOptions = {
gfm: true,
- breaks: false,
+ breaks: true,
pedantic: false,
sanitize: false,
smartypants: false,
@@ -237,6 +238,8 @@ document.addEventListener("DOMContentLoaded", function () {
headerIds: true,
mangle: false,
};
+ let lineNumberMeasure = null;
+ let lineNumberUpdateFrame = null;
const renderer = new marked.Renderer();
renderer.code = function (code, language) {
@@ -1109,6 +1112,7 @@ This is a fully client-side application. Your content never leaves your browser
updateDocumentStats();
updateFindHighlights();
cleanupImageObjectUrls();
+ scheduleLineNumberUpdate();
} catch (e) {
console.error("Markdown rendering failed:", e);
const safeMessage = escapeHtml(e && e.message ? e.message : 'Unknown error');
@@ -3042,6 +3046,94 @@ This is a fully client-side application. Your content never leaves your browser
editorHighlightLayer.scrollLeft = markdownEditor.scrollLeft;
}
+ function updateLineNumberGutter(lineCount) {
+ if (!editorPaneElement) return;
+ const digits = String(Math.max(1, lineCount)).length;
+ const gutterSize = `${Math.max(3, digits + 1)}ch`;
+ editorPaneElement.style.setProperty('--line-number-gutter', gutterSize);
+ }
+
+ function ensureLineNumberMeasure() {
+ if (!lineNumbers) return;
+ if (!lineNumberMeasure) {
+ lineNumberMeasure = document.createElement('div');
+ lineNumberMeasure.setAttribute('aria-hidden', 'true');
+ document.body.appendChild(lineNumberMeasure);
+ }
+ const styles = window.getComputedStyle(markdownEditor);
+ lineNumberMeasure.style.position = 'absolute';
+ lineNumberMeasure.style.visibility = 'hidden';
+ lineNumberMeasure.style.whiteSpace = 'pre-wrap';
+ lineNumberMeasure.style.wordWrap = 'break-word';
+ lineNumberMeasure.style.boxSizing = 'border-box';
+ lineNumberMeasure.style.padding = styles.padding;
+ lineNumberMeasure.style.fontFamily = styles.fontFamily;
+ lineNumberMeasure.style.fontSize = styles.fontSize;
+ lineNumberMeasure.style.lineHeight = styles.lineHeight;
+ lineNumberMeasure.style.letterSpacing = styles.letterSpacing;
+ lineNumberMeasure.style.width = `${markdownEditor.clientWidth}px`;
+ lineNumberMeasure.style.top = '-9999px';
+ lineNumberMeasure.style.left = '-9999px';
+ }
+
+ function getLineHeight(styles) {
+ const computed = parseFloat(styles.lineHeight);
+ if (!Number.isNaN(computed)) return computed;
+ const fontSize = parseFloat(styles.fontSize) || 14;
+ return fontSize * 1.5;
+ }
+
+ function getWrappedLineCount(line, metrics) {
+ if (!lineNumberMeasure) return 1;
+ lineNumberMeasure.textContent = line.length ? line : '\u200b';
+ const contentHeight = lineNumberMeasure.scrollHeight - metrics.paddingTop - metrics.paddingBottom;
+ return Math.max(1, Math.round(contentHeight / metrics.lineHeight));
+ }
+
+ function updateLineNumbers() {
+ if (!lineNumbers || !markdownEditor) return;
+ const lines = (markdownEditor.value || '').split('\n');
+ const lineCount = Math.max(1, lines.length);
+ updateLineNumberGutter(lineCount);
+ ensureLineNumberMeasure();
+ const styles = window.getComputedStyle(markdownEditor);
+ const lineHeight = getLineHeight(styles);
+ const metrics = {
+ lineHeight,
+ paddingTop: parseFloat(styles.paddingTop) || 0,
+ paddingBottom: parseFloat(styles.paddingBottom) || 0,
+ };
+ if (lineNumberMeasure) {
+ lineNumberMeasure.style.width = `${markdownEditor.clientWidth}px`;
+ }
+ const fragment = document.createDocumentFragment();
+ lines.forEach(function(line, index) {
+ const lineNumber = document.createElement('div');
+ lineNumber.className = 'line-number';
+ lineNumber.textContent = index + 1;
+ const wrapCount = getWrappedLineCount(line, metrics);
+ lineNumber.style.height = `${wrapCount * lineHeight}px`;
+ fragment.appendChild(lineNumber);
+ });
+ lineNumbers.textContent = '';
+ lineNumbers.appendChild(fragment);
+ syncLineNumberScroll();
+ }
+
+ function scheduleLineNumberUpdate() {
+ if (!lineNumbers) return;
+ if (lineNumberUpdateFrame) return;
+ lineNumberUpdateFrame = window.requestAnimationFrame(function() {
+ lineNumberUpdateFrame = null;
+ updateLineNumbers();
+ });
+ }
+
+ function syncLineNumberScroll() {
+ if (!lineNumbers) return;
+ lineNumbers.scrollTop = markdownEditor.scrollTop;
+ }
+
function computeFindMatches(value, query) {
if (!query) return [];
const haystack = value.toLowerCase();
@@ -3377,6 +3469,7 @@ This is a fully client-side application. Your content never leaves your browser
const previewPercent = 100 - editorWidthPercent;
editorPaneElement.style.flex = `0 0 calc(${editorWidthPercent}% - 4px)`;
previewPaneElement.style.flex = `0 0 calc(${previewPercent}% - 4px)`;
+ scheduleLineNumberUpdate();
}
function resetPaneWidths() {
@@ -3460,6 +3553,7 @@ This is a fully client-side application. Your content never leaves your browser
// Initialize resizer - Story 1.3
initResizer();
+ window.addEventListener('resize', scheduleLineNumberUpdate);
// View Mode Button Event Listeners - Story 1.1
viewModeButtons.forEach(btn => {
@@ -3489,6 +3583,7 @@ This is a fully client-side application. Your content never leaves your browser
} else {
updateFindHighlights();
}
+ scheduleLineNumberUpdate();
});
initMarkdownFormatToolbar();
@@ -3530,6 +3625,7 @@ This is a fully client-side application. Your content never leaves your browser
editorPane.addEventListener("scroll", function() {
syncEditorToPreview();
syncHighlightScroll();
+ syncLineNumberScroll();
});
previewPane.addEventListener("scroll", syncPreviewToEditor);
toggleSyncButton.addEventListener("click", toggleSyncScrolling);
diff --git a/styles.css b/styles.css
index d1c69d1..efa662b 100644
--- a/styles.css
+++ b/styles.css
@@ -83,6 +83,7 @@ body {
background-color: var(--editor-bg);
border-right: 1px solid var(--border-color);
padding-right: 0px;
+ --line-number-gutter: 0px;
}
.preview-pane {
@@ -127,10 +128,11 @@ body {
font-size: 14px;
line-height: 1.5;
padding: 10px;
+ padding-left: calc(10px + var(--line-number-gutter));
transition: background-color 0.3s ease, color 0.3s ease;
overflow-y: auto;
position: relative;
- z-index: 2;
+ z-index: 3;
}
#markdown-editor:focus {
@@ -433,9 +435,37 @@ body {
display: none;
}
+.line-numbers {
+ position: absolute;
+ top: 20px;
+ bottom: 20px;
+ left: 20px;
+ width: var(--line-number-gutter);
+ padding: 10px 8px 10px 0;
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
+ font-size: 14px;
+ line-height: 1.5;
+ text-align: right;
+ color: var(--text-color);
+ opacity: 0.55;
+ background-color: var(--editor-bg);
+ border-right: 1px solid var(--border-color);
+ box-sizing: border-box;
+ overflow: hidden;
+ pointer-events: none;
+ user-select: none;
+ z-index: 2;
+ font-variant-numeric: tabular-nums;
+}
+
+.line-numbers .line-number {
+ display: block;
+ height: auto;
+}
+
.editor-highlight-layer {
position: absolute;
- inset: 20px 0 20px 20px;
+ inset: 20px 0 20px calc(20px + var(--line-number-gutter));
padding: 10px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 14px;
From 4ec2f29ef0cc60ca178055f3a7229944d83237df Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 May 2026 09:32:04 +0000
Subject: [PATCH 2/4] Clarify line number constants
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/00696684-c211-4305-9b33-1638d9f49bdb
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 7 +++++--
script.js | 7 +++++--
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 50128a1..0ebb4a3 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -238,6 +238,9 @@ document.addEventListener("DOMContentLoaded", function () {
headerIds: true,
mangle: false,
};
+ const LINE_NUMBER_GUTTER_MIN_CH = 3;
+ const LINE_NUMBER_GUTTER_PADDING_CH = 1;
+ const LINE_NUMBER_EMPTY_PLACEHOLDER = '\u200b';
let lineNumberMeasure = null;
let lineNumberUpdateFrame = null;
@@ -3049,7 +3052,7 @@ This is a fully client-side application. Your content never leaves your browser
function updateLineNumberGutter(lineCount) {
if (!editorPaneElement) return;
const digits = String(Math.max(1, lineCount)).length;
- const gutterSize = `${Math.max(3, digits + 1)}ch`;
+ const gutterSize = `${Math.max(LINE_NUMBER_GUTTER_MIN_CH, digits + LINE_NUMBER_GUTTER_PADDING_CH)}ch`;
editorPaneElement.style.setProperty('--line-number-gutter', gutterSize);
}
@@ -3085,7 +3088,7 @@ This is a fully client-side application. Your content never leaves your browser
function getWrappedLineCount(line, metrics) {
if (!lineNumberMeasure) return 1;
- lineNumberMeasure.textContent = line.length ? line : '\u200b';
+ lineNumberMeasure.textContent = line.length ? line : LINE_NUMBER_EMPTY_PLACEHOLDER;
const contentHeight = lineNumberMeasure.scrollHeight - metrics.paddingTop - metrics.paddingBottom;
return Math.max(1, Math.round(contentHeight / metrics.lineHeight));
}
diff --git a/script.js b/script.js
index 50128a1..0ebb4a3 100644
--- a/script.js
+++ b/script.js
@@ -238,6 +238,9 @@ document.addEventListener("DOMContentLoaded", function () {
headerIds: true,
mangle: false,
};
+ const LINE_NUMBER_GUTTER_MIN_CH = 3;
+ const LINE_NUMBER_GUTTER_PADDING_CH = 1;
+ const LINE_NUMBER_EMPTY_PLACEHOLDER = '\u200b';
let lineNumberMeasure = null;
let lineNumberUpdateFrame = null;
@@ -3049,7 +3052,7 @@ This is a fully client-side application. Your content never leaves your browser
function updateLineNumberGutter(lineCount) {
if (!editorPaneElement) return;
const digits = String(Math.max(1, lineCount)).length;
- const gutterSize = `${Math.max(3, digits + 1)}ch`;
+ const gutterSize = `${Math.max(LINE_NUMBER_GUTTER_MIN_CH, digits + LINE_NUMBER_GUTTER_PADDING_CH)}ch`;
editorPaneElement.style.setProperty('--line-number-gutter', gutterSize);
}
@@ -3085,7 +3088,7 @@ This is a fully client-side application. Your content never leaves your browser
function getWrappedLineCount(line, metrics) {
if (!lineNumberMeasure) return 1;
- lineNumberMeasure.textContent = line.length ? line : '\u200b';
+ lineNumberMeasure.textContent = line.length ? line : LINE_NUMBER_EMPTY_PLACEHOLDER;
const contentHeight = lineNumberMeasure.scrollHeight - metrics.paddingTop - metrics.paddingBottom;
return Math.max(1, Math.round(contentHeight / metrics.lineHeight));
}
From 0d85b187c96efc56a5fa41bdf99f2b5a11717ae4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 May 2026 09:34:05 +0000
Subject: [PATCH 3/4] Remove redundant line number width update
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/00696684-c211-4305-9b33-1638d9f49bdb
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 3 ---
script.js | 3 ---
2 files changed, 6 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 0ebb4a3..7ce3de2 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -3106,9 +3106,6 @@ This is a fully client-side application. Your content never leaves your browser
paddingTop: parseFloat(styles.paddingTop) || 0,
paddingBottom: parseFloat(styles.paddingBottom) || 0,
};
- if (lineNumberMeasure) {
- lineNumberMeasure.style.width = `${markdownEditor.clientWidth}px`;
- }
const fragment = document.createDocumentFragment();
lines.forEach(function(line, index) {
const lineNumber = document.createElement('div');
diff --git a/script.js b/script.js
index 0ebb4a3..7ce3de2 100644
--- a/script.js
+++ b/script.js
@@ -3106,9 +3106,6 @@ This is a fully client-side application. Your content never leaves your browser
paddingTop: parseFloat(styles.paddingTop) || 0,
paddingBottom: parseFloat(styles.paddingBottom) || 0,
};
- if (lineNumberMeasure) {
- lineNumberMeasure.style.width = `${markdownEditor.clientWidth}px`;
- }
const fragment = document.createDocumentFragment();
lines.forEach(function(line, index) {
const lineNumber = document.createElement('div');
From ab3839f119cbd75da7d22473a49c77b600ae139f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 May 2026 09:37:01 +0000
Subject: [PATCH 4/4] Optimize line number updates
Agent-Logs-Url: https://github.com/ThisIs-Developer/Markdown-Viewer/sessions/00696684-c211-4305-9b33-1638d9f49bdb
Co-authored-by: ThisIs-Developer <109382325+ThisIs-Developer@users.noreply.github.com>
---
desktop-app/resources/js/script.js | 44 +++++++++++++++++-------------
script.js | 44 +++++++++++++++++-------------
2 files changed, 50 insertions(+), 38 deletions(-)
diff --git a/desktop-app/resources/js/script.js b/desktop-app/resources/js/script.js
index 7ce3de2..1644be0 100644
--- a/desktop-app/resources/js/script.js
+++ b/desktop-app/resources/js/script.js
@@ -3086,11 +3086,11 @@ This is a fully client-side application. Your content never leaves your browser
return fontSize * 1.5;
}
- function getWrappedLineCount(line, metrics) {
+ function getWrappedLineCount(line, lineHeight, paddingSum) {
if (!lineNumberMeasure) return 1;
lineNumberMeasure.textContent = line.length ? line : LINE_NUMBER_EMPTY_PLACEHOLDER;
- const contentHeight = lineNumberMeasure.scrollHeight - metrics.paddingTop - metrics.paddingBottom;
- return Math.max(1, Math.round(contentHeight / metrics.lineHeight));
+ const contentHeight = lineNumberMeasure.scrollHeight - paddingSum;
+ return Math.max(1, Math.round(contentHeight / lineHeight));
}
function updateLineNumbers() {
@@ -3101,22 +3101,28 @@ This is a fully client-side application. Your content never leaves your browser
ensureLineNumberMeasure();
const styles = window.getComputedStyle(markdownEditor);
const lineHeight = getLineHeight(styles);
- const metrics = {
- lineHeight,
- paddingTop: parseFloat(styles.paddingTop) || 0,
- paddingBottom: parseFloat(styles.paddingBottom) || 0,
- };
- const fragment = document.createDocumentFragment();
- lines.forEach(function(line, index) {
- const lineNumber = document.createElement('div');
- lineNumber.className = 'line-number';
- lineNumber.textContent = index + 1;
- const wrapCount = getWrappedLineCount(line, metrics);
- lineNumber.style.height = `${wrapCount * lineHeight}px`;
- fragment.appendChild(lineNumber);
- });
- lineNumbers.textContent = '';
- lineNumbers.appendChild(fragment);
+ const paddingSum =
+ (parseFloat(styles.paddingTop) || 0) +
+ (parseFloat(styles.paddingBottom) || 0);
+ const existingItems = lineNumbers.children;
+ if (existingItems.length !== lineCount) {
+ const fragment = document.createDocumentFragment();
+ lines.forEach(function(line, index) {
+ const lineNumber = document.createElement('div');
+ lineNumber.className = 'line-number';
+ lineNumber.textContent = index + 1;
+ const wrapCount = getWrappedLineCount(line, lineHeight, paddingSum);
+ lineNumber.style.height = `${wrapCount * lineHeight}px`;
+ fragment.appendChild(lineNumber);
+ });
+ lineNumbers.textContent = '';
+ lineNumbers.appendChild(fragment);
+ } else {
+ for (let i = 0; i < lineCount; i += 1) {
+ const wrapCount = getWrappedLineCount(lines[i], lineHeight, paddingSum);
+ existingItems[i].style.height = `${wrapCount * lineHeight}px`;
+ }
+ }
syncLineNumberScroll();
}
diff --git a/script.js b/script.js
index 7ce3de2..1644be0 100644
--- a/script.js
+++ b/script.js
@@ -3086,11 +3086,11 @@ This is a fully client-side application. Your content never leaves your browser
return fontSize * 1.5;
}
- function getWrappedLineCount(line, metrics) {
+ function getWrappedLineCount(line, lineHeight, paddingSum) {
if (!lineNumberMeasure) return 1;
lineNumberMeasure.textContent = line.length ? line : LINE_NUMBER_EMPTY_PLACEHOLDER;
- const contentHeight = lineNumberMeasure.scrollHeight - metrics.paddingTop - metrics.paddingBottom;
- return Math.max(1, Math.round(contentHeight / metrics.lineHeight));
+ const contentHeight = lineNumberMeasure.scrollHeight - paddingSum;
+ return Math.max(1, Math.round(contentHeight / lineHeight));
}
function updateLineNumbers() {
@@ -3101,22 +3101,28 @@ This is a fully client-side application. Your content never leaves your browser
ensureLineNumberMeasure();
const styles = window.getComputedStyle(markdownEditor);
const lineHeight = getLineHeight(styles);
- const metrics = {
- lineHeight,
- paddingTop: parseFloat(styles.paddingTop) || 0,
- paddingBottom: parseFloat(styles.paddingBottom) || 0,
- };
- const fragment = document.createDocumentFragment();
- lines.forEach(function(line, index) {
- const lineNumber = document.createElement('div');
- lineNumber.className = 'line-number';
- lineNumber.textContent = index + 1;
- const wrapCount = getWrappedLineCount(line, metrics);
- lineNumber.style.height = `${wrapCount * lineHeight}px`;
- fragment.appendChild(lineNumber);
- });
- lineNumbers.textContent = '';
- lineNumbers.appendChild(fragment);
+ const paddingSum =
+ (parseFloat(styles.paddingTop) || 0) +
+ (parseFloat(styles.paddingBottom) || 0);
+ const existingItems = lineNumbers.children;
+ if (existingItems.length !== lineCount) {
+ const fragment = document.createDocumentFragment();
+ lines.forEach(function(line, index) {
+ const lineNumber = document.createElement('div');
+ lineNumber.className = 'line-number';
+ lineNumber.textContent = index + 1;
+ const wrapCount = getWrappedLineCount(line, lineHeight, paddingSum);
+ lineNumber.style.height = `${wrapCount * lineHeight}px`;
+ fragment.appendChild(lineNumber);
+ });
+ lineNumbers.textContent = '';
+ lineNumbers.appendChild(fragment);
+ } else {
+ for (let i = 0; i < lineCount; i += 1) {
+ const wrapCount = getWrappedLineCount(lines[i], lineHeight, paddingSum);
+ existingItems[i].style.height = `${wrapCount * lineHeight}px`;
+ }
+ }
syncLineNumberScroll();
}