diff --git a/changelog/2025-07-scopeDocumentation.md b/changelog/2025-07-scopeDocumentation.md new file mode 100644 index 0000000000..ec0517b4f5 --- /dev/null +++ b/changelog/2025-07-scopeDocumentation.md @@ -0,0 +1,6 @@ +--- +tags: [documentation] +pullRequest: 3016 +--- + +- Visualize scope tests in docs. Visualizes scope fixtures on [cursorless.org/docs/user/languages](https://www.cursorless.org/docs/user/languages). diff --git a/package.json b/package.json index 3f6001190c..65e8db70ef 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "pnpm": { "patchedDependencies": { + "@shikijs/core": "patches/@shikijs__core.patch", "@types/nearley@2.11.5": "patches/@types__nearley@2.11.5.patch", "nearley@2.20.1": "patches/nearley@2.20.1.patch" } diff --git a/packages/common/package.json b/packages/common/package.json index d366ab0e15..073480e77b 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -25,13 +25,16 @@ "watch": "pnpm run --filter @cursorless/common --parallel '/^watch:.*/'" }, "dependencies": { + "itertools": "2.4.1", "lodash-es": "4.17.21", + "tinycolor2": "1.6.0", "vscode-uri": "3.1.0" }, "devDependencies": { "@types/js-yaml": "4.0.9", "@types/lodash-es": "4.17.12", "@types/mocha": "10.0.10", + "@types/tinycolor2": "1.4.6", "cross-spawn": "7.0.6", "fast-check": "4.1.1", "js-yaml": "4.1.0", diff --git a/packages/common/src/cursorlessCommandIds.ts b/packages/common/src/cursorlessCommandIds.ts index c9292d4cc3..51930a9e83 100644 --- a/packages/common/src/cursorlessCommandIds.ts +++ b/packages/common/src/cursorlessCommandIds.ts @@ -54,6 +54,7 @@ export const cursorlessCommandIds = [ "cursorless.toggleDecorations", "cursorless.showScopeVisualizer", "cursorless.hideScopeVisualizer", + "cursorless.scopeVisualizer.openUrl", "cursorless.tutorial.start", "cursorless.tutorial.next", "cursorless.tutorial.previous", @@ -100,6 +101,7 @@ export const cursorlessCommandDescriptions: Record< ["cursorless.hideScopeVisualizer"]: new VisibleCommand( "Hide the scope visualizer", ), + ["cursorless.scopeVisualizer.openUrl"]: new VisibleCommand("Open in browser"), ["cursorless.analyzeCommandHistory"]: new VisibleCommand( "Analyze collected command history", ), diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index b6c8572ee2..7744e99d3c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -34,6 +34,10 @@ export * from "./scopeSupportFacets/languageScopeSupport"; export * from "./scopeSupportFacets/PlaintextScopeSupportFacetInfos"; export * from "./scopeSupportFacets/scopeSupportFacetInfos"; export * from "./scopeSupportFacets/scopeSupportFacets.types"; +export * from "./scopeVisualizerUtil/decorationStyle.types"; +export * from "./scopeVisualizerUtil/decorationUtil"; +export * from "./scopeVisualizerUtil/generateDecorationsForCharacterRange"; +export * from "./scopeVisualizerUtil/generateDecorationsForLineRange"; export * from "./StoredTargetKey"; export * from "./testUtil/asyncSafety"; export * from "./testUtil/extractTargetedMarks"; @@ -93,6 +97,7 @@ export * from "./types/Token"; export * from "./types/TreeSitter"; export * from "./types/tutorial.types"; export * from "./util"; +export * from "./util/blendColors"; export * from "./util/clientSupportsFallback"; export * from "./util/CompositeKeyDefaultMap"; export * from "./util/CompositeKeyMap"; @@ -105,7 +110,6 @@ export * from "./util/Notifier"; export * from "./util/object"; export * from "./util/omitByDeep"; export * from "./util/prettifyLanguageName"; -export * from "./util/range"; export * from "./util/regex"; export * from "./util/selectionsEqual"; export * from "./util/serializedMarksToTokenHats"; diff --git a/packages/common/src/scopeVisualizerUtil/decorationStyle.types.ts b/packages/common/src/scopeVisualizerUtil/decorationStyle.types.ts new file mode 100644 index 0000000000..84bcd7254c --- /dev/null +++ b/packages/common/src/scopeVisualizerUtil/decorationStyle.types.ts @@ -0,0 +1,20 @@ +import type { Range } from "../types/Range"; + +export interface StyledRange { + style: DecorationStyle; + range: Range; +} + +export interface DecorationStyle { + top: BorderStyle; + bottom: BorderStyle; + left: BorderStyle; + right: BorderStyle; + isWholeLine?: boolean; +} + +export enum BorderStyle { + porous = "dashed", + solid = "solid", + none = "none", +} diff --git a/packages/common/src/scopeVisualizerUtil/decorationUtil.ts b/packages/common/src/scopeVisualizerUtil/decorationUtil.ts new file mode 100644 index 0000000000..8e0b9a298a --- /dev/null +++ b/packages/common/src/scopeVisualizerUtil/decorationUtil.ts @@ -0,0 +1,48 @@ +import { BorderStyle } from "./decorationStyle.types"; +import type { DecorationStyle } from "./decorationStyle.types"; + +export const BORDER_WIDTH = "1px"; +export const BORDER_RADIUS = "2px"; + +export function getBorderStyle(borders: DecorationStyle): string { + return [borders.top, borders.right, borders.bottom, borders.left].join(" "); +} + +export function getBorderColor( + solidColor: string, + porousColor: string, + borders: DecorationStyle, +): string { + return [ + borders.top === BorderStyle.solid ? solidColor : porousColor, + borders.right === BorderStyle.solid ? solidColor : porousColor, + borders.bottom === BorderStyle.solid ? solidColor : porousColor, + borders.left === BorderStyle.solid ? solidColor : porousColor, + ].join(" "); +} + +export function getBorderRadius(borders: DecorationStyle): string { + return [ + getSingleCornerBorderRadius(borders.top, borders.left), + getSingleCornerBorderRadius(borders.top, borders.right), + getSingleCornerBorderRadius(borders.bottom, borders.right), + getSingleCornerBorderRadius(borders.bottom, borders.left), + ].join(" "); +} + +export function useSingleCornerBorderRadius( + side1: BorderStyle, + side2: BorderStyle, +): boolean { + // We only round the corners if both sides are solid, as that makes them look + // more finished, whereas we want the dotted borders to look unfinished / cut + // off. + return side1 === BorderStyle.solid && side2 === BorderStyle.solid; +} + +export function getSingleCornerBorderRadius( + side1: BorderStyle, + side2: BorderStyle, +) { + return useSingleCornerBorderRadius(side1, side2) ? BORDER_RADIUS : "0px"; +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts similarity index 61% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts rename to packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts index b09c537d41..6a2caf5193 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts +++ b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/generateDecorationsForCharacterRange.ts @@ -1,5 +1,4 @@ -import type { Range, TextEditor } from "@cursorless/common"; -import { getLineRanges } from "@cursorless/common"; +import type { Range } from "../../types/Range"; import type { StyledRange } from "../decorationStyle.types"; import { BorderStyle } from "../decorationStyle.types"; import { handleMultipleLines } from "./handleMultipleLines"; @@ -10,7 +9,7 @@ import { handleMultipleLines } from "./handleMultipleLines"; * that the range is visually distinct from adjacent ranges but looks continuous. */ export function* generateDecorationsForCharacterRange( - editor: TextEditor, + getLineRanges: (range: Range) => Range[], range: Range, ): Iterable { if (range.isSingleLine) { @@ -26,5 +25,15 @@ export function* generateDecorationsForCharacterRange( return; } - yield* handleMultipleLines(getLineRanges(editor, range)); + // A list of ranges, one for each line in the given range, with the first and + // last ranges trimmed to the start and end of the given range. + const lineRanges = getLineRanges(range); + + lineRanges[0] = lineRanges[0].with(range.start); + lineRanges[lineRanges.length - 1] = lineRanges[lineRanges.length - 1].with( + undefined, + range.end, + ); + + yield* handleMultipleLines(lineRanges); } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateLineInfos.ts b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/generateLineInfos.ts similarity index 97% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateLineInfos.ts rename to packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/generateLineInfos.ts index b8ea3b4998..50452270a9 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/generateLineInfos.ts +++ b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/generateLineInfos.ts @@ -1,4 +1,4 @@ -import type { Range } from "@cursorless/common"; +import type { Range } from "../.."; /** * Generates a line info for each line in the given range, which includes diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/handleMultipleLines.test.ts similarity index 98% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts rename to packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/handleMultipleLines.test.ts index b3677e1482..a4da8afef0 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.test.ts +++ b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/handleMultipleLines.test.ts @@ -1,8 +1,8 @@ import assert from "assert"; +import { map } from "itertools"; +import { Range } from "../.."; import { BorderStyle } from "../decorationStyle.types"; import { handleMultipleLines } from "./handleMultipleLines"; -import { Range } from "@cursorless/common"; -import { map } from "itertools"; const solid = BorderStyle.solid; const porous = BorderStyle.porous; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/handleMultipleLines.ts similarity index 99% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts rename to packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/handleMultipleLines.ts index d1c537a68f..7b668f824b 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/handleMultipleLines.ts +++ b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/handleMultipleLines.ts @@ -1,7 +1,7 @@ -import { Range } from "@cursorless/common"; +import { flatmap } from "itertools"; +import { Range } from "../.."; import type { DecorationStyle, StyledRange } from "../decorationStyle.types"; import { BorderStyle } from "../decorationStyle.types"; -import { flatmap } from "itertools"; import type { LineInfo } from "./generateLineInfos"; import { generateLineInfos } from "./generateLineInfos"; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/index.ts b/packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/index.ts similarity index 100% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForCharacterRange/index.ts rename to packages/common/src/scopeVisualizerUtil/generateDecorationsForCharacterRange/index.ts diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts b/packages/common/src/scopeVisualizerUtil/generateDecorationsForLineRange.ts similarity index 88% rename from packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts rename to packages/common/src/scopeVisualizerUtil/generateDecorationsForLineRange.ts index c1b79c2f40..930b258693 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/generateDecorationsForLineRange.ts +++ b/packages/common/src/scopeVisualizerUtil/generateDecorationsForLineRange.ts @@ -1,6 +1,5 @@ -import { Range } from "@cursorless/common"; -import type { StyledRange } from "./decorationStyle.types"; -import { BorderStyle } from "./decorationStyle.types"; +import { Range } from "../types/Range"; +import { BorderStyle, type StyledRange } from "./decorationStyle.types"; export function* generateDecorationsForLineRange( startLine: number, diff --git a/packages/common/src/util/blendColors.ts b/packages/common/src/util/blendColors.ts new file mode 100644 index 0000000000..7b01416f52 --- /dev/null +++ b/packages/common/src/util/blendColors.ts @@ -0,0 +1,40 @@ +import tinycolor from "tinycolor2"; + +/** + * Blends two colors together according to their alpha channels, with the top + * color rendered on top of the base color. + * + * Basd on https://gist.github.com/JordanDelcros/518396da1c13f75ee057 + * + * @param base The color to render underneath + * @param top The color to render on top + * @returns A color that is a blend of the two colors, with the top color + * rendered on top of the base color + */ +export function blendColors(base: string, top: string): string { + const baseRgba = tinycolor(base).toRgb(); + const topRgba = tinycolor(top).toRgb(); + const blendedAlpha = 1 - (1 - topRgba.a) * (1 - baseRgba.a); + + function interpolateChannel(channel: "r" | "g" | "b"): number { + return Math.round( + (topRgba[channel] * topRgba.a) / blendedAlpha + + (baseRgba[channel] * baseRgba.a * (1 - topRgba.a)) / blendedAlpha, + ); + } + + return tinycolor({ + r: interpolateChannel("r"), + g: interpolateChannel("g"), + b: interpolateChannel("b"), + a: blendedAlpha, + }).toHex8String(); +} + +export function blendMultipleColors(colors: string[]): string { + let color = colors[0]; + for (let i = 1; i < colors.length; i++) { + color = blendColors(color, colors[i]); + } + return color; +} diff --git a/packages/common/src/util/range.ts b/packages/common/src/util/range.ts deleted file mode 100644 index 6cb9ece574..0000000000 --- a/packages/common/src/util/range.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { range as lodashRange } from "lodash-es"; -import type { Range } from "../types/Range"; -import type { TextEditor } from "../types/TextEditor"; - -/** - * @param editor The editor containing the range - * @param range The range to get the line ranges for - * @returns A list of ranges, one for each line in the given range, with the - * first and last ranges trimmed to the start and end of the given range. - */ -export function getLineRanges(editor: TextEditor, range: Range): Range[] { - const { document } = editor; - const lineRanges = lodashRange(range.start.line, range.end.line + 1).map( - (lineNumber) => document.lineAt(lineNumber).range, - ); - lineRanges[0] = lineRanges[0].with(range.start); - lineRanges[lineRanges.length - 1] = lineRanges[lineRanges.length - 1].with( - undefined, - range.end, - ); - return lineRanges; -} diff --git a/packages/cursorless-neovim/src/registerCommands.ts b/packages/cursorless-neovim/src/registerCommands.ts index 048955b38b..e25bbc7af7 100644 --- a/packages/cursorless-neovim/src/registerCommands.ts +++ b/packages/cursorless-neovim/src/registerCommands.ts @@ -98,6 +98,7 @@ export async function registerCommands( // Scope visualizer ["cursorless.showScopeVisualizer"]: dummyCommandHandler, ["cursorless.hideScopeVisualizer"]: dummyCommandHandler, + ["cursorless.scopeVisualizer.openUrl"]: dummyCommandHandler, // Command history ["cursorless.analyzeCommandHistory"]: dummyCommandHandler, diff --git a/packages/cursorless-org-docs/docusaurus.config.mts b/packages/cursorless-org-docs/docusaurus.config.mts index e96a0adaad..d25c027316 100644 --- a/packages/cursorless-org-docs/docusaurus.config.mts +++ b/packages/cursorless-org-docs/docusaurus.config.mts @@ -146,7 +146,10 @@ const config: Config = { }, ], ], - plugins: ["./src/plugins/tailwind-plugin.ts"], + plugins: [ + "./src/plugins/tailwind-plugin.ts", + "./src/plugins/scope-tests-plugin.ts", + ], themeConfig: { navbar: { diff --git a/packages/cursorless-org-docs/package.json b/packages/cursorless-org-docs/package.json index 6f3dfdc595..e72a861717 100644 --- a/packages/cursorless-org-docs/package.json +++ b/packages/cursorless-org-docs/package.json @@ -59,9 +59,11 @@ "prism-react-renderer": "2.4.1", "react": "19.1.0", "react-dom": "19.1.0", + "shiki": "3.7.0", "unist-util-visit": "5.0.0" }, "devDependencies": { + "@cursorless/node-common": "workspace:*", "@docusaurus/module-type-aliases": "3.8.1", "@docusaurus/types": "3.8.1", "@tailwindcss/postcss": "4.1.10", diff --git a/packages/cursorless-org-docs/src/css/custom.css b/packages/cursorless-org-docs/src/css/custom.css index 6dcbe7ba80..1903e2bfc8 100644 --- a/packages/cursorless-org-docs/src/css/custom.css +++ b/packages/cursorless-org-docs/src/css/custom.css @@ -46,7 +46,3 @@ .pointer { cursor: pointer; } - -.facet-name { - font-weight: 600; -} diff --git a/packages/cursorless-org-docs/src/docs/components/Code.css b/packages/cursorless-org-docs/src/docs/components/Code.css new file mode 100644 index 0000000000..18394a5cb4 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/Code.css @@ -0,0 +1,66 @@ +.code-container { + position: relative; + width: 100%; + min-height: 50px; + counter-reset: line; +} + +.code-container .shiki { + line-height: 1rem; +} + +.code-container .line { + position: relative; + margin-left: 1.5rem; +} + +.code-container .line::before { + content: counter(line); + counter-increment: line; + position: absolute; + left: -40px; + padding-left: 0.5em; + padding-right: 0.5em; + width: 1rem; + text-align: right; + height: 135%; + color: rgba(115, 138, 148, 0.4); + border-right: 1px solid rgba(115, 138, 148, 0.4); +} + +.code-ws-symbol { + color: #666; +} + +.code-copy-button, +.code-container > .code-link { + display: none; + position: absolute; + top: 0.5em; + background-color: rgb(216, 222, 233); + border: none; + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 0.75rem; + height: 1.75rem; +} + +.code-container:hover .code-copy-button, +.code-container:hover .code-link { + display: block; +} + +.code-copy-button { + right: 7.5em; +} + +.code-container > .code-link { + right: 0.5em; +} + +.code-container > .code-link:before { + display: inline-block; + width: 1.25rem; + height: 0.75rem; +} diff --git a/packages/cursorless-org-docs/src/docs/components/Code.tsx b/packages/cursorless-org-docs/src/docs/components/Code.tsx new file mode 100644 index 0000000000..ba1a7bedd9 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/Code.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from "react"; +import { codeToHtml, type DecorationItem } from "shiki"; +import "./Code.css"; + +interface Props { + languageId: string; + renderWhitespace?: boolean; + decorations?: DecorationItem[]; + link?: { + name: string; + url: string; + }; + children: string; +} + +export function Code({ + languageId, + renderWhitespace, + decorations, + link, + children, +}: Props) { + const [html, setHtml] = React.useState(""); + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (renderWhitespace) { + children = children.replaceAll(" ", "␣").replaceAll("\t", "⭾"); + } + codeToHtml(children, { + lang: getFallbackLanguage(languageId), + theme: "nord", + decorations, + }) + .then((html) => { + if (renderWhitespace) { + html = html + .replace(/␣/g, '·') + .replace(/⭾/g, ''); + } + setHtml(html); + }) + .catch(console.error); + }, [languageId, renderWhitespace, decorations, link, children]); + + if (!html) { + return
; + } + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(children); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch (err) { + console.error("Failed to copy!", err); + } + }; + + const renderLink = () => { + if (link == null) { + return null; + } + return ( + + {link.name} + + ); + }; + + return ( +
+ {renderLink()} + +
{" "} +
+ ); +} + +// Use a fallback language for languages that are not supported by Shiki +// https://shiki.style/languages +function getFallbackLanguage(languageId: string): string { + switch (languageId) { + case "javascriptreact": + return "jsx"; + case "typescriptreact": + return "tsx"; + case "scm": + return "scheme"; + default: + return languageId; + } +} diff --git a/packages/cursorless-org-docs/src/docs/components/DynamicTOC.tsx b/packages/cursorless-org-docs/src/docs/components/DynamicTOC.tsx new file mode 100644 index 0000000000..78ea7d9f7b --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/DynamicTOC.tsx @@ -0,0 +1,83 @@ +import { useEffect } from "react"; + +interface Props { + minHeadingLevel?: number; + maxHeadingLevel?: number; +} + +export function DynamicTOC({ + minHeadingLevel = 2, + maxHeadingLevel = 3, +}: Props) { + useEffect(() => { + const row = document.querySelector("main .row"); + + if (row == null) { + console.error("No row found in the main element"); + return; + } + + const toc = getTOC(minHeadingLevel, maxHeadingLevel); + + // Remove existing TOC if it exists + if (row.childNodes.length > 1) { + row.replaceChild(toc, row.childNodes[1]); + } else { + row.appendChild(toc); + } + }, []); + + return null; +} + +function getTOC(minHeadingLevel: number, maxHeadingLevel: number) { + const col = document.createElement("div"); + col.className = "col col--3"; + + const toc = document.createElement("div"); + toc.className = "tableOfContents_tkZC thin-scrollbar"; + + const ul = document.createElement("ul"); + ul.className = "table-of-contents table-of-contents__left-border"; + + let currentLevel: number | undefined = undefined; + let indent = 0; + + getHeaderElements(minHeadingLevel, maxHeadingLevel).forEach((header) => { + const level = parseInt(header.tagName[1], 10); + + if (level !== currentLevel) { + if (currentLevel != null) { + indent += level < currentLevel ? -1 : 1; + } + currentLevel = level; + } + + const li = document.createElement("li"); + + const a = document.createElement("a"); + a.href = `#${header.id}`; + a.className = "table-of-contents__link"; + a.textContent = header.textContent; + a.style.paddingLeft = `${indent}rem`; + + li.appendChild(a); + ul.appendChild(li); + }); + + toc.appendChild(ul); + col.appendChild(toc); + + return col; +} + +function getHeaderElements( + minHeadingLevel: number, + maxHeadingLevel: number, +): NodeListOf { + const queryParts = []; + for (let i = minHeadingLevel; i <= maxHeadingLevel; i++) { + queryParts.push(`h${i}`); + } + return document.querySelectorAll(queryParts.join(", ")); +} diff --git a/packages/cursorless-org-docs/src/docs/components/Header.tsx b/packages/cursorless-org-docs/src/docs/components/Header.tsx new file mode 100644 index 0000000000..eba90b11b0 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/Header.tsx @@ -0,0 +1,46 @@ +import { uriEncodeHashId } from "@cursorless/common"; +import React from "react"; + +interface Props { + className?: string; + id?: string; + title?: string; + children: string; +} + +export function H2(props: Props) { + return renderHeader(2, props); +} + +export function H3(props: Props) { + return renderHeader(3, props); +} + +export function H4(props: Props) { + return renderHeader(4, props); +} + +export function H5(props: Props) { + return renderHeader(5, props); +} + +function renderHeader( + level: number, + { className, id, title, children }: Props, +): React.JSX.Element { + const Tag = `h${level}` as keyof React.JSX.IntrinsicElements; + const encodedId = uriEncodeHashId(id ?? children); + return ( + + {children} + + + ); +} diff --git a/packages/cursorless-org-docs/src/docs/components/ScopeSupport.css b/packages/cursorless-org-docs/src/docs/components/ScopeSupport.css new file mode 100644 index 0000000000..9a9d0fffcd --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/ScopeSupport.css @@ -0,0 +1,4 @@ +.facet-name { + font-weight: 600; + margin: 0; +} diff --git a/packages/cursorless-org-docs/src/docs/components/ScopeVisualizer.tsx b/packages/cursorless-org-docs/src/docs/components/ScopeVisualizer.tsx new file mode 100644 index 0000000000..2eecbc9b5e --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/ScopeVisualizer.tsx @@ -0,0 +1,296 @@ +import { + prettifyLanguageName, + prettifyScopeType, + serializeScopeType, + type ScopeSupportFacetInfo, + type ScopeTypeType, +} from "@cursorless/common"; +import { usePluginData } from "@docusaurus/useGlobalData"; +import React, { useState } from "react"; +import { generateDecorations } from "./calculateHighlights"; +import { Code } from "./Code"; +import { H2, H3, H4, H5 } from "./Header"; +import "./ScopeSupport.css"; +import type { FacetValue, Fixture, RangeType, ScopeTests } from "./types"; +import { + getFacetInfo, + isScopeInternal, + nameComparator, + prettifyFacet, +} from "./util"; + +interface Scopes { + public: Scope[]; + internal: Scope[]; +} + +interface Scope { + scopeTypeType: ScopeTypeType; + name: string; + facets: Facet[]; +} + +interface Facet { + facet: FacetValue; + name: string; + info: ScopeSupportFacetInfo; + fixtures: Fixture[]; +} + +interface Props { + languageId?: string; + scopeTypeType?: ScopeTypeType; +} + +export function ScopeVisualizer({ languageId, scopeTypeType }: Props) { + const scopeTests = usePluginData("scope-tests-plugin") as ScopeTests; + const [scopes] = useState( + getScopeFixtures(scopeTests, languageId, scopeTypeType), + ); + const [rangeType, setRangeType] = useState("content"); + const [renderWhitespace, setRenderWhitespace] = useState(false); + + const renderOptions = () => { + return ( +
+ + + +
+ ); + }; + + const renderInternalScopes = () => { + if (scopes.internal.length === 0) { + return null; + } + return ( + <> +

Internal scopes

+ + {languageId && ( +

+ The following are internal scopes. They are not intended for user + interaction or spoken use. These scopes exist solely for internal + Cursorless functionality. +

+ )} + + {scopes.internal.map((scope) => + renderScope( + languageId, + scopeTypeType, + rangeType, + renderWhitespace, + scope, + ), + )} + + ); + }; + + return ( + <> +

Scopes

+ + {languageId && ( +

+ Below are visualizations of all our scope tests for this language. + These were created primarily for testing purposes rather than as + documentation. There are quite a few, and they may feel a bit + overwhelming from a documentation standpoint. +

+ )} + + {renderOptions()} + + {scopes.public.map((scope) => + renderScope( + languageId, + scopeTypeType, + rangeType, + renderWhitespace, + scope, + ), + )} + + {renderInternalScopes()} + + ); +} + +function renderScope( + languageId: string | undefined, + scopeTypeType: ScopeTypeType | undefined, + rangeType: RangeType, + renderWhitespace: boolean, + scope: Scope, +) { + return ( +
+ {scopeTypeType == null &&

{scope.name}

} + {scope.facets.map((facet, index) => + renderFacet( + languageId, + scopeTypeType, + rangeType, + renderWhitespace, + facet, + index, + ), + )} +
+ ); +} + +function renderFacet( + languageId: string | undefined, + scopeTypeType: ScopeTypeType | undefined, + rangeType: RangeType, + renderWhitespace: boolean, + facet: Facet, + index: number, +) { + let previousLanguageId: string | undefined; + + const renderFacetName = () => { + if (scopeTypeType != null) { + return ( +

+ {facet.name} +

+ ); + } + return ( +

+ {`${index + 1}. ${facet.name}`} +

+ ); + }; + + const renderLanguageId = (facetLanguageId: string) => { + if (scopeTypeType != null && previousLanguageId !== facetLanguageId) { + previousLanguageId = facetLanguageId; + return ( +
+ {prettifyLanguageName(facetLanguageId)} +
+ ); + } + return null; + }; + + return ( +
+ {renderFacetName()} + {facet.info.description} + {facet.fixtures.map((fixture) => ( + + {renderLanguageId(fixture.languageId)} + + {fixture.code} + + + ))} +
+ ); +} + +function getScopeFixtures( + scopeTests: ScopeTests, + languageId: string | undefined, + scopeTypeType: ScopeTypeType | undefined, +): Scopes { + const scopeMap: Partial> = {}; + const facetMap: Partial> = {}; + const languageIds = new Set( + languageId != null ? (scopeTests.imports[languageId] ?? [languageId]) : [], + ); + + for (const fixture of scopeTests.fixtures) { + const info = getFacetInfo(fixture.languageId, fixture.facet); + const fixtureScopeTypeType = serializeScopeType(info.scopeType); + + if ( + languageId != null && + (!languageIds.has(fixture.languageId) || + fixtureScopeTypeType.startsWith("private.")) + ) { + continue; + } + + if (scopeTypeType != null && fixtureScopeTypeType !== scopeTypeType) { + continue; + } + + if (scopeMap[fixtureScopeTypeType] == null) { + scopeMap[fixtureScopeTypeType] = { + scopeTypeType: fixtureScopeTypeType, + name: prettifyScopeType(info.scopeType), + facets: [], + }; + } + + if (facetMap[fixture.facet] == null) { + const facet = { + facet: fixture.facet, + name: prettifyFacet(fixture.facet), + info, + fixtures: [], + }; + facetMap[fixture.facet] = facet; + scopeMap[fixtureScopeTypeType].facets.push(facet); + } + + switch (fixture.languageId) { + case "javascript.core": + fixture.languageId = "javascript"; + break; + case "typescript.core": + fixture.languageId = "typescript"; + break; + case "javascript.jsx": + fixture.languageId = "javascriptreact"; + break; + } + + facetMap[fixture.facet]?.fixtures.push(fixture); + } + + const result: Scopes = { public: [], internal: [] }; + + Object.values(scopeMap) + .sort(nameComparator) + .forEach((scope) => { + scope.facets.sort(nameComparator); + scope.facets.forEach((f) => f.fixtures.sort(nameComparator)); + if (scopeTypeType == null && isScopeInternal(scope.scopeTypeType)) { + result.internal.push(scope); + } else { + result.public.push(scope); + } + }); + + return result; +} diff --git a/packages/cursorless-org-docs/src/docs/components/ScrollToHashId.tsx b/packages/cursorless-org-docs/src/docs/components/ScrollToHashId.tsx new file mode 100644 index 0000000000..6f1219e55e --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/ScrollToHashId.tsx @@ -0,0 +1,45 @@ +import { useEffect } from "react"; +import { useLocation } from "@docusaurus/router"; + +/** + * Scrolls to the element with the ID matching the current hash in the URL. + * This is needed when a hash ID is provided in the initial load to scroll to a heading rendered by a react component. + */ +export function ScrollToHashId() { + const location = useLocation(); + + useEffect(() => { + if (location.hash) { + const id = location.hash.replace("#", ""); + const delay = 100; + let attemptsLeft = 5; + + const scrollToId = () => { + const element = document.getElementById(id); + + if (element != null) { + if (isElementAtTop(element)) { + return; + } + element.scrollIntoView(); + } + + attemptsLeft--; + + if (attemptsLeft > 0) { + setTimeout(scrollToId, delay); + } + }; + + setTimeout(scrollToId, delay); + } + }, []); + + return null; +} + +function isElementAtTop(el: HTMLElement, tolerance = 10) { + const rect = el.getBoundingClientRect(); + // 68px is the offset for the navbar + return Math.abs(rect.top - 68) <= tolerance; +} diff --git a/packages/cursorless-org-docs/src/docs/components/calculateHighlights.ts b/packages/cursorless-org-docs/src/docs/components/calculateHighlights.ts new file mode 100644 index 0000000000..6de1c8b0ce --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/calculateHighlights.ts @@ -0,0 +1,110 @@ +import { + generateDecorationsForCharacterRange, + Range, + useSingleCornerBorderRadius, + type DecorationStyle, +} from "@cursorless/common"; +import type { DecorationItem } from "shiki"; +import { flattenHighlights } from "./flattenHighlights"; +import { highlightColors } from "./highlightColors"; +import { highlightsToDecorations } from "./highlightsToDecorations"; +import type { Fixture, Highlight, RangeType, RangeTypeColors } from "./types"; + +export function generateDecorations( + fixture: Fixture, + rangeType: RangeType, +): DecorationItem[] { + const { domainRanges, targetRanges } = getRanges(fixture, rangeType); + + const codeLineRanges = getCodeLineRanges(fixture.code); + const colors = getColors(rangeType); + + const domainDecorations = getDecorations(codeLineRanges, domainRanges); + const targetRangeDecorations = getDecorations(codeLineRanges, targetRanges); + + const domainHighlights = domainDecorations.map((d) => + getHighlights(colors.domain, d.range, d.style), + ); + const targetRangeHighlights = targetRangeDecorations.map((d) => + getHighlights(colors.target, d.range, d.style), + ); + + const highlights = flattenHighlights([ + ...domainHighlights, + ...targetRangeHighlights, + ]); + + return highlightsToDecorations(highlights); +} + +function getColors(rangeType: RangeType) { + return { + domain: highlightColors.domain, + target: + rangeType === "content" + ? highlightColors.content + : highlightColors.removal, + }; +} + +function getRanges(fixture: Fixture, rangeType: RangeType) { + const domainRanges: Range[] = []; + const targetRanges: Range[] = []; + + for (const { domain, targets } of fixture.scopes) { + if (domain != null) { + domainRanges.push(Range.fromConcise(domain)); + } + + for (const t of targets) { + const range = + rangeType === "content" ? t.content : (t.removal ?? t.content); + targetRanges.push(Range.fromConcise(range)); + } + } + + return { domainRanges, targetRanges }; +} + +function getHighlights( + colors: RangeTypeColors, + range: Range, + borders: DecorationStyle, +): Highlight { + return { + range, + style: { + backgroundColor: colors.background, + borderColorSolid: colors.borderSolid, + borderColorPorous: colors.borderPorous, + borderStyle: borders, + borderRadius: { + topLeft: useSingleCornerBorderRadius(borders.top, borders.left), + topRight: useSingleCornerBorderRadius(borders.top, borders.right), + bottomRight: useSingleCornerBorderRadius(borders.bottom, borders.right), + bottomLeft: useSingleCornerBorderRadius(borders.bottom, borders.left), + }, + }, + }; +} + +function getDecorations(lineRanges: Range[], ranges: Range[]) { + return ranges.flatMap((range) => + Array.from( + generateDecorationsForCharacterRange( + (range) => getLineRanges(lineRanges, range), + new Range(range.start, range.end), + ), + ), + ); +} + +function getCodeLineRanges(code: string): Range[] { + return code + .split("\n") + .map((line, index) => new Range(index, 0, index, line.length)); +} + +function getLineRanges(lineRanges: Range[], range: Range): Range[] { + return lineRanges.slice(range.start.line, range.end.line + 1); +} diff --git a/packages/cursorless-org-docs/src/docs/components/flattenHighlights.test.ts b/packages/cursorless-org-docs/src/docs/components/flattenHighlights.test.ts new file mode 100644 index 0000000000..b7bbe11d95 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/flattenHighlights.test.ts @@ -0,0 +1,123 @@ +import { BorderStyle, Range } from "@cursorless/common"; +import * as assert from "assert"; +import { flattenHighlights } from "./flattenHighlights"; +import type { Highlight, Scope } from "./types"; + +interface Test { + name: string; + scopes: Scope[]; + expected: string[]; +} + +const tests: Test[] = [ + { + name: "Distant targets", + scopes: [ + { + targets: [{ content: "0:3-0:5" }, { content: "0:7-0:9" }], + }, + ], + expected: ["0:3-0:5", "0:7-0:9"], + }, + { + name: "Adjacent targets", + scopes: [ + { + targets: [{ content: "0:3-0:5" }, { content: "0:5-0:9" }], + }, + ], + expected: ["0:3-0:5", "0:5-0:9"], + }, + { + name: "Overlapping targets", + scopes: [ + { + targets: [{ content: "0:3-0:5" }, { content: "0:4-0:9" }], + }, + ], + expected: ["0:3-0:4", "0:4-0:5", "0:5-0:9"], + }, + { + name: "Domain == target", + scopes: [ + { + domain: "0:3-0:5", + targets: [{ content: "0:3-0:5" }], + }, + ], + expected: ["0:3-0:5"], + }, + { + name: "Domain contains target", + scopes: [ + { + domain: "0:3-0:6", + targets: [{ content: "0:3-0:5" }], + }, + ], + expected: ["0:3-0:5", "0:5-0:6"], + }, + { + name: "Target contains domain", + scopes: [ + { + domain: "0:3-0:5", + targets: [{ content: "0:3-0:6" }], + }, + ], + expected: ["0:3-0:5", "0:5-0:6"], + }, + { + name: "Domain overlaps target", + scopes: [ + { + domain: "0:3-0:5", + targets: [{ content: "0:4-0:6" }], + }, + ], + expected: ["0:3-0:4", "0:4-0:5", "0:5-0:6"], + }, +]; + +suite("flatten highlights", () => { + tests.forEach((t) => { + test(t.name, () => { + const highlights = t.scopes.flatMap((s) => { + const result: Highlight[] = []; + if (s.domain) { + result.push(createHighlight(s.domain)); + } + result.push(...s.targets.map((t) => createHighlight(t.content))); + return result; + }); + const actual = flattenHighlights(highlights); + assert.equal(actual.length, t.expected.length); + for (let i = 0; i < actual.length; i++) { + assert.equal(actual[i].range.concise(), t.expected[i]); + } + }); + }); +}); + +function createHighlight(range: string): Highlight { + return { + range: Range.fromConcise(range), + style: { + backgroundColor: "black", + borderColorSolid: "red", + borderColorPorous: "pink", + borderRadius: { + topLeft: false, + topRight: false, + bottomLeft: false, + bottomRight: false, + }, + borderStyle: { + top: BorderStyle.none, + bottom: BorderStyle.none, + left: BorderStyle.none, + right: BorderStyle.none, + }, + }, + }; +} diff --git a/packages/cursorless-org-docs/src/docs/components/flattenHighlights.ts b/packages/cursorless-org-docs/src/docs/components/flattenHighlights.ts new file mode 100644 index 0000000000..454cab8928 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/flattenHighlights.ts @@ -0,0 +1,132 @@ +import { + type DecorationStyle, + type Position, + blendMultipleColors, + BorderStyle, + Range, +} from "@cursorless/common"; +import type { BorderRadius, Highlight, Style } from "./types"; + +export function flattenHighlights(highlights: Highlight[]): Highlight[] { + const positions = getUniquePositions(highlights); + const results: Highlight[] = []; + + for (let i = 0; i < positions.length - 1; i++) { + const range = new Range(positions[i], positions[i + 1]); + + const matchingHighlights = highlights.filter((h) => { + const intersection = h.range.intersection(range); + return intersection && !intersection.isEmpty; + }); + + // This range could be between two scopes. + if (matchingHighlights.length === 0) { + continue; + } + + const style = combineHighlightStyles(range, matchingHighlights); + + results.push({ range, style }); + } + + const emptyHighlights = highlights.filter((h) => h.range.isEmpty); + + if (emptyHighlights.length > 0) { + for (const emptyHighlight of emptyHighlights) { + const { range } = emptyHighlight; + if (!results.some((h) => h.range.isRangeEqual(range))) { + const matchingHighlights = highlights.filter((h) => + h.range.contains(range), + ); + const style = combineHighlightStyles(range, matchingHighlights); + + results.push({ range, style }); + } + } + + sortHighlights(results); + } + + return results; +} + +function getUniquePositions(highlights: Highlight[]): Position[] { + const result: Position[] = []; + const positions = highlights + .flatMap((h) => [h.range.start, h.range.end]) + .sort((a, b) => + a.line === b.line ? a.character - b.character : a.line - b.line, + ); + for (let i = 0; i < positions.length; i++) { + if (i === 0 || !positions[i].isEqual(positions[i - 1])) { + result.push(positions[i]); + } + } + return result; +} + +function combineHighlightStyles(range: Range, highlights: Highlight[]): Style { + const lastHighlight = highlights[highlights.length - 1]; + + const borderStyle: DecorationStyle = { + left: BorderStyle.none, + right: BorderStyle.none, + top: lastHighlight.style.borderStyle.top, + bottom: lastHighlight.style.borderStyle.bottom, + }; + const borderRadius: BorderRadius = { + topLeft: false, + topRight: false, + bottomRight: false, + bottomLeft: false, + }; + + const matchingStart = highlights.filter((h) => + h.range.start.isEqual(range.start), + ); + const matchingEnd = highlights.filter((h) => h.range.end.isEqual(range.end)); + + if (matchingStart.length > 0) { + const start = matchingStart.at(-1)!; + borderStyle.left = start.style.borderStyle.left; + borderRadius.topLeft = start.style.borderRadius.topLeft; + borderRadius.bottomLeft = start.style.borderRadius.bottomLeft; + } + + if (matchingEnd.length > 0) { + const end = matchingEnd.at(-1)!; + borderStyle.right = end.style.borderStyle.right; + borderRadius.topRight = end.style.borderRadius.topRight; + borderRadius.bottomRight = end.style.borderRadius.bottomRight; + } + + const backgroundColor = blendMultipleColors( + highlights.map((h) => h.style.backgroundColor), + ); + + return { + backgroundColor, + borderStyle, + borderRadius, + borderColorSolid: lastHighlight.style.borderColorSolid, + borderColorPorous: lastHighlight.style.borderColorPorous, + }; +} + +function sortHighlights(highlights: Highlight[]) { + highlights.sort((a, b) => { + if (a.range.start.isBefore(b.range.start)) { + return -1; + } + if (a.range.start.isAfter(b.range.start)) { + return 1; + } + if (a.range.end.isBefore(b.range.end)) { + return -1; + } + if (a.range.end.isAfter(b.range.end)) { + return 1; + } + return 0; + }); +} diff --git a/packages/cursorless-org-docs/src/docs/components/highlightColors.ts b/packages/cursorless-org-docs/src/docs/components/highlightColors.ts new file mode 100644 index 0000000000..bb7870c79f --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/highlightColors.ts @@ -0,0 +1,24 @@ +/* https://github.com/cursorless-dev/cursorless/blob/a9affbb83a0d81476760c5c4fdd5b67c8162ae25/packages/cursorless-vscode/package.json#L560-L581 */ + +export const highlightColors = { + domain: { + background: "#00e1ff18", + borderSolid: "#ebdeec84", + borderPorous: "#ebdeec3b", + }, + content: { + background: "#ad00bc5b", + borderSolid: "#ee00ff78", + borderPorous: "#ebdeec3b", + }, + removal: { + background: "#ff00002d", + borderSolid: "#ff000078", + borderPorous: "#ff00004a", + }, + iteration: { + background: "#00725f6c", + borderSolid: "#00ffd578", + borderPorous: "#00ffd525", + }, +}; diff --git a/packages/cursorless-org-docs/src/docs/components/highlightsToDecorations.ts b/packages/cursorless-org-docs/src/docs/components/highlightsToDecorations.ts new file mode 100644 index 0000000000..9e3f2eb9d2 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/highlightsToDecorations.ts @@ -0,0 +1,52 @@ +import { + BORDER_RADIUS, + BORDER_WIDTH, + getBorderColor, + getBorderStyle, +} from "@cursorless/common"; +import type { DecorationItem } from "shiki"; +import type { BorderRadius, Highlight, Style } from "./types"; + +export function highlightsToDecorations( + highlights: Highlight[], +): DecorationItem[] { + return highlights.map((highlight): DecorationItem => { + const { start, end } = highlight.range; + return { + start, + end, + alwaysWrap: true, + properties: { + style: getStyleString(highlight.style), + }, + }; + }); +} + +function getStyleString(style: Style): string { + const borderColor = getBorderColor( + style.borderColorSolid, + style.borderColorPorous, + style.borderStyle, + ); + return ( + `background-color: ${style.backgroundColor};` + + `border-color: ${borderColor};` + + `border-style: ${getBorderStyle(style.borderStyle)};` + + `border-radius: ${getBorderRadius(style.borderRadius)};` + + `border-width: ${BORDER_WIDTH};` + ); +} + +function getBorderRadius(borders: BorderRadius): string { + return [ + getSingleBorderRadius(borders.topLeft), + getSingleBorderRadius(borders.topRight), + getSingleBorderRadius(borders.bottomRight), + getSingleBorderRadius(borders.bottomLeft), + ].join(" "); +} + +function getSingleBorderRadius(border: boolean | string): string { + return border ? BORDER_RADIUS : "0px"; +} diff --git a/packages/cursorless-org-docs/src/docs/components/types.ts b/packages/cursorless-org-docs/src/docs/components/types.ts new file mode 100644 index 0000000000..09a018a1d4 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/types.ts @@ -0,0 +1,63 @@ +import type { + BorderStyle, + PlaintextScopeSupportFacet, + ScopeSupportFacet, + Range, +} from "@cursorless/common"; + +export type RangeType = "content" | "removal"; +export type FacetValue = ScopeSupportFacet | PlaintextScopeSupportFacet; + +export interface ScopeTests { + imports: Record; + fixtures: Fixture[]; +} + +export interface Fixture { + name: string; + facet: FacetValue; + languageId: string; + code: string; + scopes: Scope[]; +} + +export interface Scope { + domain?: string; + targets: Target[]; +} + +export interface Target { + content: string; + removal?: string; +} + +export interface RangeTypeColors { + background: string; + borderSolid: string; + borderPorous: string; +} + +export interface Highlight { + range: Range; + style: Style; +} + +export interface Style { + backgroundColor: string; + borderColorSolid: string; + borderColorPorous: string; + borderRadius: BorderRadius; + borderStyle: { + top: BorderStyle; + bottom: BorderStyle; + left: BorderStyle; + right: BorderStyle; + }; +} + +export interface BorderRadius { + topLeft: boolean; + topRight: boolean; + bottomRight: boolean; + bottomLeft: boolean; +} diff --git a/packages/cursorless-org-docs/src/docs/components/util.ts b/packages/cursorless-org-docs/src/docs/components/util.ts new file mode 100644 index 0000000000..d8237f2969 --- /dev/null +++ b/packages/cursorless-org-docs/src/docs/components/util.ts @@ -0,0 +1,75 @@ +import { + camelCaseToAllDown, + capitalize, + plaintextScopeSupportFacetInfos, + scopeSupportFacetInfos, + type PlaintextScopeSupportFacet, + type ScopeSupportFacet, + type ScopeSupportFacetInfo, + type ScopeTypeType, +} from "@cursorless/common"; + +export function prettifyFacet( + facet: ScopeSupportFacet | PlaintextScopeSupportFacet, +): string { + const parts = facet.split(".").map(camelCaseToAllDown); + + if (parts.length === 1) { + return capitalize(parts[0]); + } + + const iterationIndex = parts.indexOf("iteration"); + + // Capitalize scope + parts[0] = capitalize(parts[0]); + + // Unless the second part is the iteration scope we want to add `:` to the first part + // and capitalize the second part. + if (iterationIndex < 0 || iterationIndex > 1) { + parts[0] = parts[0] + ":"; + parts[1] = capitalize(parts[1]); + } + + // If we have an iteration, we want to put it in parentheses at the end + if (iterationIndex > 0) { + const iteration = parts.slice(iterationIndex).join(" "); + parts.length = iterationIndex; + parts.push(`(${iteration})`); + } + + return parts.join(" "); +} + +export function getFacetInfo( + languageId: string, + facetId: ScopeSupportFacet | PlaintextScopeSupportFacet, +): ScopeSupportFacetInfo { + const facetInfo = + languageId === "plaintext" + ? plaintextScopeSupportFacetInfos[facetId as PlaintextScopeSupportFacet] + : scopeSupportFacetInfos[facetId as ScopeSupportFacet]; + + if (facetInfo == null) { + throw Error(`Missing scope support facet info for: ${facetId}`); + } + + return facetInfo; +} + +export function nameComparator( + a: { name: string }, + b: { name: string }, +): number { + return a.name.localeCompare(b.name); +} + +export function isScopeInternal(scope: ScopeTypeType): boolean { + switch (scope) { + case "disqualifyDelimiter": + case "pairDelimiter": + case "textFragment": + return true; + default: + return false; + } +} diff --git a/packages/cursorless-org-docs/src/docs/contributing/scopes/components/Scopes.tsx b/packages/cursorless-org-docs/src/docs/contributing/scopes/components/Scopes.tsx index 4cc039957f..a45af8b497 100644 --- a/packages/cursorless-org-docs/src/docs/contributing/scopes/components/Scopes.tsx +++ b/packages/cursorless-org-docs/src/docs/contributing/scopes/components/Scopes.tsx @@ -1,5 +1,20 @@ +import type { ScopeTypeType } from "@cursorless/common"; import React from "react"; +import { DynamicTOC } from "../../../components/DynamicTOC"; +import { ScopeVisualizer } from "../../../components/ScopeVisualizer"; +import { ScrollToHashId } from "../../../components/ScrollToHashId"; -export function Scopes() { - return
Coming soon!
; +interface Props { + scopeTypeType: ScopeTypeType; +} + +export function Scopes({ scopeTypeType }: Props) { + return ( + <> + + + + + + ); } diff --git a/packages/cursorless-org-docs/src/docs/user/languages/components/Language.tsx b/packages/cursorless-org-docs/src/docs/user/languages/components/Language.tsx index 1fa19d671d..193074484f 100644 --- a/packages/cursorless-org-docs/src/docs/user/languages/components/Language.tsx +++ b/packages/cursorless-org-docs/src/docs/user/languages/components/Language.tsx @@ -1,10 +1,19 @@ import React from "react"; -import { ScopeSupport } from "./ScopeSupport"; +import { DynamicTOC } from "../../../components/DynamicTOC"; +import { ScopeVisualizer } from "../../../components/ScopeVisualizer"; +import { ScrollToHashId } from "../../../components/ScrollToHashId"; interface Props { languageId: string; } export function Language({ languageId }: Props) { - return ; + return ( + <> + + + + + + ); } diff --git a/packages/cursorless-org-docs/src/docs/user/languages/components/ScopeSupport.tsx b/packages/cursorless-org-docs/src/docs/user/languages/components/ScopeSupport.tsx deleted file mode 100644 index a6da7c9c1e..0000000000 --- a/packages/cursorless-org-docs/src/docs/user/languages/components/ScopeSupport.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { - ScopeSupportFacetLevel, - languageScopeSupport, - scopeSupportFacets, -} from "@cursorless/common"; -import * as React from "react"; -import { ScopeSupportForLevel } from "./ScopeSupportForLevel"; - -interface Props { - languageId: string; -} - -export function ScopeSupport({ languageId }: Props) { - if (languageId === "plaintext") { - return
Coming soon!
; - } - const facetsSorted = [...scopeSupportFacets].sort(); - const scopeSupport = languageScopeSupport[languageId] ?? {}; - - const supportedFacets = facetsSorted.filter( - (facet) => scopeSupport[facet] === ScopeSupportFacetLevel.supported, - ); - const unsupportedFacets = facetsSorted.filter( - (facet) => scopeSupport[facet] === ScopeSupportFacetLevel.unsupported, - ); - const unspecifiedFacets = facetsSorted.filter( - (facet) => scopeSupport[facet] == null, - ); - - return ( - <> -

Scopes

- - - - - We would happily accept{" "} -
- contributions - - - } - /> - - - Note that in many instances we actually do support these scopes and - facets, but we have not yet updated 'languageScopeSupport' to - reflect this fact. -
- We would happily accept{" "} - - contributions - - - } - /> - - ); -} diff --git a/packages/cursorless-org-docs/src/docs/user/languages/components/ScopeSupportForLevel.tsx b/packages/cursorless-org-docs/src/docs/user/languages/components/ScopeSupportForLevel.tsx deleted file mode 100644 index 144d64b9f8..0000000000 --- a/packages/cursorless-org-docs/src/docs/user/languages/components/ScopeSupportForLevel.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - camelCaseToAllDown, - capitalize, - groupBy, - scopeSupportFacetInfos, - serializeScopeType, - type ScopeSupportFacet, - type ScopeSupportFacetInfo, -} from "@cursorless/common"; -import React, { useState, type JSX } from "react"; - -interface Props { - facets: ScopeSupportFacet[]; - title: string; - subtitle: string; - description?: React.ReactNode; - open?: boolean; -} - -export function ScopeSupportForLevel({ - facets, - title, - subtitle, - description, - open: openProp, -}: Props): JSX.Element | null { - const [open, setOpen] = useState(openProp ?? false); - - const facetInfos = facets.map( - (facet): AugmentedFacetInfo => ({ - facet, - ...scopeSupportFacetInfos[facet], - }), - ); - const scopeGroups: Map = groupBy( - facetInfos, - (facetInfo) => serializeScopeType(facetInfo.scopeType), - ); - const scopeTypes = Array.from(scopeGroups.keys()) - .filter((scope) => !scope.startsWith("private.")) - .sort(); - - if (scopeTypes.length === 0) { - return null; - } - - const renderBody = () => { - if (!open) { - return ( -
setOpen(true)}> - + Click to expand -
- ); - } - - return ( -
- {description &&

{description}

} - - {scopeTypes.map((scopeType) => { - const facetInfos = scopeGroups.get(scopeType) ?? []; - return ( -
-

{prettifyScopeType(scopeType)}

-
    - {facetInfos.map((facetInfo) => { - return ( -
  • - - {prettifyFacet(facetInfo.facet)} - - : {facetInfo.description} -
  • - ); - })} -
-
- ); - })} -
- ); - }; - - return ( -
-
setOpen(!open)}> -

{title}

- {subtitle} -
- - {renderBody()} -
- ); -} - -interface AugmentedFacetInfo extends ScopeSupportFacetInfo { - facet: ScopeSupportFacet; -} - -function prettifyScopeType(scopeType: string): string { - return capitalize(camelCaseToAllDown(scopeType)); -} - -function prettifyFacet(facet: ScopeSupportFacet): string { - const parts = facet.split(".").map(camelCaseToAllDown); - if (parts.length === 1) { - return capitalize(parts[0]); - } - const isIteration = parts[parts.length - 1] === "iteration"; - if (isIteration) { - parts.pop(); - } - const name = capitalize(parts.slice(1).join(" ")); - return isIteration ? `${name} (iteration)` : name; -} diff --git a/packages/cursorless-org-docs/src/plugins/scope-tests-plugin.ts b/packages/cursorless-org-docs/src/plugins/scope-tests-plugin.ts new file mode 100644 index 0000000000..e0b7240f15 --- /dev/null +++ b/packages/cursorless-org-docs/src/plugins/scope-tests-plugin.ts @@ -0,0 +1,193 @@ +import { + getScopeTestLanguagesRecursively, + getScopeTestPaths, + type ScopeTestPath, +} from "@cursorless/node-common"; +import type { LoadContext, Plugin, PluginOptions } from "@docusaurus/types"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import type { + Fixture, + Scope, + ScopeTests, + Target, +} from "../docs/components/types"; + +export default function prepareAssetsPlugin( + _context: LoadContext, + _options: PluginOptions, +): Plugin { + return { + name: "scope-tests-plugin", + + loadContent(): ScopeTests { + // eslint-disable-next-line unicorn/prefer-module + const repoRoot = path.join(__dirname, "../../../.."); + process.env.CURSORLESS_REPO_ROOT = repoRoot; + return prepareAssets(); + }, + + contentLoaded({ content, actions }) { + actions.setGlobalData(content); + }, + }; +} + +function prepareAssets(): ScopeTests { + const fixtures: Fixture[] = []; + + const importedLanguages = getScopeTestLanguagesRecursively(); + + for (const test of getScopeTestPaths()) { + const fixture = parseTest(test); + if (fixture != null) { + fixtures.push(fixture); + } + } + + return { + imports: importedLanguages, + fixtures, + }; +} + +function parseTest(test: ScopeTestPath) { + const fixture = fs + .readFileSync(test.path, "utf8") + .toString() + .replaceAll("\r\n", "\n"); + + const delimiterIndex = fixture.match(/^---$/m)?.index; + + if (delimiterIndex === undefined) { + throw Error(`Can't find delimiter '---' in scope fixture ${test.path}`); + } + + const code = fixture.slice(0, delimiterIndex - 1); + const lines = fixture.substring(delimiterIndex + 4).split(/\n/); + const scopes: Scope[] = []; + const unprocessedTypes: string[] = []; + let currentScopeIndex = "1"; + let currentTargetIndex = "1"; + let currentTarget: Target = { content: "" }; + let currentScope: Scope = { targets: [currentTarget] }; + + function processLine(type: string, value: string) { + switch (type) { + case "Domain": + currentScope.domain = value; + break; + case "Content": + currentTarget.content = value; + break; + case "Removal": + currentTarget.removal = value; + break; + case "Insertion delimiter": + case "Leading delimiter": + case "Leading delimiter: Content": + case "Leading delimiter: Removal": + case "Trailing delimiter": + case "Trailing delimiter: Content": + case "Trailing delimiter: Removal": + case "Interior": + case "Interior: Content": + case "Interior: Removal": + case "Boundary L": + case "Boundary L: Content": + case "Boundary L: Removal": + case "Boundary R": + case "Boundary R: Content": + case "Boundary R: Removal": + // Do nothing + break; + default: + throw Error(`Unknown type '${type}' in scope fixture ${test.path}`); + } + } + + for (const line of lines) { + const parsedLine = parseLine(line); + + if (parsedLine == null) { + continue; + } + + const { scopeIndex, targetIndex, type, value } = parsedLine; + + if (scopeIndex !== currentScopeIndex) { + scopes.push(currentScope); + currentScopeIndex = scopeIndex; + currentTargetIndex = "1"; + currentTarget = { content: "" }; + currentScope = { targets: [currentTarget] }; + } else if (targetIndex != null && targetIndex !== currentTargetIndex) { + currentTargetIndex = targetIndex; + currentTarget = { content: "" }; + currentScope.targets.push(currentTarget); + } + + if (value == null) { + unprocessedTypes.push(type); + continue; + } + + if (unprocessedTypes.length > 0) { + for (const unprocessedType of unprocessedTypes) { + processLine(unprocessedType, value); + } + unprocessedTypes.length = 0; + } + + processLine(type, value); + } + + scopes.push(currentScope); + + if (scopes.some((s) => s.targets.some((t) => !t.content))) { + throw Error(`Scope fixture ${test.path} contains targets without content.`); + } + if (scopes.some((s) => s.targets.length === 0)) { + throw Error(`Scope fixture ${test.path} contains empty scopes.`); + } + + const result: Fixture = { + name: test.name, + languageId: test.languageId, + facet: test.facet, + code, + scopes, + }; + + return result; +} + +function parseLine(line: string) { + if (line[0] !== "[") { + return null; + } + + const header = line.substring(1, line.indexOf("]")); + const { scopeIndex, targetIndex, type } = (() => { + if (header[0] === "#") { + const spaceIndex = header.indexOf(" "); + const fullIndex = header.substring(1, spaceIndex); + const [scopeIndex, targetIndex] = fullIndex.split("."); + return { + scopeIndex: scopeIndex, + targetIndex: targetIndex, + type: header.substring(spaceIndex + 1), + }; + } + return { + scopeIndex: "1", + targetIndex: undefined, + type: header, + }; + })(); + + const rawValue = line.substring(line.indexOf("=") + 1).trim(); + const value = rawValue.length > 0 ? rawValue : undefined; + + return { scopeIndex, targetIndex, type, value }; +} diff --git a/packages/cursorless-org-docs/tsconfig.json b/packages/cursorless-org-docs/tsconfig.json index c6b0d70b9c..d723b09e49 100644 --- a/packages/cursorless-org-docs/tsconfig.json +++ b/packages/cursorless-org-docs/tsconfig.json @@ -18,6 +18,9 @@ "references": [ { "path": "../common" + }, + { + "path": "../node-common" } ] } diff --git a/packages/cursorless-vscode/package.json b/packages/cursorless-vscode/package.json index d5e824199f..a965a2739a 100644 --- a/packages/cursorless-vscode/package.json +++ b/packages/cursorless-vscode/package.json @@ -120,6 +120,10 @@ "command": "cursorless.hideScopeVisualizer", "title": "Cursorless: Hide the scope visualizer" }, + { + "command": "cursorless.scopeVisualizer.openUrl", + "title": "Cursorless: Open in browser" + }, { "command": "cursorless.analyzeCommandHistory", "title": "Cursorless: Analyze collected command history" @@ -1233,6 +1237,15 @@ "icon": "images/logo.svg" } ] + }, + "menus": { + "view/item/context": [ + { + "command": "cursorless.scopeVisualizer.openUrl", + "when": "view == cursorless.scopes && viewItem == scopeVisualizerTreeItem", + "group": "navigation" + } + ] } }, "scripts": { @@ -1277,7 +1290,6 @@ "nearley": "2.20.1", "semver": "7.7.2", "talon-snippets": "1.3.0", - "tinycolor2": "1.6.0", "trie-search": "2.2.0", "uuid": "11.1.0", "vscode-uri": "3.1.0" @@ -1290,7 +1302,6 @@ "@types/node": "20.17.50", "@types/semver": "7.7.0", "@types/sinon": "17.0.4", - "@types/tinycolor2": "1.4.6", "@types/uuid": "10.0.0", "@types/vscode": "1.82.0", "esbuild": "0.25.5", diff --git a/packages/cursorless-vscode/src/ScopeTreeProvider.ts b/packages/cursorless-vscode/src/ScopeTreeProvider.ts index bf0b2fb3fe..b1d742ee13 100644 --- a/packages/cursorless-vscode/src/ScopeTreeProvider.ts +++ b/packages/cursorless-vscode/src/ScopeTreeProvider.ts @@ -9,6 +9,8 @@ import { DOCS_URL, ScopeSupport, disposableFrom, + serializeScopeType, + uriEncodeHashId, } from "@cursorless/common"; import type { CustomSpokenFormGenerator } from "@cursorless/cursorless-engine"; import type { VscodeApi } from "@cursorless/vscode-common"; @@ -213,6 +215,7 @@ function getSupportCategories(): SupportCategoryTreeItem[] { class ScopeSupportTreeItem extends TreeItem { public declare readonly label: TreeItemLabel; + public url: string | undefined; /** * @param scopeTypeInfo The scope type info @@ -276,14 +279,25 @@ class ScopeSupportTreeItem extends TreeItem { "cursorless-dummy://dummy/dummy" + fileExtension, ); } + this.setUrl(languageId); } if (this.resourceUri == null) { // Fall back to a generic icon this.iconPath = new ThemeIcon("code"); } + } else { + this.setUrl("plaintext"); } } + + private setUrl(languageId: string) { + const id = uriEncodeHashId( + serializeScopeType(this.scopeTypeInfo.scopeType), + ); + this.url = `${DOCS_URL}/user/languages/${languageId}#${id}`; + this.contextValue = "scopeVisualizerTreeItem"; + } } class SupportCategoryTreeItem extends TreeItem { diff --git a/packages/cursorless-vscode/src/commands.ts b/packages/cursorless-vscode/src/commands.ts index 2b2cd82228..d306935f01 100644 --- a/packages/cursorless-vscode/src/commands.ts +++ b/packages/cursorless-vscode/src/commands.ts @@ -1,7 +1,20 @@ +import { CURSORLESS_ORG_URL, DOCS_URL } from "@cursorless/common"; import * as vscode from "vscode"; -export const showDocumentation = () => - vscode.env.openExternal(vscode.Uri.parse("https://www.cursorless.org/docs/")); +export const showDocumentation = () => { + return vscode.env.openExternal(vscode.Uri.parse(DOCS_URL)); +}; -export const showQuickPick = () => - vscode.commands.executeCommand("workbench.action.quickOpen", ">Cursorless"); +export const showScopeVisualizerItemDocumentation = (item?: { + url: string; +}) => { + const url = item?.url ?? CURSORLESS_ORG_URL; + return vscode.env.openExternal(vscode.Uri.parse(url)); +}; + +export const showQuickPick = () => { + return vscode.commands.executeCommand( + "workbench.action.quickOpen", + ">Cursorless", + ); +}; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts index 80f1b60524..d53c0bb1d4 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighter.ts @@ -1,13 +1,16 @@ -import type { GeneralizedRange } from "@cursorless/common"; -import { Range } from "@cursorless/common"; +import type { GeneralizedRange, TextEditor } from "@cursorless/common"; +import { + generateDecorationsForCharacterRange, + generateDecorationsForLineRange, + Range, +} from "@cursorless/common"; import { flatmap } from "itertools"; +import { range as lodashRange } from "lodash-es"; import type { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; import type { RangeTypeColors } from "../RangeTypeColors"; import { VscodeFancyRangeHighlighterRenderer } from "./VscodeFancyRangeHighlighterRenderer"; -import { generateDecorationsForCharacterRange } from "./generateDecorationsForCharacterRange"; -import { generateDecorationsForLineRange } from "./generateDecorationsForLineRange"; -import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; import type { DifferentiatedStyledRange } from "./decorationStyle.types"; +import { generateDifferentiatedRanges } from "./generateDifferentiatedRanges"; import { groupDifferentiatedStyledRanges } from "./groupDifferentiatedStyledRanges"; /** @@ -46,7 +49,7 @@ export class VscodeFancyRangeHighlighter { range.type === "line" ? generateDecorationsForLineRange(range.start, range.end) : generateDecorationsForCharacterRange( - editor, + (range) => getLineRanges(editor, range), new Range(range.start, range.end), ); @@ -71,3 +74,9 @@ export class VscodeFancyRangeHighlighter { this.renderer.dispose(); } } + +function getLineRanges(editor: TextEditor, range: Range): Range[] { + return lodashRange(range.start.line, range.end.line + 1).map( + (lineNumber) => editor.document.lineAt(lineNumber).range, + ); +} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts index f79e4455ec..883d263822 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/VscodeFancyRangeHighlighterRenderer.ts @@ -1,4 +1,11 @@ -import { CompositeKeyDefaultMap } from "@cursorless/common"; +import { + BORDER_WIDTH, + CompositeKeyDefaultMap, + getBorderColor, + getBorderRadius, + getBorderStyle, + type DecorationStyle, +} from "@cursorless/common"; import { toVscodeRange } from "@cursorless/vscode-common"; import type { DecorationRenderOptions, TextEditorDecorationType } from "vscode"; import { DecorationRangeBehavior } from "vscode"; @@ -6,16 +13,11 @@ import { vscodeApi } from "../../../../vscodeApi"; import type { VscodeTextEditorImpl } from "../../VscodeTextEditorImpl"; import type { RangeTypeColors } from "../RangeTypeColors"; import type { - DecorationStyle, DifferentiatedStyle, DifferentiatedStyledRangeList, } from "./decorationStyle.types"; -import { BorderStyle } from "./decorationStyle.types"; import { getDifferentiatedStyleMapKey } from "./getDifferentiatedStyleMapKey"; -const BORDER_WIDTH = "1px"; -const BORDER_RADIUS = "2px"; - /** * Handles the actual rendering of decorations for * {@link VscodeFancyRangeHighlighter}. @@ -116,38 +118,3 @@ function getDecorationStyle( return vscodeApi.window.createTextEditorDecorationType(options); } - -function getBorderStyle(borders: DecorationStyle): string { - return [borders.top, borders.right, borders.bottom, borders.left].join(" "); -} - -function getBorderColor( - solidColor: string, - porousColor: string, - borders: DecorationStyle, -): string { - return [ - borders.top === BorderStyle.solid ? solidColor : porousColor, - borders.right === BorderStyle.solid ? solidColor : porousColor, - borders.bottom === BorderStyle.solid ? solidColor : porousColor, - borders.left === BorderStyle.solid ? solidColor : porousColor, - ].join(" "); -} - -function getBorderRadius(borders: DecorationStyle): string { - return [ - getSingleCornerBorderRadius(borders.top, borders.left), - getSingleCornerBorderRadius(borders.top, borders.right), - getSingleCornerBorderRadius(borders.bottom, borders.right), - getSingleCornerBorderRadius(borders.bottom, borders.left), - ].join(" "); -} - -function getSingleCornerBorderRadius(side1: BorderStyle, side2: BorderStyle) { - // We only round the corners if both sides are solid, as that makes them look - // more finished, whereas we want the dotted borders to look unfinished / cut - // off. - return side1 === BorderStyle.solid && side2 === BorderStyle.solid - ? BORDER_RADIUS - : "0px"; -} diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts index 832e995ce8..fbbd3de26f 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/decorationStyle.types.ts @@ -1,18 +1,8 @@ -import type { GeneralizedRange, Range } from "@cursorless/common"; - -export enum BorderStyle { - porous = "dashed", - solid = "solid", - none = "none", -} - -export interface DecorationStyle { - top: BorderStyle; - bottom: BorderStyle; - left: BorderStyle; - right: BorderStyle; - isWholeLine?: boolean; -} +import type { + DecorationStyle, + GeneralizedRange, + Range, +} from "@cursorless/common"; /** * A decoration style that is differentiated from other styles by a number. We @@ -30,11 +20,6 @@ export interface DifferentiatedStyle { differentiationIndex: number; } -export interface StyledRange { - style: DecorationStyle; - range: Range; -} - export interface DifferentiatedStyledRange { differentiatedStyle: DifferentiatedStyle; range: Range; diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts index 5a0cee93cc..19eeaaeca9 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/VscodeFancyRangeHighlighter/getDifferentiatedStyleMapKey.ts @@ -1,3 +1,4 @@ +import type { BorderStyle } from "@cursorless/common"; import type { DifferentiatedStyle } from "./decorationStyle.types"; /** @@ -7,6 +8,13 @@ import type { DifferentiatedStyle } from "./decorationStyle.types"; export function getDifferentiatedStyleMapKey({ style: { top, right, bottom, left, isWholeLine }, differentiationIndex, -}: DifferentiatedStyle) { +}: DifferentiatedStyle): [ + BorderStyle, + BorderStyle, + BorderStyle, + BorderStyle, + boolean, + number, +] { return [top, right, bottom, left, isWholeLine ?? false, differentiationIndex]; } diff --git a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts index ad8a499df8..abf099abe9 100644 --- a/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts +++ b/packages/cursorless-vscode/src/ide/vscode/VSCodeScopeVisualizer/blendRangeTypeColors.ts @@ -1,4 +1,4 @@ -import tinycolor from "tinycolor2"; +import { blendColors } from "@cursorless/common"; import type { RangeTypeColors } from "./RangeTypeColors"; /** @@ -44,34 +44,3 @@ export function blendRangeTypeColors( }, }; } - -/** - * Blends two colors together according to their alpha channels, with the top - * color rendered on top of the base color. - * - * Basd on https://gist.github.com/JordanDelcros/518396da1c13f75ee057 - * - * @param base The color to render underneath - * @param top The color to render on top - * @returns A color that is a blend of the two colors, with the top color - * rendered on top of the base color - */ -function blendColors(base: string, top: string): string { - const baseRgba = tinycolor(base).toRgb(); - const topRgba = tinycolor(top).toRgb(); - const blendedAlpha = 1 - (1 - topRgba.a) * (1 - baseRgba.a); - - function interpolateChannel(channel: "r" | "g" | "b"): number { - return Math.round( - (topRgba[channel] * topRgba.a) / blendedAlpha + - (baseRgba[channel] * baseRgba.a * (1 - topRgba.a)) / blendedAlpha, - ); - } - - return tinycolor({ - r: interpolateChannel("r"), - g: interpolateChannel("g"), - b: interpolateChannel("b"), - a: blendedAlpha, - }).toHex8String(); -} diff --git a/packages/cursorless-vscode/src/registerCommands.ts b/packages/cursorless-vscode/src/registerCommands.ts index 4675bf9d4f..5f735a5c01 100644 --- a/packages/cursorless-vscode/src/registerCommands.ts +++ b/packages/cursorless-vscode/src/registerCommands.ts @@ -21,7 +21,11 @@ import type { InstallationDependencies } from "./InstallationDependencies"; import type { ScopeVisualizer } from "./ScopeVisualizerCommandApi"; import type { VscodeSnippets } from "./VscodeSnippets"; import type { VscodeTutorial } from "./VscodeTutorial"; -import { showDocumentation, showQuickPick } from "./commands"; +import { + showDocumentation, + showQuickPick, + showScopeVisualizerItemDocumentation, +} from "./commands"; import type { VscodeIDE } from "./ide/vscode/VscodeIDE"; import type { VscodeHats } from "./ide/vscode/hats/VscodeHats"; import type { KeyboardCommands } from "./keyboard/KeyboardCommands"; @@ -100,6 +104,8 @@ export function registerCommands( // Scope visualizer ["cursorless.showScopeVisualizer"]: scopeVisualizer.start, ["cursorless.hideScopeVisualizer"]: scopeVisualizer.stop, + ["cursorless.scopeVisualizer.openUrl"]: + showScopeVisualizerItemDocumentation, // Command history ["cursorless.analyzeCommandHistory"]: () => diff --git a/patches/@shikijs__core.patch b/patches/@shikijs__core.patch new file mode 100644 index 0000000000..cd9869747b --- /dev/null +++ b/patches/@shikijs__core.patch @@ -0,0 +1,15 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index 5ba9a077425954c8099a26820a3411b017093b0d..6f19e2251ffb3e69abeeadcb16f18399599300c9 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -450,6 +450,10 @@ function verifyIntersections(items) { + continue; + if (isBarHasFooStart && isBarHasFooEnd) + continue; ++ if (isBarHasFooStart && foo.start.offset === foo.end.offset) ++ continue // leading adjacent empty ++ if (isFooHasBarEnd && bar.start.offset === bar.end.offset) ++ continue // trailing adjacent empty + throw new ShikiError$1(`Decorations ${JSON.stringify(foo.start)} and ${JSON.stringify(bar.start)} intersect.`); + } + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f6cfbee41..e280101c70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false patchedDependencies: + '@shikijs/core': + hash: pboip74ghl4mfpkgi5l4nlq6x4 + path: patches/@shikijs__core.patch '@types/nearley@2.11.5': hash: 5bomp3nnmdzdyzcgrxyr5kymae path: patches/@types__nearley@2.11.5.patch @@ -137,7 +140,7 @@ importers: devDependencies: '@effortlessmotion/html-webpack-inline-source-plugin': specifier: 1.0.3 - version: 1.0.3(html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + version: 1.0.3(html-webpack-plugin@5.6.3(webpack@5.99.9))(webpack@5.99.9) '@testing-library/dom': specifier: 10.4.0 version: 10.4.0 @@ -158,19 +161,19 @@ importers: version: 19.1.6(@types/react@19.1.8) '@types/webpack': specifier: 5.28.5 - version: 5.28.5(esbuild@0.25.5)(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9)) + version: 5.28.5(esbuild@0.25.5)(webpack-cli@6.0.1) '@webpack-cli/generators': specifier: 3.0.7 - version: 3.0.7(encoding@0.1.13)(mem-fs@2.3.0)(prettier@3.3.3)(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + version: 3.0.7(encoding@0.1.13)(mem-fs@2.3.0)(prettier@3.3.3)(webpack-cli@6.0.1)(webpack@5.99.9) autoprefixer: specifier: 10.4.21 version: 10.4.21(postcss@8.5.5) css-loader: specifier: 7.1.2 - version: 7.1.2(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + version: 7.1.2(webpack@5.99.9) html-webpack-plugin: specifier: 5.6.3 - version: 5.6.3(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + version: 5.6.3(webpack@5.99.9) jest: specifier: 30.0.0 version: 30.0.0(@types/node@20.17.50)(ts-node@10.9.2(@types/node@20.17.50)(typescript@5.8.3)) @@ -179,16 +182,16 @@ importers: version: 8.5.5 postcss-loader: specifier: 8.1.1 - version: 8.1.1(postcss@8.5.5)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + version: 8.1.1(postcss@8.5.5)(typescript@5.8.3)(webpack@5.99.9) style-loader: specifier: 4.0.0 - version: 4.0.0(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + version: 4.0.0(webpack@5.99.9) tailwindcss: specifier: 3.4.17 version: 3.4.17(ts-node@10.9.2(@types/node@20.17.50)(typescript@5.8.3)) ts-loader: specifier: 9.5.2 - version: 9.5.2(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + version: 9.5.2(typescript@5.8.3)(webpack@5.99.9) ts-node: specifier: 10.9.2 version: 10.9.2(@types/node@20.17.50)(typescript@5.8.3) @@ -207,9 +210,15 @@ importers: packages/common: dependencies: + itertools: + specifier: 2.4.1 + version: 2.4.1 lodash-es: specifier: 4.17.21 version: 4.17.21 + tinycolor2: + specifier: 1.6.0 + version: 1.6.0 vscode-uri: specifier: 3.1.0 version: 3.1.0 @@ -223,6 +232,9 @@ importers: '@types/mocha': specifier: 10.0.10 version: 10.0.10 + '@types/tinycolor2': + specifier: 1.4.6 + version: 1.4.6 cross-spawn: specifier: 7.0.6 version: 7.0.6 @@ -566,10 +578,16 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + shiki: + specifier: 3.7.0 + version: 3.7.0 unist-util-visit: specifier: 5.0.0 version: 5.0.0 devDependencies: + '@cursorless/node-common': + specifier: workspace:* + version: link:../node-common '@docusaurus/module-type-aliases': specifier: 3.8.1 version: 3.8.1(esbuild@0.25.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -661,9 +679,6 @@ importers: talon-snippets: specifier: 1.3.0 version: 1.3.0 - tinycolor2: - specifier: 1.6.0 - version: 1.6.0 trie-search: specifier: 2.2.0 version: 2.2.0 @@ -695,9 +710,6 @@ importers: '@types/sinon': specifier: 17.0.4 version: 17.0.4 - '@types/tinycolor2': - specifier: 1.4.6 - version: 1.4.6 '@types/uuid': specifier: 10.0.0 version: 10.0.0 @@ -3316,6 +3328,27 @@ packages: '@rushstack/eslint-patch@1.10.4': resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} + '@shikijs/core@3.7.0': + resolution: {integrity: sha512-yilc0S9HvTPyahHpcum8eonYrQtmGTU0lbtwxhA6jHv4Bm1cAdlPFRCJX4AHebkCm75aKTjjRAW+DezqD1b/cg==} + + '@shikijs/engine-javascript@3.7.0': + resolution: {integrity: sha512-0t17s03Cbv+ZcUvv+y33GtX75WBLQELgNdVghnsdhTgU3hVcWcMsoP6Lb0nDTl95ZJfbP1mVMO0p3byVh3uuzA==} + + '@shikijs/engine-oniguruma@3.7.0': + resolution: {integrity: sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==} + + '@shikijs/langs@3.7.0': + resolution: {integrity: sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==} + + '@shikijs/themes@3.7.0': + resolution: {integrity: sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==} + + '@shikijs/types@3.7.0': + resolution: {integrity: sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sideway/address@4.1.5': resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} @@ -6343,6 +6376,9 @@ packages: hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.2: resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} @@ -8273,6 +8309,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.3: + resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + open@10.1.2: resolution: {integrity: sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==} engines: {node: '>=18'} @@ -9134,6 +9176,9 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -9368,6 +9413,15 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -9714,6 +9768,9 @@ packages: engines: {node: '>=4'} hasBin: true + shiki@3.7.0: + resolution: {integrity: sha512-ZcI4UT9n6N2pDuM2n3Jbk0sR4Swzq43nLPgS/4h0E3B/NrFn2HKElrDtceSf8Zx/OWYOo7G1SAtBLypCp+YXqg==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -12979,10 +13036,10 @@ snapshots: - uglify-js - webpack-cli - '@effortlessmotion/html-webpack-inline-source-plugin@1.0.3(html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1))': + '@effortlessmotion/html-webpack-inline-source-plugin@1.0.3(html-webpack-plugin@5.6.3(webpack@5.99.9))(webpack@5.99.9)': dependencies: escape-string-regexp: 4.0.0 - html-webpack-plugin: 5.6.3(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + html-webpack-plugin: 5.6.3(webpack@5.99.9) slash: 3.0.0 source-map-url: 0.4.1 webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) @@ -14351,6 +14408,39 @@ snapshots: '@rushstack/eslint-patch@1.10.4': {} + '@shikijs/core@3.7.0(patch_hash=pboip74ghl4mfpkgi5l4nlq6x4)': + dependencies: + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + + '@shikijs/engine-oniguruma@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + + '@shikijs/themes@3.7.0': + dependencies: + '@shikijs/types': 3.7.0 + + '@shikijs/types@3.7.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sideway/address@4.1.5': dependencies: '@hapi/hoek': 9.3.0 @@ -14934,7 +15024,7 @@ snapshots: '@types/vscode@1.82.0': {} - '@types/webpack@5.28.5(esbuild@0.25.5)(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))': + '@types/webpack@5.28.5(esbuild@0.25.5)(webpack-cli@6.0.1)': dependencies: '@types/node': 20.17.50 tapable: 2.2.1 @@ -15194,12 +15284,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1))': + '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.99.9)': dependencies: webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9) - '@webpack-cli/generators@3.0.7(encoding@0.1.13)(mem-fs@2.3.0)(prettier@3.3.3)(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1))': + '@webpack-cli/generators@3.0.7(encoding@0.1.13)(mem-fs@2.3.0)(prettier@3.3.3)(webpack-cli@6.0.1)(webpack@5.99.9)': dependencies: webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9) @@ -15213,12 +15303,12 @@ snapshots: - mem-fs - supports-color - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1))': + '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.99.9)': dependencies: webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9) - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack-dev-server@5.2.2(webpack-cli@6.0.1)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1))': + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.99.9)': dependencies: webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9) @@ -16293,7 +16383,7 @@ snapshots: optionalDependencies: webpack: 5.99.9(esbuild@0.25.5) - css-loader@7.1.2(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)): + css-loader@7.1.2(webpack@5.99.9): dependencies: icss-utils: 5.1.0(postcss@8.5.5) postcss: 8.5.5 @@ -16944,7 +17034,7 @@ snapshots: debug: 4.4.1(supports-color@8.1.1) enhanced-resolve: 5.18.1 eslint: 9.28.0(jiti@2.4.2) - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@2.4.2)) fast-glob: 3.3.3 get-tsconfig: 4.10.1 is-bun-module: 1.2.1 @@ -16972,7 +17062,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.28.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -16983,7 +17073,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: @@ -17005,7 +17095,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.28.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.0(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.3)(eslint@9.28.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -17806,6 +17896,20 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.2: dependencies: '@types/estree': 1.0.7 @@ -17924,7 +18028,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)): + html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.5)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -17932,9 +18036,9 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) + webpack: 5.99.9(esbuild@0.25.5) - html-webpack-plugin@5.6.3(webpack@5.99.9(esbuild@0.25.5)): + html-webpack-plugin@5.6.3(webpack@5.99.9): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -17942,7 +18046,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.99.9(esbuild@0.25.5) + webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) htmlparser2@6.1.0: dependencies: @@ -20380,6 +20484,14 @@ snapshots: dependencies: mimic-function: 5.0.1 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.3: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.0.1 + regex-recursion: 6.0.2 + open@10.1.2: dependencies: default-browser: 5.2.1 @@ -20866,7 +20978,7 @@ snapshots: transitivePeerDependencies: - typescript - postcss-loader@8.1.1(postcss@8.5.5)(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)): + postcss-loader@8.1.1(postcss@8.5.5)(typescript@5.8.3)(webpack@5.99.9): dependencies: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.6 @@ -21264,6 +21376,8 @@ snapshots: property-information@6.5.0: {} + property-information@7.1.0: {} + proto-list@1.2.4: {} proxy-addr@2.0.7: @@ -21541,6 +21655,16 @@ snapshots: regenerate@1.4.2: {} + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + regexp-tree@0.1.27: {} regexp.prototype.flags@1.5.3: @@ -21969,6 +22093,17 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 + shiki@3.7.0: + dependencies: + '@shikijs/core': 3.7.0(patch_hash=pboip74ghl4mfpkgi5l4nlq6x4) + '@shikijs/engine-javascript': 3.7.0 + '@shikijs/engine-oniguruma': 3.7.0 + '@shikijs/langs': 3.7.0 + '@shikijs/themes': 3.7.0 + '@shikijs/types': 3.7.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -22361,7 +22496,7 @@ snapshots: strnum@2.1.1: {} - style-loader@4.0.0(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)): + style-loader@4.0.0(webpack@5.99.9): dependencies: webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) @@ -22501,25 +22636,25 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)): + terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.35.0 - webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) + webpack: 5.99.9(esbuild@0.25.5) optionalDependencies: esbuild: 0.25.5 - terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)): + terser-webpack-plugin@5.3.14(esbuild@0.25.5)(webpack@5.99.9): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 terser: 5.35.0 - webpack: 5.99.9(esbuild@0.25.5) + webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) optionalDependencies: esbuild: 0.25.5 @@ -22659,7 +22794,7 @@ snapshots: esbuild: 0.25.5 jest-util: 30.0.0 - ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)): + ts-loader@9.5.2(typescript@5.8.3)(webpack@5.99.9): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.1 @@ -23065,9 +23200,9 @@ snapshots: webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9): dependencies: '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.99.9))(webpack-dev-server@5.2.2(webpack-cli@6.0.1)(webpack@5.99.9))(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.99.9) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.99.9) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.99.9) colorette: 2.0.20 commander: 12.1.0 cross-spawn: 7.0.6 @@ -23090,7 +23225,7 @@ snapshots: schema-utils: 4.3.2 webpack: 5.99.9(esbuild@0.25.5) - webpack-dev-middleware@7.4.2(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)): + webpack-dev-middleware@7.4.2(webpack@5.99.9): dependencies: colorette: 2.0.20 memfs: 4.17.2 @@ -23169,7 +23304,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + webpack-dev-middleware: 7.4.2(webpack@5.99.9) ws: 8.18.2 optionalDependencies: webpack: 5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1) @@ -23248,7 +23383,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.9(esbuild@0.25.5)(webpack-cli@6.0.1)) + terser-webpack-plugin: 5.3.14(esbuild@0.25.5)(webpack@5.99.9) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: