Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 4 additions & 2 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
},

"ignorePatterns": [
"/packages/app-web-docs/src/docs/contributing/private-scopes/*.mdx",
"/packages/app-web-docs/src/docs/user/actions/*.mdx",
"/packages/app-web-docs/src/docs/user/languages/*.mdx",
"/packages/app-web-docs/src/docs/user/modifiers/*.mdx",
"/packages/app-web-docs/src/docs/user/scopes/*.mdx",
"/packages/app-web-docs/src/docs/user/languages/*.mdx",
"/packages/app-web-docs/src/docs/user/tutorial/*.mdx",
"/packages/app-web-docs/src/docs/contributing/private-scopes/*.mdx",
"/packages/app-web-docs/src/docs/**/fixtures/*.json",
"/packages/app-vscode/src/keyboard/grammar/generated/",
"/packages/lib-engine/src/customCommandGrammar/generated/",
"/packages/lib-engine/src/snippets/vendor/",
Expand Down
124 changes: 103 additions & 21 deletions packages/app-vscode/src/ide/vscode/VscodeEnabledHatStyleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ export type ExtendedHatStyleMap = Partial<
Record<VscodeHatStyleName, ExtendedHatStyleInfo>
>;

interface Settings {
shapeEnablement: Record<HatShape, boolean>;
colorEnablement: Record<HatColor, boolean>;
shapePenalties: Record<HatShape, number>;
colorPenalties: Record<HatColor, number>;
maxPenalty: number;
}

/**
* Keeps track of the available hat styles, along with their associated color
* and shape identifiers, and penalties. Note that this class is not
Expand Down Expand Up @@ -52,32 +60,25 @@ export class VscodeEnabledHatStyleManager {
}

private constructHatStyleMap() {
const shapeEnablement = vscode.workspace
.getConfiguration("cursorless.hatEnablement")
.get<Record<HatShape, boolean>>("shapes")!;
const colorEnablement = vscode.workspace
.getConfiguration("cursorless.hatEnablement")
.get<Record<HatColor, boolean>>("colors")!;
const shapePenalties = vscode.workspace
.getConfiguration("cursorless.hatPenalties")
.get<Record<HatShape, number>>("shapes")!;
const colorPenalties = vscode.workspace
.getConfiguration("cursorless.hatPenalties")
.get<Record<HatColor, number>>("colors")!;
const maxPenalty = vscode.workspace
.getConfiguration("cursorless")
.get<number>("maximumHatStylePenalty")!;
const {
shapeEnablement,
colorEnablement,
shapePenalties,
colorPenalties,
maxPenalty,
} =
this.extensionContext.extensionMode === vscode.ExtensionMode.Test
? this.getDefaultSettings()
: this.getSettings();

shapeEnablement.default = true;
colorEnablement.default = true;
shapePenalties.default = 0;
colorPenalties.default = 0;

// So that unit tests don't fail locally if you have some colors disabled
const activeHatColors =
this.extensionContext.extensionMode === vscode.ExtensionMode.Test
? HAT_COLORS.filter((color) => !color.startsWith("user"))
: HAT_COLORS.filter((color) => colorEnablement[color]);
const activeHatColors = HAT_COLORS.filter(
(color) => colorEnablement[color],
);
const activeNonDefaultHatShapes = HAT_NON_DEFAULT_SHAPES.filter(
(shape) => shapeEnablement[shape],
);
Expand All @@ -101,7 +102,7 @@ export class VscodeEnabledHatStyleManager {
]),
),
),
} as ExtendedHatStyleMap;
};

if (maxPenalty > 0) {
this.hatStyleMap = pickBy(
Expand All @@ -110,4 +111,85 @@ export class VscodeEnabledHatStyleManager {
);
}
}

private getSettings(): Settings {
const shapeEnablement = vscode.workspace
.getConfiguration("cursorless.hatEnablement")
.get<Record<HatShape, boolean>>("shapes")!;
const colorEnablement = vscode.workspace
.getConfiguration("cursorless.hatEnablement")
.get<Record<HatColor, boolean>>("colors")!;
const shapePenalties = vscode.workspace
.getConfiguration("cursorless.hatPenalties")
.get<Record<HatShape, number>>("shapes")!;
const colorPenalties = vscode.workspace
.getConfiguration("cursorless.hatPenalties")
.get<Record<HatColor, number>>("colors")!;
const maxPenalty = vscode.workspace
.getConfiguration("cursorless")
.get<number>("maximumHatStylePenalty")!;
return {
shapeEnablement,
colorEnablement,
shapePenalties,
colorPenalties,
maxPenalty,
};
}

private getDefaultSettings(): Settings {
return {
shapeEnablement: {
default: true,
bolt: false,
curve: false,
fox: false,
frame: false,
play: false,
wing: false,
hole: false,
ex: false,
crosshairs: false,
eye: false,
},
colorEnablement: {
default: true,
blue: true,
green: true,
red: true,
pink: true,
yellow: true,
userColor1: false,
userColor2: false,
userColor3: false,
userColor4: false,
},
shapePenalties: {
default: 1,
bolt: 1,
curve: 1,
fox: 1,
frame: 1,
play: 1,
wing: 1,
hole: 1,
ex: 1,
crosshairs: 1,
eye: 1,
},
colorPenalties: {
default: 1,
blue: 1,
green: 1,
red: 1,
pink: 1,
yellow: 1,
userColor1: 1,
userColor2: 1,
userColor3: 1,
userColor4: 1,
},
maxPenalty: 0,
};
}
}
6 changes: 1 addition & 5 deletions packages/app-web-docs/docusaurus.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,7 @@ const config: Config = {
],
],

plugins: [
"docusaurus-plugin-sass",
"./src/plugins/recorded-tests-plugin.ts",
"./src/plugins/scope-tests-plugin.ts",
],
plugins: ["docusaurus-plugin-sass"],

themeConfig: {
navbar: {
Expand Down
19 changes: 3 additions & 16 deletions packages/app-web-docs/src/docs/components/Code.css
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,8 @@
background-color: #e5c02c;
}

/* Code hat referenced */
/* Code mark referenced */

.code-hat-referenced::before {
animation: code-hat-referenced-pulse 2s ease-in-out infinite;
}

@keyframes code-hat-referenced-pulse {
50% {
background-color: var(--code-hat-referenced-color);
}
}

@media (prefers-reduced-motion: reduce) {
.code-hat-referenced::before {
background-color: var(--code-hat-referenced-color);
animation: none;
}
.code-mark {
background-color: var(--code-mark-color);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { usePluginData } from "@docusaurus/useGlobalData";
import type { Dispatch, JSX, ReactNode, SetStateAction } from "react";
import { createContext, useContext, useMemo, useState } from "react";
import type { DecorationItem } from "shiki";
import type {
Position,
Range,
Selection,
TestCaseSnapshot,
} from "@cursorless/lib-common";
Expand All @@ -25,14 +25,15 @@ const RecordedTestVisualizerContext = createContext<
RecordedTestVisualizerContextValue | undefined
>(undefined);

interface ProviderProps {
children: ReactNode;
recordedTests: RecordedTest[];
}

export function RecordedTestVisualizerProvider({
children,
}: {
children: ReactNode;
}) {
const recordedTests = usePluginData(
"recorded-tests-plugin",
) as RecordedTest[];
recordedTests,
}: ProviderProps): JSX.Element {
const [renderWhitespace, setRenderWhitespace] = useState(true);
const fixtures = useMemo(
() =>
Expand All @@ -57,7 +58,7 @@ export function RecordedTestVisualizerProvider({
);
}

export function RecordedTestVisualizerOptions() {
export function RecordedTestVisualizerOptions(): JSX.Element {
const { renderWhitespace, setRenderWhitespace } = useRecordedTestVisualizer();

return (
Expand Down Expand Up @@ -99,7 +100,7 @@ export function RecordedTestVisualizer({
// oxlint-disable-next-line react_perf/jsx-no-new-object-as-prop
const link = {
name: "GitHub",
url: `https://github.com/cursorless-dev/cursorless/blob/main/resources/fixtures/recorded/docs/${path}`,
url: `https://github.com/cursorless-dev/cursorless/blob/main/resources/fixtures/recorded/${path}`,
};

const renderHats =
Expand All @@ -109,7 +110,7 @@ export function RecordedTestVisualizer({
return (
<div className="row">
<div className="col">
Input
Before
<CodeState
renderWhitespace={renderWhitespace}
renderHats={renderHats}
Expand All @@ -119,7 +120,7 @@ export function RecordedTestVisualizer({
/>
</div>
<div className="col">
Output
After
<CodeState
renderWhitespace={renderWhitespace}
renderHats={renderHats}
Expand Down Expand Up @@ -204,16 +205,21 @@ function toDecorations(
plainObjectToRange(hatRange),
)
: [];
const markRanges = renderHats
? Object.values(state.marks ?? {}).map(plainObjectToRange)
: [];

// Shiki rejects intersecting decorations. A zero-width cursor at the end of
// a hat range intersects the hat decoration, so render both on one wrapper.
// We only merge at the end because code-cursor-after recreates that exact
// boundary without competing with the hat's ::before pseudo-element.
// a hat or mark range intersects that decoration, so render both on one
// wrapper. We only merge at the end because code-cursor-after recreates that
// exact boundary without competing with the hat's ::before pseudo-element.
const mergedCursorPositions = selections
.filter(
(selection) =>
selection.isEmpty &&
hatRanges.some(({ end }) => end.isEqual(selection.active)),
[...hatRanges, ...markRanges].some(({ end }) =>
end.isEqual(selection.active),
),
)
.map(({ active }) => active);

Expand All @@ -228,10 +234,46 @@ function toDecorations(
),
)
.map(toDecoration),
...toMarkDecorations(markRanges, hatRanges, mergedCursorPositions),
...toHatDecorations(state, renderHats, mergedCursorPositions),
];
}

function toMarkDecorations(
markRanges: readonly Range[],
hatRanges: readonly Range[],
mergedCursorPositions: readonly Position[],
): DecorationItem[] {
return markRanges
.filter(
(range, index) =>
markRanges.findIndex((otherRange) => otherRange.isRangeEqual(range)) ===
index,
)
.map((range) => {
const className = ["code-mark"];

// Prefer placing the cursor on the smaller hat wrapper when both the hat
// and its containing mark end at the cursor position.
if (
mergedCursorPositions.some((position) => position.isEqual(range.end)) &&
!hatRanges.some(({ end }) => end.isEqual(range.end))
) {
className.push("code-cursor-after");
}

return {
start: range.start,
end: range.end,
alwaysWrap: true,
properties: {
className,
style: `--code-mark-color: ${highlightColors.content.background};`,
},
};
});
}

function toHatDecorations(
state: TestCaseSnapshot,
renderHats: boolean,
Expand All @@ -241,23 +283,12 @@ function toHatDecorations(
return [];
}

const markRanges = Object.values(state.marks ?? {}).map(plainObjectToRange);

return state.hatTokenMap.map(({ hatStyle, hatRange }) => {
const range = plainObjectToRange(hatRange);
const properties: DecorationItem["properties"] = {
className: ["code-hat", `code-hat-${hatStyle}`],
};

const isReferenced = markRanges.some((markRange) =>
markRange.contains(range),
);

if (isReferenced) {
properties.className?.push("code-hat-referenced");
properties.style = `--code-hat-referenced-color: ${highlightColors.content.background};`;
}

if (mergedCursorPositions.some((position) => position.isEqual(range.end))) {
// The hat uses ::before and the cursor uses ::after, allowing both
// visuals to share this wrapper without overlapping Shiki decorations.
Expand Down
Loading
Loading