Skip to content

Commit 27a1b5e

Browse files
committed
Big refactor
1 parent 371e1dd commit 27a1b5e

File tree

15 files changed

+341
-210
lines changed

15 files changed

+341
-210
lines changed

data/fixtures/recorded/tutorial/unit-1-basics/script.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
"steps": [
55
"Say {step:takeCap.yml}",
66
"Well done! 🙌 You just used the code word for 'c', {grapheme:c}, to refer to the word with a gray hat over the 'c'.\nWhen a hat is not gray, we say its color: say {step:takeBlueSun.yml}",
7-
"Selecting a single token is great, but oftentimes we need something bigger. Say {step:takeHarpPastDrum.yml} to select a range.",
8-
"Despite its name, one of the most powerful aspects of cursorless is the ability to use more than one cursor. Let's try that: {step:takeNearAndSun.yml}",
7+
"Selecting a single token is great, but oftentimes we need something bigger.\nSay {step:takeHarpPastDrum.yml} to select a range.",
8+
"Despite its name, one of the most powerful aspects of cursorless is the ability to use more than one cursor.\nLet's try that: {step:takeNearAndSun.yml}",
99
"But let's show that cursorless can live up to its name: we can say {step:chuckTrap.yml} to delete a word without ever moving our cursor.",
10-
"Tokens are great, but they're just one way to think of a document. Let's try working with lines: {step:chuckLineOdd.yml}",
10+
"Tokens are great, but they're just one way to think of a document.\nLet's try working with lines: {step:chuckLineOdd.yml}",
1111
"We can also use {scopeType:line} to refer to the line containing our cursor: {step:takeLine.yml}",
1212
"You now know how to select and delete; let's give you a couple more actions to play with: say {action:pre} to place the cursor before a target, as in {step:preUrge.yml}",
1313
"Say {action:post} to place the cursor after a target: {step:postAir.yml}",

packages/common/src/types/tutorial.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ interface ActiveTutorialState extends BaseTutorialInfo {
5959

6060
export interface ActiveTutorialNoErrorsState extends ActiveTutorialState {
6161
hasErrors: false;
62-
stepContent: TutorialStepFragment[];
62+
stepContent: TutorialStepFragment[][];
6363
stepCount: number;
6464
}
6565

packages/cursorless-engine/src/api/Tutorial.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ export type TutorialStepTrigger =
8080

8181
export interface TutorialStep {
8282
/**
83-
* The text content of the current step
83+
* The content of the current step. Each element in the array represents a
84+
* paragraph in the tutorial step.
8485
*/
85-
content: TutorialStepFragment[];
86+
content: TutorialStepFragment[][];
8687

8788
/**
8889
* The path to the yaml file that should be used to setup the current step (if
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ActionType } from "@cursorless/common";
2+
import { invertBy } from "lodash";
3+
import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl";
4+
import { defaultSpokenFormMap } from "../spokenForms/defaultSpokenFormMap";
5+
import { StepComponent, StepComponentParser } from "./StepComponent";
6+
7+
export class ActionComponentHandler implements StepComponentParser {
8+
private actionMap: Record<string, ActionType[]> = invertBy(
9+
defaultSpokenFormMap.action,
10+
(val) => val.spokenForms[0],
11+
) as Record<string, ActionType[]>;
12+
13+
constructor(
14+
private customSpokenFormGenerator: CustomSpokenFormGeneratorImpl,
15+
) {}
16+
17+
async parse(arg: string): Promise<StepComponent> {
18+
return {
19+
content: {
20+
type: "term",
21+
value: this.getActionSpokenForm(this.parseActionId(arg)),
22+
},
23+
};
24+
}
25+
26+
private getActionSpokenForm(actionId: ActionType) {
27+
const spokenForm =
28+
this.customSpokenFormGenerator.actionIdToSpokenForm(actionId);
29+
30+
return spokenForm.spokenForms[0];
31+
}
32+
33+
private parseActionId(arg: string): ActionType {
34+
const actionIds = this.actionMap[arg];
35+
36+
if (actionIds == null || actionIds.length === 0) {
37+
throw new Error(`Unknown action: ${arg}`);
38+
}
39+
40+
return actionIds[0];
41+
}
42+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { CommandComplete, TutorialId, loadFixture } from "@cursorless/common";
2+
import path from "path";
3+
import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl";
4+
import { canonicalizeAndValidateCommand } from "./commandVersionUpgrades/canonicalizeAndValidateCommand";
5+
import { StepComponent, StepComponentParser } from "./StepComponent";
6+
import { TutorialError } from "./TutorialError";
7+
8+
export class CursorlessCommandHandler implements StepComponentParser {
9+
constructor(
10+
private tutorialRootDir: string,
11+
private tutorialId: TutorialId,
12+
private customSpokenFormGenerator: CustomSpokenFormGeneratorImpl,
13+
) {}
14+
15+
async parse(arg: string): Promise<StepComponent> {
16+
const fixture = await loadFixture(
17+
path.join(this.tutorialRootDir, this.tutorialId, arg),
18+
);
19+
const command = canonicalizeAndValidateCommand(fixture.command);
20+
21+
return {
22+
initialState: fixture.initialState,
23+
languageId: fixture.languageId,
24+
trigger: {
25+
type: "command",
26+
command,
27+
},
28+
content: {
29+
type: "command",
30+
value: this.getCommandSpokenForm(command),
31+
},
32+
};
33+
}
34+
35+
/**
36+
* Handle the argument of a "{step:cloneStateInk.yml}""
37+
*/
38+
private getCommandSpokenForm(command: CommandComplete) {
39+
// command to be said for moving to the next step
40+
const spokenForm =
41+
this.customSpokenFormGenerator.commandToSpokenForm(command);
42+
43+
if (spokenForm.type === "error") {
44+
throw new TutorialError(
45+
`Error while processing spoken form for command: ${spokenForm.reason}`,
46+
{ requiresTalonUpdate: spokenForm.requiresTalonUpdate },
47+
);
48+
}
49+
50+
return spokenForm.spokenForms[0];
51+
}
52+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl";
2+
import { StepComponent, StepComponentParser } from "./StepComponent";
3+
4+
export class GraphemeComponentHandler implements StepComponentParser {
5+
constructor(
6+
private customSpokenFormGenerator: CustomSpokenFormGeneratorImpl,
7+
) {}
8+
9+
async parse(arg: string): Promise<StepComponent> {
10+
return {
11+
content: {
12+
type: "term",
13+
value: this.getGraphemeSpokenForm(arg),
14+
},
15+
};
16+
}
17+
18+
private getGraphemeSpokenForm(grapheme: string) {
19+
const spokenForm =
20+
this.customSpokenFormGenerator.graphemeToSpokenForm(grapheme);
21+
22+
return spokenForm.spokenForms[0];
23+
}
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { TestCaseSnapshot, TutorialStepFragment } from "@cursorless/common";
2+
import { TutorialStepTrigger } from "../api/Tutorial";
3+
4+
export interface StepComponent {
5+
initialState?: TestCaseSnapshot;
6+
languageId?: string;
7+
trigger?: TutorialStepTrigger;
8+
content: TutorialStepFragment;
9+
}
10+
11+
export interface StepComponentParser {
12+
parse(arg: string): Promise<StepComponent>;
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export class TutorialError extends Error {
2+
public readonly requiresTalonUpdate: boolean;
3+
4+
constructor(
5+
message: string,
6+
{ requiresTalonUpdate }: { requiresTalonUpdate: boolean },
7+
) {
8+
super(message);
9+
10+
this.requiresTalonUpdate = requiresTalonUpdate;
11+
}
12+
}

packages/cursorless-engine/src/core/TutorialImpl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import { Tutorial, TutorialContent, TutorialStep } from "../api/Tutorial";
2424
import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl";
2525
import { ide } from "../singletons/ide.singleton";
2626
import { Debouncer } from "./Debouncer";
27-
import { TutorialError, TutorialScriptParser } from "./TutorialScriptParser";
27+
import { TutorialError } from "./TutorialError";
28+
import { TutorialScriptParser } from "./TutorialScriptParser";
2829
import { loadTutorialScript } from "./loadTutorialScript";
2930

3031
const HIGHLIGHT_COLOR = "highlight0";

0 commit comments

Comments
 (0)