mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-13 15:35:20 +01:00
Fix some bugs and clean up how markdown change indicators are rendered
This commit is contained in:
@@ -134,7 +134,8 @@ body.showEditorSelection .code-line {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.diff-change-indicator-arrow:hover > .diff-change-indicator-tooltip {
|
||||
.diff-change-indicator-arrow:hover > .diff-change-indicator-tooltip,
|
||||
.diff-change-indicator-arrow > .diff-change-indicator-tooltip:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -179,12 +180,13 @@ body.showEditorSelection .code-line {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.diff-modification-gutter:hover > .diff-change-indicator-tooltip {
|
||||
.diff-modification-gutter:hover > .diff-change-indicator-tooltip,
|
||||
.diff-modification-gutter > .diff-change-indicator-tooltip:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.diff-modification-gutter > .diff-change-indicator-tooltip {
|
||||
left: 8px;
|
||||
left: 4px;
|
||||
top: 0;
|
||||
right: auto;
|
||||
width: max-content;
|
||||
@@ -201,14 +203,18 @@ body.showEditorSelection .code-line {
|
||||
background-color: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder, rgba(127, 127, 127, 0.35)));
|
||||
border-radius: 3px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
max-height: min(400px, 80vh);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.diff-change-indicator-tooltip pre {
|
||||
margin: 0;
|
||||
font-size: 0.9em;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--vscode-editor-foreground);
|
||||
@@ -217,32 +223,30 @@ body.showEditorSelection .code-line {
|
||||
.diff-tooltip-deleted {
|
||||
background-color: var(--vscode-diffEditor-removedTextBackground);
|
||||
border-left: 4px solid var(--vscode-diffEditorGutter-removedLineBackground, var(--vscode-diffEditor-removedTextBackground));
|
||||
padding: 4px 8px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.diff-tooltip-added {
|
||||
background-color: var(--vscode-diffEditor-insertedTextBackground);
|
||||
border-left: 4px solid var(--vscode-diffEditorGutter-insertedLineBackground, var(--vscode-diffEditor-insertedTextBackground));
|
||||
padding: 4px 8px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.diff-tooltip-unchanged {
|
||||
padding: 4px 8px;
|
||||
border-left: 4px solid transparent;
|
||||
.diff-tooltip-inner-deleted,
|
||||
.diff-tooltip-inner-added {
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
.diff-inline-deleted {
|
||||
.diff-tooltip-inner-deleted {
|
||||
background-color: var(--vscode-diffEditor-removedTextBackground);
|
||||
text-decoration: line-through;
|
||||
text-decoration-color: var(--vscode-editor-foreground);
|
||||
}
|
||||
|
||||
.diff-inline-added {
|
||||
.diff-tooltip-inner-added {
|
||||
background-color: var(--vscode-diffEditor-insertedTextBackground);
|
||||
}
|
||||
|
||||
|
||||
|
||||
body.showEditorSelection :not(tr,ul,ol).code-active-line:before,
|
||||
body.showEditorSelection :not(tr,ul,ol).code-line:hover:before {
|
||||
content: "";
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
import { ActiveLineMarker } from './activeLineMarker';
|
||||
import { onceDocumentLoaded } from './events';
|
||||
import { createPosterForVsCode } from './messaging';
|
||||
import { getEditorLineNumberForPageOffset, getElementsForSourceLine, getLineElementForFragment, scrollToRevealSourceLine } from './scroll-sync';
|
||||
import { getEditorLineNumberForPageOffset, getElementsForSourceLine, getElementsForSourceLineRange, getLineElementForFragment, scrollToRevealSourceLine } from './scroll-sync';
|
||||
import { SettingsManager, getData, getRawData } from './settings';
|
||||
import throttle = require('lodash.throttle');
|
||||
import morphdom from 'morphdom';
|
||||
import type { MarkdownPreviewLineChanges, ToWebviewMessage } from '../types/previewMessaging';
|
||||
import type { MarkdownPreviewChangeIndicator, MarkdownPreviewInnerChange, MarkdownPreviewLineChanges, ToWebviewMessage } from '../types/previewMessaging';
|
||||
import { isOfScheme, Schemes } from '../src/util/schemes';
|
||||
import { DiffScrollSyncManager } from './diffScrollSync';
|
||||
|
||||
@@ -384,62 +384,82 @@ function applyChangeIndicators(lineChanges: MarkdownPreviewLineChanges | undefin
|
||||
return;
|
||||
}
|
||||
|
||||
for (const indicator of lineChanges.changeIndicators) {
|
||||
const { previous, next } = getElementsForSourceLine(indicator.modifiedLine, documentVersion);
|
||||
for (const block of getRenderedChangeBlocks(lineChanges.changeIndicators)) {
|
||||
if (block.indicator.type === 'deletion') {
|
||||
const wrapper = createChangeIndicatorElement(block.indicator);
|
||||
block.elements[0].parentElement?.insertBefore(wrapper, block.elements[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indicator.type === 'deletion') {
|
||||
// For pure deletions, the indicator should appear just before the line at modifiedLine.
|
||||
// Use `next` if previous doesn't exactly match the target line, since the deletion
|
||||
// point is between two elements.
|
||||
const targetElement = (previous.line === indicator.modifiedLine)
|
||||
? (previous.codeElement || previous.element)
|
||||
: (next?.codeElement || next?.element || previous.codeElement || previous.element);
|
||||
if (!targetElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const wrapper = createChangeIndicatorElement(indicator);
|
||||
targetElement.parentElement?.insertBefore(wrapper, targetElement);
|
||||
} else {
|
||||
// For modifications, mark each modified line and add a gutter bar
|
||||
const seen = new Set<HTMLElement>();
|
||||
let isFirst = true;
|
||||
for (let i = 0; i < indicator.modifiedLineCount; i++) {
|
||||
const line = indicator.modifiedLine + i;
|
||||
const { previous: p, next: n } = getElementsForSourceLine(line, documentVersion);
|
||||
const lineElement = p.line >= 0 ? p : n;
|
||||
const element = (lineElement?.codeElement || lineElement?.element) as HTMLElement | undefined;
|
||||
if (element && !seen.has(element)) {
|
||||
seen.add(element);
|
||||
element.classList.add('code-line-diff-modified');
|
||||
addModificationGutterBar(element, isFirst ? indicator : undefined);
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
let isFirst = true;
|
||||
for (const element of block.elements) {
|
||||
element.classList.add('code-line-diff-modified');
|
||||
addModificationGutterBar(element, isFirst ? block.indicator : undefined);
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createChangeIndicatorElement(indicator: { type: string; originalLineCount: number; originalContent: string; modifiedContent: string }): HTMLDivElement {
|
||||
interface RenderedChangeBlock {
|
||||
readonly indicator: MarkdownPreviewChangeIndicator;
|
||||
readonly elements: readonly HTMLElement[];
|
||||
}
|
||||
|
||||
function getRenderedChangeBlocks(indicators: readonly MarkdownPreviewChangeIndicator[]): RenderedChangeBlock[] {
|
||||
const blocks: RenderedChangeBlock[] = [];
|
||||
for (const indicator of indicators) {
|
||||
const elements = indicator.type === 'deletion'
|
||||
? getDeletionChangeElements(indicator.modifiedLine)
|
||||
: getModificationChangeElements(indicator.modifiedLine, indicator.modifiedLineCount);
|
||||
if (elements.length) {
|
||||
blocks.push({ indicator, elements });
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function getDeletionChangeElements(modifiedLine: number): readonly HTMLElement[] {
|
||||
const { previous, next } = getElementsForSourceLine(modifiedLine, documentVersion);
|
||||
const targetElement = (previous.line === modifiedLine)
|
||||
? (previous.codeElement || previous.element)
|
||||
: (next?.codeElement || next?.element || previous.codeElement || previous.element);
|
||||
return targetElement ? [targetElement] : [];
|
||||
}
|
||||
|
||||
function getModificationChangeElements(modifiedLine: number, modifiedLineCount: number): readonly HTMLElement[] {
|
||||
const elements: HTMLElement[] = [];
|
||||
const seen = new Set<HTMLElement>();
|
||||
const lineElements = getElementsForSourceLineRange(modifiedLine, modifiedLine + modifiedLineCount, documentVersion);
|
||||
for (const lineElement of lineElements) {
|
||||
const element = lineElement.codeElement || lineElement.element;
|
||||
if (element && !seen.has(element)) {
|
||||
seen.add(element);
|
||||
elements.push(element);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
function createChangeIndicatorElement(indicator: MarkdownPreviewChangeIndicator): HTMLDivElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = `diff-change-indicator diff-change-indicator-${indicator.type}`;
|
||||
wrapper.setAttribute('data-original-line-count', String(indicator.originalLineCount));
|
||||
|
||||
const arrowLine = document.createElement('span');
|
||||
arrowLine.className = 'diff-change-indicator-arrow';
|
||||
const tooltip = createDiffTooltip(indicator.originalContent, indicator.modifiedContent);
|
||||
const tooltip = createDiffTooltip(indicator);
|
||||
arrowLine.appendChild(tooltip);
|
||||
wrapper.appendChild(arrowLine);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function addModificationGutterBar(element: HTMLElement, indicator?: { originalContent: string; modifiedContent: string }): void {
|
||||
function addModificationGutterBar(element: HTMLElement, indicator?: MarkdownPreviewChangeIndicator): void {
|
||||
const gutter = document.createElement('div');
|
||||
gutter.className = 'diff-modification-gutter';
|
||||
|
||||
if (indicator) {
|
||||
const tooltip = createDiffTooltip(indicator.originalContent, indicator.modifiedContent);
|
||||
const tooltip = createDiffTooltip(indicator);
|
||||
gutter.appendChild(tooltip);
|
||||
}
|
||||
|
||||
@@ -447,201 +467,104 @@ function addModificationGutterBar(element: HTMLElement, indicator?: { originalCo
|
||||
element.appendChild(gutter);
|
||||
}
|
||||
|
||||
function createDiffTooltip(originalContent: string, modifiedContent: string): HTMLDivElement {
|
||||
function createDiffTooltip(indicator: MarkdownPreviewChangeIndicator): HTMLDivElement {
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.className = 'diff-change-indicator-tooltip';
|
||||
|
||||
const originalLines = originalContent ? originalContent.split('\n') : [];
|
||||
const modifiedLines = modifiedContent ? modifiedContent.split('\n') : [];
|
||||
const lineDiff = computeLineLCS(originalLines, modifiedLines);
|
||||
|
||||
for (const entry of lineDiff) {
|
||||
if (entry.type === 'equal') {
|
||||
// Show unchanged lines in both sections with no highlight
|
||||
const section = document.createElement('div');
|
||||
section.className = 'diff-tooltip-unchanged';
|
||||
const pre = document.createElement('pre');
|
||||
pre.textContent = entry.text;
|
||||
section.appendChild(pre);
|
||||
tooltip.appendChild(section);
|
||||
} else if (entry.type === 'delete') {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'diff-tooltip-deleted';
|
||||
const pre = document.createElement('pre');
|
||||
pre.textContent = entry.text;
|
||||
section.appendChild(pre);
|
||||
tooltip.appendChild(section);
|
||||
} else if (entry.type === 'insert') {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'diff-tooltip-added';
|
||||
const pre = document.createElement('pre');
|
||||
pre.textContent = entry.text;
|
||||
section.appendChild(pre);
|
||||
tooltip.appendChild(section);
|
||||
} else if (entry.type === 'modify') {
|
||||
// Show old and new with word-level highlights
|
||||
const delSection = document.createElement('div');
|
||||
delSection.className = 'diff-tooltip-deleted';
|
||||
const delPre = document.createElement('pre');
|
||||
appendInlineHighlights(delPre, entry.oldTokens!, entry.newTokens!, 'delete');
|
||||
delSection.appendChild(delPre);
|
||||
tooltip.appendChild(delSection);
|
||||
|
||||
const addSection = document.createElement('div');
|
||||
addSection.className = 'diff-tooltip-added';
|
||||
const addPre = document.createElement('pre');
|
||||
appendInlineHighlights(addPre, entry.oldTokens!, entry.newTokens!, 'insert');
|
||||
addSection.appendChild(addPre);
|
||||
tooltip.appendChild(addSection);
|
||||
}
|
||||
if (indicator.originalContent) {
|
||||
appendDiffTooltipSection(tooltip, 'diff-tooltip-deleted', indicator.originalContent, indicator.originalInnerChanges, 'diff-tooltip-inner-deleted');
|
||||
}
|
||||
if (indicator.modifiedContent) {
|
||||
appendDiffTooltipSection(tooltip, 'diff-tooltip-added', indicator.modifiedContent, indicator.modifiedInnerChanges, 'diff-tooltip-inner-added');
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
interface DiffEntry {
|
||||
type: 'equal' | 'delete' | 'insert' | 'modify';
|
||||
text: string;
|
||||
oldTokens?: string[];
|
||||
newTokens?: string[];
|
||||
function appendDiffTooltipSection(tooltip: HTMLElement, className: string, content: string, innerChanges: readonly MarkdownPreviewInnerChange[] | undefined, innerChangeClassName: string): void {
|
||||
const section = document.createElement('div');
|
||||
section.className = className;
|
||||
const pre = document.createElement('pre');
|
||||
appendDiffTooltipContent(pre, content, innerChanges, innerChangeClassName);
|
||||
section.appendChild(pre);
|
||||
tooltip.appendChild(section);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a line-level diff using LCS, grouping adjacent changed lines.
|
||||
* When a block has both deletions and insertions, pairs them as 'modify' for inline highlighting.
|
||||
*/
|
||||
function computeLineLCS(oldLines: string[], newLines: string[]): DiffEntry[] {
|
||||
const m = oldLines.length;
|
||||
const n = newLines.length;
|
||||
|
||||
// Build LCS table
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (oldLines[i - 1] === newLines[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||
}
|
||||
}
|
||||
function appendDiffTooltipContent(container: HTMLElement, content: string, innerChanges: readonly MarkdownPreviewInnerChange[] | undefined, innerChangeClassName: string): void {
|
||||
if (!innerChanges?.length) {
|
||||
container.textContent = content;
|
||||
return;
|
||||
}
|
||||
|
||||
// Backtrack to get diff operations
|
||||
const rawDiff: Array<{ type: 'equal' | 'delete' | 'insert'; line: string }> = [];
|
||||
let i = m, j = n;
|
||||
while (i > 0 || j > 0) {
|
||||
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
||||
rawDiff.push({ type: 'equal', line: oldLines[i - 1] });
|
||||
i--; j--;
|
||||
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||||
rawDiff.push({ type: 'insert', line: newLines[j - 1] });
|
||||
j--;
|
||||
} else {
|
||||
rawDiff.push({ type: 'delete', line: oldLines[i - 1] });
|
||||
i--;
|
||||
const innerChangesByLine = groupInnerChangesByLine(innerChanges);
|
||||
const lines = content.split('\n');
|
||||
for (let line = 0; line < lines.length; ++line) {
|
||||
appendDiffTooltipLine(container, lines[line], innerChangesByLine.get(line), innerChangeClassName);
|
||||
if (line + 1 < lines.length) {
|
||||
container.appendChild(document.createTextNode('\n'));
|
||||
}
|
||||
}
|
||||
rawDiff.reverse();
|
||||
|
||||
// Group into entries, pairing adjacent delete/insert blocks as 'modify'
|
||||
const result: DiffEntry[] = [];
|
||||
let idx = 0;
|
||||
while (idx < rawDiff.length) {
|
||||
const item = rawDiff[idx];
|
||||
if (item.type === 'equal') {
|
||||
result.push({ type: 'equal', text: item.line });
|
||||
idx++;
|
||||
} else {
|
||||
// Collect contiguous changed block
|
||||
const delLines: string[] = [];
|
||||
const insLines: string[] = [];
|
||||
while (idx < rawDiff.length && rawDiff[idx].type !== 'equal') {
|
||||
if (rawDiff[idx].type === 'delete') {
|
||||
delLines.push(rawDiff[idx].line);
|
||||
} else {
|
||||
insLines.push(rawDiff[idx].line);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Pair up lines for inline highlighting
|
||||
const paired = Math.min(delLines.length, insLines.length);
|
||||
for (let k = 0; k < paired; k++) {
|
||||
result.push({
|
||||
type: 'modify',
|
||||
text: '',
|
||||
oldTokens: tokenize(delLines[k]),
|
||||
newTokens: tokenize(insLines[k]),
|
||||
});
|
||||
}
|
||||
// Remaining unpaired lines
|
||||
for (let k = paired; k < delLines.length; k++) {
|
||||
result.push({ type: 'delete', text: delLines[k] });
|
||||
}
|
||||
for (let k = paired; k < insLines.length; k++) {
|
||||
result.push({ type: 'insert', text: insLines[k] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Split a line into alternating word and whitespace tokens */
|
||||
function tokenize(line: string): string[] {
|
||||
return line.match(/\S+|\s+/g) || [''];
|
||||
function appendDiffTooltipLine(container: HTMLElement, lineText: string, innerChanges: readonly MarkdownPreviewInnerChange[] | undefined, innerChangeClassName: string): void {
|
||||
const normalizedInnerChanges = normalizeInnerChanges(innerChanges, lineText.length);
|
||||
let offset = 0;
|
||||
for (const change of normalizedInnerChanges) {
|
||||
if (offset < change.startColumn) {
|
||||
container.appendChild(document.createTextNode(lineText.slice(offset, change.startColumn)));
|
||||
}
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = innerChangeClassName;
|
||||
span.textContent = lineText.slice(change.startColumn, change.endColumn);
|
||||
container.appendChild(span);
|
||||
offset = change.endColumn;
|
||||
}
|
||||
|
||||
if (offset < lineText.length) {
|
||||
container.appendChild(document.createTextNode(lineText.slice(offset)));
|
||||
}
|
||||
}
|
||||
|
||||
/** Compute LCS indices for two token arrays */
|
||||
function tokenLCS(a: string[], b: string[]): Set<number>[] {
|
||||
const m = a.length, n = b.length;
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
||||
for (let i = 1; i <= m; i++) {
|
||||
for (let j = 1; j <= n; j++) {
|
||||
if (a[i - 1] === b[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const aMatch = new Set<number>();
|
||||
const bMatch = new Set<number>();
|
||||
let i = m, j = n;
|
||||
while (i > 0 && j > 0) {
|
||||
if (a[i - 1] === b[j - 1]) {
|
||||
aMatch.add(i - 1);
|
||||
bMatch.add(j - 1);
|
||||
i--; j--;
|
||||
} else if (dp[i][j - 1] >= dp[i - 1][j]) {
|
||||
j--;
|
||||
function groupInnerChangesByLine(innerChanges: readonly MarkdownPreviewInnerChange[]): Map<number, readonly MarkdownPreviewInnerChange[]> {
|
||||
const groupedInnerChanges = new Map<number, MarkdownPreviewInnerChange[]>();
|
||||
for (const change of innerChanges) {
|
||||
const lineChanges = groupedInnerChanges.get(change.line);
|
||||
if (lineChanges) {
|
||||
lineChanges.push(change);
|
||||
} else {
|
||||
i--;
|
||||
groupedInnerChanges.set(change.line, [change]);
|
||||
}
|
||||
}
|
||||
return [aMatch, bMatch];
|
||||
return groupedInnerChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append tokens to a <pre> element with inline highlights for changed tokens.
|
||||
* @param side 'delete' renders oldTokens highlighting removals; 'insert' renders newTokens highlighting additions
|
||||
*/
|
||||
function appendInlineHighlights(pre: HTMLPreElement, oldTokens: string[], newTokens: string[], side: 'delete' | 'insert'): void {
|
||||
const [oldMatch, newMatch] = tokenLCS(oldTokens, newTokens);
|
||||
const tokens = side === 'delete' ? oldTokens : newTokens;
|
||||
const matchSet = side === 'delete' ? oldMatch : newMatch;
|
||||
const highlightClass = side === 'delete' ? 'diff-inline-deleted' : 'diff-inline-added';
|
||||
function normalizeInnerChanges(innerChanges: readonly MarkdownPreviewInnerChange[] | undefined, lineLength: number): { startColumn: number; endColumn: number }[] {
|
||||
if (!innerChanges?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (matchSet.has(i)) {
|
||||
pre.appendChild(document.createTextNode(tokens[i]));
|
||||
const sortedInnerChanges = innerChanges
|
||||
.map(change => ({
|
||||
startColumn: clampColumn(change.startColumn, lineLength),
|
||||
endColumn: clampColumn(change.endColumn, lineLength),
|
||||
}))
|
||||
.filter(change => change.startColumn < change.endColumn)
|
||||
.sort((a, b) => a.startColumn - b.startColumn || a.endColumn - b.endColumn);
|
||||
const normalizedInnerChanges: { startColumn: number; endColumn: number }[] = [];
|
||||
for (const change of sortedInnerChanges) {
|
||||
const previous = normalizedInnerChanges[normalizedInnerChanges.length - 1];
|
||||
if (previous && change.startColumn <= previous.endColumn) {
|
||||
previous.endColumn = Math.max(previous.endColumn, change.endColumn);
|
||||
} else {
|
||||
const span = document.createElement('span');
|
||||
span.className = highlightClass;
|
||||
span.textContent = tokens[i];
|
||||
pre.appendChild(span);
|
||||
normalizedInnerChanges.push(change);
|
||||
}
|
||||
}
|
||||
return normalizedInnerChanges;
|
||||
}
|
||||
|
||||
function clampColumn(column: number, lineLength: number): number {
|
||||
return Math.min(Math.max(column, 0), lineLength);
|
||||
}
|
||||
|
||||
|
||||
@@ -666,19 +589,11 @@ function applyInnerChangeHighlights(lineChanges: MarkdownPreviewLineChanges | un
|
||||
return;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (true) {
|
||||
const startMarker = root.querySelector(`[data-diff-start="${i}"]`);
|
||||
const endMarker = root.querySelector(`[data-diff-end="${i}"]`);
|
||||
if (!startMarker || !endMarker) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const { startMarker, endMarker } of getDiffMarkerPairs(root)) {
|
||||
const range = new Range();
|
||||
range.setStartAfter(startMarker);
|
||||
range.setEndBefore(endMarker);
|
||||
ranges.push(range);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (ranges.length > 0) {
|
||||
@@ -686,6 +601,31 @@ function applyInnerChangeHighlights(lineChanges: MarkdownPreviewLineChanges | un
|
||||
}
|
||||
}
|
||||
|
||||
interface DiffMarkerPair {
|
||||
readonly startMarker: Element;
|
||||
readonly endMarker: Element;
|
||||
}
|
||||
|
||||
function getDiffMarkerPairs(root: Element): DiffMarkerPair[] {
|
||||
const endMarkersById = new Map<string, Element>();
|
||||
for (const endMarker of root.querySelectorAll('[data-diff-end]')) {
|
||||
const id = endMarker.getAttribute('data-diff-end');
|
||||
if (id !== null) {
|
||||
endMarkersById.set(id, endMarker);
|
||||
}
|
||||
}
|
||||
|
||||
const pairs: DiffMarkerPair[] = [];
|
||||
for (const startMarker of root.querySelectorAll('[data-diff-start]')) {
|
||||
const id = startMarker.getAttribute('data-diff-start');
|
||||
const endMarker = id === null ? undefined : endMarkersById.get(id);
|
||||
if (endMarker && !(startMarker.compareDocumentPosition(endMarker) & Node.DOCUMENT_POSITION_PRECEDING)) {
|
||||
pairs.push({ startMarker, endMarker });
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
document.addEventListener('dblclick', event => {
|
||||
|
||||
@@ -99,6 +99,29 @@ export function getElementsForSourceLine(targetLine: number, documentVersion: nu
|
||||
return { previous };
|
||||
}
|
||||
|
||||
export function getElementsForSourceLineRange(startLine: number, endLine: number, documentVersion: number): readonly CodeLineElement[] {
|
||||
const rangeStart = Math.floor(startLine);
|
||||
const rangeEnd = Math.max(rangeStart + 1, Math.ceil(endLine));
|
||||
const lines = getCodeLineElements(documentVersion).filter(element => element.line >= 0);
|
||||
const elements: CodeLineElement[] = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const element = lines[i];
|
||||
const next = lines[i + 1];
|
||||
const elementStart = element.line;
|
||||
const elementEnd = element.endLine !== undefined
|
||||
? element.endLine + 1
|
||||
: (next && next.line > element.line ? next.line : element.line + 1);
|
||||
if (rangesIntersect(rangeStart, rangeEnd, elementStart, elementEnd)) {
|
||||
elements.push(element);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
function rangesIntersect(startA: number, endA: number, startB: number, endB: number): boolean {
|
||||
return startA < endB && startB < endA;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the html elements that are at a specific pixel offset on the page.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"rootDir": "..",
|
||||
"outDir": "./dist/",
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
|
||||
@@ -21,6 +21,8 @@ interface LineMappings {
|
||||
readonly modifiedToOriginal: number[];
|
||||
}
|
||||
|
||||
type ChangedLineRange = Pick<vscode.TextDiffChange, 'originalRange' | 'modifiedRange'>;
|
||||
|
||||
export class MarkdownPreviewLineDiffProvider {
|
||||
|
||||
readonly #originalDocument: vscode.TextDocument;
|
||||
@@ -92,7 +94,7 @@ async function computeLineChanges(originalDocument: vscode.TextDocument, modifie
|
||||
const deleted: number[] = [];
|
||||
const originalInnerChanges: MarkdownPreviewInnerChange[] = [];
|
||||
const modifiedInnerChanges: MarkdownPreviewInnerChange[] = [];
|
||||
const changeIndicators: MarkdownPreviewChangeIndicator[] = [];
|
||||
const changedLineRanges: ChangedLineRange[] = [];
|
||||
const mappings = createEmptyLineMappings(originalLineCount, modifiedLineCount);
|
||||
|
||||
let lastOriginalEnd = 0;
|
||||
@@ -120,22 +122,7 @@ async function computeLineChanges(originalDocument: vscode.TextDocument, modifie
|
||||
// Collect change indicators for deletions and modifications
|
||||
const origChangedCount = origEnd - origStart;
|
||||
if (origChangedCount > 0) {
|
||||
const originalLines: string[] = [];
|
||||
for (let i = origStart; i < origEnd; ++i) {
|
||||
originalLines.push(originalDocument.lineAt(i).text);
|
||||
}
|
||||
const modifiedLines: string[] = [];
|
||||
for (let i = modStart; i < modEnd; ++i) {
|
||||
modifiedLines.push(modifiedDocument.lineAt(i).text);
|
||||
}
|
||||
changeIndicators.push({
|
||||
modifiedLine: modStart,
|
||||
modifiedLineCount: modEnd - modStart,
|
||||
originalLineCount: origChangedCount,
|
||||
originalContent: originalLines.join('\n'),
|
||||
modifiedContent: modifiedLines.join('\n'),
|
||||
type: modEnd === modStart ? 'deletion' : 'modification',
|
||||
});
|
||||
changedLineRanges.push(change);
|
||||
}
|
||||
|
||||
// Collect inner changes (character-level changes within modified lines)
|
||||
@@ -153,10 +140,88 @@ async function computeLineChanges(originalDocument: vscode.TextDocument, modifie
|
||||
// Map unchanged lines after the last change
|
||||
fillUnchangedLineMappings(mappings, lastOriginalEnd, originalLineCount, lastModifiedEnd, modifiedLineCount);
|
||||
fillMissingLineMappings(mappings);
|
||||
const splitChangedLineRanges = splitChangedLineRangesByMarkdownBlocks(changedLineRanges, originalDocument, modifiedDocument);
|
||||
const changeIndicators = createChangeIndicators(splitChangedLineRanges, originalDocument, modifiedDocument, originalInnerChanges, modifiedInnerChanges);
|
||||
|
||||
return { added, deleted, originalInnerChanges, modifiedInnerChanges, changeIndicators, ...mappings };
|
||||
}
|
||||
|
||||
function createChangeIndicators(ranges: readonly ChangedLineRange[], originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument, originalInnerChanges: readonly MarkdownPreviewInnerChange[], modifiedInnerChanges: readonly MarkdownPreviewInnerChange[]): MarkdownPreviewChangeIndicator[] {
|
||||
return ranges.map(range => {
|
||||
const modifiedLineCount = range.modifiedRange.end.line - range.modifiedRange.start.line;
|
||||
return {
|
||||
modifiedLine: range.modifiedRange.start.line,
|
||||
modifiedLineCount,
|
||||
originalLineCount: range.originalRange.end.line - range.originalRange.start.line,
|
||||
originalContent: getLineRangeText(originalDocument, range.originalRange),
|
||||
originalInnerChanges: getRelativeInnerChanges(originalInnerChanges, range.originalRange),
|
||||
modifiedContent: getLineRangeText(modifiedDocument, range.modifiedRange),
|
||||
modifiedInnerChanges: getRelativeInnerChanges(modifiedInnerChanges, range.modifiedRange),
|
||||
type: modifiedLineCount === 0 ? 'deletion' : 'modification',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getRelativeInnerChanges(innerChanges: readonly MarkdownPreviewInnerChange[], range: vscode.Range): MarkdownPreviewInnerChange[] | undefined {
|
||||
const relativeInnerChanges: MarkdownPreviewInnerChange[] = [];
|
||||
for (const change of innerChanges) {
|
||||
if (change.line >= range.start.line && change.line < range.end.line) {
|
||||
relativeInnerChanges.push({
|
||||
line: change.line - range.start.line,
|
||||
startColumn: change.startColumn,
|
||||
endColumn: change.endColumn,
|
||||
});
|
||||
}
|
||||
}
|
||||
return relativeInnerChanges.length ? relativeInnerChanges : undefined;
|
||||
}
|
||||
|
||||
function splitChangedLineRangesByMarkdownBlocks(ranges: readonly ChangedLineRange[], originalDocument: vscode.TextDocument, modifiedDocument: vscode.TextDocument): ChangedLineRange[] {
|
||||
const splitRanges: ChangedLineRange[] = [];
|
||||
for (const range of ranges) {
|
||||
const originalBlocks = getNonBlankLineRanges(originalDocument, range.originalRange);
|
||||
const modifiedBlocks = getNonBlankLineRanges(modifiedDocument, range.modifiedRange);
|
||||
if (originalBlocks.length > 1 && originalBlocks.length === modifiedBlocks.length) {
|
||||
for (let i = 0; i < originalBlocks.length; ++i) {
|
||||
splitRanges.push({
|
||||
originalRange: originalBlocks[i],
|
||||
modifiedRange: modifiedBlocks[i],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
splitRanges.push(range);
|
||||
}
|
||||
}
|
||||
return splitRanges;
|
||||
}
|
||||
|
||||
function getNonBlankLineRanges(document: vscode.TextDocument, range: vscode.Range): vscode.Range[] {
|
||||
const ranges: vscode.Range[] = [];
|
||||
let blockStartLine: number | undefined;
|
||||
for (let line = range.start.line; line < range.end.line; ++line) {
|
||||
if (document.lineAt(line).text.trim().length === 0) {
|
||||
if (blockStartLine !== undefined) {
|
||||
ranges.push(new vscode.Range(blockStartLine, 0, line, 0));
|
||||
blockStartLine = undefined;
|
||||
}
|
||||
} else if (blockStartLine === undefined) {
|
||||
blockStartLine = line;
|
||||
}
|
||||
}
|
||||
if (blockStartLine !== undefined) {
|
||||
ranges.push(new vscode.Range(blockStartLine, 0, range.end.line, 0));
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function getLineRangeText(document: vscode.TextDocument, range: vscode.Range): string {
|
||||
const lines: string[] = [];
|
||||
for (let line = range.start.line; line < range.end.line; ++line) {
|
||||
lines.push(document.lineAt(line).text);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a Range into per-line inner change entries.
|
||||
* For single-line ranges, emits one entry. For multi-line ranges,
|
||||
|
||||
@@ -25,8 +25,12 @@ export interface MarkdownPreviewChangeIndicator {
|
||||
readonly originalLineCount: number;
|
||||
/** The original text content that was deleted or replaced */
|
||||
readonly originalContent: string;
|
||||
/** Character-level changes within originalContent, using originalContent-relative line numbers */
|
||||
readonly originalInnerChanges?: readonly MarkdownPreviewInnerChange[];
|
||||
/** The modified text content (for modifications) */
|
||||
readonly modifiedContent: string;
|
||||
/** Character-level changes within modifiedContent, using modifiedContent-relative line numbers */
|
||||
readonly modifiedInnerChanges?: readonly MarkdownPreviewInnerChange[];
|
||||
/** Whether this is a pure deletion or a modification of existing lines */
|
||||
readonly type: 'deletion' | 'modification';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user