Skip to content

Commit 52316c2

Browse files
fix(find): implement preview highlighting, auto-scrolling, and resolve editor alignment (fixes #169)
1 parent 841a184 commit 52316c2

4 files changed

Lines changed: 336 additions & 0 deletions

File tree

desktop-app/resources/js/script.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4659,6 +4659,8 @@ document.addEventListener("DOMContentLoaded", function () {
46594659
}
46604660

46614661
function updateFindHighlights() {
4662+
updatePreviewFindHighlights();
4663+
46624664
if (!editorHighlightLayer) return;
46634665
if (!isEditorVisible()) return;
46644666
if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !findMatches.length) {
@@ -4687,6 +4689,157 @@ document.addEventListener("DOMContentLoaded", function () {
46874689
editorHighlightLayer.scrollLeft = scrollLeft;
46884690
}
46894691

4692+
let previewHighlights = [];
4693+
let activePreviewHighlightIndex = -1;
4694+
4695+
function isPreviewVisible() {
4696+
return currentViewMode === 'preview' || currentViewMode === 'split';
4697+
}
4698+
4699+
function clearPreviewFindHighlights() {
4700+
if (!markdownPreview) return;
4701+
const highlights = markdownPreview.querySelectorAll('.preview-find-highlight');
4702+
highlights.forEach(function(el) {
4703+
const parent = el.parentNode;
4704+
if (parent) {
4705+
parent.replaceChild(document.createTextNode(el.textContent), el);
4706+
}
4707+
});
4708+
markdownPreview.normalize();
4709+
}
4710+
4711+
function highlightPreviewText(node, regex) {
4712+
if (!node) return;
4713+
if (node.nodeType === Node.TEXT_NODE) {
4714+
const val = node.nodeValue;
4715+
if (!val) return;
4716+
4717+
regex.lastIndex = 0;
4718+
let match;
4719+
const matches = [];
4720+
while ((match = regex.exec(val)) !== null) {
4721+
if (match[0].length === 0) {
4722+
regex.lastIndex++;
4723+
continue;
4724+
}
4725+
matches.push({
4726+
start: match.index,
4727+
end: match.index + match[0].length,
4728+
text: match[0]
4729+
});
4730+
}
4731+
4732+
if (matches.length > 0) {
4733+
const parent = node.parentNode;
4734+
if (!parent) return;
4735+
4736+
const fragment = document.createDocumentFragment();
4737+
let lastIdx = 0;
4738+
4739+
matches.forEach(function(m) {
4740+
if (m.start > lastIdx) {
4741+
fragment.appendChild(document.createTextNode(val.slice(lastIdx, m.start)));
4742+
}
4743+
const mark = document.createElement('mark');
4744+
mark.className = 'preview-find-highlight';
4745+
mark.textContent = m.text;
4746+
fragment.appendChild(mark);
4747+
lastIdx = m.end;
4748+
});
4749+
4750+
if (lastIdx < val.length) {
4751+
fragment.appendChild(document.createTextNode(val.slice(lastIdx)));
4752+
}
4753+
4754+
parent.replaceChild(fragment, node);
4755+
}
4756+
} else if (node.nodeType === Node.ELEMENT_NODE) {
4757+
const tagName = node.tagName.toLowerCase();
4758+
if (tagName === 'script' || tagName === 'style' || tagName === 'textarea' || tagName === 'noscript' || tagName === 'svg') {
4759+
return;
4760+
}
4761+
if (node.classList.contains('mermaid') || node.classList.contains('mjx-container') || node.closest('.mermaid') || node.closest('.mjx-container')) {
4762+
return;
4763+
}
4764+
4765+
const children = Array.from(node.childNodes);
4766+
children.forEach(function(child) {
4767+
highlightPreviewText(child, regex);
4768+
});
4769+
}
4770+
}
4771+
4772+
function updatePreviewFindHighlights() {
4773+
clearPreviewFindHighlights();
4774+
previewHighlights = [];
4775+
4776+
if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !isPreviewVisible()) {
4777+
return;
4778+
}
4779+
4780+
const query = findReplaceInput.value;
4781+
const isRegex = document.getElementById('find-regex').classList.contains('active');
4782+
const isCaseSensitive = document.getElementById('find-case').classList.contains('active');
4783+
const isWholeWord = document.getElementById('find-word').classList.contains('active');
4784+
4785+
let regex;
4786+
try {
4787+
let pattern = isRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4788+
if (isWholeWord) {
4789+
pattern = `\\b${pattern}\\b`;
4790+
}
4791+
const flags = isCaseSensitive ? 'g' : 'gi';
4792+
regex = new RegExp(pattern, flags);
4793+
} catch (e) {
4794+
return;
4795+
}
4796+
4797+
highlightPreviewText(markdownPreview, regex);
4798+
previewHighlights = Array.from(markdownPreview.querySelectorAll('.preview-find-highlight'));
4799+
updateActivePreviewHighlight();
4800+
}
4801+
4802+
function updateActivePreviewHighlight() {
4803+
previewHighlights.forEach(function(el) {
4804+
el.classList.remove('active');
4805+
});
4806+
4807+
if (!previewHighlights.length) {
4808+
activePreviewHighlightIndex = -1;
4809+
return;
4810+
}
4811+
4812+
if (findMatches.length > 0 && activeFindIndex >= 0) {
4813+
const ratio = activeFindIndex / findMatches.length;
4814+
activePreviewHighlightIndex = Math.min(
4815+
previewHighlights.length - 1,
4816+
Math.floor(ratio * previewHighlights.length)
4817+
);
4818+
} else {
4819+
activePreviewHighlightIndex = 0;
4820+
}
4821+
4822+
if (activePreviewHighlightIndex >= 0 && activePreviewHighlightIndex < previewHighlights.length) {
4823+
const activeEl = previewHighlights[activePreviewHighlightIndex];
4824+
activeEl.classList.add('active');
4825+
scrollPreviewHighlightIntoView(activeEl);
4826+
}
4827+
}
4828+
4829+
function scrollPreviewHighlightIntoView(element) {
4830+
if (!element || !previewPane) return;
4831+
const paneRect = previewPane.getBoundingClientRect();
4832+
const elemRect = element.getBoundingClientRect();
4833+
const isVisible = (
4834+
elemRect.top >= paneRect.top + 40 &&
4835+
elemRect.bottom <= paneRect.bottom - 40
4836+
);
4837+
if (!isVisible) {
4838+
const scrollTop = previewPane.scrollTop + (elemRect.top - paneRect.top) - (paneRect.height / 2) + (elemRect.height / 2);
4839+
previewPane.scrollTop = scrollTop;
4840+
}
4841+
}
4842+
46904843
function syncHighlightScroll() {
46914844
if (!editorHighlightLayer) return;
46924845
editorHighlightLayer.scrollTop = cachedScrollTop;

desktop-app/resources/styles.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,11 +613,26 @@ body {
613613
background-color: var(--fr-match-highlight, rgba(255, 223, 93, 0.4)) !important;
614614
border-radius: 2px;
615615
color: transparent !important;
616+
padding: 0 !important;
617+
margin: 0 !important;
616618
}
617619

618620
.find-highlight.active {
619621
background-color: var(--fr-match-active, #ff9b30) !important;
620622
color: transparent !important;
623+
padding: 0 !important;
624+
margin: 0 !important;
625+
}
626+
627+
.preview-find-highlight {
628+
background-color: var(--fr-match-highlight, rgba(255, 223, 93, 0.4)) !important;
629+
border-radius: 2px;
630+
padding: 0 1px !important;
631+
margin: 0 !important;
632+
}
633+
634+
.preview-find-highlight.active {
635+
background-color: var(--fr-match-active, #ff9b30) !important;
621636
}
622637

623638
/* Dropdown improvements */

script.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4659,6 +4659,8 @@ document.addEventListener("DOMContentLoaded", function () {
46594659
}
46604660

46614661
function updateFindHighlights() {
4662+
updatePreviewFindHighlights();
4663+
46624664
if (!editorHighlightLayer) return;
46634665
if (!isEditorVisible()) return;
46644666
if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !findMatches.length) {
@@ -4687,6 +4689,157 @@ document.addEventListener("DOMContentLoaded", function () {
46874689
editorHighlightLayer.scrollLeft = scrollLeft;
46884690
}
46894691

4692+
let previewHighlights = [];
4693+
let activePreviewHighlightIndex = -1;
4694+
4695+
function isPreviewVisible() {
4696+
return currentViewMode === 'preview' || currentViewMode === 'split';
4697+
}
4698+
4699+
function clearPreviewFindHighlights() {
4700+
if (!markdownPreview) return;
4701+
const highlights = markdownPreview.querySelectorAll('.preview-find-highlight');
4702+
highlights.forEach(function(el) {
4703+
const parent = el.parentNode;
4704+
if (parent) {
4705+
parent.replaceChild(document.createTextNode(el.textContent), el);
4706+
}
4707+
});
4708+
markdownPreview.normalize();
4709+
}
4710+
4711+
function highlightPreviewText(node, regex) {
4712+
if (!node) return;
4713+
if (node.nodeType === Node.TEXT_NODE) {
4714+
const val = node.nodeValue;
4715+
if (!val) return;
4716+
4717+
regex.lastIndex = 0;
4718+
let match;
4719+
const matches = [];
4720+
while ((match = regex.exec(val)) !== null) {
4721+
if (match[0].length === 0) {
4722+
regex.lastIndex++;
4723+
continue;
4724+
}
4725+
matches.push({
4726+
start: match.index,
4727+
end: match.index + match[0].length,
4728+
text: match[0]
4729+
});
4730+
}
4731+
4732+
if (matches.length > 0) {
4733+
const parent = node.parentNode;
4734+
if (!parent) return;
4735+
4736+
const fragment = document.createDocumentFragment();
4737+
let lastIdx = 0;
4738+
4739+
matches.forEach(function(m) {
4740+
if (m.start > lastIdx) {
4741+
fragment.appendChild(document.createTextNode(val.slice(lastIdx, m.start)));
4742+
}
4743+
const mark = document.createElement('mark');
4744+
mark.className = 'preview-find-highlight';
4745+
mark.textContent = m.text;
4746+
fragment.appendChild(mark);
4747+
lastIdx = m.end;
4748+
});
4749+
4750+
if (lastIdx < val.length) {
4751+
fragment.appendChild(document.createTextNode(val.slice(lastIdx)));
4752+
}
4753+
4754+
parent.replaceChild(fragment, node);
4755+
}
4756+
} else if (node.nodeType === Node.ELEMENT_NODE) {
4757+
const tagName = node.tagName.toLowerCase();
4758+
if (tagName === 'script' || tagName === 'style' || tagName === 'textarea' || tagName === 'noscript' || tagName === 'svg') {
4759+
return;
4760+
}
4761+
if (node.classList.contains('mermaid') || node.classList.contains('mjx-container') || node.closest('.mermaid') || node.closest('.mjx-container')) {
4762+
return;
4763+
}
4764+
4765+
const children = Array.from(node.childNodes);
4766+
children.forEach(function(child) {
4767+
highlightPreviewText(child, regex);
4768+
});
4769+
}
4770+
}
4771+
4772+
function updatePreviewFindHighlights() {
4773+
clearPreviewFindHighlights();
4774+
previewHighlights = [];
4775+
4776+
if (!isFindModalOpen || !findReplaceInput || !findReplaceInput.value || !isPreviewVisible()) {
4777+
return;
4778+
}
4779+
4780+
const query = findReplaceInput.value;
4781+
const isRegex = document.getElementById('find-regex').classList.contains('active');
4782+
const isCaseSensitive = document.getElementById('find-case').classList.contains('active');
4783+
const isWholeWord = document.getElementById('find-word').classList.contains('active');
4784+
4785+
let regex;
4786+
try {
4787+
let pattern = isRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4788+
if (isWholeWord) {
4789+
pattern = `\\b${pattern}\\b`;
4790+
}
4791+
const flags = isCaseSensitive ? 'g' : 'gi';
4792+
regex = new RegExp(pattern, flags);
4793+
} catch (e) {
4794+
return;
4795+
}
4796+
4797+
highlightPreviewText(markdownPreview, regex);
4798+
previewHighlights = Array.from(markdownPreview.querySelectorAll('.preview-find-highlight'));
4799+
updateActivePreviewHighlight();
4800+
}
4801+
4802+
function updateActivePreviewHighlight() {
4803+
previewHighlights.forEach(function(el) {
4804+
el.classList.remove('active');
4805+
});
4806+
4807+
if (!previewHighlights.length) {
4808+
activePreviewHighlightIndex = -1;
4809+
return;
4810+
}
4811+
4812+
if (findMatches.length > 0 && activeFindIndex >= 0) {
4813+
const ratio = activeFindIndex / findMatches.length;
4814+
activePreviewHighlightIndex = Math.min(
4815+
previewHighlights.length - 1,
4816+
Math.floor(ratio * previewHighlights.length)
4817+
);
4818+
} else {
4819+
activePreviewHighlightIndex = 0;
4820+
}
4821+
4822+
if (activePreviewHighlightIndex >= 0 && activePreviewHighlightIndex < previewHighlights.length) {
4823+
const activeEl = previewHighlights[activePreviewHighlightIndex];
4824+
activeEl.classList.add('active');
4825+
scrollPreviewHighlightIntoView(activeEl);
4826+
}
4827+
}
4828+
4829+
function scrollPreviewHighlightIntoView(element) {
4830+
if (!element || !previewPane) return;
4831+
const paneRect = previewPane.getBoundingClientRect();
4832+
const elemRect = element.getBoundingClientRect();
4833+
const isVisible = (
4834+
elemRect.top >= paneRect.top + 40 &&
4835+
elemRect.bottom <= paneRect.bottom - 40
4836+
);
4837+
if (!isVisible) {
4838+
const scrollTop = previewPane.scrollTop + (elemRect.top - paneRect.top) - (paneRect.height / 2) + (elemRect.height / 2);
4839+
previewPane.scrollTop = scrollTop;
4840+
}
4841+
}
4842+
46904843
function syncHighlightScroll() {
46914844
if (!editorHighlightLayer) return;
46924845
editorHighlightLayer.scrollTop = cachedScrollTop;

styles.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,11 +613,26 @@ body {
613613
background-color: var(--fr-match-highlight, rgba(255, 223, 93, 0.4)) !important;
614614
border-radius: 2px;
615615
color: transparent !important;
616+
padding: 0 !important;
617+
margin: 0 !important;
616618
}
617619

618620
.find-highlight.active {
619621
background-color: var(--fr-match-active, #ff9b30) !important;
620622
color: transparent !important;
623+
padding: 0 !important;
624+
margin: 0 !important;
625+
}
626+
627+
.preview-find-highlight {
628+
background-color: var(--fr-match-highlight, rgba(255, 223, 93, 0.4)) !important;
629+
border-radius: 2px;
630+
padding: 0 1px !important;
631+
margin: 0 !important;
632+
}
633+
634+
.preview-find-highlight.active {
635+
background-color: var(--fr-match-active, #ff9b30) !important;
621636
}
622637

623638
/* Dropdown improvements */

0 commit comments

Comments
 (0)