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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app-web-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"scripts": {
"typecheck": "tsc",
"clean": "docusaurus clear && rm -rf ./out ./dist ./tsconfig.tsbuildinfo",
"test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test src/sidebarItemsGenerator.test.ts",
"dev": "docusaurus start",
"build": "docusaurus build --out-dir out"
},
Expand Down
221 changes: 121 additions & 100 deletions packages/app-web-docs/sidebarItemsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,12 @@ import type * as ClassicPreset from "@docusaurus/preset-classic";
type DocsOptions = Exclude<ClassicPreset.Options["docs"], false | undefined>;
type SidebarItemsGenerator = NonNullable<DocsOptions["sidebarItemsGenerator"]>;

interface Group {
label: string;
items: string[];
}

const items: (Group | string)[] = [
"Cursorless",
{
label: "Getting started",
items: [
"Installation",
"Recommendations",
"Learning resources",
"How-to guides",
"Troubleshooting",
],
},
{
label: "Reference",
items: [
"Actions",
"Modifiers",
"Scopes",
"Targets",
"Destinations",
"Paired delimiters",
"Alphabet and symbols",
],
},
"Supported languages",
{
label: "Customization",
items: [
"Customization",
"Hat assignment",
"Visual Accessibility",
"Custom IDE actions",
"Custom regex scopes",
"Public Talon API",
],
},
{
label: "Tools",
items: [
"Cursorless sidebar",
"Scope visualizer",
"Local command history",
"Draft editor",
],
},
{
label: "Advanced",
items: ["Talon dependencies", "Unicode support", "Experimental features"],
},
{
label: "Project",
items: ["Updating", "Glossary", "Release notes"],
},
];
/**
* Set directly in Markdown front matter. For `_category_.json`, set it under
* `customProps`, which is Docusaurus's supported category metadata extension.
*/
const sidebarGroupKey = "sidebar_group";
const sidebarGroupPositionKey = "sidebar_group_position";

export const sidebarItemsGenerator: SidebarItemsGenerator = async ({
defaultSidebarItemsGenerator,
Expand Down Expand Up @@ -93,6 +40,60 @@ export const sidebarItemsGenerator: SidebarItemsGenerator = async ({
return docId != null && docsById.get(docId)?.sidebarPosition != null;
}

function getSidebarGroup(item: NormalizedSidebarItem): string | undefined {
let sidebarGroup: unknown;
if (item.type === "doc" || item.type === "ref") {
sidebarGroup = docsById.get(item.id)?.frontMatter[sidebarGroupKey];
} else if (item.type === "category") {
sidebarGroup = item.customProps?.[sidebarGroupKey];
}
if (sidebarGroup === undefined) {
return undefined;
}
if (typeof sidebarGroup !== "string" || sidebarGroup.trim().length === 0) {
throw new Error(
`Invalid ${sidebarGroupKey} for sidebar item '${getLabel(item)}'`,
);
}
return sidebarGroup.trim();
}

function getSidebarGroupPosition(
item: NormalizedSidebarItem,
): number | undefined {
let sidebarGroupPosition: unknown;
if (item.type === "doc" || item.type === "ref") {
sidebarGroupPosition = docsById.get(item.id)?.frontMatter[
sidebarGroupPositionKey
];
} else if (item.type === "category") {
sidebarGroupPosition = item.customProps?.[sidebarGroupPositionKey];
}
if (sidebarGroupPosition === undefined) {
return undefined;
}
if (
typeof sidebarGroupPosition !== "number" ||
!Number.isFinite(sidebarGroupPosition)
) {
// oxlint-disable-next-line unicorn/prefer-type-error
throw new Error(
`Invalid ${sidebarGroupPositionKey} for sidebar item '${getLabel(item)}'`,
);
}
return sidebarGroupPosition;
}

function getSidebarPosition(item: NormalizedSidebarItem): number | undefined {
let docId: string | undefined;
if (item.type === "doc" || item.type === "ref") {
docId = item.id;
} else if (item.type === "category" && item.link?.type === "doc") {
docId = item.link.id;
}
return docId == null ? undefined : docsById.get(docId)?.sidebarPosition;
}

function sortByLabel(
items: NormalizedSidebarItem[],
): NormalizedSidebarItem[] {
Expand Down Expand Up @@ -122,64 +123,84 @@ export const sidebarItemsGenerator: SidebarItemsGenerator = async ({
});
}

const sortedItems = sortByLabel(generatedItems);

if (args.item.dirName !== "user") {
return sortedItems;
}

const remainingItems = sortedItems.slice();

function takeItem(label: string): NormalizedSidebarItem {
const index = remainingItems.findIndex(
(item) => getLabel(item)?.toLowerCase() === label.toLowerCase(),
);
const item = remainingItems[index];
if (item == null) {
throw new Error(`Missing generated sidebar item '${label}'`);
}
remainingItems.splice(index, 1);
return item;
return sortByLabel(generatedItems);
}

function createGroup(group: Group): NormalizedSidebarItem {
const items = group.items.map(takeItem);
function createGroup(
label: string,
groupItems: NormalizedSidebarItem[],
): NormalizedSidebarItem {
const items = groupItems.slice();
const linkIndex = items.findIndex(
(item) => item.type === "doc" && getLabel(item) === group.label,
(item) =>
item.type === "doc" &&
getLabel(item)?.toLowerCase() === label.toLowerCase(),
);
const [link] = linkIndex === -1 ? [] : items.splice(linkIndex, 1);

return {
type: "category",
label: group.label,
label,
collapsible: false,
collapsed: false,
...(link?.type === "doc" ? { link: { type: "doc", id: link.id } } : {}),
items,
};
}

const groupedItems = items.map((item): NormalizedSidebarItem => {
if (typeof item === "string") {
return takeItem(item);
const itemsByGroup = new Map<string, NormalizedSidebarItem[]>();
const positionsByGroup = new Map<string, number>();
const ungroupedItems: NormalizedSidebarItem[] = [];
for (const item of generatedItems) {
const sidebarGroup = getSidebarGroup(item);
const sidebarGroupPosition = getSidebarGroupPosition(item);
if (sidebarGroup == null) {
if (sidebarGroupPosition != null) {
throw new Error(
`${sidebarGroupPositionKey} requires ${sidebarGroupKey} for sidebar item '${getLabel(item)}'`,
);
}
ungroupedItems.push(item);
continue;
}
return createGroup(item);
});

if (remainingItems.length > 0) {
const remainingJson = remainingItems
.map((item) => {
if (item.type === "category") {
const { items, ...rest } = item;
return JSON.stringify(rest);
}
return JSON.stringify(item);
})
.join(", ");
throw new Error(
`Unassigned generated user sidebar items: ${remainingJson}`,
);

if (sidebarGroupPosition != null) {
const existingPosition = positionsByGroup.get(sidebarGroup);
if (
existingPosition != null &&
existingPosition !== sidebarGroupPosition
) {
throw new Error(
`Conflicting ${sidebarGroupPositionKey} values for sidebar group '${sidebarGroup}': ${existingPosition} and ${sidebarGroupPosition}`,
);
}
positionsByGroup.set(sidebarGroup, sidebarGroupPosition);
}

const groupItems = itemsByGroup.get(sidebarGroup) ?? [];
groupItems.push(item);
itemsByGroup.set(sidebarGroup, groupItems);
}

const topLevelItems = sortByLabel(ungroupedItems).map((item) => ({
item,
position: getSidebarPosition(item),
}));
for (const [label, groupItems] of itemsByGroup) {
topLevelItems.push({
item: createGroup(label, sortByLabel(groupItems)),
position: positionsByGroup.get(label),
});
}

return groupedItems;
return topLevelItems
.toSorted((a, b) => {
const aPosition = a.position ?? Number.POSITIVE_INFINITY;
const bPosition = b.position ?? Number.POSITIVE_INFINITY;
return aPosition === bPosition
? (getLabel(a.item) ?? "").localeCompare(getLabel(b.item) ?? "")
: aPosition - bPosition;
})
.map(({ item }) => item);
};
1 change: 1 addition & 0 deletions packages/app-web-docs/src/docs/user/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
slug: /
sidebar_position: 1
---

# Cursorless
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{
"label": "Actions"
"label": "Actions",
"position": 1,
"customProps": {
"sidebar_group": "Reference",
"sidebar_group_position": 3
}
}
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/alphabet-and-symbols.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Reference
sidebar_position: 7
---

# Alphabet and symbols

Cursorless uses the [Talon Community](https://github.com/talonhub/community) alphabet, digits, and symbol spoken forms via the [`user.any_alphanumeric_key`](https://github.com/talonhub/community/blob/607c3415f5f29a5f75db6fe5648e37f514f62ac5/core/keys/keys.py#L71-L74) capture.
Expand Down
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/custom-ide-actions.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Customization
sidebar_position: 3
---

# Custom IDE actions

You can use Cursorless to run any built-in IDE command on a specific target.
Expand Down
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/custom-regex-scopes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Customization
sidebar_position: 4
---

# Custom regex scopes

You can define custom scopes using regexes in `user/cursorless-settings/experimental/regex_scope_types.csv`
Expand Down
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/customization.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Customization
sidebar_group_position: 5
---

# Customization

Cursorless consists of
Expand Down
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/destinations.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Reference
sidebar_position: 5
---

# Destinations

Some actions expect a destination, rather than a target. Destinations combine a preposition and a target, to define a place and behavior for a Cursorless action.
Expand Down
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/draft-editor.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Tools
sidebar_position: 4
---

# Draft editor

The draft editor is a [Talon Community](https://github.com/talonhub/community) plugin that lets you revise text in a full text editor before inserting it
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"label": "Experimental features"
"label": "Experimental features",
"position": 3,
"customProps": {
"sidebar_group": "Advanced"
}
}
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/glossary.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Project
sidebar_position: 2
---

# Glossary

Here we provide definitions for several commonly used Cursorless terms.
Expand Down
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/hat-assignment.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Customization
sidebar_position: 1
---

# Hat assignment

Every time you move your cursor, edit a document, or scroll, Cursorless assigns hats to the tokens in each visible editor. When selecting hats, Cursorless attempts to give "good" hats to tokens near your cursor, while at the same time keeping hats from moving around too much.
Expand Down
5 changes: 5 additions & 0 deletions packages/app-web-docs/src/docs/user/how-to.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
---
sidebar_group: Getting started
sidebar_position: 4
---

# How-to guides

This section contains some how-to guides for common tasks.
Expand Down
6 changes: 6 additions & 0 deletions packages/app-web-docs/src/docs/user/installation.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
sidebar_group: Getting started
sidebar_group_position: 2
sidebar_position: 1
---

# Installation

1. Install [Talon](https://talonvoice.com/)
Expand Down
4 changes: 4 additions & 0 deletions packages/app-web-docs/src/docs/user/languages/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
sidebar_position: 4
---

# Supported languages

Cursorless supports a wide range of languages, though our level of support varies a bit
Expand Down
Loading
Loading