mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 03:41:28 +00:00
perf: linkAware slow in large files (#6422)
* bugfix: linkAware slow in large files - Added link detection and class addition operations in an editor.operation block for atomic operations and prevent multiple small rerenders. - linkAware works on currently visible lines in the viewport. This is to speedup linkAware and defer detection for lines not in viewport. - linkAware now runs after initial render and not before it. This ensures that we calculate to the lines on viewport and does not pause render. * test(bruno-app): fix linkAware spec for debounced viewport marking
This commit is contained in:
committed by
GitHub
parent
80e09d1a26
commit
678fa88a7c
@@ -1,8 +1,27 @@
|
||||
import LinkifyIt from 'linkify-it';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import { debounce } from 'lodash';
|
||||
/**
|
||||
* Gets the visible line range using scroll info and lineAtHeight
|
||||
* @param {Object} editor - The CodeMirror editor instance
|
||||
* @param {number} padding - Number of lines to add above and below viewport
|
||||
* @returns {Object} Object with from and to line numbers
|
||||
*/
|
||||
function getVisibleLineRange(editor, padding = 3) {
|
||||
const doc = editor.getDoc();
|
||||
const scroll = editor.getScrollInfo();
|
||||
const topLine = editor.lineAtHeight(scroll.top, 'local');
|
||||
const bottomLine = editor.lineAtHeight(scroll.top + scroll.clientHeight, 'local');
|
||||
|
||||
return {
|
||||
from: Math.max(0, topLine - padding),
|
||||
to: Math.min(doc.lineCount(), bottomLine + padding + 1) // +1 because to is exclusive
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks URLs in the CodeMirror editor with clickable link styling
|
||||
* Only processes links in the visible viewport for performance
|
||||
* @param {Object} editor - The CodeMirror editor instance
|
||||
* @param {Object} linkify - The LinkifyIt instance for URL detection
|
||||
* @param {string} linkClass - CSS class name for links
|
||||
@@ -10,25 +29,55 @@ import { debounce } from 'lodash';
|
||||
*/
|
||||
function markUrls(editor, linkify, linkClass, linkHint) {
|
||||
const doc = editor.getDoc();
|
||||
const text = doc.getValue();
|
||||
const { from: fromLine, to: toLine } = getVisibleLineRange(editor, 3);
|
||||
|
||||
// Clear existing link marks
|
||||
editor.getAllMarks().forEach((mark) => {
|
||||
if (mark.className === linkClass) mark.clear();
|
||||
});
|
||||
// Use editor.operation() to batch all mark operations for better performance
|
||||
editor.operation(() => {
|
||||
// Clear only link marks that overlap the visible range
|
||||
editor.getAllMarks().forEach((mark) => {
|
||||
if (mark.className !== linkClass) return;
|
||||
|
||||
// Find and mark new URLs
|
||||
const matches = linkify.match(text);
|
||||
matches?.forEach(({ index, lastIndex, url }) => {
|
||||
const from = editor.posFromIndex(index);
|
||||
const to = editor.posFromIndex(lastIndex);
|
||||
editor.markText(from, to, {
|
||||
className: linkClass,
|
||||
attributes: {
|
||||
'data-url': url,
|
||||
'title': linkHint
|
||||
// Check if mark overlaps visible range
|
||||
const pos = mark.find?.();
|
||||
if (!pos) {
|
||||
// If we can't find position, clear it to be safe
|
||||
mark.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear marks that overlap the visible range
|
||||
if (pos.to.line >= fromLine && pos.from.line < toLine) {
|
||||
mark.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// Find and mark URLs in visible lines only
|
||||
for (let lineNum = fromLine; lineNum < toLine; lineNum++) {
|
||||
const lineContent = doc.getLine(lineNum);
|
||||
if (!lineContent) continue;
|
||||
|
||||
const matches = linkify.match(lineContent);
|
||||
if (!matches) continue;
|
||||
|
||||
matches.forEach(({ index, lastIndex, url }) => {
|
||||
try {
|
||||
editor.markText(
|
||||
{ line: lineNum, ch: index },
|
||||
{ line: lineNum, ch: lastIndex },
|
||||
{
|
||||
className: linkClass,
|
||||
attributes: {
|
||||
'data-url': url,
|
||||
'title': linkHint
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// Silently ignore marking errors (e.g., if positions are invalid)
|
||||
// This can happen if the line content changed between getting it and marking
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -153,16 +202,24 @@ function setupLinkAware(editor, options = {}) {
|
||||
const boundHandleMouseEnter = (event) => handleMouseEnter(event, linkClass, linkHoverClass, boundUpdateCmdCtrlClass);
|
||||
const boundHandleMouseLeave = (event) => handleMouseLeave(event, linkClass, linkHoverClass);
|
||||
|
||||
// Create debounced version of markUrls
|
||||
// Create debounced version of markUrls that runs after rendering
|
||||
const debouncedMarkUrls = debounce(() => {
|
||||
requestAnimationFrame(boundMarkUrls);
|
||||
requestAnimationFrame(() => {
|
||||
// Skip if the editor is hidden (e.g., tab not visible)
|
||||
if (!editorWrapper.offsetParent) return;
|
||||
boundMarkUrls();
|
||||
});
|
||||
}, 150);
|
||||
|
||||
// Initial URL marking
|
||||
boundMarkUrls();
|
||||
// Run after the first render/refresh
|
||||
editor.on('refresh', debouncedMarkUrls);
|
||||
|
||||
// Set up event listeners
|
||||
editor.on('changes', debouncedMarkUrls);
|
||||
|
||||
// Listen for scroll events to update marks when viewport changes
|
||||
editor.on('scroll', debouncedMarkUrls);
|
||||
|
||||
window.addEventListener('keydown', boundUpdateCmdCtrlClass);
|
||||
window.addEventListener('keyup', boundUpdateCmdCtrlClass);
|
||||
editorWrapper.addEventListener('click', boundHandleClick);
|
||||
@@ -171,7 +228,9 @@ function setupLinkAware(editor, options = {}) {
|
||||
|
||||
// Cleanup function to remove all event listeners
|
||||
editor._destroyLinkAware = () => {
|
||||
editor.off('refresh', debouncedMarkUrls);
|
||||
editor.off('changes', debouncedMarkUrls);
|
||||
editor.off('scroll', debouncedMarkUrls);
|
||||
window.removeEventListener('keydown', boundUpdateCmdCtrlClass);
|
||||
window.removeEventListener('keyup', boundUpdateCmdCtrlClass);
|
||||
editorWrapper.removeEventListener('click', boundHandleClick);
|
||||
|
||||
@@ -54,7 +54,9 @@ describe('setupLinkAware', () => {
|
||||
remove: jest.fn()
|
||||
},
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn()
|
||||
removeEventListener: jest.fn(),
|
||||
// Link marking is skipped if editor is hidden; offsetParent is null when hidden
|
||||
offsetParent: {}
|
||||
};
|
||||
|
||||
mockMark = {
|
||||
@@ -63,7 +65,11 @@ describe('setupLinkAware', () => {
|
||||
};
|
||||
|
||||
mockDoc = {
|
||||
getValue: jest.fn().mockReturnValue('Check out https://example.com and http://test.org')
|
||||
getValue: jest.fn().mockReturnValue('Check out https://example.com and http://test.org'),
|
||||
getLine: jest.fn().mockImplementation((lineNum) =>
|
||||
lineNum === 0 ? 'Check out https://example.com and http://test.org' : ''
|
||||
),
|
||||
lineCount: jest.fn().mockReturnValue(1)
|
||||
};
|
||||
|
||||
mockEditor = {
|
||||
@@ -72,6 +78,9 @@ describe('setupLinkAware', () => {
|
||||
markText: jest.fn(),
|
||||
posFromIndex: jest.fn().mockImplementation((index) => ({ line: 0, ch: index })),
|
||||
getWrapperElement: jest.fn().mockReturnValue(mockWrapperElement),
|
||||
operation: jest.fn((fn) => fn()),
|
||||
getScrollInfo: jest.fn().mockReturnValue({ top: 0, clientHeight: 100 }),
|
||||
lineAtHeight: jest.fn().mockReturnValue(0),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
_destroyLinkAware: undefined
|
||||
@@ -142,6 +151,10 @@ describe('setupLinkAware', () => {
|
||||
isMacOS.mockReturnValue(true);
|
||||
setupLinkAware(mockEditor);
|
||||
|
||||
const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];
|
||||
changeHandler();
|
||||
jest.runAllTimers();
|
||||
|
||||
// Verify that markUrls was called which sets the hint
|
||||
expect(mockEditor.markText).toHaveBeenCalledWith(expect.anything(),
|
||||
expect.anything(),
|
||||
@@ -156,6 +169,10 @@ describe('setupLinkAware', () => {
|
||||
isMacOS.mockReturnValue(false);
|
||||
setupLinkAware(mockEditor);
|
||||
|
||||
const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];
|
||||
changeHandler();
|
||||
jest.runAllTimers();
|
||||
|
||||
// Verify that markUrls was called which sets the hint
|
||||
expect(mockEditor.markText).toHaveBeenCalledWith(expect.anything(),
|
||||
expect.anything(),
|
||||
@@ -303,8 +320,13 @@ describe('setupLinkAware', () => {
|
||||
});
|
||||
|
||||
it('should apply link tooltips when marking URLs', () => {
|
||||
isMacOS.mockReturnValue(true);
|
||||
setupLinkAware(mockEditor);
|
||||
|
||||
const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];
|
||||
changeHandler();
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(mockEditor.markText).toHaveBeenCalledWith({ line: 0, ch: 10 },
|
||||
{ line: 0, ch: 28 },
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user