Skip to content

Commit e4b7349

Browse files
committed
chore: visually show adjucent line git marks for add/del/change as a single scroll line
1 parent 86a70ba commit e4b7349

File tree

3 files changed

+350
-86
lines changed

3 files changed

+350
-86
lines changed

src/search/FindReplace.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ define(function (require, exports, module) {
396396
function clearCurrentMatchHighlight(editor, state) {
397397
if (state.markedCurrent) {
398398
state.markedCurrent.clear();
399-
ScrollTrackMarkers.markCurrent(-1, editor);
399+
ScrollTrackMarkers._markCurrent(-1, editor);
400400
}
401401
}
402402

@@ -424,7 +424,7 @@ define(function (require, exports, module) {
424424
{from: thisMatch.start, to: thisMatch.end}, false);
425425
// Update current-tickmark indicator - only if highlighting enabled (disabled if FIND_HIGHLIGHT_MAX threshold hit)
426426
if (state.marked.length) {
427-
ScrollTrackMarkers.markCurrent(state.matchIndex, editor); // _updateFindBarWithMatchInfo() has updated this index
427+
ScrollTrackMarkers._markCurrent(state.matchIndex, editor); // _updateFindBarWithMatchInfo() has updated this index
428428
}
429429
}
430430

@@ -464,7 +464,7 @@ define(function (require, exports, module) {
464464
{from: nextMatch.start, to: nextMatch.end}, searchBackwards);
465465
// Update current-tickmark indicator - only if highlighting enabled (disabled if FIND_HIGHLIGHT_MAX threshold hit)
466466
if (state.marked.length) {
467-
ScrollTrackMarkers.markCurrent(state.matchIndex, editor); // _updateFindBarWithMatchInfo() has updated this index
467+
ScrollTrackMarkers._markCurrent(state.matchIndex, editor); // _updateFindBarWithMatchInfo() has updated this index
468468
}
469469
}
470470

@@ -522,8 +522,6 @@ define(function (require, exports, module) {
522522
} else {
523523
$(editor.getRootElement()).removeClass("find-highlighting");
524524
}
525-
526-
ScrollTrackMarkers.setVisible(editor, enabled);
527525
}
528526

529527
/**
@@ -579,7 +577,9 @@ define(function (require, exports, module) {
579577
return result.from;
580578
});
581579

582-
ScrollTrackMarkers.addTickmarks(editor, scrollTrackPositions);
580+
ScrollTrackMarkers.addTickmarks(editor, scrollTrackPositions, {
581+
dontMerge: true
582+
});
583583
}
584584

585585
// Here we only update find bar with no result. In the case of a match

src/search/ScrollTrackMarkers.js

Lines changed: 171 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -169,53 +169,50 @@ define(function (require, exports, module) {
169169
* @param {Array.<{line: number, ch: number}>} posArray
170170
*/
171171
function _renderMarks(editor, posArray) {
172-
const cm = editor._codeMirror;
173-
const markerState = _getMarkerState(editor);
174-
const $track = $(".tickmark-track", editor.getRootElement());
175-
const editorHt = cm.getScrollerElement().scrollHeight;
172+
const cm = editor._codeMirror;
173+
const markerState = _getMarkerState(editor);
174+
const $track = $(".tickmark-track", editor.getRootElement());
175+
const editorHt = cm.getScrollerElement().scrollHeight;
176+
const wrapping = cm.getOption("lineWrapping");
176177

177-
const wrapping = cm.getOption("lineWrapping");
178-
const singleLineH = wrapping && cm.defaultTextHeight() * 1.5;
179-
180-
// For performance, precompute top for each mark
178+
// We'll collect all the normalized (top, bottom) positions here
181179
const markPositions = [];
182-
let curLine = null, curLineObj = null;
183-
184-
function getY(pos) {
185-
if (curLine !== pos.line) {
186-
curLine = pos.line;
187-
curLineObj = cm.getLineHandle(curLine);
188-
}
189-
if (wrapping && curLineObj && curLineObj.height > singleLineH) {
190-
return cm.charCoords(pos, "local").top;
191-
}
192-
return cm.heightAtLine(curLineObj, "local");
193-
}
194180

195181
posArray.forEach(function (pos) {
196-
const y = getY(pos);
197-
const ratio = editorHt ? (y / editorHt) : 0;
198-
const top = Math.round(ratio * markerState.trackHt) + markerState.trackOffset - 1;
199-
200-
let markerHeight = MARKER_HEIGHT_LINE;
201-
let isLine = true;
202-
if (pos.options.trackStyle === TRACK_STYLES.ON_LEFT) {
203-
markerHeight = MARKER_HEIGHT_LEFT;
204-
isLine = false;
205-
}
182+
// Extract the style info
183+
const trackStyle = pos.options.trackStyle || TRACK_STYLES.LINE;
184+
const cssColorClass = pos.options.cssColorClass || "";
185+
186+
// Decide which marker height to use
187+
const isLineMarker = (trackStyle === TRACK_STYLES.LINE);
188+
const markerHeight = isLineMarker ? MARKER_HEIGHT_LINE : MARKER_HEIGHT_LEFT;
189+
190+
// We'll measure the 'start' of the range and the 'end' of the range
191+
const startPos = pos.start || pos; // Fallback, in case it's single
192+
const endPos = pos.end || pos; // Fallback, in case it's single
193+
194+
// Compute the top offset for the start
195+
const startY = _computeY(startPos);
196+
// Compute the top offset for the end
197+
const endY = _computeY(endPos);
198+
199+
// Put them in ascending order
200+
const topY = Math.min(startY, endY);
201+
const bottomY = Math.max(startY, endY) + markerHeight;
206202

207203
markPositions.push({
208-
top: top,
209-
bottom: top + markerHeight,
210-
isLine: isLine,
211-
cssColorClass: pos.options.cssColorClass || ""
204+
top: topY,
205+
bottom: bottomY,
206+
isLine: isLineMarker,
207+
cssColorClass
212208
});
213209
});
214210

215-
// Sort them by top coordinate
216-
markPositions.sort(function (a, b) { return a.top - b.top; });
211+
// Merge/condense overlapping or adjacent segments, same as before
212+
markPositions.sort(function (a, b) {
213+
return a.top - b.top;
214+
});
217215

218-
// Merge nearby or overlapping segments
219216
const mergedLineMarks = [];
220217
const mergedLeftMarks = [];
221218

@@ -234,20 +231,38 @@ define(function (require, exports, module) {
234231
mergedMarks.push(mark);
235232
});
236233

237-
// Build HTML for horizontal marks
234+
// Now render them into the DOM
235+
// (1) For the "line" style
238236
let html = mergedLineMarks.map(function (m) {
239-
return `<div class='tickmark ${m.cssColorClass}' style='top: ${m.top}px; height: ${m.height}px;'></div>`;
237+
return `<div class='tickmark ${m.cssColorClass}'
238+
style='top: ${m.top}px; height: ${m.height}px;'></div>`;
240239
}).join("");
241240
$track.append($(html));
242241

243-
// Build HTML for vertical marks
242+
// (2) For the "left" style
244243
html = mergedLeftMarks.map(function (m) {
245-
return `<div class='tickmark tickmark-side ${
246-
m.cssColorClass}' style='top: ${m.top}px; height: ${m.height}px;'></div>`;
244+
return `<div class='tickmark tickmark-side ${m.cssColorClass}'
245+
style='top: ${m.top}px; height: ${m.height}px;'></div>`;
247246
}).join("");
248247
$track.append($(html));
248+
249+
/**
250+
* Helper function to compute Y offset for a given {line, ch} position
251+
*/
252+
function _computeY(cmPos) {
253+
if (wrapping) {
254+
// For wrapped lines, measure the exact Y-position in the editor
255+
return cm.charCoords(cmPos, "local").top / editorHt * markerState.trackHt
256+
+ markerState.trackOffset - 1;
257+
}
258+
// For unwrapped lines, we can do a simpler approach
259+
const cursorTop = cm.heightAtLine(cmPos.line, "local");
260+
const ratio = editorHt ? (cursorTop / editorHt) : 0;
261+
return Math.round(ratio * markerState.trackHt) + markerState.trackOffset - 1;
262+
}
249263
}
250264

265+
251266
/**
252267
* Private helper: Show the track if it's not already visible.
253268
* @param {!Editor} editor
@@ -381,58 +396,143 @@ define(function (require, exports, module) {
381396
}
382397

383398
/**
384-
* Adds tickmarks for the given positions into the editor's tickmark track.
399+
* Merges an array of tickmark ranges if they are adjacent or overlapping in lines.
400+
* All items are assumed to be in the shape:
401+
* {
402+
* start: { line: number, ch: number },
403+
* end: { line: number, ch: number },
404+
* options: Object
405+
* }
406+
*
407+
* @param {Array} markArray
408+
* @return {Array} A new array with merged ranges.
409+
*/
410+
function _mergeMarks(markArray) {
411+
// 1) Sort by starting line (and ch if you want a finer sort)
412+
markArray.sort((a, b) => {
413+
if (a.start.line !== b.start.line) {
414+
return a.start.line - b.start.line;
415+
}
416+
return a.start.ch - b.start.ch;
417+
});
418+
419+
const merged = [];
420+
let current = null;
421+
422+
for (const mark of markArray) {
423+
// If we're not currently building a merged range, start one
424+
if (!current) {
425+
current = {
426+
start: { ...mark.start },
427+
end: { ...mark.end },
428+
options: mark.options
429+
};
430+
} else {
431+
// Check if the new mark is adjacent or overlaps the current range
432+
// i.e. if mark's start is <= current's end.line + 1
433+
if (mark.start.line <= current.end.line + 1) {
434+
// Merge them by extending current.end if needed
435+
if (mark.end.line > current.end.line) {
436+
current.end.line = mark.end.line;
437+
current.end.ch = mark.end.ch;
438+
} else if (mark.end.line === current.end.line && mark.end.ch > current.end.ch) {
439+
current.end.ch = mark.end.ch;
440+
}
441+
// If you need to unify other fields (like color classes),
442+
// decide how to handle current.options vs. mark.options here.
443+
} else {
444+
// Not adjacent => push the old range and start a fresh one
445+
merged.push(current);
446+
current = {
447+
start: { ...mark.start },
448+
end: { ...mark.end },
449+
options: mark.options
450+
};
451+
}
452+
}
453+
}
454+
455+
// Flush any final in-progress range
456+
if (current) {
457+
merged.push(current);
458+
}
459+
460+
return merged;
461+
}
462+
463+
/**
464+
* Adds tickmarks or range-markers for the given positions (or ranges) into the editor's tickmark track.
385465
* If the track was not visible and new marks are added, it is automatically shown.
466+
*
386467
* @param {!Editor} editor
387-
* @param {Array.<{line: number, ch: number}>} posArray
468+
* @param {Array.<{line: number, ch: number} | {start: {line, ch}, end: {line, ch}}>} posArray
469+
* Each element can be:
470+
* (A) a single point: `{ line: number, ch: number }`, or
471+
* (B) a range: `{ start: { line, ch }, end: { line, ch } }`
388472
* @param {Object} [options]
389-
* @param {string} [options.name] you can assign a name to marks and then use this name to selectively
390-
* clear these marks.
391-
* @param {string} [options.trackStyle] one of TRACK_STYLES.*
392-
* @param {string} [options.cssColorClass] a css class that should override the --mark-color css var.
473+
* @param {string} [options.name] Optionally assign a name to these marks and later selectively clear them.
474+
* @param {string} [options.trackStyle] one of TRACK_STYLES.* (e.g., "line" or "left").
475+
* @param {string} [options.cssColorClass] A CSS class that can override or extend styling.
476+
* @param {string} [options.dontMerge] If set to true, will not merge nearby lines in posArray to single mark.
393477
*/
394478
function addTickmarks(editor, posArray, options = {}) {
395479
const markerState = _getMarkerState(editor);
396480
if (!markerState) {
397481
return;
398482
}
399483

400-
// Make sure we have a valid editor instance
401-
if (!editor) {
402-
console.error("Calling ScrollTrackMarkers.addTickmarks without an editor is deprecated.");
403-
editor = EditorManager.getActiveEditor();
404-
}
405-
406-
// If track was empty before, note it
484+
// Keep track of whether the track was empty before adding marks
407485
const wasEmpty = (markerState.marks.length === 0);
408486

409-
// Normalize the new positions to include the same options object
410-
const newPosArray = posArray.map(pos => ({ ...pos, options }));
487+
// Normalize each incoming item so that every mark has both {start, end} internally
488+
const newMarks = posArray.map(pos => {
489+
// If this looks like { start: {...}, end: {...} }, use it directly
490+
if (pos.start && pos.end) {
491+
return {
492+
start: pos.start,
493+
end: pos.end,
494+
options
495+
};
496+
}
411497

412-
// Concat the new positions
413-
markerState.marks = markerState.marks.concat(newPosArray);
498+
// Otherwise assume it's a single point { line, ch }
499+
// Treat it as a zero-length range
500+
return {
501+
start: pos,
502+
end: pos,
503+
options
504+
};
505+
});
414506

415-
// If we were empty and now have tickmarks, show track
507+
const mergedMarks = options.dontMerge ? newMarks : _mergeMarks(newMarks);
508+
509+
// Concat the new marks onto the existing marks
510+
markerState.marks = markerState.marks.concat(mergedMarks);
511+
512+
// If we were empty and now have marks, show the scroll track
416513
if (wasEmpty && markerState.marks.length > 0) {
417514
_showTrack(editor);
418515
}
419516

420-
// If track is visible, re-render
517+
// If the track is visible, re-render everything
421518
if (markerState.visible) {
422519
$(".tickmark-track", editor.getRootElement()).empty();
423520
_renderMarks(editor, markerState.marks);
424521
}
425522
}
426523

524+
427525
/**
428526
* Highlights the "current" tickmark at the given index (into the marks array provided to addTickmarks),
429527
* or clears if `index === -1`.
430528
* @param {number} index
431529
* @param {!Editor} editor
530+
* @private
432531
*/
433-
function markCurrent(index, editor) {
532+
function _markCurrent(index, editor) {
434533
if (!editor) {
435-
throw new Error("Calling ScrollTrackMarkers.markCurrent without editor instance is deprecated.");
534+
throw new Error(
535+
"Calling private API ScrollTrackMarkers._markCurrent without editor instance is deprecated.");
436536
}
437537
const markerState = _getMarkerState(editor);
438538

@@ -446,7 +546,7 @@ define(function (require, exports, module) {
446546
}
447547

448548
// Position the highlight
449-
const top = _getTop(editor, markerState.marks[index]);
549+
const top = _getTop(editor, markerState.marks[index].start);
450550
const $currentTick = $(
451551
`<div class='tickmark tickmark-current' style='top: ${top}px; height: ${MARKER_HEIGHT_LINE}px;'></div>`
452552
);
@@ -468,11 +568,15 @@ define(function (require, exports, module) {
468568
// For unit tests
469569
exports._getTickmarks = _getTickmarks;
470570

571+
// private API
572+
exports._markCurrent = _markCurrent;
573+
574+
// deprecated public API
575+
exports.setVisible = setVisible; // Deprecated
576+
471577
// Public API
578+
exports.addTickmarks = addTickmarks;
472579
exports.clear = clear;
473580
exports.clearAll = clearAll;
474-
exports.setVisible = setVisible; // Deprecated
475-
exports.addTickmarks = addTickmarks;
476-
exports.markCurrent = markCurrent;
477581
exports.TRACK_STYLES = TRACK_STYLES;
478582
});

0 commit comments

Comments
 (0)