diff --git a/cursorless-talon/src/cursorless.py b/cursorless-talon/src/cursorless.py index a4191691ae..df3ea7832e 100644 --- a/cursorless-talon/src/cursorless.py +++ b/cursorless-talon/src/cursorless.py @@ -34,7 +34,7 @@ def private_cursorless_show_command_statistics(): def private_cursorless_start_tutorial(): """Start the introductory Cursorless tutorial""" actions.user.private_cursorless_run_rpc_command_no_wait( - "cursorless.tutorial.start", "tutorial-1-basics" + "cursorless.tutorial.start", "1-introduction" ) def private_cursorless_tutorial_next(): diff --git a/packages/app-web-docs/src/docs/user/README.md b/packages/app-web-docs/src/docs/user/README.md index d9063fac6b..1bf4fde92d 100644 --- a/packages/app-web-docs/src/docs/user/README.md +++ b/packages/app-web-docs/src/docs/user/README.md @@ -5,7 +5,7 @@ sidebar_position: 1 # Cursorless -Welcome to Cursorless! If you're new, start with the [learning resources](./learning-resources.md) or the [how-to guides](./how-to.md). +Welcome to Cursorless! If you're new, start with the [tutorial](./tutorial/README.mdx) or the [learning resources](./learning-resources.md). This guide assumes you've already [installed Cursorless](./installation.md). diff --git a/packages/app-web-docs/src/docs/user/tutorial/1-basics.mdx b/packages/app-web-docs/src/docs/user/tutorial/1-introduction.mdx similarity index 60% rename from packages/app-web-docs/src/docs/user/tutorial/1-basics.mdx rename to packages/app-web-docs/src/docs/user/tutorial/1-introduction.mdx index 388ff316dd..6ff32d8afd 100644 --- a/packages/app-web-docs/src/docs/user/tutorial/1-basics.mdx +++ b/packages/app-web-docs/src/docs/user/tutorial/1-introduction.mdx @@ -1,8 +1,10 @@ import { RecordedTestVisualizer, RecordedTestVisualizerOptions, RecordedTestVisualizerProvider } from "@site/src/docs/components/RecordedTestVisualizer"; -import recordedTests from "./fixtures/1-basics.json"; +import recordedTests from "./fixtures/1-introduction.json"; # 1. Introduction +This tutorial will teach you the basics of Cursorless. + @@ -11,7 +13,7 @@ import recordedTests from "./fixtures/1-basics.json"; Say `"take cap"` - + ## Step 2 @@ -19,7 +21,7 @@ Well done! 🙌 You just used the code word for 'c', "cap", to refer to the word When a hat is not gray, we say its color: say `"take blue sun"` - + ## Step 3 @@ -27,7 +29,7 @@ Selecting a single token is great, but often we need something bigger. Say `"take harp past drum"` to select a range. - + ## Step 4 @@ -35,13 +37,13 @@ Despite its name, one of the most powerful aspects of Cursorless is the ability Let's try that: `"take near and sun"` - + ## Step 5 But let's show that Cursorless can live up to its name: we can say `"chuck trap"` to delete a word without ever moving our cursor. - + ## Step 6 @@ -49,40 +51,38 @@ Tokens are great, but they're just one way to think of a document. Let's try working with lines: `"chuck line odd"` - + ## Step 7 We can also use "line" to refer to the line containing our cursor: `"take line"` - + ## Step 8 You now know how to select and delete; let's give you a couple more actions to play with: say "pre" to place the cursor before a target, as in `"pre urge"` - + ## Step 9 Say "post" to place the cursor after a target: `"post air"` - + ## Step 10 Say "change" to delete a word and move your cursor to where it used to be: `"change sit"` - + ## Step 11 And that wraps up unit 1 of the Cursorless tutorial! Next time, we'll write some code 🙌. -Feel free to keep playing with this document, then say `"tutorial next"` to continue. + -:::note -This step has no visualisation. To get the full experience try the interactive tutorial in VS Code by saying `"cursorless tutorial"`. -::: +## Next tutorial: 2. Basic coding - +[Continue to Basic coding](./2-basic-coding.mdx) - This tutorial will teach you the basics of coding with Cursorless. diff --git a/packages/app-web-docs/src/docs/user/tutorial/2-basic-coding.mdx b/packages/app-web-docs/src/docs/user/tutorial/2-basic-coding.mdx index 125e6b7a95..0e1257b79e 100644 --- a/packages/app-web-docs/src/docs/user/tutorial/2-basic-coding.mdx +++ b/packages/app-web-docs/src/docs/user/tutorial/2-basic-coding.mdx @@ -3,6 +3,8 @@ import recordedTests from "./fixtures/2-basic-coding.json"; # 2. Basic coding +This tutorial will teach you the basics of coding with Cursorless. + @@ -11,80 +13,56 @@ import recordedTests from "./fixtures/2-basic-coding.json"; When editing code, we often think in terms of statements, functions, etc. Let's clone a statement: `"clone state sit"` - + ## Step 2 -"state" is one of many scopes supported by Cursorless. To see all available scopes, have a look at the Scopes section below, and use the "visualize" command to see them live: `"visualize funk"` - -:::note -This step has no visualisation. To get the full experience try the interactive tutorial in VS Code by saying `"cursorless tutorial"`. -::: - -## Step 3 - -Say `"visualize nothing"` to hide the visualization. - -:::note -This step has no visualisation. To get the full experience try the interactive tutorial in VS Code by saying `"cursorless tutorial"`. -::: - -## Step 4 - Cursorless tries its best to keep your commands short. In the following command, we just say "string" once, but Cursorless infers that both targets are strings: `"swap string air with whale"` - + -## Step 5 +## Step 3 Great. Let's learn a new action. The "pour" action lets you start editing a new line below any line on your screen: `"pour urge"` - + -## Step 6 +## Step 4 Now let's try applying a Cursorless action to the current line: `"dedent this"` - + -## Step 7 +## Step 5 Code reuse is a fact of life as a programmer. Cursorless makes this easy with the "bring" command: `"bring state urge"` - + -## Step 8 +## Step 6 "bring" also works with two targets just like "swap": `"bring blue cap to value red"` - + -## Step 9 +## Step 7 Cursorless tries its best to use its knowledge of programming languages to leave you with syntactically valid code. Note how it cleans up the comma here: `"chuck arg blue vest"` - + -## Step 10 +## Step 8 We introduced a lot of different scopes today. If you're anything like us, you've already forgotten them all. The important thing to remember is that you can always say `"cursorless help"` to see a list. -:::note -This step has no visualisation. To get the full experience try the interactive tutorial in VS Code by saying `"cursorless tutorial"`. -::: - -## Step 11 - -As always, feel free to stick around and play with this file to practice what you've just learned. Happy coding 😊. Say `"tutorial next"` to get back home. +## Step 9 -:::note -This step has no visualisation. To get the full experience try the interactive tutorial in VS Code by saying `"cursorless tutorial"`. -::: +That wraps up the Basic coding tutorial. Happy coding 😊. diff --git a/packages/app-web-docs/src/docs/user/tutorial/README.mdx b/packages/app-web-docs/src/docs/user/tutorial/README.mdx index 6a0db449ae..ddbedf3e64 100644 --- a/packages/app-web-docs/src/docs/user/tutorial/README.mdx +++ b/packages/app-web-docs/src/docs/user/tutorial/README.mdx @@ -6,11 +6,11 @@ To follow along interactively, focus VS Code and say `"cursorless tutorial"`. ## 1. Introduction -[Start Introduction](./1-basics.mdx) +[Start Introduction](./1-introduction.mdx) - This tutorial will teach you the basics of Cursorless. ## 2. Basic coding -[Start Basic coding](./2-basic-coding.mdx) +[Start Basic coding](./2-basic-coding.mdx) - This tutorial will teach you the basics of coding with Cursorless. ## Use the interactive tutorial diff --git a/packages/app-web-docs/src/docs/user/tutorial/fixtures/1-basics.json b/packages/app-web-docs/src/docs/user/tutorial/fixtures/1-introduction.json similarity index 98% rename from packages/app-web-docs/src/docs/user/tutorial/fixtures/1-basics.json rename to packages/app-web-docs/src/docs/user/tutorial/fixtures/1-introduction.json index 724311d918..5db7a48cdc 100644 --- a/packages/app-web-docs/src/docs/user/tutorial/fixtures/1-basics.json +++ b/packages/app-web-docs/src/docs/user/tutorial/fixtures/1-introduction.json @@ -1,7 +1,7 @@ [ { - "path": "tutorial/tutorial-1-basics/takeCap.yml", - "name": "tutorial/tutorial-1-basics/takeCap", + "path": "tutorial/1-introduction/takeCap.yml", + "name": "tutorial/1-introduction/takeCap", "fixture": { "languageId": "plaintext", "command": { @@ -629,8 +629,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/takeBlueSun.yml", - "name": "tutorial/tutorial-1-basics/takeBlueSun", + "path": "tutorial/1-introduction/takeBlueSun.yml", + "name": "tutorial/1-introduction/takeBlueSun", "fixture": { "languageId": "plaintext", "command": { @@ -1258,8 +1258,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/takeHarpPastDrum.yml", - "name": "tutorial/tutorial-1-basics/takeHarpPastDrum", + "path": "tutorial/1-introduction/takeHarpPastDrum.yml", + "name": "tutorial/1-introduction/takeHarpPastDrum", "fixture": { "languageId": "plaintext", "command": { @@ -1910,8 +1910,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/takeNearAndSun.yml", - "name": "tutorial/tutorial-1-basics/takeNearAndSun", + "path": "tutorial/1-introduction/takeNearAndSun.yml", + "name": "tutorial/1-introduction/takeNearAndSun", "fixture": { "languageId": "plaintext", "command": { @@ -2572,8 +2572,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/chuckTrap.yml", - "name": "tutorial/tutorial-1-basics/chuckTrap", + "path": "tutorial/1-introduction/chuckTrap.yml", + "name": "tutorial/1-introduction/chuckTrap", "fixture": { "languageId": "plaintext", "command": { @@ -3207,8 +3207,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/chuckLineOdd.yml", - "name": "tutorial/tutorial-1-basics/chuckLineOdd", + "path": "tutorial/1-introduction/chuckLineOdd.yml", + "name": "tutorial/1-introduction/chuckLineOdd", "fixture": { "languageId": "plaintext", "command": { @@ -3752,8 +3752,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/takeLine.yml", - "name": "tutorial/tutorial-1-basics/takeLine", + "path": "tutorial/1-introduction/takeLine.yml", + "name": "tutorial/1-introduction/takeLine", "fixture": { "languageId": "plaintext", "command": { @@ -4187,8 +4187,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/preUrge.yml", - "name": "tutorial/tutorial-1-basics/preUrge", + "path": "tutorial/1-introduction/preUrge.yml", + "name": "tutorial/1-introduction/preUrge", "fixture": { "languageId": "plaintext", "command": { @@ -4620,8 +4620,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/postAir.yml", - "name": "tutorial/tutorial-1-basics/postAir", + "path": "tutorial/1-introduction/postAir.yml", + "name": "tutorial/1-introduction/postAir", "fixture": { "languageId": "plaintext", "command": { @@ -5053,8 +5053,8 @@ } }, { - "path": "tutorial/tutorial-1-basics/changeSit.yml", - "name": "tutorial/tutorial-1-basics/changeSit", + "path": "tutorial/1-introduction/changeSit.yml", + "name": "tutorial/1-introduction/changeSit", "fixture": { "languageId": "plaintext", "command": { diff --git a/packages/app-web-docs/src/docs/user/tutorial/fixtures/2-basic-coding.json b/packages/app-web-docs/src/docs/user/tutorial/fixtures/2-basic-coding.json index e411ea3ff8..3fa913c0f1 100644 --- a/packages/app-web-docs/src/docs/user/tutorial/fixtures/2-basic-coding.json +++ b/packages/app-web-docs/src/docs/user/tutorial/fixtures/2-basic-coding.json @@ -1,7 +1,7 @@ [ { - "path": "tutorial/tutorial-2-basic-coding/cloneStateInk.yml", - "name": "tutorial/tutorial-2-basic-coding/cloneStateInk", + "path": "tutorial/2-basic-coding/cloneStateInk.yml", + "name": "tutorial/2-basic-coding/cloneStateInk", "fixture": { "languageId": "python", "command": { @@ -1547,8 +1547,8 @@ } }, { - "path": "tutorial/tutorial-2-basic-coding/swapStringAirWithWhale.yml", - "name": "tutorial/tutorial-2-basic-coding/swapStringAirWithWhale", + "path": "tutorial/2-basic-coding/swapStringAirWithWhale.yml", + "name": "tutorial/2-basic-coding/swapStringAirWithWhale", "fixture": { "languageId": "python", "command": { @@ -3211,8 +3211,8 @@ } }, { - "path": "tutorial/tutorial-2-basic-coding/pourUrge.yml", - "name": "tutorial/tutorial-2-basic-coding/pourUrge", + "path": "tutorial/2-basic-coding/pourUrge.yml", + "name": "tutorial/2-basic-coding/pourUrge", "fixture": { "languageId": "python", "command": { @@ -4848,8 +4848,8 @@ } }, { - "path": "tutorial/tutorial-2-basic-coding/dedentThis.yml", - "name": "tutorial/tutorial-2-basic-coding/dedentThis", + "path": "tutorial/2-basic-coding/dedentThis.yml", + "name": "tutorial/2-basic-coding/dedentThis", "fixture": { "languageId": "python", "command": { @@ -6472,8 +6472,8 @@ } }, { - "path": "tutorial/tutorial-2-basic-coding/bringStateUrge.yml", - "name": "tutorial/tutorial-2-basic-coding/bringStateUrge", + "path": "tutorial/2-basic-coding/bringStateUrge.yml", + "name": "tutorial/2-basic-coding/bringStateUrge", "fixture": { "languageId": "python", "command": { @@ -8148,8 +8148,8 @@ } }, { - "path": "tutorial/tutorial-2-basic-coding/bringBlueCapToValueRisk.yml", - "name": "tutorial/tutorial-2-basic-coding/bringBlueCapToValueRisk", + "path": "tutorial/2-basic-coding/bringBlueCapToValueRisk.yml", + "name": "tutorial/2-basic-coding/bringBlueCapToValueRisk", "fixture": { "languageId": "python", "command": { @@ -9871,8 +9871,8 @@ } }, { - "path": "tutorial/tutorial-2-basic-coding/chuckArgueBlueVest.yml", - "name": "tutorial/tutorial-2-basic-coding/chuckArgueBlueVest", + "path": "tutorial/2-basic-coding/chuckArgueBlueVest.yml", + "name": "tutorial/2-basic-coding/chuckArgueBlueVest", "fixture": { "languageId": "python", "command": { diff --git a/packages/lib-common/src/ide/spy/SpyIDE.ts b/packages/lib-common/src/ide/spy/SpyIDE.ts index 69d0b00c98..ed47982302 100644 --- a/packages/lib-common/src/ide/spy/SpyIDE.ts +++ b/packages/lib-common/src/ide/spy/SpyIDE.ts @@ -56,4 +56,13 @@ export class SpyIDE extends PassthroughIDE { }); return super.setHighlightRanges(highlightId, editor, ranges); } + + setInitialHighlightRanges( + highlightId: string | undefined, + editor: TextEditor, + ranges: GeneralizedRange[], + ): Promise { + // Establish test preconditions without recording them as command output. + return super.setHighlightRanges(highlightId, editor, ranges); + } } diff --git a/packages/lib-common/src/ide/types/KeyValueStore.ts b/packages/lib-common/src/ide/types/KeyValueStore.ts index 8b432f048f..ebd3511df5 100644 --- a/packages/lib-common/src/ide/types/KeyValueStore.ts +++ b/packages/lib-common/src/ide/types/KeyValueStore.ts @@ -2,7 +2,6 @@ import type { TutorialId } from "../../types/tutorial.types"; interface SingleTutorialProgress { currentStep: number; - version: number; } export type TutorialProgress = Partial< diff --git a/packages/lib-common/src/ide/types/TutorialContentProvider.ts b/packages/lib-common/src/ide/types/TutorialContentProvider.ts index df3304b5ff..0fe8cd053d 100644 --- a/packages/lib-common/src/ide/types/TutorialContentProvider.ts +++ b/packages/lib-common/src/ide/types/TutorialContentProvider.ts @@ -2,12 +2,6 @@ import type { TestCaseFixtureLegacy } from "../../types/TestCaseFixture"; import type { TutorialId } from "../../types/tutorial.types"; export interface TutorialContentProvider { - /** - * Loads the raw content of all tutorials. Just includes the information in - * the scripts, not the fixtures that represent commands to run. - */ - loadRawTutorials(): Promise; - /** * Loads a fixture file from the tutorial directory, eg "takeNear.yml" * @@ -20,25 +14,3 @@ export interface TutorialContentProvider { fixtureName: string, ): Promise; } - -export interface RawTutorialContent { - /** - * The unique identifier for the tutorial - */ - id: TutorialId; - - /** - * The title of the tutorial - */ - title: string; - - /** - * The version of the tutorial - */ - version: number; - - /** - * The steps of the current tutorial - */ - steps: string[]; -} diff --git a/packages/lib-common/src/index.ts b/packages/lib-common/src/index.ts index ff0b3990aa..0afcf8578f 100644 --- a/packages/lib-common/src/index.ts +++ b/packages/lib-common/src/index.ts @@ -53,6 +53,7 @@ export * from "./testUtil/shouldUpdateFixtures"; export * from "./testUtil/spyToPlainObject"; export * from "./testUtil/TestCaseSnapshot"; export * from "./testUtil/testConstants"; +export * from "./tutorials"; export * from "./types/command/ActionDescriptor"; export * from "./types/command/command.types"; export * from "./types/command/CommandV6.types"; diff --git a/packages/lib-common/src/testUtil/TestCaseSnapshot.ts b/packages/lib-common/src/testUtil/TestCaseSnapshot.ts index 47b538c15d..08dd5e6bf3 100644 --- a/packages/lib-common/src/testUtil/TestCaseSnapshot.ts +++ b/packages/lib-common/src/testUtil/TestCaseSnapshot.ts @@ -1,5 +1,6 @@ import type { StoredTargetKey } from "../StoredTargetKey"; import type { + PlainHighlight, RangePlainObject, SelectionPlainObject, SerializedMarks, @@ -20,6 +21,9 @@ export interface TestCaseSnapshot extends MarkKeys { visibleRanges?: RangePlainObject[]; marks?: SerializedMarks; hatTokenMap?: SimpleTokenHat[]; + + /** Highlights to establish when this snapshot is used as an initial state. */ + highlights?: PlainHighlight[]; timeOffsetSeconds?: number; /** diff --git a/packages/lib-common/src/testUtil/fromPlainObject.ts b/packages/lib-common/src/testUtil/fromPlainObject.ts index 892fe91a9c..c097e0cf6d 100644 --- a/packages/lib-common/src/testUtil/fromPlainObject.ts +++ b/packages/lib-common/src/testUtil/fromPlainObject.ts @@ -1,7 +1,9 @@ +import type { GeneralizedRange } from "../types/GeneralizedRange"; import { Position } from "../types/Position"; import { Range } from "../types/Range"; import { Selection } from "../types/Selection"; import type { + GeneralizedRangePlainObject, PositionPlainObject, RangePlainObject, SelectionPlainObject, @@ -18,6 +20,18 @@ export function plainObjectToRange({ start, end }: RangePlainObject): Range { return new Range(plainObjectToPosition(start), plainObjectToPosition(end)); } +export function plainObjectToGeneralizedRange( + range: GeneralizedRangePlainObject, +): GeneralizedRange { + return range.type === "line" + ? range + : { + type: "character", + start: plainObjectToPosition(range.start), + end: plainObjectToPosition(range.end), + }; +} + export function plainObjectToSelection({ anchor, active, diff --git a/packages/lib-common/src/tutorials/index.ts b/packages/lib-common/src/tutorials/index.ts new file mode 100644 index 0000000000..72f39278b8 --- /dev/null +++ b/packages/lib-common/src/tutorials/index.ts @@ -0,0 +1,2 @@ +export * from "./tutorials"; +export * from "./tutorial.types"; diff --git a/packages/lib-common/src/tutorials/tutorial.types.ts b/packages/lib-common/src/tutorials/tutorial.types.ts new file mode 100644 index 0000000000..ec6452fb3e --- /dev/null +++ b/packages/lib-common/src/tutorials/tutorial.types.ts @@ -0,0 +1,23 @@ +import type { TutorialId } from "../types/tutorial.types"; + +export type TutorialContext = "documentation" | "interactive"; + +export interface RawTutorialContent { + id: TutorialId; + title: string; + description: string; + excludeIn?: TutorialContext[]; + steps: (StepContent | ContextStep)[]; +} + +type StepContent = string | string[]; + +type ContextStep = Record; + +export interface ResolvedTutorialContent extends Omit< + RawTutorialContent, + "steps" +> { + position: number; + steps: string[]; +} diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/script.json b/packages/lib-common/src/tutorials/tutorial1Introduction.ts similarity index 58% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/script.json rename to packages/lib-common/src/tutorials/tutorial1Introduction.ts index 30bf88c1f9..3b012c4e72 100644 --- a/resources/fixtures/recorded/tutorial/tutorial-1-basics/script.json +++ b/packages/lib-common/src/tutorials/tutorial1Introduction.ts @@ -1,9 +1,18 @@ -{ - "title": "Introduction", - "version": 0, - "steps": [ +import type { RawTutorialContent } from "./tutorial.types"; + +const wrapsUp = + "And that wraps up unit 1 of the Cursorless tutorial! Next time, we'll write some code 🙌."; + +export const tutorial1Introduction: RawTutorialContent = { + id: "1-introduction", + title: "Introduction", + description: "This tutorial will teach you the basics of Cursorless.", + steps: [ "Say {command:takeCap.yml}", - "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 {command:takeBlueSun.yml}", + [ + "Well done! 🙌 You just used the code word for 'c', {grapheme:c}, to refer to the word with a gray hat over the 'c'.", + "When a hat is not gray, we say its color: say {command:takeBlueSun.yml}", + ], "Selecting a single token is great, but often we need something bigger.\nSay {command:takeHarpPastDrum.yml} to select a range.", "Despite its name, one of the most powerful aspects of Cursorless is the ability to use more than one cursor.\nLet's try that: {command:takeNearAndSun.yml}", "But let's show that Cursorless can live up to its name: we can say {command:chuckTrap.yml} to delete a word without ever moving our cursor.", @@ -12,6 +21,12 @@ "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 {command:preUrge.yml}", "Say {action:post} to place the cursor after a target: {command:postAir.yml}", "Say {action:change} to delete a word and move your cursor to where it used to be: {command:changeSit.yml}", - "And that wraps up unit 1 of the Cursorless tutorial! Next time, we'll write some code 🙌.\nFeel free to keep playing with this document, then say {special:next} to continue." - ] -} + { + interactive: [ + wrapsUp, + "Feel free to keep playing with this document, then say {special:next} to continue.", + ], + documentation: wrapsUp, + }, + ], +}; diff --git a/packages/lib-common/src/tutorials/tutorial2BasicCoding.ts b/packages/lib-common/src/tutorials/tutorial2BasicCoding.ts new file mode 100644 index 0000000000..23b220c0a4 --- /dev/null +++ b/packages/lib-common/src/tutorials/tutorial2BasicCoding.ts @@ -0,0 +1,33 @@ +import type { RawTutorialContent } from "./tutorial.types"; + +export const tutorial2BasicCoding: RawTutorialContent = { + id: "2-basic-coding", + title: "Basic coding", + description: + "This tutorial will teach you the basics of coding with Cursorless.", + steps: [ + "When editing code, we often think in terms of statements, functions, etc. Let's clone a statement: {command:cloneStateInk.yml}", + [ + "Cursorless tries its best to keep your commands short.", + "In the following command, we just say {scopeType:string} once, but Cursorless infers that both targets are strings: {command:swapStringAirWithWhale.yml}", + ], + "Great. Let's learn a new action. The {action:pour} action lets you start editing a new line below any line on your screen: {command:pourUrge.yml}", + "Now let's try applying a Cursorless action to the current line: {command:dedentThis.yml}", + "Code reuse is a fact of life as a programmer. Cursorless makes this easy with the {action:bring} command: {command:bringStateUrge.yml}", + "{action:bring} also works with two targets just like {action:swap}: {command:bringBlueCapToValueRisk.yml}", + [ + "Cursorless tries its best to use its knowledge of programming languages to leave you with syntactically valid code.", + "Note how it cleans up the comma here: {command:chuckArgueBlueVest.yml}", + ], + [ + "We introduced a lot of different scopes today. If you're anything like us, you've already forgotten them all.", + "The important thing to remember is that you can always say {special:help} to see a list.", + ], + { + interactive: + "As always, feel free to stick around and play with this file to practice what you've just learned. Happy coding 😊. Say {special:next} to get back home.", + documentation: + "That wraps up the Basic coding tutorial. Happy coding 😊.", + }, + ], +}; diff --git a/packages/lib-common/src/tutorials/tutorial3Visualization.ts b/packages/lib-common/src/tutorials/tutorial3Visualization.ts new file mode 100644 index 0000000000..a136554df7 --- /dev/null +++ b/packages/lib-common/src/tutorials/tutorial3Visualization.ts @@ -0,0 +1,17 @@ +import type { RawTutorialContent } from "./tutorial.types"; + +export const tutorial3Visualization: RawTutorialContent = { + id: "3-visualization", + title: "Visualization", + description: + "This tutorial will teach you how to visualize different scopes in Cursorless.", + excludeIn: ["documentation"], + steps: [ + "The {action:flash} action briefly flashes a target without changing the document: {command:flashState.yml}", + "The {action:highlight} action keeps a target highlighted so that you can refer back to it visually. Try it now: {command:highlightState.yml}", + "Unlike {action:flash}, {action:highlight} remains visible until you replace or clear it. Clear the highlight now: {command:highlightNothing.yml}", + "The scope visualizer shows every instance of a scope in the active editor. Visualize all statements by saying {visualize:state}.", + "Say {special:visualizeNothing} to hide the scope visualizer. {scopeType:state} is one of many scopes supported by Cursorless. To see all available scopes, have a look at the Scopes section below.", + "That wraps up the visualization tutorial. Feel free to experiment, then say {special:next} to return to the tutorial list.", + ], +}; diff --git a/packages/lib-common/src/tutorials/tutorials.test.ts b/packages/lib-common/src/tutorials/tutorials.test.ts new file mode 100644 index 0000000000..880211bb9b --- /dev/null +++ b/packages/lib-common/src/tutorials/tutorials.test.ts @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import { getTutorialsForContext } from "./tutorials"; + +suite("tutorials", () => { + test("filters tutorials by context", () => { + assert.deepEqual( + getTutorialsForContext("documentation").map(({ id }) => id), + ["1-introduction", "2-basic-coding"], + ); + assert.deepEqual( + getTutorialsForContext("interactive").map(({ id }) => id), + ["1-introduction", "2-basic-coding", "3-visualization"], + ); + }); + + test("resolves steps for context", () => { + const documentationTutorial = getTutorialsForContext("documentation")[0]; + const interactiveTutorial = getTutorialsForContext("interactive")[0]; + + assert.equal( + documentationTutorial.steps[1], + "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 {command:takeBlueSun.yml}", + ); + assert.equal( + documentationTutorial.steps.at(-1), + "And that wraps up unit 1 of the Cursorless tutorial! Next time, we'll write some code 🙌.", + ); + assert.equal( + interactiveTutorial.steps.at(-1), + "And that wraps up unit 1 of the Cursorless tutorial! Next time, we'll write some code 🙌.\nFeel free to keep playing with this document, then say {special:next} to continue.", + ); + }); +}); diff --git a/packages/lib-common/src/tutorials/tutorials.ts b/packages/lib-common/src/tutorials/tutorials.ts new file mode 100644 index 0000000000..c06bc298f3 --- /dev/null +++ b/packages/lib-common/src/tutorials/tutorials.ts @@ -0,0 +1,45 @@ +import type { TutorialId } from ".."; +import type { + RawTutorialContent, + ResolvedTutorialContent, + TutorialContext, +} from "./tutorial.types"; +import { tutorial1Introduction } from "./tutorial1Introduction"; +import { tutorial2BasicCoding } from "./tutorial2BasicCoding"; +import { tutorial3Visualization } from "./tutorial3Visualization"; + +export const tutorials: RawTutorialContent[] = [ + tutorial1Introduction, + tutorial2BasicCoding, + tutorial3Visualization, +]; + +export function getTutorialsForContext( + context: TutorialContext, +): ResolvedTutorialContent[] { + return tutorials + .filter((tutorial) => tutorial.excludeIn?.includes(context) !== true) + .map((tutorial) => ({ + ...tutorial, + position: parseTutorialPosition(tutorial.id), + steps: tutorial.steps.map((step) => { + const content = + typeof step === "string" || Array.isArray(step) + ? step + : step[context]; + + return Array.isArray(content) ? content.join("\n") : content; + }), + })); +} + +function parseTutorialPosition(id: TutorialId) { + const match = id.match(/^(?\d+)-.+$/u); + const position = match?.groups?.position; + + if (position == null) { + throw new Error(`Invalid tutorial fixture id: ${id}`); + } + + return Number(position); +} diff --git a/packages/lib-common/src/types/SpokenFormType.ts b/packages/lib-common/src/types/SpokenFormType.ts index 77889c73ae..be296d1086 100644 --- a/packages/lib-common/src/types/SpokenFormType.ts +++ b/packages/lib-common/src/types/SpokenFormType.ts @@ -28,7 +28,7 @@ type Connective = | "at" | "on"; -type ScopeVisualizer = +export type ScopeVisualizer = | "showScopeVisualizer" | "hideScopeVisualizer" | "removal" diff --git a/packages/lib-common/src/types/tutorial.types.ts b/packages/lib-common/src/types/tutorial.types.ts index 8865f151ca..45084ab75b 100644 --- a/packages/lib-common/src/types/tutorial.types.ts +++ b/packages/lib-common/src/types/tutorial.types.ts @@ -1,4 +1,7 @@ -export type TutorialId = "tutorial-1-basics" | "tutorial-2-basic-coding"; +export type TutorialId = + | "1-introduction" + | "2-basic-coding" + | "3-visualization"; interface BaseTutorialInfo { id: TutorialId; @@ -6,7 +9,6 @@ interface BaseTutorialInfo { } export interface TutorialInfo extends BaseTutorialInfo { - version: number; stepCount: number; currentStep: number; } @@ -65,6 +67,7 @@ export interface ActiveTutorialNoErrorsState extends ActiveTutorialState { export interface ActiveTutorialErrorsState extends ActiveTutorialState { hasErrors: true; + errorMessage: string; requiresTalonUpdate: boolean; } diff --git a/packages/lib-engine/src/api/CursorlessEngineApi.ts b/packages/lib-engine/src/api/CursorlessEngineApi.ts index dc30a5fd0c..3991474e20 100644 --- a/packages/lib-engine/src/api/CursorlessEngineApi.ts +++ b/packages/lib-engine/src/api/CursorlessEngineApi.ts @@ -9,6 +9,7 @@ import type { ReadOnlyHatMap, ScopeProvider, ScopeType, + ScopeVisualizer, SpokenForm, } from "@cursorless/lib-common"; import type { CommandRunner } from "../CommandRunner"; @@ -40,6 +41,7 @@ export interface CustomSpokenFormGenerator { commandToSpokenForm(command: CommandComplete): SpokenForm; scopeTypeToSpokenForm(scopeType: ScopeType): SpokenForm; + scopeVisualizerIdToSpokenForm(id: ScopeVisualizer): SpokenForm; actionIdToSpokenForm(actionId: ActionType): SpokenForm; graphemeToSpokenForm(grapheme: string): SpokenForm; } diff --git a/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts b/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts index 50fad298d2..2f7b206be1 100644 --- a/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts +++ b/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.test.ts @@ -31,6 +31,11 @@ suite("CustomSpokenFormGeneratorImpl", () => { id: "currentSelection", spokenForms: ["this"], }, + { + type: "scopeVisualizer", + id: "showScopeVisualizer", + spokenForms: ["inspect scopes"], + }, ], }); }, @@ -53,6 +58,13 @@ suite("CustomSpokenFormGeneratorImpl", () => { spokenForms: ["foo alabaster"], }, ); + assert.deepEqual( + generator.scopeVisualizerIdToSpokenForm("showScopeVisualizer"), + { + type: "success", + spokenForms: ["inspect scopes"], + }, + ); assert.deepEqual( generator.commandToSpokenForm({ version: LATEST_VERSION, diff --git a/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.ts b/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.ts index 80a3da55e2..8a60e9d1e0 100644 --- a/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.ts +++ b/packages/lib-engine/src/generateSpokenForm/CustomSpokenFormGeneratorImpl.ts @@ -5,6 +5,7 @@ import type { IDE, Listener, ScopeType, + ScopeVisualizer, TalonSpokenForms, } from "@cursorless/lib-common"; import type { CustomSpokenFormGenerator } from "../api/CursorlessEngineApi"; @@ -56,6 +57,13 @@ export class CustomSpokenFormGeneratorImpl implements CustomSpokenFormGenerator return this.spokenFormGenerator.processScopeType(scopeType); } + scopeVisualizerIdToSpokenForm(id: ScopeVisualizer) { + return this.spokenFormGenerator.getSpokenFormForSingleTerm( + "scopeVisualizer", + id, + ); + } + actionIdToSpokenForm(actionId: ActionType) { return this.spokenFormGenerator.getSpokenFormForSingleTerm( "action", diff --git a/packages/lib-node-common/src/FileSystemTutorialContentProvider.ts b/packages/lib-node-common/src/FileSystemTutorialContentProvider.ts index 2371a0ee62..0b54623520 100644 --- a/packages/lib-node-common/src/FileSystemTutorialContentProvider.ts +++ b/packages/lib-node-common/src/FileSystemTutorialContentProvider.ts @@ -1,7 +1,5 @@ -import { readFile, readdir } from "node:fs/promises"; import path from "node:path"; import type { - RawTutorialContent, TestCaseFixtureLegacy, TutorialContentProvider, TutorialId, @@ -15,31 +13,6 @@ export class FileSystemTutorialContentProvider implements TutorialContentProvide this.tutorialRootDir = path.join(assetsRoot, "tutorial"); } - async loadRawTutorials() { - const tutorialDirs = await readdir(this.tutorialRootDir, { - withFileTypes: true, - }); - - return await Promise.all( - tutorialDirs - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => this.loadTutorialScript(dirent.name as TutorialId)), - ); - } - - private async loadTutorialScript( - tutorialId: string, - ): Promise { - const buffer = await readFile( - path.join(this.tutorialRootDir, tutorialId, "script.json"), - ); - - return { - id: tutorialId, - ...JSON.parse(buffer.toString()), - }; - } - loadFixture( tutorialId: TutorialId, fixtureName: string, diff --git a/packages/lib-node-common/src/runRecordedTest.ts b/packages/lib-node-common/src/runRecordedTest.ts index 9a9f3d82d2..026c677843 100644 --- a/packages/lib-node-common/src/runRecordedTest.ts +++ b/packages/lib-node-common/src/runRecordedTest.ts @@ -19,6 +19,7 @@ import { clientSupportsFallback, getSnapshotForComparison, omitByDeep, + plainObjectToGeneralizedRange, Position, rangeToPlainObject, Selection, @@ -146,6 +147,16 @@ export async function runRecordedTest({ serializedMarksToTokenHats(fixture.initialState.marks, editor), ); + await Promise.all( + (fixture.initialState.highlights ?? []).map((highlight) => + spyIde.setInitialHighlightRanges( + highlight.highlightId, + editor, + highlight.ranges.map(plainObjectToGeneralizedRange), + ), + ), + ); + const initialHatTokenMap = await hatTokenMap.getReadableMap(usePrePhraseSnapshot); diff --git a/packages/lib-tutorial/src/TutorialImpl.ts b/packages/lib-tutorial/src/TutorialImpl.ts index ddfe2e1253..d78cf24b05 100644 --- a/packages/lib-tutorial/src/TutorialImpl.ts +++ b/packages/lib-tutorial/src/TutorialImpl.ts @@ -1,12 +1,12 @@ import { produce } from "immer"; import { isEqual } from "lodash-es"; import type { - CharacterRange, Disposable, + GeneralizedRange, Hats, HatTokenMap, IDE, - RawTutorialContent, + ResolvedTutorialContent, ReadOnlyHatMap, ScopeType, TextEditor, @@ -14,7 +14,11 @@ import type { TutorialId, TutorialState, } from "@cursorless/lib-common"; -import { Debouncer, Notifier } from "@cursorless/lib-common"; +import { + Debouncer, + getTutorialsForContext, + Notifier, +} from "@cursorless/lib-common"; import type { CommandRunner, CommandRunnerDecorator, @@ -40,7 +44,7 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { * if any. We store these so that we can remove them when user has gone off * the tutorial path and then restore them when they come back. */ - private highlightRanges: CharacterRange[] = []; + private highlightRanges: GeneralizedRange[] = []; /** * The current state of the tutorial, as exposed by {@link Tutorial.state}. @@ -57,11 +61,7 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { private disposables: Disposable[] = []; - /** - * The raw tutorials that are available to the user. These are the tutorials - * that are loaded from disk and have not been parsed yet. - */ - private rawTutorials!: RawTutorialContent[]; + private rawTutorials: ResolvedTutorialContent[]; constructor( private ide: IDE, @@ -73,12 +73,11 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { const debouncer = new Debouncer(() => this.checkPreconditions(), 100); const runDebouncer = () => debouncer.run(); - void (async () => { - await this.loadTutorials(); - if (this.state_.type === "loading") { - this.setState(this.getPickingTutorialState()); - } - })(); + this.rawTutorials = getTutorialsForContext("interactive"); + + if (this.state_.type === "loading") { + this.setState(this.getPickingTutorialState()); + } this.disposables.push( this.ide.onDidChangeActiveTextEditor(runDebouncer), @@ -102,8 +101,10 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { * @param scopeType The scope type that was visualized */ scopeTypeVisualized(scopeType: ScopeType | undefined): void { - if (this.state_.type === "doingTutorial") { - const currentStep = this.currentTutorial!.steps[this.state_.stepNumber]; + const state = this.state_; + + if (state.type === "doingTutorial" && !state.hasErrors) { + const currentStep = this.currentTutorial!.steps[state.stepNumber]; if ( currentStep.trigger?.type === "visualize" && isEqual(currentStep.trigger.scopeType, scopeType) @@ -113,10 +114,6 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { } } - async loadTutorials() { - this.rawTutorials = await this.contentProvider.loadRawTutorials(); - } - /** * @returns A {@link TutorialState} object to use when the user is picking a * tutorial to start. @@ -129,7 +126,6 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { tutorials: this.rawTutorials.map((rawContent) => ({ id: rawContent.id, title: rawContent.title, - version: rawContent.version, stepCount: rawContent.steps.length, currentStep: tutorialProgress[rawContent.id]?.currentStep ?? 0, })), @@ -219,8 +215,10 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { } documentationOpened() { - if (this.state_.type === "doingTutorial") { - const currentStep = this.currentTutorial!.steps[this.state_.stepNumber]; + const state = this.state_; + + if (state.type === "doingTutorial" && !state.hasErrors) { + const currentStep = this.currentTutorial!.steps[state.stepNumber]; if (currentStep.trigger?.type === "help") { void this.next(); } @@ -300,7 +298,6 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { produce(this.ide.keyValueStore.get("tutorialProgress"), (draft) => { draft[state.id] = { currentStep: state.stepNumber, - version: this.currentTutorial!.version, }; }), ); @@ -347,22 +344,31 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { } private async checkPreconditions() { - if (this.state_.type === "doingTutorial") { - const currentStep = this.currentTutorial!.steps[this.state_.stepNumber]; - - const preConditionsMet = await arePreconditionsMet( - this.ide.activeTextEditor, - this.editor, - this.hatTokenMap, - currentStep, - ); - if (preConditionsMet !== this.state_.preConditionsMet) { - this.setState({ - ...this.state_, - preConditionsMet, - }); - await this.ensureHighlights(); - } + const state = this.state_; + + if (state.type !== "doingTutorial" || state.hasErrors) { + return; + } + + const currentStep = this.currentTutorial!.steps[state.stepNumber]; + + const preConditionsMet = await arePreconditionsMet( + this.ide.activeTextEditor, + this.editor, + this.hatTokenMap, + currentStep, + ); + + if (this.state_ !== state) { + return; + } + + if (preConditionsMet !== state.preConditionsMet) { + this.setState({ + ...state, + preConditionsMet, + }); + await this.ensureHighlights(); } } } diff --git a/packages/lib-tutorial/src/TutorialStepParser.ts b/packages/lib-tutorial/src/TutorialStepParser.ts index c6d9eb9c65..219ecd0eda 100644 --- a/packages/lib-tutorial/src/TutorialStepParser.ts +++ b/packages/lib-tutorial/src/TutorialStepParser.ts @@ -8,7 +8,6 @@ import type { import type { CustomSpokenFormGenerator } from "@cursorless/lib-engine"; import { parseScopeType } from "@cursorless/lib-engine"; import { getScopeTypeSpokenFormStrict } from "./getScopeTypeSpokenFormStrict"; -import { specialTerms } from "./specialTerms"; import { ActionComponentParser } from "./stepComponentParsers/ActionComponentParser"; import { CursorlessCommandComponentParser } from "./stepComponentParsers/CursorlessCommandComponentParser"; import { GraphemeComponentParser } from "./stepComponentParsers/GraphemeComponentParser"; @@ -59,18 +58,11 @@ export class TutorialStepParser { this.componentParsers = { command: (arg) => cursorlessCommandParser.parse(arg), - special: (arg) => Promise.resolve(parseSpecialComponent(arg)), + special: (arg) => + Promise.resolve(parseSpecialComponent(customSpokenFormGenerator, arg)), action: (arg) => actionParser.parse(arg), grapheme: (arg) => graphemeParser.parse(arg), - term: (arg) => - Promise.resolve({ - content: { - type: "term", - value: specialTerms[arg as keyof typeof specialTerms], - }, - }), - scopeType: (arg) => Promise.resolve({ content: { diff --git a/packages/lib-tutorial/src/loadTutorial.ts b/packages/lib-tutorial/src/loadTutorial.ts index 0893556aeb..e3fb2a6160 100644 --- a/packages/lib-tutorial/src/loadTutorial.ts +++ b/packages/lib-tutorial/src/loadTutorial.ts @@ -1,11 +1,12 @@ import type { Hats, KeyValueStore, - RawTutorialContent, + ResolvedTutorialContent, TutorialContentProvider, TutorialId, TutorialState, } from "@cursorless/lib-common"; +import { getErrorMessage } from "@cursorless/lib-common"; import type { CustomSpokenFormGenerator } from "@cursorless/lib-engine"; import { TutorialError } from "./TutorialError"; import { TutorialStepParser } from "./TutorialStepParser"; @@ -15,7 +16,7 @@ export async function loadTutorial( contentProvider: TutorialContentProvider, tutorialId: TutorialId, customSpokenFormGenerator: CustomSpokenFormGenerator, - rawContent: RawTutorialContent, + rawContent: ResolvedTutorialContent, keyValueStore: KeyValueStore, hats: Hats, ) { @@ -32,8 +33,22 @@ export async function loadTutorial( try { tutorialContent = { title: rawContent.title, - version: rawContent.version, - steps: await Promise.all(rawContent.steps.map(parser.parseTutorialStep)), + steps: await Promise.all( + rawContent.steps.map(async (step, index) => { + try { + return await parser.parseTutorialStep(step); + } catch (error) { + if (error instanceof TutorialError) { + throw error; + } + + throw new Error( + `Failed to parse tutorial "${tutorialId}" step ${index + 1}: ${getErrorMessage(error)}`, + { cause: error }, + ); + } + }), + ), }; let stepNumber = @@ -57,7 +72,6 @@ export async function loadTutorial( tutorialContent = { title: rawContent.title, steps: [], - version: rawContent.version, }; state = { type: "doingTutorial", @@ -66,6 +80,7 @@ export async function loadTutorial( stepNumber: 0, title: tutorialContent.title, preConditionsMet: true, + errorMessage: getErrorMessage(error), requiresTalonUpdate: error instanceof TutorialError && error.requiresTalonUpdate, }; diff --git a/packages/lib-tutorial/src/setupStep.ts b/packages/lib-tutorial/src/setupStep.ts index 61c4d8b319..f40b298e5d 100644 --- a/packages/lib-tutorial/src/setupStep.ts +++ b/packages/lib-tutorial/src/setupStep.ts @@ -1,5 +1,5 @@ import type { - CharacterRange, + GeneralizedRange, HatTokenMap, IDE, TestCaseSnapshot, @@ -7,6 +7,7 @@ import type { TutorialState, } from "@cursorless/lib-common"; import { + plainObjectToGeneralizedRange, plainObjectToRange, plainObjectToSelection, serializedMarksToTokenHats, @@ -16,7 +17,7 @@ import type { TutorialContent } from "./types/tutorial.types"; interface HighlightedEditor { editor: TextEditor | undefined; - highlightRanges: CharacterRange[]; + highlightRanges: GeneralizedRange[]; } /** @@ -114,9 +115,14 @@ async function applySnapshot( return { editor, - highlightRanges: Object.values(snapshot.marks ?? {}).map((range) => - toCharacterRange(plainObjectToRange(range)), - ), + highlightRanges: [ + ...Object.values(snapshot.marks ?? {}).map((range) => + toCharacterRange(plainObjectToRange(range)), + ), + ...(snapshot.highlights ?? []).flatMap((highlight) => + highlight.ranges.map(plainObjectToGeneralizedRange), + ), + ], }; } catch (error) { if (retry) { diff --git a/packages/lib-tutorial/src/specialTerms.ts b/packages/lib-tutorial/src/specialTerms.ts deleted file mode 100644 index eb00beba2c..0000000000 --- a/packages/lib-tutorial/src/specialTerms.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Special terms used in spoken forms that are referred to in the tutorial, but - * don't constitute a complete command. - */ -export const specialTerms = { - visualize: "visualize", -}; diff --git a/packages/lib-tutorial/src/stepComponentParsers/parseSpecialComponent.ts b/packages/lib-tutorial/src/stepComponentParsers/parseSpecialComponent.ts index fb063c70f4..8347971a45 100644 --- a/packages/lib-tutorial/src/stepComponentParsers/parseSpecialComponent.ts +++ b/packages/lib-tutorial/src/stepComponentParsers/parseSpecialComponent.ts @@ -1,38 +1,53 @@ +import type { CustomSpokenFormGenerator } from "@cursorless/lib-engine"; +import { getSpokenFormStrict } from "../getSpokenFormStrict"; import type { StepComponent } from "../types/StepComponent"; import type { TutorialStepTrigger } from "../types/TutorialStepTrigger"; const SPECIAL_COMMANDS = { help: "cursorless help", next: "tutorial next", - visualizeNothing: "visualize nothing", }; /** * Parses components of the form `{special:help}`. These are special commands * that don't correspond to any cursorless command. */ -export function parseSpecialComponent(arg: string): StepComponent { +export function parseSpecialComponent( + customSpokenFormGenerator: CustomSpokenFormGenerator, + arg: string, +): StepComponent { let trigger: TutorialStepTrigger | undefined = undefined; + let value: string; switch (arg) { case "help": trigger = { type: "help", }; + value = SPECIAL_COMMANDS.help; + break; + case "next": + value = SPECIAL_COMMANDS.next; break; case "visualizeNothing": trigger = { type: "visualize", scopeType: undefined, }; + value = getSpokenFormStrict( + customSpokenFormGenerator.scopeVisualizerIdToSpokenForm( + "hideScopeVisualizer", + ), + ); break; - // No default + default: + throw new Error(`Unknown special command: ${arg}`); } return { content: { type: "command", - value: SPECIAL_COMMANDS[arg as keyof typeof SPECIAL_COMMANDS], + value, }, trigger, }; diff --git a/packages/lib-tutorial/src/stepComponentParsers/parseVisualizeComponent.ts b/packages/lib-tutorial/src/stepComponentParsers/parseVisualizeComponent.ts index ac711f1dad..cb8aa862e4 100644 --- a/packages/lib-tutorial/src/stepComponentParsers/parseVisualizeComponent.ts +++ b/packages/lib-tutorial/src/stepComponentParsers/parseVisualizeComponent.ts @@ -1,7 +1,7 @@ import type { CustomSpokenFormGenerator } from "@cursorless/lib-engine"; import { parseScopeType } from "@cursorless/lib-engine"; import { getScopeTypeSpokenFormStrict } from "../getScopeTypeSpokenFormStrict"; -import { specialTerms } from "../specialTerms"; +import { getSpokenFormStrict } from "../getSpokenFormStrict"; import type { StepComponent } from "../types/StepComponent"; /** @@ -14,11 +14,16 @@ export function parseVisualizeComponent( arg: string, ): StepComponent { const scopeType = parseScopeType(arg); + const visualizeSpokenForm = getSpokenFormStrict( + customSpokenFormGenerator.scopeVisualizerIdToSpokenForm( + "showScopeVisualizer", + ), + ); return { content: { type: "command", - value: `${specialTerms.visualize} ${getScopeTypeSpokenFormStrict(customSpokenFormGenerator, scopeType)}`, + value: `${visualizeSpokenForm} ${getScopeTypeSpokenFormStrict(customSpokenFormGenerator, scopeType)}`, }, trigger: { type: "visualize", diff --git a/packages/lib-tutorial/src/types/tutorial.types.ts b/packages/lib-tutorial/src/types/tutorial.types.ts index 1334737d5f..b23bed6645 100644 --- a/packages/lib-tutorial/src/types/tutorial.types.ts +++ b/packages/lib-tutorial/src/types/tutorial.types.ts @@ -14,11 +14,6 @@ export interface TutorialContent { */ title: string; - /** - * The version of the tutorial - */ - version: number; - /** * The steps of the current tutorial */ diff --git a/packages/lib-vscode-tutorial-webview/src/App.tsx b/packages/lib-vscode-tutorial-webview/src/App.tsx index 38ea95bd49..77623e2a96 100644 --- a/packages/lib-vscode-tutorial-webview/src/App.tsx +++ b/packages/lib-vscode-tutorial-webview/src/App.tsx @@ -66,6 +66,7 @@ export function App({ vscode }: Props) { return (

Error

+

{state.errorMessage}

{state.requiresTalonUpdate && (

Please{" "} diff --git a/packages/test-vscode-e2e/src/suite/tutorial/tutorial.vscode.test.ts b/packages/test-vscode-e2e/src/suite/tutorial/tutorial.vscode.test.ts index f540d9d820..b863859549 100644 --- a/packages/test-vscode-e2e/src/suite/tutorial/tutorial.vscode.test.ts +++ b/packages/test-vscode-e2e/src/suite/tutorial/tutorial.vscode.test.ts @@ -30,7 +30,7 @@ suite("tutorial", function () { ); }); -const BASICS_TUTORIAL_ID = "tutorial-1-basics"; +const INTRODUCTION_TUTORIAL_ID = "1-introduction"; async function runBasicTutorialTest(spyIde: SpyIDE) { const { hatTokenMap, takeSnapshot, getTutorialWebviewEventLog, vscodeApi } = @@ -47,7 +47,7 @@ async function runBasicTutorialTest(spyIde: SpyIDE) { const tutorialDirectory = path.join( getRecordedTestsDirPath(), "tutorial", - BASICS_TUTORIAL_ID, + INTRODUCTION_TUTORIAL_ID, ); const fixtures = await Promise.all( @@ -79,7 +79,7 @@ async function runBasicTutorialTest(spyIde: SpyIDE) { // Test starting tutorial await commands.executeCommand( "cursorless.tutorial.start", - BASICS_TUTORIAL_ID, + INTRODUCTION_TUTORIAL_ID, ); await checkStepSetup(fixtures[0]); @@ -100,7 +100,7 @@ async function runBasicTutorialTest(spyIde: SpyIDE) { data: { type: "doingTutorial", hasErrors: false, - id: "tutorial-1-basics", + id: "1-introduction", stepNumber: 0, stepContent: [ [ @@ -184,17 +184,21 @@ async function runBasicTutorialTest(spyIde: SpyIDE) { type: "pickingTutorial", tutorials: [ { - id: "tutorial-1-basics", + id: "1-introduction", title: "Introduction", - version: 0, stepCount: 11, currentStep: 1, }, { - id: "tutorial-2-basic-coding", + id: "2-basic-coding", title: "Basic coding", - version: 0, - stepCount: 11, + stepCount: 9, + currentStep: 0, + }, + { + id: "3-visualization", + title: "Visualization", + stepCount: 6, currentStep: 0, }, ], diff --git a/packages/tool-meta-updater/src/metaUpdater.ts b/packages/tool-meta-updater/src/metaUpdater.ts index 481de86683..14aa90484b 100644 --- a/packages/tool-meta-updater/src/metaUpdater.ts +++ b/packages/tool-meta-updater/src/metaUpdater.ts @@ -9,6 +9,7 @@ import type { ScopeTypeType } from "@cursorless/lib-common"; import { actionReferenceGroups, actionReferences, + getTutorialsForContext, graphemeDefaultSpokenForms, languageReferences, modifierReferenceGroups, @@ -43,7 +44,6 @@ import { updateSpokenForms } from "./updateSpokenForms"; import { updateTSConfig } from "./updateTSConfig"; import { updateTSConfigBase } from "./updateTSConfigBase"; import { - parseTutorialId, updateTutorialFixtureData, updateTutorialMdx, updateTutorialReadmeMdx, @@ -76,7 +76,7 @@ export const updater = async (workspaceDir: string) => { const tutorialContentProvider = new FileSystemTutorialContentProvider( getRecordedTestsDirPath(), ); - const tutorials = await tutorialContentProvider.loadRawTutorials(); + const tutorials = getTutorialsForContext("documentation"); return createUpdateOptions({ files: { @@ -101,15 +101,19 @@ export const updater = async (workspaceDir: string) => { tutorials, ), ...Object.fromEntries( - tutorials.flatMap((tutorial) => { - const { shortId } = parseTutorialId(tutorial.id); + tutorials.flatMap((tutorial, index) => { return [ [ - `${userDir}/tutorial/${shortId}.mdx`, - updateTutorialMdx.bind(null, tutorial, tutorialContentProvider), + `${userDir}/tutorial/${tutorial.id}.mdx`, + updateTutorialMdx.bind( + null, + tutorial, + tutorials[index + 1], + tutorialContentProvider, + ), ], [ - `${userDir}/tutorial/fixtures/${shortId}.json`, + `${userDir}/tutorial/fixtures/${tutorial.id}.json`, updateTutorialFixtureData.bind( null, tutorial, diff --git a/packages/tool-meta-updater/src/renderRecordedTestVisualizer.ts b/packages/tool-meta-updater/src/renderRecordedTestVisualizer.ts index ae814ff413..589dac7ea1 100644 --- a/packages/tool-meta-updater/src/renderRecordedTestVisualizer.ts +++ b/packages/tool-meta-updater/src/renderRecordedTestVisualizer.ts @@ -6,6 +6,7 @@ import { getRecordedTestsDirPath, loadFixture, } from "@cursorless/lib-node-common"; +import { isAppWebDocs } from "./util/isManifest"; export const recordedTestVisualizerImport = `import { RecordedTestVisualizer, RecordedTestVisualizerOptions, RecordedTestVisualizerProvider } from "@site/src/docs/components/RecordedTestVisualizer";`; @@ -20,7 +21,7 @@ export function updateRecordedTestFixtureData( _actual: unknown, options: FormatPluginFnOptions, ): Promise { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return Promise.resolve(null); } if (recordedTestPaths.length === 0) { diff --git a/packages/tool-meta-updater/src/updateGraphemeDefaultSpokenFormsMd.ts b/packages/tool-meta-updater/src/updateGraphemeDefaultSpokenFormsMd.ts index e5b6193696..2e992343c3 100644 --- a/packages/tool-meta-updater/src/updateGraphemeDefaultSpokenFormsMd.ts +++ b/packages/tool-meta-updater/src/updateGraphemeDefaultSpokenFormsMd.ts @@ -1,4 +1,5 @@ import type { FormatPluginFnOptions } from "@pnpm/meta-updater"; +import { isAppWebDocs } from "./util/isManifest"; const HEADERS = ["Character", "Default spoken form"] as const; @@ -7,7 +8,7 @@ export function updateGraphemeDefaultSpokenFormsMd( actual: string | null, options: FormatPluginFnOptions, ): string | null { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } diff --git a/packages/tool-meta-updater/src/updateLanguages.ts b/packages/tool-meta-updater/src/updateLanguages.ts index b7d3f47faf..4e852f3526 100644 --- a/packages/tool-meta-updater/src/updateLanguages.ts +++ b/packages/tool-meta-updater/src/updateLanguages.ts @@ -6,13 +6,14 @@ import { scopeVisualizerImport, } from "./renderScopeVisualizerMdx"; import type { ScopeFixtureGroup } from "./scopeFixtureGroups"; +import { isAppWebDocs } from "./util/isManifest"; export function updateLanguagesReadmeMd( languageIds: LanguageId[], _actual: string | null, options: FormatPluginFnOptions, ): string | null { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } @@ -37,7 +38,7 @@ export function updateLanguageMdx( actual: string | null, options: FormatPluginFnOptions, ): string | null { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } diff --git a/packages/tool-meta-updater/src/updatePackageJson.ts b/packages/tool-meta-updater/src/updatePackageJson.ts index 0968cd62ee..e009a5841e 100644 --- a/packages/tool-meta-updater/src/updatePackageJson.ts +++ b/packages/tool-meta-updater/src/updatePackageJson.ts @@ -4,6 +4,12 @@ import type { PackageJson } from "type-fest"; import { omitByDeep } from "@cursorless/lib-common"; import type { Context } from "./Context"; import { getCursorlessVscodeFields } from "./util/getCursorlessVscodeFields"; +import { + isAppNeovim, + isAppVscode, + isAppWebDocs, + isTestRunner, +} from "./util/isManifest"; const LIB_ENTRY_POINT = "./src/index.ts"; @@ -30,19 +36,19 @@ export function updatePackageJson( const isRoot = packageDir === workspaceDir; const isLib = !isRoot && !input.private; - const isCursorlessVscode = input.name === "@cursorless/app-vscode"; - const isCursorlessNeovim = input.name === "@cursorless/app-neovim"; - const isCursorlessOrgDocs = input.name === "@cursorless/app-web-docs"; - const isCursorlessTestRunner = input.name === "@cursorless/test-runner"; + const isCursorlessVscode = isAppVscode(options); + const isCursorlessNeovim = isAppNeovim(options); + const isCursorlessOrgDocs = isAppWebDocs(options); + const isCursorlessTestRunner = isTestRunner(options); if (input.description == null || input.description === "") { throw new Error(`No description found in ${packageDir}/package.json`); } const name = - isRoot || input.name?.startsWith("@cursorless/") - ? input.name - : `@cursorless/${input.name}`; + isRoot || options.manifest.name?.startsWith("@cursorless/") + ? options.manifest.name + : `@cursorless/${options.manifest.name}`; if (isLib) { const exports = diff --git a/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts b/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts index 6dba4ea2d4..519d2f4199 100644 --- a/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts +++ b/packages/tool-meta-updater/src/updatePairedDelimitersMd.ts @@ -1,6 +1,7 @@ import type { FormatPluginFnOptions } from "@pnpm/meta-updater"; import type { PairedDelimiterReference } from "@cursorless/lib-common"; import { DISABLED_BY_DEFAULT } from "./util/constants"; +import { isAppWebDocs } from "./util/isManifest"; const HEADERS = [ "Default spoken form", @@ -16,7 +17,7 @@ export function updatePairedDelimitersMd( actual: string | null, options: FormatPluginFnOptions, ): string | null { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } diff --git a/packages/tool-meta-updater/src/updateReferences.ts b/packages/tool-meta-updater/src/updateReferences.ts index b0553a0a53..b6f7351759 100644 --- a/packages/tool-meta-updater/src/updateReferences.ts +++ b/packages/tool-meta-updater/src/updateReferences.ts @@ -15,6 +15,7 @@ import { cleanId } from "./util/cleanId"; import { DISABLED_BY_DEFAULT } from "./util/constants"; import { formatVariables } from "./util/formatVariables"; import { injectSpokenForm } from "./util/injectSpokenForm"; +import { isAppWebDocs } from "./util/isManifest"; export function updateReferenceReadmeMd( title: string, @@ -23,7 +24,7 @@ export function updateReferenceReadmeMd( actual: string | null, options: FormatPluginFnOptions, ): string | null { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } @@ -79,7 +80,7 @@ export async function updateReferenceMdx( actual: string | null, options: FormatPluginFnOptions, ): Promise { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } diff --git a/packages/tool-meta-updater/src/updateScopeFixtureData.ts b/packages/tool-meta-updater/src/updateScopeFixtureData.ts index 828ad3d83a..d3505c4d58 100644 --- a/packages/tool-meta-updater/src/updateScopeFixtureData.ts +++ b/packages/tool-meta-updater/src/updateScopeFixtureData.ts @@ -6,6 +6,7 @@ import type { } from "@cursorless/lib-common"; import type { ScopeTestPath } from "@cursorless/lib-node-common"; import type { ScopeFixtureGroup } from "./scopeFixtureGroups"; +import { isAppWebDocs } from "./util/isManifest"; type FacetValue = ScopeSupportFacet | PlaintextScopeSupportFacet; @@ -33,7 +34,7 @@ export function updateScopeFixtureData( _actual: unknown, options: FormatPluginFnOptions, ): ScopeFixtureData[] | null { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } diff --git a/packages/tool-meta-updater/src/updateTutorialMdx.ts b/packages/tool-meta-updater/src/updateTutorialMdx.ts index ff797a1be7..eedcc43cc6 100644 --- a/packages/tool-meta-updater/src/updateTutorialMdx.ts +++ b/packages/tool-meta-updater/src/updateTutorialMdx.ts @@ -1,12 +1,13 @@ import type { FormatPluginFnOptions } from "@pnpm/meta-updater"; import type { - RawTutorialContent, + ResolvedTutorialContent, TestCaseFixtureLegacy, TutorialContentProvider, TutorialId, } from "@cursorless/lib-common"; import { graphemeDefaultSpokenForms } from "@cursorless/lib-common"; import { recordedTestVisualizerImport } from "./renderRecordedTestVisualizer"; +import { isAppWebDocs } from "./util/isManifest"; const componentRegex = /\{(\w+):([^}]+)\}/gu; @@ -28,19 +29,14 @@ interface TutorialRecordedTest { } export function updateTutorialReadmeMdx( - tutorials: RawTutorialContent[], + tutorials: ResolvedTutorialContent[], _actual: string | null, options: FormatPluginFnOptions, ): string | null { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } - const enrichedTutorials = tutorials.map((tutorial) => ({ - ...tutorial, - ...parseTutorialId(tutorial.id), - })); - return [ "# Tutorial", "", @@ -48,12 +44,12 @@ export function updateTutorialReadmeMdx( "", 'To follow along interactively, focus VS Code and say `"cursorless tutorial"`.', "", - ...enrichedTutorials + ...tutorials .toSorted((a, b) => a.position - b.position) .flatMap((tutorial) => [ `## ${tutorial.position}. ${tutorial.title}`, "", - `[Start ${tutorial.title}](./${tutorial.shortId}.mdx)`, + `[Start ${tutorial.title}](./${tutorial.id}.mdx) - ${tutorial.description}`, "", ]), "## Use the interactive tutorial", @@ -83,22 +79,23 @@ export function updateTutorialReadmeMdx( } export async function updateTutorialMdx( - tutorial: RawTutorialContent, + tutorial: ResolvedTutorialContent, + nextTutorial: ResolvedTutorialContent | undefined, contentProvider: TutorialContentProvider, _actual: string | null, options: FormatPluginFnOptions, ): Promise { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } - const { position, shortId } = parseTutorialId(tutorial.id); - const lines = [ recordedTestVisualizerImport, - `import recordedTests from "./fixtures/${shortId}.json";`, + `import recordedTests from "./fixtures/${tutorial.id}.json";`, "", - `# ${position}. ${tutorial.title}`, + `# ${tutorial.position}. ${tutorial.title}`, + "", + tutorial.description, "", "", "", @@ -113,28 +110,29 @@ export async function updateTutorialMdx( for (const fixtureName of step.fixtureNames) { lines.push(``, ""); } - if (step.fixtureNames.length === 0) { - lines.push( - ":::note", - - 'This step has no visualisation. To get the full experience try the interactive tutorial in VS Code by saying `"cursorless tutorial"`.', - ":::", - "", - ); - } } lines.push("", ""); + + if (nextTutorial != null) { + lines.push( + `## Next tutorial: ${nextTutorial.position}. ${nextTutorial.title}`, + "", + `[Continue to ${nextTutorial.title}](./${nextTutorial.id}.mdx) - ${nextTutorial.description}`, + "", + ); + } + return lines.join("\n"); } export async function updateTutorialFixtureData( - tutorial: RawTutorialContent, + tutorial: ResolvedTutorialContent, contentProvider: TutorialContentProvider, _actual: unknown, options: FormatPluginFnOptions, ): Promise { - if (options.manifest.name !== "@cursorless/app-web-docs") { + if (!isAppWebDocs(options)) { return null; } @@ -162,7 +160,7 @@ export async function updateTutorialFixtureData( async function renderStep( contentProvider: TutorialContentProvider, - tutorialId: RawTutorialContent["id"], + tutorialId: TutorialId, rawStep: string, ): Promise { const fixtureNames: string[] = []; @@ -205,7 +203,7 @@ async function renderStep( } function getTutorialFixtureName( - tutorialId: RawTutorialContent["id"], + tutorialId: TutorialId, argument: string, ): string { return `tutorial/${tutorialId}/${argument.replace(/\.ya?ml$/u, "")}`; @@ -235,18 +233,3 @@ function formatSpokenForm(spokenForm: string): string { function formatTerm(term: string): string { return `"${term}"`; } - -export function parseTutorialId(id: TutorialId) { - const match = id.match(/^tutorial-(?(?\d+)-.+)$/u); - const position = match?.groups?.number; - const shortId = match?.groups?.identifier; - - if (position == null || shortId == null) { - throw new Error(`Invalid tutorial fixture id: ${id}`); - } - - return { - position: Number(position), - shortId, - }; -} diff --git a/packages/tool-meta-updater/src/util/isManifest.ts b/packages/tool-meta-updater/src/util/isManifest.ts new file mode 100644 index 0000000000..830715d408 --- /dev/null +++ b/packages/tool-meta-updater/src/util/isManifest.ts @@ -0,0 +1,17 @@ +import type { FormatPluginFnOptions } from "@pnpm/meta-updater"; + +export function isAppWebDocs(options: FormatPluginFnOptions): boolean { + return options.manifest.name === "@cursorless/app-web-docs"; +} + +export function isAppVscode(options: FormatPluginFnOptions): boolean { + return options.manifest.name === "@cursorless/app-vscode"; +} + +export function isAppNeovim(options: FormatPluginFnOptions): boolean { + return options.manifest.name === "@cursorless/app-neovim"; +} + +export function isTestRunner(options: FormatPluginFnOptions): boolean { + return options.manifest.name === "@cursorless/test-runner"; +} diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/changeSit.yml b/resources/fixtures/recorded/tutorial/1-introduction/changeSit.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/changeSit.yml rename to resources/fixtures/recorded/tutorial/1-introduction/changeSit.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/chuckLineOdd.yml b/resources/fixtures/recorded/tutorial/1-introduction/chuckLineOdd.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/chuckLineOdd.yml rename to resources/fixtures/recorded/tutorial/1-introduction/chuckLineOdd.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/chuckTrap.yml b/resources/fixtures/recorded/tutorial/1-introduction/chuckTrap.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/chuckTrap.yml rename to resources/fixtures/recorded/tutorial/1-introduction/chuckTrap.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/postAir.yml b/resources/fixtures/recorded/tutorial/1-introduction/postAir.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/postAir.yml rename to resources/fixtures/recorded/tutorial/1-introduction/postAir.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/preUrge.yml b/resources/fixtures/recorded/tutorial/1-introduction/preUrge.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/preUrge.yml rename to resources/fixtures/recorded/tutorial/1-introduction/preUrge.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/takeBlueSun.yml b/resources/fixtures/recorded/tutorial/1-introduction/takeBlueSun.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/takeBlueSun.yml rename to resources/fixtures/recorded/tutorial/1-introduction/takeBlueSun.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/takeCap.yml b/resources/fixtures/recorded/tutorial/1-introduction/takeCap.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/takeCap.yml rename to resources/fixtures/recorded/tutorial/1-introduction/takeCap.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/takeHarpPastDrum.yml b/resources/fixtures/recorded/tutorial/1-introduction/takeHarpPastDrum.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/takeHarpPastDrum.yml rename to resources/fixtures/recorded/tutorial/1-introduction/takeHarpPastDrum.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/takeLine.yml b/resources/fixtures/recorded/tutorial/1-introduction/takeLine.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/takeLine.yml rename to resources/fixtures/recorded/tutorial/1-introduction/takeLine.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-1-basics/takeNearAndSun.yml b/resources/fixtures/recorded/tutorial/1-introduction/takeNearAndSun.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-1-basics/takeNearAndSun.yml rename to resources/fixtures/recorded/tutorial/1-introduction/takeNearAndSun.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/bringBlueCapToValueRisk.yml b/resources/fixtures/recorded/tutorial/2-basic-coding/bringBlueCapToValueRisk.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/bringBlueCapToValueRisk.yml rename to resources/fixtures/recorded/tutorial/2-basic-coding/bringBlueCapToValueRisk.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/bringStateUrge.yml b/resources/fixtures/recorded/tutorial/2-basic-coding/bringStateUrge.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/bringStateUrge.yml rename to resources/fixtures/recorded/tutorial/2-basic-coding/bringStateUrge.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/chuckArgueBlueVest.yml b/resources/fixtures/recorded/tutorial/2-basic-coding/chuckArgueBlueVest.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/chuckArgueBlueVest.yml rename to resources/fixtures/recorded/tutorial/2-basic-coding/chuckArgueBlueVest.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/cloneStateInk.yml b/resources/fixtures/recorded/tutorial/2-basic-coding/cloneStateInk.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/cloneStateInk.yml rename to resources/fixtures/recorded/tutorial/2-basic-coding/cloneStateInk.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/dedentThis.yml b/resources/fixtures/recorded/tutorial/2-basic-coding/dedentThis.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/dedentThis.yml rename to resources/fixtures/recorded/tutorial/2-basic-coding/dedentThis.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/pourUrge.yml b/resources/fixtures/recorded/tutorial/2-basic-coding/pourUrge.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/pourUrge.yml rename to resources/fixtures/recorded/tutorial/2-basic-coding/pourUrge.yml diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/swapStringAirWithWhale.yml b/resources/fixtures/recorded/tutorial/2-basic-coding/swapStringAirWithWhale.yml similarity index 100% rename from resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/swapStringAirWithWhale.yml rename to resources/fixtures/recorded/tutorial/2-basic-coding/swapStringAirWithWhale.yml diff --git a/resources/fixtures/recorded/tutorial/3-visualization/flashState.yml b/resources/fixtures/recorded/tutorial/3-visualization/flashState.yml new file mode 100644 index 0000000000..caffafeb04 --- /dev/null +++ b/resources/fixtures/recorded/tutorial/3-visualization/flashState.yml @@ -0,0 +1,30 @@ +languageId: typescript +command: + version: 7 + spokenForm: flash state + action: + name: flashTargets + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: false +initialState: + documentContents: const value = 123; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: const value = 123; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - type: ScopeTypeTarget + contentRange: + start: {line: 0, character: 0} + end: {line: 0, character: 18} + isReversed: false + hasExplicitRange: true diff --git a/resources/fixtures/recorded/tutorial/3-visualization/highlightNothing.yml b/resources/fixtures/recorded/tutorial/3-visualization/highlightNothing.yml new file mode 100644 index 0000000000..eb605690fd --- /dev/null +++ b/resources/fixtures/recorded/tutorial/3-visualization/highlightNothing.yml @@ -0,0 +1,30 @@ +languageId: typescript +command: + version: 7 + spokenForm: highlight nothing + action: + name: highlight + target: + type: primitive + mark: {type: nothing} + usePrePhraseSnapshot: true +initialState: + documentContents: const value = 123; + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + marks: {} + highlights: + - ranges: + - type: character + start: {line: 0, character: 0} + end: {line: 0, character: 18} +finalState: + documentContents: const value = 123; + selections: + - anchor: {line: 0, character: 18} + active: {line: 0, character: 18} + thatMark: [] +ide: + highlights: + - ranges: [] diff --git a/resources/fixtures/recorded/tutorial/3-visualization/highlightState.yml b/resources/fixtures/recorded/tutorial/3-visualization/highlightState.yml new file mode 100644 index 0000000000..c5e4ef8321 --- /dev/null +++ b/resources/fixtures/recorded/tutorial/3-visualization/highlightState.yml @@ -0,0 +1,36 @@ +languageId: typescript +command: + version: 7 + spokenForm: highlight state + action: + name: highlight + target: + type: primitive + modifiers: + - type: containingScope + scopeType: {type: statement} + usePrePhraseSnapshot: false +initialState: + documentContents: const value = 123; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: const value = 123; + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - type: ScopeTypeTarget + contentRange: + start: {line: 0, character: 0} + end: {line: 0, character: 18} + isReversed: false + hasExplicitRange: true +ide: + highlights: + - ranges: + - type: character + start: {line: 0, character: 0} + end: {line: 0, character: 18} diff --git a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/script.json b/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/script.json deleted file mode 100644 index f74eeb45eb..0000000000 --- a/resources/fixtures/recorded/tutorial/tutorial-2-basic-coding/script.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "title": "Basic coding", - "version": 0, - "steps": [ - "When editing code, we often think in terms of statements, functions, etc. Let's clone a statement: {command:cloneStateInk.yml}", - "{scopeType:state} is one of many scopes supported by Cursorless. To see all available scopes, have a look at the Scopes section below, and use the {term:visualize} command to see them live: {visualize:funk}", - "Say {special:visualizeNothing} to hide the visualization.", - "Cursorless tries its best to keep your commands short.\nIn the following command, we just say {scopeType:string} once, but Cursorless infers that both targets are strings: {command:swapStringAirWithWhale.yml}", - "Great. Let's learn a new action. The {action:pour} action lets you start editing a new line below any line on your screen: {command:pourUrge.yml}", - "Now let's try applying a Cursorless action to the current line: {command:dedentThis.yml}", - "Code reuse is a fact of life as a programmer. Cursorless makes this easy with the {action:bring} command: {command:bringStateUrge.yml}", - "{action:bring} also works with two targets just like {action:swap}: {command:bringBlueCapToValueRisk.yml}", - "Cursorless tries its best to use its knowledge of programming languages to leave you with syntactically valid code.\nNote how it cleans up the comma here: {command:chuckArgueBlueVest.yml}", - "We introduced a lot of different scopes today. If you're anything like us, you've already forgotten them all.\nThe important thing to remember is that you can always say {special:help} to see a list.", - "As always, feel free to stick around and play with this file to practice what you've just learned. Happy coding 😊. Say {special:next} to get back home." - ] -}