Merge pull request #6589 from continuedev/jacob/enhancement/nextedit-polish
enhancement: Improve Next Edit DX
This commit is contained in:
@@ -9,9 +9,7 @@
|
||||
* creating a render of it.
|
||||
*/
|
||||
import {
|
||||
transformerMetaHighlight,
|
||||
transformerNotationDiff,
|
||||
transformerNotationFocus,
|
||||
transformerNotationHighlight,
|
||||
} from "@shikijs/transformers";
|
||||
import { JSDOM } from "jsdom";
|
||||
@@ -21,6 +19,7 @@ import {
|
||||
getSingletonHighlighter,
|
||||
Highlighter,
|
||||
} from "shiki";
|
||||
import { DiffLine } from "..";
|
||||
import { escapeForSVG, kebabOfStr } from "../util/text";
|
||||
|
||||
interface CodeRendererOptions {
|
||||
@@ -37,6 +36,10 @@ interface HTMLOptions {
|
||||
interface ConversionOptions extends HTMLOptions {
|
||||
transparent?: boolean;
|
||||
imageType: "svg";
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
dimensions: Dimensions;
|
||||
lineHeight: number;
|
||||
}
|
||||
|
||||
interface Dimensions {
|
||||
@@ -166,83 +169,102 @@ export class CodeRenderer {
|
||||
code: string,
|
||||
language: string = "javascript",
|
||||
currLineOffsetFromTop: number,
|
||||
newDiffLines: DiffLine[],
|
||||
): Promise<string> {
|
||||
const annotatedCode = code
|
||||
.split("\n")
|
||||
.map((line, i) =>
|
||||
i === currLineOffsetFromTop
|
||||
? line + " \/\/ \[\!code highlight\]"
|
||||
: line,
|
||||
)
|
||||
.join("\n");
|
||||
const lines = code.split("\n");
|
||||
const newDiffLineMap = new Set();
|
||||
|
||||
if (newDiffLines) {
|
||||
newDiffLines.forEach((diffLine) => {
|
||||
if (diffLine.type === "new") {
|
||||
newDiffLineMap.add(diffLine.line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const annotatedLines = [];
|
||||
|
||||
// NOTE: Shiki's preprocessor deletes transformer annotations when applied to an empty line.
|
||||
// If you are transforming an empty line, make sure that
|
||||
// the transformation is applied to a non-empty line first.
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Add highlight comment before target line.
|
||||
if (i + 1 === currLineOffsetFromTop && currLineOffsetFromTop >= 0) {
|
||||
annotatedLines.push("// [!code highlight:1]");
|
||||
}
|
||||
|
||||
// Handle diff lines
|
||||
if (newDiffLineMap.has(line)) {
|
||||
if (line.trim() === "") {
|
||||
// For empty lines, add the magic comment on a separate line before.
|
||||
annotatedLines.push("// [!code ++]");
|
||||
annotatedLines.push(line); // The empty line itself.
|
||||
} else {
|
||||
// For non-empty lines, append the magic comment.
|
||||
annotatedLines.push(line + "// [!code ++]");
|
||||
}
|
||||
newDiffLineMap.delete(line);
|
||||
} else {
|
||||
annotatedLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
const annotatedCode = annotatedLines.join("\n");
|
||||
|
||||
await this.highlighter!.loadLanguage(language as BundledLanguage);
|
||||
|
||||
return this.highlighter!.codeToHtml(annotatedCode, {
|
||||
lang: language,
|
||||
theme: this.currentTheme,
|
||||
transformers: [
|
||||
// transformerColorizedBrackets(),
|
||||
transformerMetaHighlight(),
|
||||
transformerNotationHighlight(),
|
||||
transformerNotationDiff(),
|
||||
transformerNotationFocus(),
|
||||
],
|
||||
transformers: [transformerNotationHighlight(), transformerNotationDiff()],
|
||||
});
|
||||
}
|
||||
|
||||
async convertToSVG(
|
||||
code: string,
|
||||
language: string = "javascript",
|
||||
fontSize: number,
|
||||
fontFamily: string,
|
||||
dimensions: Dimensions,
|
||||
lineHeight: number,
|
||||
options: ConversionOptions,
|
||||
currLineOffsetFromTop: number,
|
||||
newDiffLines: DiffLine[],
|
||||
): Promise<Buffer> {
|
||||
const strokeWidth = 1;
|
||||
const highlightedCodeHtml = await this.highlightCode(
|
||||
code,
|
||||
language,
|
||||
currLineOffsetFromTop,
|
||||
newDiffLines,
|
||||
);
|
||||
// console.log(highlightedCodeHtml);
|
||||
|
||||
const { guts, lineBackgrounds } = this.convertShikiHtmlToSvgGut(
|
||||
highlightedCodeHtml,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
lineHeight,
|
||||
dimensions,
|
||||
options,
|
||||
);
|
||||
const backgroundColor = this.getBackgroundColor(highlightedCodeHtml);
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${dimensions.width}" height="${dimensions.height}" shape-rendering="crispEdges">
|
||||
<style>
|
||||
:root {
|
||||
--purple: rgb(112, 114, 209);
|
||||
--green: rgb(136, 194, 163);
|
||||
--blue: rgb(107, 166, 205);
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<rect x="0" y="0" rx="10" ry="10" width="${dimensions.width}" height="${dimensions.height}" fill="${this.editorBackground}" shape-rendering="crispEdges" />
|
||||
${lineBackgrounds}
|
||||
${guts}
|
||||
</g>
|
||||
</svg>`;
|
||||
console.log(svg);
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${options.dimensions.width}" height="${options.dimensions.height}" shape-rendering="crispEdges">
|
||||
<style>
|
||||
:root {
|
||||
--purple: rgb(112, 114, 209);
|
||||
--green: rgb(136, 194, 163);
|
||||
--blue: rgb(107, 166, 205);
|
||||
}
|
||||
</style>
|
||||
<g>
|
||||
<rect x="0" y="0" rx="10" ry="10" width="${options.dimensions.width}" height="${options.dimensions.height}" fill="${this.editorBackground}" shape-rendering="crispEdges" />
|
||||
${lineBackgrounds}
|
||||
${guts}
|
||||
</g>
|
||||
</svg>`;
|
||||
// console.log(svg);
|
||||
|
||||
return Buffer.from(svg, "utf8");
|
||||
}
|
||||
|
||||
convertShikiHtmlToSvgGut(
|
||||
shikiHtml: string,
|
||||
fontSize: number,
|
||||
fontFamily: string,
|
||||
lineHeight: number,
|
||||
dimensions: Dimensions,
|
||||
options: ConversionOptions,
|
||||
): { guts: string; lineBackgrounds: string } {
|
||||
const dom = new JSDOM(shikiHtml);
|
||||
const document = dom.window.document;
|
||||
@@ -263,6 +285,7 @@ export class CodeRenderer {
|
||||
if (classes.includes("highlighted")) {
|
||||
fill = ` fill="${this.editorLineHighlight}"`;
|
||||
}
|
||||
|
||||
const content = el.textContent || "";
|
||||
return `<tspan xml:space="preserve"${fill}>${escapeForSVG(content)}</tspan>`;
|
||||
})
|
||||
@@ -278,8 +301,8 @@ export class CodeRenderer {
|
||||
// The first step is to add lineHeight / 2 to move the axis down.
|
||||
// The second step is to add 'dominant-baseline="central"' to vertically center the text.
|
||||
// Note that we choose "central" over "middle". "middle" will center the text too perfectly, which is actually undesirable!
|
||||
const y = index * lineHeight + lineHeight / 2;
|
||||
return `<text x="0" y="${y}" font-family="${fontFamily}" font-size="${fontSize.toString()}" xml:space="preserve" dominant-baseline="central" shape-rendering="crispEdges">${spans}</text>`;
|
||||
const y = index * options.lineHeight + options.lineHeight / 2;
|
||||
return `<text x="0" y="${y}" font-family="${options.fontFamily}" font-size="${options.fontSize.toString()}" xml:space="preserve" dominant-baseline="central" shape-rendering="crispEdges">${spans}</text>`;
|
||||
});
|
||||
|
||||
const lineBackgrounds = lines
|
||||
@@ -287,8 +310,11 @@ export class CodeRenderer {
|
||||
const classes = line?.getAttribute("class") || "";
|
||||
const bgColor = classes.includes("highlighted")
|
||||
? this.editorLineHighlight
|
||||
: this.editorBackground;
|
||||
const y = index * lineHeight;
|
||||
: classes.includes("diff add")
|
||||
? "rgba(255, 255, 0, 0.2)"
|
||||
: this.editorBackground;
|
||||
|
||||
const y = index * options.lineHeight;
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === lines.length - 1;
|
||||
const radius = 10;
|
||||
@@ -297,24 +323,24 @@ export class CodeRenderer {
|
||||
// This is undesirable in our case because pixel-perfect alignment of these rectangles will introduce thin gaps.
|
||||
// Turning it off with 'shape-rendering="crispEdges"' solves the issue.
|
||||
return isFirst
|
||||
? `<path d="M ${0} ${y + lineHeight}
|
||||
L ${0} ${y + radius}
|
||||
Q ${0} ${y} ${radius} ${y}
|
||||
L ${dimensions.width - radius} ${y}
|
||||
Q ${dimensions.width} ${y} ${dimensions.width} ${y + radius}
|
||||
L ${dimensions.width} ${y + lineHeight}
|
||||
Z"
|
||||
fill="${bgColor}" />`
|
||||
? `<path d="M ${0} ${y + options.lineHeight}
|
||||
L ${0} ${y + radius}
|
||||
Q ${0} ${y} ${radius} ${y}
|
||||
L ${options.dimensions.width - radius} ${y}
|
||||
Q ${options.dimensions.width} ${y} ${options.dimensions.width} ${y + radius}
|
||||
L ${options.dimensions.width} ${y + options.lineHeight}
|
||||
Z"
|
||||
fill="${bgColor}" />`
|
||||
: isLast
|
||||
? `<path d="M ${0} ${y}
|
||||
L ${0} ${y + lineHeight - radius}
|
||||
Q ${0} ${y + lineHeight} ${radius} ${y + lineHeight}
|
||||
L ${dimensions.width - radius} ${y + lineHeight}
|
||||
Q ${dimensions.width} ${y + lineHeight} ${dimensions.width} ${y + lineHeight - 10}
|
||||
L ${dimensions.width} ${y}
|
||||
Z"
|
||||
fill="${bgColor}" />`
|
||||
: `<rect x="0" y="${y}" rx="${radius}" ry="${radius}" width="100%" height="${lineHeight}" fill="${bgColor}" shape-rendering="crispEdges" />`;
|
||||
? `<path d="M ${0} ${y}
|
||||
L ${0} ${y + options.lineHeight - radius}
|
||||
Q ${0} ${y + options.lineHeight} ${radius} ${y + options.lineHeight}
|
||||
L ${options.dimensions.width - radius} ${y + options.lineHeight}
|
||||
Q ${options.dimensions.width} ${y + options.lineHeight} ${options.dimensions.width} ${y + options.lineHeight - 10}
|
||||
L ${options.dimensions.width} ${y}
|
||||
Z"
|
||||
fill="${bgColor}" />`
|
||||
: `<rect x="0" y="${y}" width="100%" height="${options.lineHeight}" fill="${bgColor}" shape-rendering="crispEdges" />`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
@@ -344,12 +370,9 @@ export class CodeRenderer {
|
||||
async getDataUri(
|
||||
code: string,
|
||||
language: string = "javascript",
|
||||
fontSize: number,
|
||||
fontFamily: string,
|
||||
dimensions: Dimensions,
|
||||
lineHeight: number,
|
||||
options: ConversionOptions,
|
||||
currLineOffsetFromTop: number,
|
||||
newDiffLines: DiffLine[],
|
||||
): Promise<DataUri> {
|
||||
switch (options.imageType) {
|
||||
// case "png":
|
||||
@@ -366,12 +389,9 @@ export class CodeRenderer {
|
||||
const svgBuffer = await this.convertToSVG(
|
||||
code,
|
||||
language,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
dimensions,
|
||||
lineHeight,
|
||||
options,
|
||||
currLineOffsetFromTop,
|
||||
newDiffLines,
|
||||
);
|
||||
return `data:image/svg+xml;base64,${svgBuffer.toString("base64")}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { diffLines, type Change } from "diff";
|
||||
import { diffChars, diffLines, type Change } from "diff";
|
||||
|
||||
import { DiffLine } from "..";
|
||||
import { DiffChar, DiffLine } from "..";
|
||||
|
||||
export function convertMyersChangeToDiffLines(change: Change): DiffLine[] {
|
||||
const type: DiffLine["type"] = change.added
|
||||
@@ -55,3 +55,157 @@ export function myersDiff(oldContent: string, newContent: string): DiffLine[] {
|
||||
|
||||
return ourFormat;
|
||||
}
|
||||
|
||||
export function myersCharDiff(
|
||||
oldContent: string,
|
||||
newContent: string,
|
||||
): DiffChar[] {
|
||||
// Process the content character by character.
|
||||
// We will handle newlines separately,
|
||||
// because diffChars does not have an option to ignore eol newlines.
|
||||
const theirFormat = diffChars(oldContent, newContent);
|
||||
|
||||
// Track indices as we process the diff.
|
||||
let oldIndex = 0;
|
||||
let newIndex = 0;
|
||||
let oldLineIndex = 0;
|
||||
let newLineIndex = 0;
|
||||
let oldCharIndexInLine = 0;
|
||||
let newCharIndexInLine = 0;
|
||||
|
||||
const result: DiffChar[] = [];
|
||||
|
||||
for (const change of theirFormat) {
|
||||
// Split the change value by newlines to handle them separately.
|
||||
if (change.value.includes("\n")) {
|
||||
const parts = change.value.split(/(\n)/g); // This keeps the newlines as separate entries.
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
if (part === "") continue;
|
||||
|
||||
if (part === "\n") {
|
||||
// Handle newline.
|
||||
if (change.added) {
|
||||
result.push({
|
||||
type: "new",
|
||||
char: part,
|
||||
newIndex: newIndex,
|
||||
newLineIndex: newLineIndex,
|
||||
newCharIndexInLine: newCharIndexInLine,
|
||||
});
|
||||
newIndex += part.length;
|
||||
newLineIndex++;
|
||||
newCharIndexInLine = 0; // Reset when moving to a new line.
|
||||
} else if (change.removed) {
|
||||
result.push({
|
||||
type: "old",
|
||||
char: part,
|
||||
oldIndex: oldIndex,
|
||||
oldLineIndex: oldLineIndex,
|
||||
oldCharIndexInLine: oldCharIndexInLine,
|
||||
});
|
||||
oldIndex += part.length;
|
||||
oldLineIndex++;
|
||||
oldCharIndexInLine = 0; // Reset when moving to a new line.
|
||||
} else {
|
||||
result.push({
|
||||
type: "same",
|
||||
char: part,
|
||||
oldIndex: oldIndex,
|
||||
newIndex: newIndex,
|
||||
oldLineIndex: oldLineIndex,
|
||||
newLineIndex: newLineIndex,
|
||||
oldCharIndexInLine: oldCharIndexInLine,
|
||||
newCharIndexInLine: newCharIndexInLine,
|
||||
});
|
||||
oldIndex += part.length;
|
||||
newIndex += part.length;
|
||||
oldLineIndex++;
|
||||
newLineIndex++;
|
||||
oldCharIndexInLine = 0;
|
||||
newCharIndexInLine = 0;
|
||||
}
|
||||
} else {
|
||||
// Handle regular text.
|
||||
if (change.added) {
|
||||
result.push({
|
||||
type: "new",
|
||||
char: part,
|
||||
newIndex: newIndex,
|
||||
newLineIndex: newLineIndex,
|
||||
newCharIndexInLine: newCharIndexInLine,
|
||||
});
|
||||
newIndex += part.length;
|
||||
newCharIndexInLine += part.length;
|
||||
} else if (change.removed) {
|
||||
result.push({
|
||||
type: "old",
|
||||
char: part,
|
||||
oldIndex: oldIndex,
|
||||
oldLineIndex: oldLineIndex,
|
||||
oldCharIndexInLine: oldCharIndexInLine,
|
||||
});
|
||||
oldIndex += part.length;
|
||||
oldCharIndexInLine += part.length;
|
||||
} else {
|
||||
result.push({
|
||||
type: "same",
|
||||
char: part,
|
||||
oldIndex: oldIndex,
|
||||
newIndex: newIndex,
|
||||
oldLineIndex: oldLineIndex,
|
||||
newLineIndex: newLineIndex,
|
||||
oldCharIndexInLine: oldCharIndexInLine,
|
||||
newCharIndexInLine: newCharIndexInLine,
|
||||
});
|
||||
oldIndex += part.length;
|
||||
newIndex += part.length;
|
||||
oldCharIndexInLine += part.length;
|
||||
newCharIndexInLine += part.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No newlines, handle as a simple change.
|
||||
if (change.added) {
|
||||
result.push({
|
||||
type: "new",
|
||||
char: change.value,
|
||||
newIndex: newIndex,
|
||||
newLineIndex: newLineIndex,
|
||||
newCharIndexInLine: newCharIndexInLine,
|
||||
});
|
||||
newIndex += change.value.length;
|
||||
newCharIndexInLine += change.value.length;
|
||||
} else if (change.removed) {
|
||||
result.push({
|
||||
type: "old",
|
||||
char: change.value,
|
||||
oldIndex: oldIndex,
|
||||
oldLineIndex: oldLineIndex,
|
||||
oldCharIndexInLine: oldCharIndexInLine,
|
||||
});
|
||||
oldIndex += change.value.length;
|
||||
oldCharIndexInLine += change.value.length;
|
||||
} else {
|
||||
result.push({
|
||||
type: "same",
|
||||
char: change.value,
|
||||
oldIndex: oldIndex,
|
||||
newIndex: newIndex,
|
||||
oldLineIndex: oldLineIndex,
|
||||
newLineIndex: newLineIndex,
|
||||
oldCharIndexInLine: oldCharIndexInLine,
|
||||
newCharIndexInLine: newCharIndexInLine,
|
||||
});
|
||||
oldIndex += change.value.length;
|
||||
newIndex += change.value.length;
|
||||
oldCharIndexInLine += change.value.length;
|
||||
newCharIndexInLine += change.value.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest";
|
||||
|
||||
import { dedent } from "../util";
|
||||
|
||||
import { myersDiff } from "./myers";
|
||||
import { myersCharDiff, myersDiff } from "./myers";
|
||||
|
||||
describe("Test myers diff function", () => {
|
||||
test("should ...", () => {
|
||||
@@ -56,3 +56,589 @@ describe("Test myers diff function", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test myersCharDiff function on the same line", () => {
|
||||
test("should differentiate character changes", () => {
|
||||
const oldContent = "hello world";
|
||||
const newContent = "hello earth";
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "hello ",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "wo",
|
||||
oldIndex: 6,
|
||||
oldCharIndexInLine: 6,
|
||||
oldLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "ea",
|
||||
newIndex: 6,
|
||||
newCharIndexInLine: 6,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "r",
|
||||
oldIndex: 8,
|
||||
newIndex: 8,
|
||||
oldCharIndexInLine: 8,
|
||||
newCharIndexInLine: 8,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "ld",
|
||||
oldIndex: 9,
|
||||
oldCharIndexInLine: 9,
|
||||
oldLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "th",
|
||||
newIndex: 9,
|
||||
newCharIndexInLine: 9,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle insertions", () => {
|
||||
const oldContent = "abc";
|
||||
const newContent = "abxyzc";
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "ab",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "xyz",
|
||||
newIndex: 2,
|
||||
newCharIndexInLine: 2,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "c",
|
||||
oldIndex: 2,
|
||||
newIndex: 5,
|
||||
oldCharIndexInLine: 2,
|
||||
newCharIndexInLine: 5,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle deletions", () => {
|
||||
const oldContent = "abxyzc";
|
||||
const newContent = "abc";
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "ab",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "xyz",
|
||||
oldIndex: 2,
|
||||
oldCharIndexInLine: 2,
|
||||
oldLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "c",
|
||||
oldIndex: 5,
|
||||
newIndex: 2,
|
||||
oldCharIndexInLine: 5,
|
||||
newCharIndexInLine: 2,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle empty strings", () => {
|
||||
const oldContent = "";
|
||||
const newContent = "abc";
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "new",
|
||||
char: "abc",
|
||||
newIndex: 0,
|
||||
newCharIndexInLine: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle identical strings", () => {
|
||||
const content = "no changes here";
|
||||
|
||||
const diffChars = myersCharDiff(content, content);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "no changes here",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle whitespace changes", () => {
|
||||
const oldContent = "hello world";
|
||||
const newContent = "hello world";
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "hello ",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: " ",
|
||||
newIndex: 6,
|
||||
newCharIndexInLine: 6,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "world",
|
||||
oldIndex: 6,
|
||||
newIndex: 7,
|
||||
oldCharIndexInLine: 6,
|
||||
newCharIndexInLine: 7,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle complex changes", () => {
|
||||
const oldContent = "The quick brown fox jumps over the lazy dog";
|
||||
const newContent = "The fast brown fox leaps over the sleeping dog";
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "The ",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "quick",
|
||||
oldIndex: 4,
|
||||
oldCharIndexInLine: 4,
|
||||
oldLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "fast",
|
||||
newIndex: 4,
|
||||
newCharIndexInLine: 4,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: " brown fox ",
|
||||
oldIndex: 9,
|
||||
newIndex: 8,
|
||||
oldCharIndexInLine: 9,
|
||||
newCharIndexInLine: 8,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "jum",
|
||||
oldIndex: 20,
|
||||
oldCharIndexInLine: 20,
|
||||
oldLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "lea",
|
||||
newIndex: 19,
|
||||
newCharIndexInLine: 19,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "ps over the ",
|
||||
oldIndex: 23,
|
||||
newIndex: 22,
|
||||
oldCharIndexInLine: 23,
|
||||
newCharIndexInLine: 22,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "s",
|
||||
newIndex: 34,
|
||||
newCharIndexInLine: 34,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "l",
|
||||
oldIndex: 35,
|
||||
newIndex: 35,
|
||||
oldCharIndexInLine: 35,
|
||||
newCharIndexInLine: 35,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "azy",
|
||||
oldIndex: 36,
|
||||
oldCharIndexInLine: 36,
|
||||
oldLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "eeping",
|
||||
newIndex: 36,
|
||||
newCharIndexInLine: 36,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: " dog",
|
||||
oldIndex: 39,
|
||||
newIndex: 42,
|
||||
oldCharIndexInLine: 39,
|
||||
newCharIndexInLine: 42,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test myersCharDiff function on different lines", () => {
|
||||
test("should track line indices for multi-line changes", () => {
|
||||
const oldContent = ["Line one", "Line two", "Line three"].join("\n");
|
||||
|
||||
const newContent = ["Line one", "Modified line", "Line three"].join("\n");
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "Line one",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "\n",
|
||||
oldIndex: 8,
|
||||
newIndex: 8,
|
||||
oldCharIndexInLine: 8,
|
||||
newCharIndexInLine: 8,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "L",
|
||||
oldIndex: 9,
|
||||
oldCharIndexInLine: 0,
|
||||
oldLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "Mod",
|
||||
newIndex: 9,
|
||||
newCharIndexInLine: 0,
|
||||
newLineIndex: 1,
|
||||
},
|
||||
{
|
||||
char: "i",
|
||||
newCharIndexInLine: 3,
|
||||
newIndex: 12,
|
||||
newLineIndex: 1,
|
||||
oldCharIndexInLine: 1,
|
||||
oldIndex: 10,
|
||||
oldLineIndex: 1,
|
||||
type: "same",
|
||||
},
|
||||
{
|
||||
char: "n",
|
||||
oldCharIndexInLine: 2,
|
||||
oldIndex: 11,
|
||||
oldLineIndex: 1,
|
||||
type: "old",
|
||||
},
|
||||
{
|
||||
char: "fi",
|
||||
newCharIndexInLine: 4,
|
||||
newIndex: 13,
|
||||
newLineIndex: 1,
|
||||
type: "new",
|
||||
},
|
||||
{
|
||||
char: "e",
|
||||
newCharIndexInLine: 6,
|
||||
newIndex: 15,
|
||||
newLineIndex: 1,
|
||||
oldCharIndexInLine: 3,
|
||||
oldIndex: 12,
|
||||
oldLineIndex: 1,
|
||||
type: "same",
|
||||
},
|
||||
{
|
||||
char: "d",
|
||||
newCharIndexInLine: 7,
|
||||
newIndex: 16,
|
||||
newLineIndex: 1,
|
||||
type: "new",
|
||||
},
|
||||
{
|
||||
char: " ",
|
||||
newCharIndexInLine: 8,
|
||||
newIndex: 17,
|
||||
newLineIndex: 1,
|
||||
oldCharIndexInLine: 4,
|
||||
oldIndex: 13,
|
||||
oldLineIndex: 1,
|
||||
type: "same",
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "two",
|
||||
oldCharIndexInLine: 5,
|
||||
oldIndex: 14,
|
||||
oldLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "line",
|
||||
newCharIndexInLine: 9,
|
||||
newIndex: 18,
|
||||
newLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "\n",
|
||||
oldIndex: 17,
|
||||
oldCharIndexInLine: 8,
|
||||
oldLineIndex: 1,
|
||||
newIndex: 22,
|
||||
newCharIndexInLine: 13,
|
||||
newLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "Line three",
|
||||
oldIndex: 18,
|
||||
newIndex: 23,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 2,
|
||||
newLineIndex: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should track line indices when adding new lines", () => {
|
||||
const oldContent = ["First line", "Last line"].join("\n");
|
||||
|
||||
const newContent = [
|
||||
"First line",
|
||||
"Middle line",
|
||||
"Another middle",
|
||||
"Last line",
|
||||
].join("\n");
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "First line",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "\n",
|
||||
oldIndex: 10,
|
||||
newIndex: 10,
|
||||
oldCharIndexInLine: 10,
|
||||
newCharIndexInLine: 10,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "Middle line",
|
||||
newIndex: 11,
|
||||
newCharIndexInLine: 0,
|
||||
newLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "\n",
|
||||
newIndex: 22,
|
||||
newCharIndexInLine: 11,
|
||||
newLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "Another middle",
|
||||
newCharIndexInLine: 0,
|
||||
newIndex: 23,
|
||||
newLineIndex: 2,
|
||||
},
|
||||
{
|
||||
type: "new",
|
||||
char: "\n",
|
||||
newCharIndexInLine: 14,
|
||||
newIndex: 37,
|
||||
newLineIndex: 2,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "Last line",
|
||||
oldIndex: 11,
|
||||
oldCharIndexInLine: 0,
|
||||
oldLineIndex: 1,
|
||||
newIndex: 38,
|
||||
newCharIndexInLine: 0,
|
||||
newLineIndex: 3,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should track line indices when removing lines", () => {
|
||||
const oldContent = [
|
||||
"Start",
|
||||
"Line to remove",
|
||||
"Another to remove",
|
||||
"End",
|
||||
].join("\n");
|
||||
|
||||
const newContent = ["Start", "End"].join("\n");
|
||||
|
||||
const diffChars = myersCharDiff(oldContent, newContent);
|
||||
expect(diffChars).toEqual([
|
||||
{
|
||||
type: "same",
|
||||
char: "Start",
|
||||
oldIndex: 0,
|
||||
newIndex: 0,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "\n",
|
||||
oldIndex: 5,
|
||||
newIndex: 5,
|
||||
oldCharIndexInLine: 5,
|
||||
newCharIndexInLine: 5,
|
||||
oldLineIndex: 0,
|
||||
newLineIndex: 0,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "Line to remove",
|
||||
oldIndex: 6,
|
||||
oldCharIndexInLine: 0,
|
||||
oldLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "\n",
|
||||
oldCharIndexInLine: 14,
|
||||
oldIndex: 20,
|
||||
oldLineIndex: 1,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "Another to remove",
|
||||
oldCharIndexInLine: 0,
|
||||
oldIndex: 21,
|
||||
oldLineIndex: 2,
|
||||
},
|
||||
{
|
||||
type: "old",
|
||||
char: "\n",
|
||||
oldCharIndexInLine: 17,
|
||||
oldIndex: 38,
|
||||
oldLineIndex: 2,
|
||||
},
|
||||
{
|
||||
type: "same",
|
||||
char: "End",
|
||||
oldIndex: 39,
|
||||
newIndex: 6,
|
||||
oldCharIndexInLine: 0,
|
||||
newCharIndexInLine: 0,
|
||||
oldLineIndex: 3,
|
||||
newLineIndex: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DiffLine, DiffLineType } from "../index.js";
|
||||
import { DiffLine, DiffType } from "../index.js";
|
||||
|
||||
import { LineStream, matchLine } from "./util.js";
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function* streamDiff(
|
||||
seenIndentationMistake = true;
|
||||
}
|
||||
|
||||
let type: DiffLineType;
|
||||
let type: DiffType;
|
||||
|
||||
const isNewLine = matchIndex === -1;
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import { describe, expect, test } from "vitest";
|
||||
// @ts-ignore no typings available
|
||||
import { changed, diff as myersDiff } from "myers-diff";
|
||||
import { streamDiff } from "../diff/streamDiff.js";
|
||||
import { DiffLine, DiffLineType } from "../index.js";
|
||||
import { DiffLine, DiffType } from "../index.js";
|
||||
import { generateLines } from "./util.js";
|
||||
|
||||
// "modification" is an extra type used to represent an "old" + "new" diff line
|
||||
type MyersDiffTypes = Extract<DiffLineType, "new" | "old"> | "modification";
|
||||
type MyersDiffTypes = Extract<DiffType, "new" | "old"> | "modification";
|
||||
|
||||
const UNIFIED_DIFF_SYMBOLS = {
|
||||
same: "",
|
||||
|
||||
19
core/index.d.ts
vendored
19
core/index.d.ts
vendored
@@ -666,13 +666,26 @@ export type CustomLLM = RequireAtLeastOne<
|
||||
|
||||
// IDE
|
||||
|
||||
export type DiffLineType = "new" | "old" | "same";
|
||||
export type DiffType = "new" | "old" | "same";
|
||||
|
||||
export interface DiffLine {
|
||||
type: DiffLineType;
|
||||
export interface DiffObject {
|
||||
type: DiffType;
|
||||
}
|
||||
|
||||
export interface DiffLine extends DiffObject {
|
||||
line: string;
|
||||
}
|
||||
|
||||
interface DiffChar extends DiffObject {
|
||||
char: string;
|
||||
oldIndex?: number; // Character index assuming a flattened line string.
|
||||
newIndex?: number;
|
||||
oldCharIndexInLine?: number; // Character index assuming new lines reset the character index to 0.
|
||||
newCharIndexInLine?: number;
|
||||
oldLineIndex?: number;
|
||||
newLineIndex?: number;
|
||||
}
|
||||
|
||||
export interface Problem {
|
||||
filepath: string;
|
||||
range: Range;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const IS_NEXT_EDIT_ACTIVE = false;
|
||||
export const NEXT_EDIT_EDITABLE_REGION_TOP_MARGIN = 5;
|
||||
export const NEXT_EDIT_EDITABLE_REGION_TOP_MARGIN = 0;
|
||||
export const NEXT_EDIT_EDITABLE_REGION_BOTTOM_MARGIN = 5;
|
||||
export const USER_CURSOR_IS_HERE_TOKEN = "<|user_cursor_is_here|>";
|
||||
export const EDITABLE_REGION_START_TOKEN = "<|editable_region_start|>";
|
||||
|
||||
@@ -3,8 +3,9 @@ import { EXTENSION_NAME } from "core/control-plane/env";
|
||||
// @ts-ignore
|
||||
import * as vscode from "vscode";
|
||||
|
||||
import { DiffLine } from "core";
|
||||
import { DiffChar, DiffLine } from "core";
|
||||
import { CodeRenderer } from "core/codeRenderer/CodeRenderer";
|
||||
import { myersCharDiff } from "core/diff/myers";
|
||||
import {
|
||||
NEXT_EDIT_EDITABLE_REGION_BOTTOM_MARGIN,
|
||||
NEXT_EDIT_EDITABLE_REGION_TOP_MARGIN,
|
||||
@@ -295,14 +296,19 @@ export class NextEditWindowManager {
|
||||
);
|
||||
|
||||
// Create and apply decoration with the text.
|
||||
await this.renderTooltip(
|
||||
await this.renderWindow(
|
||||
editor,
|
||||
currCursorPos,
|
||||
oldEditRangeSlice,
|
||||
newEditRangeSlice,
|
||||
editableRegionStartLine,
|
||||
diffLines,
|
||||
);
|
||||
|
||||
const diffChars = myersCharDiff(oldEditRangeSlice, newEditRangeSlice);
|
||||
|
||||
this.renderDeletes(editor, editableRegionStartLine, diffChars);
|
||||
|
||||
// Reserve tab and esc to either accept or reject the displayed next edit contents.
|
||||
await NextEditWindowManager.reserveTabAndEsc();
|
||||
}
|
||||
@@ -531,6 +537,7 @@ export class NextEditWindowManager {
|
||||
private async createCodeRender(
|
||||
text: string,
|
||||
currLineOffsetFromTop: number,
|
||||
newDiffLines: DiffLine[],
|
||||
): Promise<
|
||||
| { uri: vscode.Uri; dimensions: { width: number; height: number } }
|
||||
| undefined
|
||||
@@ -546,14 +553,15 @@ export class NextEditWindowManager {
|
||||
const uri = await this.codeRenderer.getDataUri(
|
||||
text,
|
||||
"typescript",
|
||||
this.fontSize,
|
||||
this.fontFamily,
|
||||
dimensions,
|
||||
SVG_CONFIG.lineHeight,
|
||||
{
|
||||
imageType: "svg",
|
||||
fontSize: this.fontSize,
|
||||
fontFamily: this.fontFamily,
|
||||
dimensions: dimensions,
|
||||
lineHeight: SVG_CONFIG.lineHeight,
|
||||
},
|
||||
currLineOffsetFromTop,
|
||||
newDiffLines,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -576,11 +584,13 @@ export class NextEditWindowManager {
|
||||
predictedCode: string,
|
||||
position: vscode.Position,
|
||||
editableRegionStartLine: number,
|
||||
newDiffLines: DiffLine[],
|
||||
): Promise<vscode.TextEditorDecorationType | undefined> {
|
||||
const currLineOffsetFromTop = position.line - editableRegionStartLine;
|
||||
const uriAndDimensions = await this.createCodeRender(
|
||||
predictedCode,
|
||||
currLineOffsetFromTop,
|
||||
newDiffLines,
|
||||
);
|
||||
if (!uriAndDimensions) {
|
||||
return undefined;
|
||||
@@ -598,12 +608,12 @@ export class NextEditWindowManager {
|
||||
SVG_CONFIG.getTipWidth(originalCode) -
|
||||
SVG_CONFIG.getTipWidth(originalCode.split("\n")[currLineOffsetFromTop]);
|
||||
|
||||
console.log(marginLeft);
|
||||
console.log(SVG_CONFIG.getTipWidth(originalCode));
|
||||
console.log(
|
||||
SVG_CONFIG.getTipWidth(originalCode.split("\n")[currLineOffsetFromTop]),
|
||||
);
|
||||
console.log(originalCode.split("\n")[currLineOffsetFromTop]);
|
||||
// console.log(marginLeft);
|
||||
// console.log(SVG_CONFIG.getTipWidth(originalCode));
|
||||
// console.log(
|
||||
// SVG_CONFIG.getTipWidth(originalCode.split("\n")[currLineOffsetFromTop]),
|
||||
// );
|
||||
// console.log(originalCode.split("\n")[currLineOffsetFromTop]);
|
||||
return vscode.window.createTextEditorDecorationType({
|
||||
before: {
|
||||
contentIconPath: uri,
|
||||
@@ -703,16 +713,16 @@ export class NextEditWindowManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a tooltip with the given text at the specified position.
|
||||
* Render a window with the given text at the specified position.
|
||||
*/
|
||||
private async renderTooltip(
|
||||
private async renderWindow(
|
||||
editor: vscode.TextEditor,
|
||||
position: vscode.Position,
|
||||
originalCode: string,
|
||||
predictedCode: string,
|
||||
editableRegionStartLine: number,
|
||||
newDiffLines: DiffLine[],
|
||||
) {
|
||||
console.log("renderTooltip");
|
||||
// Capture document version to detect changes.
|
||||
const docVersion = editor.document.version;
|
||||
|
||||
@@ -722,6 +732,7 @@ export class NextEditWindowManager {
|
||||
predictedCode,
|
||||
position,
|
||||
editableRegionStartLine,
|
||||
newDiffLines,
|
||||
);
|
||||
if (!decoration) {
|
||||
console.error("Failed to create decoration for text:", predictedCode);
|
||||
@@ -737,7 +748,8 @@ export class NextEditWindowManager {
|
||||
|
||||
// Store the decoration and editor.
|
||||
await this.hideAllNextEditWindows();
|
||||
this.currentDecoration = decoration;
|
||||
this.currentDecoration = decoration; // TODO: This might be redundant.
|
||||
this.disposables.push(decoration);
|
||||
this.activeEditor = editor;
|
||||
|
||||
// Calculate how far off to the right of the cursor the decoration should be.
|
||||
@@ -773,6 +785,66 @@ export class NextEditWindowManager {
|
||||
this.mostRecentCompletionId,
|
||||
);
|
||||
}
|
||||
|
||||
private renderDeletes(
|
||||
editor: vscode.TextEditor,
|
||||
editableRegionStartLine: number,
|
||||
// oldEditRangeSlice: string,
|
||||
// newEditRangeSlice: string,
|
||||
oldDiffChars: DiffChar[],
|
||||
) {
|
||||
const charsToDelete: vscode.DecorationOptions[] = [];
|
||||
|
||||
// const diffChars = myersCharDiff(oldEditRangeSlice, newEditRangeSlice);
|
||||
|
||||
oldDiffChars.forEach((diff) => {
|
||||
// TODO: This check if technically redundant.
|
||||
if (diff.type === "old") {
|
||||
charsToDelete.push({
|
||||
range: new vscode.Range(
|
||||
new vscode.Position(
|
||||
editableRegionStartLine + diff.oldLineIndex!,
|
||||
diff.oldCharIndexInLine!,
|
||||
),
|
||||
new vscode.Position(
|
||||
editableRegionStartLine + diff.oldLineIndex!,
|
||||
diff.oldCharIndexInLine! + diff.char.length,
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const deleteDecorationType = vscode.window.createTextEditorDecorationType({
|
||||
backgroundColor: "rgba(255, 0, 0, 0.5)",
|
||||
textDecoration: "line-through",
|
||||
});
|
||||
|
||||
editor.setDecorations(deleteDecorationType, charsToDelete);
|
||||
this.disposables.push(deleteDecorationType);
|
||||
}
|
||||
|
||||
async getExactCharacterWidth(): Promise<number> {
|
||||
// For VS Code extensions, you can sometimes access the editor's text metrics
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
if (activeEditor) {
|
||||
// VS Code has internal methods to measure text, but they're not all exposed
|
||||
// in the public API. You might need to use reflection or known properties.
|
||||
|
||||
// Example accessing through reflection (this is pseudocode)
|
||||
const editorInstance = activeEditor as any;
|
||||
if (editorInstance._modelData && editorInstance._modelData.viewModel) {
|
||||
const viewModel = editorInstance._modelData.viewModel;
|
||||
return (
|
||||
viewModel.getLineWidth(0) /
|
||||
activeEditor.document.lineAt(0).text.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If all else fails, return a reasonable default
|
||||
return SVG_CONFIG.fontSize * 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function setupNextEditWindowManager(
|
||||
|
||||
Reference in New Issue
Block a user