Skip to content

Commit de9399e

Browse files
committed
Merge branch 'main' into mermaid-diagrams-2
2 parents 8302ddc + 7165a7b commit de9399e

File tree

103 files changed

+530
-223
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+530
-223
lines changed

core/codeRenderer/CodeRenderer.ts

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,19 @@
88
* we rarely ever need syntax highlighting outside of
99
* creating a render of it.
1010
*/
11+
import {
12+
transformerMetaHighlight,
13+
transformerNotationDiff,
14+
transformerNotationFocus,
15+
transformerNotationHighlight,
16+
} from "@shikijs/transformers";
1117
import { JSDOM } from "jsdom";
12-
import { BundledTheme, codeToHtml, getSingletonHighlighter } from "shiki";
18+
import {
19+
BundledLanguage,
20+
BundledTheme,
21+
getSingletonHighlighter,
22+
Highlighter,
23+
} from "shiki";
1324
import { escapeForSVG, kebabOfStr } from "../util/text";
1425

1526
interface CodeRendererOptions {
@@ -42,6 +53,8 @@ export class CodeRenderer {
4253
private currentTheme: string = "dark-plus";
4354
private editorBackground: string = "#000000";
4455
private editorForeground: string = "#FFFFFF";
56+
private editorLineHighlight: string = "#000000";
57+
private highlighter: Highlighter | null = null;
4558

4659
private constructor() {}
4760

@@ -62,13 +75,17 @@ export class CodeRenderer {
6275
? "dark-plus"
6376
: kebabOfStr(themeName);
6477

65-
const highlighter = await getSingletonHighlighter({
78+
this.highlighter = await getSingletonHighlighter({
79+
langs: ["typescript"],
6680
themes: [this.currentTheme],
6781
});
6882

69-
const th = highlighter.getTheme(this.currentTheme);
83+
const th = this.highlighter.getTheme(this.currentTheme);
84+
7085
this.editorBackground = th.bg;
7186
this.editorForeground = th.fg;
87+
this.editorLineHighlight =
88+
th.colors!["editor.lineHighlightBackground"] ?? "#000000";
7289
} else {
7390
this.currentTheme = "dark-plus";
7491
}
@@ -148,10 +165,29 @@ export class CodeRenderer {
148165
async highlightCode(
149166
code: string,
150167
language: string = "javascript",
168+
currLineOffsetFromTop: number,
151169
): Promise<string> {
152-
return await codeToHtml(code, {
170+
const annotatedCode = code
171+
.split("\n")
172+
.map((line, i) =>
173+
i === currLineOffsetFromTop
174+
? line + " \/\/ \[\!code highlight\]"
175+
: line,
176+
)
177+
.join("\n");
178+
179+
await this.highlighter!.loadLanguage(language as BundledLanguage);
180+
181+
return this.highlighter!.codeToHtml(annotatedCode, {
153182
lang: language,
154183
theme: this.currentTheme,
184+
transformers: [
185+
// transformerColorizedBrackets(),
186+
transformerMetaHighlight(),
187+
transformerNotationHighlight(),
188+
transformerNotationDiff(),
189+
transformerNotationFocus(),
190+
],
155191
});
156192
}
157193

@@ -163,23 +199,40 @@ export class CodeRenderer {
163199
dimensions: Dimensions,
164200
lineHeight: number,
165201
options: ConversionOptions,
202+
currLineOffsetFromTop: number,
166203
): Promise<Buffer> {
167-
const highlightedCodeHtml = await this.highlightCode(code, language);
204+
const strokeWidth = 1;
205+
const highlightedCodeHtml = await this.highlightCode(
206+
code,
207+
language,
208+
currLineOffsetFromTop,
209+
);
210+
// console.log(highlightedCodeHtml);
168211

169-
const guts = this.convertShikiHtmlToSvgGut(
212+
const { guts, lineBackgrounds } = this.convertShikiHtmlToSvgGut(
170213
highlightedCodeHtml,
171214
fontSize,
172215
fontFamily,
173216
lineHeight,
217+
dimensions,
174218
);
175219
const backgroundColor = this.getBackgroundColor(highlightedCodeHtml);
176220

177-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${dimensions.width}" height="${dimensions.height}">
221+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${dimensions.width}" height="${dimensions.height}" shape-rendering="crispEdges">
222+
<style>
223+
:root {
224+
--purple: rgb(112, 114, 209);
225+
--green: rgb(136, 194, 163);
226+
--blue: rgb(107, 166, 205);
227+
}
228+
</style>
178229
<g>
179-
<rect width="${dimensions.width}" height="${dimensions.height}" fill="${backgroundColor}" stroke="${this.editorForeground}" stroke-width="1" />
230+
<rect x="0" y="0" rx="10" ry="10" width="${dimensions.width}" height="${dimensions.height}" fill="${this.editorBackground}" shape-rendering="crispEdges" />
231+
${lineBackgrounds}
180232
${guts}
181233
</g>
182234
</svg>`;
235+
console.log(svg);
183236

184237
return Buffer.from(svg, "utf8");
185238
}
@@ -189,7 +242,8 @@ export class CodeRenderer {
189242
fontSize: number,
190243
fontFamily: string,
191244
lineHeight: number,
192-
): string {
245+
dimensions: Dimensions,
246+
): { guts: string; lineBackgrounds: string } {
193247
const dom = new JSDOM(shikiHtml);
194248
const document = dom.window.document;
195249

@@ -204,19 +258,70 @@ export class CodeRenderer {
204258
const el = node as HTMLElement;
205259
const style = el.getAttribute("style") || "";
206260
const colorMatch = style.match(/color:\s*(#[0-9a-fA-F]{6})/);
207-
const fill = colorMatch ? ` fill="${colorMatch[1]}"` : "";
261+
const classes = el.getAttribute("class") || "";
262+
let fill = colorMatch ? ` fill="${colorMatch[1]}"` : "";
263+
if (classes.includes("highlighted")) {
264+
fill = ` fill="${this.editorLineHighlight}"`;
265+
}
208266
const content = el.textContent || "";
209267
return `<tspan xml:space="preserve"${fill}>${escapeForSVG(content)}</tspan>`;
210268
})
211269
.join("");
212270

213-
const y = (index + 1) * lineHeight;
214-
return `<text x="0" y="${y}" font-family="${fontFamily}" font-size="${fontSize.toString()}" xml:space="preserve">${spans}</text>`;
271+
// Typography notes:
272+
// Each line of code is a <text> inside a <rect>.
273+
// Math becomes interesting here; the y value is actually aligned to the topmost border.
274+
// So y = 0 will have the rect be flush with the top border.
275+
// More importantly, text will also be positioned that way.
276+
// Since y = 0 is the axis the text will align itself to, the default settings will actually have the text sitting "on top of" the y = 0 axis, which effectively shifts them up.
277+
// To prevent this, we want the alignment axis to be at the middle of each rect, and have the text align itself vertically to the center (skwered by the axis).
278+
// The first step is to add lineHeight / 2 to move the axis down.
279+
// The second step is to add 'dominant-baseline="central"' to vertically center the text.
280+
// Note that we choose "central" over "middle". "middle" will center the text too perfectly, which is actually undesirable!
281+
const y = index * lineHeight + lineHeight / 2;
282+
return `<text x="0" y="${y}" font-family="${fontFamily}" font-size="${fontSize.toString()}" xml:space="preserve" dominant-baseline="central" shape-rendering="crispEdges">${spans}</text>`;
215283
});
216284

217-
return `
218-
${svgLines.join("\n")}
219-
`.trim();
285+
const lineBackgrounds = lines
286+
.map((line, index) => {
287+
const classes = line?.getAttribute("class") || "";
288+
const bgColor = classes.includes("highlighted")
289+
? this.editorLineHighlight
290+
: this.editorBackground;
291+
const y = index * lineHeight;
292+
const isFirst = index === 0;
293+
const isLast = index === lines.length - 1;
294+
const radius = 10;
295+
// SVG notes:
296+
// By default SVGs have anti-aliasing on.
297+
// This is undesirable in our case because pixel-perfect alignment of these rectangles will introduce thin gaps.
298+
// Turning it off with 'shape-rendering="crispEdges"' solves the issue.
299+
return isFirst
300+
? `<path d="M ${0} ${y + lineHeight}
301+
L ${0} ${y + radius}
302+
Q ${0} ${y} ${radius} ${y}
303+
L ${dimensions.width - radius} ${y}
304+
Q ${dimensions.width} ${y} ${dimensions.width} ${y + radius}
305+
L ${dimensions.width} ${y + lineHeight}
306+
Z"
307+
fill="${bgColor}" />`
308+
: isLast
309+
? `<path d="M ${0} ${y}
310+
L ${0} ${y + lineHeight - radius}
311+
Q ${0} ${y + lineHeight} ${radius} ${y + lineHeight}
312+
L ${dimensions.width - radius} ${y + lineHeight}
313+
Q ${dimensions.width} ${y + lineHeight} ${dimensions.width} ${y + lineHeight - 10}
314+
L ${dimensions.width} ${y}
315+
Z"
316+
fill="${bgColor}" />`
317+
: `<rect x="0" y="${y}" rx="${radius}" ry="${radius}" width="100%" height="${lineHeight}" fill="${bgColor}" shape-rendering="crispEdges" />`;
318+
})
319+
.join("\n");
320+
321+
return {
322+
guts: svgLines.join("\n"),
323+
lineBackgrounds,
324+
};
220325
}
221326

222327
getBackgroundColor(shikiHtml: string): string {
@@ -244,6 +349,7 @@ export class CodeRenderer {
244349
dimensions: Dimensions,
245350
lineHeight: number,
246351
options: ConversionOptions,
352+
currLineOffsetFromTop: number,
247353
): Promise<DataUri> {
248354
switch (options.imageType) {
249355
// case "png":
@@ -265,6 +371,7 @@ export class CodeRenderer {
265371
dimensions,
266372
lineHeight,
267373
options,
374+
currLineOffsetFromTop,
268375
);
269376
return `data:image/svg+xml;base64,${svgBuffer.toString("base64")}`;
270377
}

core/edit/streamDiffLines.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ export async function* addIndentation(
5555
}
5656

5757
function modelIsInept(model: string): boolean {
58-
return !(model.includes("gpt") || model.includes("claude"));
58+
return !(
59+
model.includes("gpt") ||
60+
model.includes("claude") ||
61+
model.includes("nova")
62+
);
5963
}
6064

6165
export async function* streamDiffLines({

core/llm/autodetect.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ function autodetectTemplateType(model: string): TemplateType | undefined {
237237
return "none";
238238
}
239239

240+
// Nova Pro requests always sent through Converse API, so formatting not necessary
241+
if (lower.includes("nova")) {
242+
return "none";
243+
}
244+
240245
if (lower.includes("codestral")) {
241246
return "none";
242247
}

core/llm/autodetect.vitest.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ test("autodetectTemplateType returns 'none' for Codestral models", () => {
106106
expect(autodetectTemplateType("codestral-22b")).toBe("none");
107107
});
108108

109+
test("autodetectTemplateType returns 'none' for Nova models", () => {
110+
expect(autodetectTemplateType("nova")).toBe("none");
111+
expect(autodetectTemplateType("Nova")).toBe("none");
112+
expect(autodetectTemplateType("nova-pro")).toBe("none");
113+
expect(autodetectTemplateType("nova-lite")).toBe("none");
114+
expect(autodetectTemplateType("nova-micro")).toBe("none");
115+
expect(autodetectTemplateType("nova-premier")).toBe("none");
116+
expect(autodetectTemplateType("amazon-nova-pro")).toBe("none");
117+
expect(autodetectTemplateType("amazon-nova-lite")).toBe("none");
118+
expect(autodetectTemplateType("amazon-nova-micro")).toBe("none");
119+
expect(autodetectTemplateType("amazon-nova-premier")).toBe("none");
120+
});
121+
109122
test("autodetectTemplateType returns 'alpaca' for Alpaca and Wizard models", () => {
110123
expect(autodetectTemplateType("alpaca")).toBe("alpaca");
111124
expect(autodetectTemplateType("Alpaca")).toBe("alpaca");

core/llm/toolSupport.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ export const PROVIDER_TOOL_SUPPORT: Record<string, (model: string) => boolean> =
9696
"claude-3.7-sonnet",
9797
"claude-sonnet-4",
9898
"claude-opus-4",
99+
"nova-lite",
100+
"nova-pro",
101+
"nova-micro",
102+
"nova-premier",
99103
].some((part) => model.toLowerCase().includes(part))
100104
) {
101105
return true;

0 commit comments

Comments
 (0)