Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cursorless tutorial ced #2131

Closed
Closed
Changes from 1 commit
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
466fe64
Initial tutorial work
pokey Dec 7, 2023
617ea7c
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Dec 7, 2023
ef66688
Update spoken forms
pokey Dec 7, 2023
ae958d3
Upgrade commands
pokey Dec 7, 2023
1b4cae3
Fix extension tests
pokey Dec 7, 2023
bb303aa
whoops
pokey Dec 7, 2023
6ecaf87
Merge branch 'main' into cursorless-tutorial
pokey Dec 12, 2023
016e4d8
basic communication between talon and the extension for the tutorial
Dec 13, 2023
f761306
initial way to populate a window even if not really the right way
Dec 13, 2023
d4b8aa6
use the injected ide
Dec 19, 2023
9673f52
remove dependencies unneeded now
Dec 19, 2023
8b95e02
moved tutorial to a class
Dec 20, 2023
b0e2f8c
Merge remote-tracking branch 'upstream/main' into cursorless-tutorial…
Dec 21, 2023
25be0ca
start parsing things from the extension side
Jan 2, 2024
a7c7fa2
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 2, 2024
203bdd2
refactor and parse literalStep
Jan 4, 2024
1bbfe44
finished converting all the spoken forms
Jan 4, 2024
f3a3bf3
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 4, 2024
1436353
document functions and use a tutorial directory
Jan 5, 2024
43734e7
initial working version of the tutorial
Jan 5, 2024
171abdd
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 5, 2024
cd5bfc6
tweaks
pokey Jan 17, 2024
e022e69
change function
pokey Jan 17, 2024
e0f3c02
more cleanup
pokey Jan 17, 2024
142ec58
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 17, 2024
7ed3aac
fix
pokey Jan 17, 2024
a5ed0ff
More cleanup
pokey Jan 17, 2024
f789d24
More cleanup
pokey Jan 17, 2024
f607f16
Tweak imports
pokey Jan 17, 2024
01fc505
More tweaks# Please enter the commit message for your changes. Lines …
pokey Jan 17, 2024
af491e9
More cleanup
pokey Jan 17, 2024
06025d4
more tweaks
pokey Jan 17, 2024
55b596d
Remove comment
pokey Jan 17, 2024
6e101de
Bugfixes
pokey Jan 17, 2024
0e1cf6b
More fixes
pokey Jan 17, 2024
179098b
Merge branch 'main' into pr/saidelike/2131
pokey Feb 23, 2024
46be1bd
run meta-updater
pokey Feb 23, 2024
20bd5ed
Initial cursorless-vscode-tutorial scaffolding
pokey Feb 23, 2024
ea0481c
cursorless-vscode-tutorial => cursorless-vscode-tutorial-webview
pokey Feb 23, 2024
47fd057
more tutorial hacking
pokey Feb 28, 2024
69bd66b
Tweak package.json
pokey Mar 10, 2024
e9d8dbe
Update adding-a-new-package.md (#2247)
pokey Feb 23, 2024
5cbc8e1
bump pnpm => 8.15.3 (#2248)
pokey Feb 24, 2024
6dc5a2d
Update adding-a-new-package.md (#2249)
pokey Feb 24, 2024
5cd2c12
{grand, every} rephrasings for clarity and consistency (#2250)
jmegner Feb 25, 2024
ed7ae67
Update adding-a-new-package.md (#2255)
pokey Feb 27, 2024
401cb10
Get actual js from our webview package
pokey Mar 10, 2024
d5dddff
Initial React scaffolding
pokey Mar 10, 2024
ff3f0cd
Fix tsconfig
pokey Mar 10, 2024
419d830
more cleanup
pokey Mar 10, 2024
571a979
fix:meta
pokey Mar 10, 2024
2f1d786
more cleanup
pokey Mar 10, 2024
dce7d96
Merge branch 'main' into pr/saidelike/2131
pokey Mar 15, 2024
db75743
Add tailwind
pokey Mar 15, 2024
4d63d67
Let VscodeTutorial own tutorial state
pokey Mar 15, 2024
04c5653
Clean up some TutorialImpl stuff
pokey Mar 15, 2024
b59cccb
More PR feedback
pokey Mar 15, 2024
ac51e1f
Initial step content and step init code connected to webview
pokey Mar 15, 2024
26a2cd9
tweaks
pokey Mar 16, 2024
1f07077
tweaks
pokey Mar 26, 2024
0dbbfcd
some more tweaks
pokey Mar 27, 2024
32ab372
Merge branch 'main' into pr/saidelike/2131
pokey Mar 27, 2024
c9f56b3
more tweaks
pokey Mar 28, 2024
e8a66f8
Merge branch 'main' into pr/saidelike/2131
pokey Mar 28, 2024
ad8a152
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Mar 28, 2024
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
Prev Previous commit
Next Next commit
some more tweaks
pokey committed Mar 27, 2024
commit 0dbbfcd70c094cd639949334cc2427810f4cd119
3 changes: 2 additions & 1 deletion packages/common/src/types/tutorial.types.ts
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@ interface PickingTutorialState {
type: "pickingTutorial";
}

interface ActiveTutorialState {
export interface ActiveTutorialState {
type: "doingTutorial";
title: string;
tutorialId: TutorialId;
stepNumber: number;
stepContent: string;
29 changes: 12 additions & 17 deletions packages/cursorless-engine/src/api/Tutorial.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { TutorialId } from "@cursorless/common";

export interface TutorialGetContentArg {
export interface TutorialContent {
/**
* The version of the tutorial command.
* The title of the tutorial
*/
version: 0;
title: string;

/**
* The name of the current tutorial
* The steps of the current tutorial
*/
tutorialName: string;
steps: Array<TutorialStep>;
}

export interface TutorialGetContentResponse {
export interface RawTutorialContent {
/**
* The version of the tutorial command.
* The title of the tutorial
*/
version: 0;
title: string;

/**
* The steps of the current tutorial
*/
steps: Array<TutorialStep>;
steps: string[];
}

export interface TutorialStep {
@@ -39,14 +39,9 @@ export interface TutorialStep {

export interface TutorialSetupStepArg {
/**
* The version of the tutorial command.
*/
version: 0;

/**
* The name of the current tutorial
* The id of the current tutorial
*/
tutorialName: string;
tutorialId: string;

/**
* The yaml file for the current step
@@ -55,6 +50,6 @@ export interface TutorialSetupStepArg {
}

export interface Tutorial {
getContent(id: TutorialId): Promise<TutorialGetContentResponse>;
getContent(id: TutorialId): Promise<TutorialContent>;
setupStep(arg: TutorialSetupStepArg): Promise<void>;
}
55 changes: 31 additions & 24 deletions packages/cursorless-engine/src/core/TutorialImpl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ScopeType,
TestCaseFixture,
TextEditor,
TutorialId,
plainObjectToSelection,
serializedMarksToTokenHats,
@@ -9,8 +10,9 @@ import * as yaml from "js-yaml";
import { readFile } from "node:fs/promises";
import path from "path";
import {
RawTutorialContent,
Tutorial,
TutorialGetContentResponse,
TutorialContent,
TutorialSetupStepArg,
} from "../api/Tutorial";
import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl";
@@ -21,6 +23,7 @@ import { canonicalizeAndValidateCommand } from "./commandVersionUpgrades/canonic

export class TutorialImpl implements Tutorial {
private tutorialRootDir: string;
private editor?: TextEditor;

constructor(
private hatTokenMap: HatTokenMapImpl,
@@ -86,42 +89,43 @@ export class TutorialImpl implements Tutorial {
/**
* Load the "script.json" script for the current tutorial
*/
private async loadTutorialScript(tutorialName: string): Promise<string[]> {
private async loadTutorialScript(
tutorialName: string,
): Promise<RawTutorialContent> {
const scriptFile = path.join(
this.tutorialRootDir,
tutorialName,
"script.json",
);

const buffer = await readFile(scriptFile);
const contentList = JSON.parse(buffer.toString());
return contentList;
return JSON.parse(buffer.toString());
}

/**
* Handle the "cursorless.tutorial.getContent" command
*/
async getContent(tutorialName: TutorialId) {
const contentList = await this.loadTutorialScript(tutorialName);
async getContent(tutorialId: TutorialId) {
const rawContent = await this.loadTutorialScript(tutorialId);

// this is trying to catch occurrences of things like "%%step:cloneStateInk.yml%%"
const re = /%%(\w+):([^%]+)%%/;

let spokenForm;
const response: TutorialGetContentResponse = {
version: 0,
const response: TutorialContent = {
title: rawContent.title,
steps: [],
};
// we need to replace the {...} with the right content
for (let content of contentList) {
for (let content of rawContent.steps) {
let fixturePath: string | undefined = undefined;
let m = re.exec(content);
while (m) {
const [fullMatch, type, arg] = m;
switch (type) {
case "step":
fixturePath = arg;
spokenForm = await this.processStep(tutorialName, arg);
spokenForm = await this.processStep(tutorialId, arg);
content = content.replace(fullMatch, `<cmd@${spokenForm}/>`);
break;
case "literalStep":
@@ -155,23 +159,24 @@ export class TutorialImpl implements Tutorial {
* Handle the "cursorless.tutorial.setupStep" command
* @see packages/cursorless-vscode-e2e/src/suite/recorded.vscode.test.ts
*/
async setupStep({
version,
tutorialName,
fixturePath,
}: TutorialSetupStepArg) {
if (version !== 0) {
throw new Error(`Unsupported tutorial api version: ${version}`);
async setupStep({ tutorialId, fixturePath }: TutorialSetupStepArg) {
const fixture = await this.loadFixture(tutorialId, fixturePath);

if (this.editor == null) {
this.editor = await ide().openUntitledTextDocument({
content: fixture.initialState.documentContents,
language: fixture.languageId,
});
}

// TODO check for directory traversal?
const fixture = await this.loadFixture(tutorialName, fixturePath);
const editableEditor = ide().getEditableTextEditor(this.editor);

const editor = await ide().openUntitledTextDocument({
content: fixture.initialState.documentContents,
language: fixture.languageId,
await editableEditor.edit((editBuilder) => {
editBuilder.replace(
editableEditor.document.range,
fixture.initialState.documentContents,
);
});
const editableEditor = ide().getEditableTextEditor(editor);

// Ensure that the expected cursor/selections are present
editableEditor.selections = fixture.initialState.selections.map(
@@ -180,7 +185,9 @@ export class TutorialImpl implements Tutorial {

// Ensure that the expected hats are present
await this.hatTokenMap.allocateHats(
serializedMarksToTokenHats(fixture.initialState.marks, editor),
serializedMarksToTokenHats(fixture.initialState.marks, this.editor),
);

await editableEditor.focus();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
[
"Every cursorless command consists of an action performed on a target. For example, the command {step:takeWhale.yml} selects the token with a grey hat over the 'w'.",
"When a hat is not gray, we need to use a color to refer to it: {step:takeBlueSun.yml}",
"Selecting a single token is great but oftentimes we need something bigger. Say {step:takeEachPastKick.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. Let's try that: {step:takeCapAndWhale.yml}",
"But let's show that cursorless can live up to its name: we can say {step:chuckDrum.yml} to delete a word without ever moving our cursor.",
"Tokens are great, but they're just one way to think of a document. Let's try working with lines: {step:chuckLineLook.yml}",
"We can also use {scopeType:line} to refer to the line containing our cursor: {step:takeLine.yml}",
"You now know how to select and delete; let's give you a couple more actions to play with: say {action:setSelectionBefore} to place the cursor before a target, as in {step:preInk.yml}",
"Say {action:setSelectionAfter} to place the cursor after a target: {step:postLook.yml}",
"Say {action:clearAndSetSelection} to delete a word and move your cursor to where it used to be: {step:clearTrap.yml}",
"Chaining commands is a great way to code faster: {step:clearWhaleWordYou.yml+wordYou}",
"And that wraps up unit 1 of the cursorless tutorial! Next time, we'll write some code ☺️"
]
{
"title": "Introduction",
"steps": [
"Every cursorless command consists of an action performed on a target. For example, the command {step:takeWhale.yml} selects the token with a grey hat over the 'w'.",
"When a hat is not gray, we need to use a color to refer to it: {step:takeBlueSun.yml}",
"Selecting a single token is great but oftentimes we need something bigger. Say {step:takeEachPastKick.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. Let's try that: {step:takeCapAndWhale.yml}",
"But let's show that cursorless can live up to its name: we can say {step:chuckDrum.yml} to delete a word without ever moving our cursor.",
"Tokens are great, but they're just one way to think of a document. Let's try working with lines: {step:chuckLineLook.yml}",
"We can also use {scopeType:line} to refer to the line containing our cursor: {step:takeLine.yml}",
"You now know how to select and delete; let's give you a couple more actions to play with: say {action:setSelectionBefore} to place the cursor before a target, as in {step:preInk.yml}",
"Say {action:setSelectionAfter} to place the cursor after a target: {step:postLook.yml}",
"Say {action:clearAndSetSelection} to delete a word and move your cursor to where it used to be: {step:clearTrap.yml}",
"Chaining commands is a great way to code faster: {step:clearWhaleWordYou.yml+wordYou}",
"And that wraps up unit 1 of the cursorless tutorial! Next time, we'll write some code ☺️"
]
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
[
"When editing code, we often think in terms of statements, functions, etc. Let's clone a statement: %%step:cloneStateInk.yml%%",
"%%scopeType:{type: statement}%% is one of many scopes supported by cursorless. To see all available scopes, use the command %%literalStep:cursorless help%%, and look at the Scopes section.",
"Cursorless tries its best to keep your commands short. In the following command, we just say %%scopeType:{type: surroundingPair, delimiter: string}%% once, but cursorless infers that both targets are strings: %%step:swapStringAirWithWhale.yml%%",
"Great. Let's learn a new action. The %%action:editNewLineAfter%% action lets you start editing a new line below any line on your screen: %%step:pourUrge.yml%%",
"Now let's try applying a cursorless action to the current line: %%step:dedentThis.yml%%",
"Code reuse is a fact of life as a programmer. Cursorless makes this easy with the %%action:replaceWithTarget%% command: %%step:bringStateUrge.yml%%",
"%%action:replaceWithTarget%% also works with two targets just like %%action:swapTargets%%: %%step: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: %%step: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 %%literalStep:cursorless 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 :)"
]
{
"title": "Basic coding",
"steps": [
"When editing code, we often think in terms of statements, functions, etc. Let's clone a statement: %%step:cloneStateInk.yml%%",
"%%scopeType:{type: statement}%% is one of many scopes supported by cursorless. To see all available scopes, use the command %%literalStep:cursorless help%%, and look at the Scopes section.",
"Cursorless tries its best to keep your commands short. In the following command, we just say %%scopeType:{type: surroundingPair, delimiter: string}%% once, but cursorless infers that both targets are strings: %%step:swapStringAirWithWhale.yml%%",
"Great. Let's learn a new action. The %%action:editNewLineAfter%% action lets you start editing a new line below any line on your screen: %%step:pourUrge.yml%%",
"Now let's try applying a cursorless action to the current line: %%step:dedentThis.yml%%",
"Code reuse is a fact of life as a programmer. Cursorless makes this easy with the %%action:replaceWithTarget%% command: %%step:bringStateUrge.yml%%",
"%%action:replaceWithTarget%% also works with two targets just like %%action:swapTargets%%: %%step: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: %%step: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 %%literalStep:cursorless 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 :)"
]
}
3 changes: 2 additions & 1 deletion packages/cursorless-vscode-tutorial-webview/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TutorialState } from "@cursorless/common";
import { useEffect, useState, type FunctionComponent } from "react";
import { WebviewApi } from "vscode-webview";
import { TutorialStep } from "./TutorialStep";

interface Props {
vscode: WebviewApi<undefined>;
@@ -29,6 +30,6 @@ export const App: FunctionComponent<Props> = ({ vscode }) => {
return state.type === "pickingTutorial" ? (
<span>Say "cursorless tutorial"</span>
) : (
<span>{state.stepContent}</span>
<TutorialStep state={state} />
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ActiveTutorialState } from "@cursorless/common";
import { type FunctionComponent } from "react";

interface TutorialStepProps {
state: ActiveTutorialState;
}

export const TutorialStep: FunctionComponent<TutorialStepProps> = ({
state,
}) => {
return (
<div>
<h1>{state.title}</h1>
{state.stepContent}
</div>
);
};
11 changes: 4 additions & 7 deletions packages/cursorless-vscode/src/VscodeTutorial.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { TutorialId, TutorialState } from "@cursorless/common";
import {
Tutorial,
TutorialGetContentResponse,
} from "@cursorless/cursorless-engine";
import { Tutorial, TutorialContent } from "@cursorless/cursorless-engine";
import { VscodeApi } from "@cursorless/vscode-common";
import {
CancellationToken,
@@ -19,7 +16,7 @@ const VSCODE_TUTORIAL_WEBVIEW_ID = "cursorless.tutorial";

export class VscodeTutorial implements WebviewViewProvider {
private state: TutorialState = { type: "pickingTutorial" };
private currentTutorial?: TutorialGetContentResponse;
private currentTutorial?: TutorialContent;
private view?: WebviewView;

constructor(
@@ -70,6 +67,7 @@ export class VscodeTutorial implements WebviewViewProvider {
stepNumber: 0,
stepContent: this.currentTutorial.steps[0].content,
stepCount: this.currentTutorial.steps.length,
title: this.currentTutorial.title,
});

if (this.view != null) {
@@ -79,9 +77,8 @@ export class VscodeTutorial implements WebviewViewProvider {
}

await this.tutorial.setupStep({
version: 0,
fixturePath: this.currentTutorial.steps[0].fixturePath!,
tutorialName: tutorialId,
tutorialId: tutorialId,
});
}