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
start parsing things from the extension side
we have 2 commands (getContent and setupStep) respectively to get the full content of the tutorial and to ask the extension to set up the state in vscode for the current step.

we already support parsing the spoken form for the command to be said by the user during the getContent command and spawning the contents in vscode for the setupStep.

We need to parse the other spoken forms and to initialize better the vscode content with the hats, etc.
Cedric Halbronn committed Jan 2, 2024
commit 25be0ca7b5ae5ac353d81db600c7bdfe01040cc8
134 changes: 66 additions & 68 deletions cursorless-talon/src/tutorial.py
Original file line number Diff line number Diff line change
@@ -3,92 +3,90 @@
from pathlib import Path
from typing import Callable

import yaml
from talon import actions, app

from .get_action_spoken_form import lookup_action

regex = re.compile(r"\{(\w+):([^}]+)\}")
tutorial_dir = Path(
r"C:\work\tools\voicecoding\cursorless_fork\packages\cursorless-vscode-e2e\src\suite\fixtures\recorded\tutorial\unit-2-basic-coding"
)


# {literalStep:...}
# "To see all available scopes, use the command {literalStep:cursorless help}, and look at the Scopes section."
def process_literal_step(argument: str):
return f"<cmd@{argument}/>"


# {action:...}
# "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}",
# The {action:editNewLineAfter} action" => "The "pour" action"
def process_action(argument: str):
_, spoken_form = lookup_action(argument)
return f'<*"{spoken_form}"/>'


# {scopeType:...}
# "We can also use {scopeType:line} to refer to the line containing our cursor: {step:takeLine.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: {step:swapStringAirWithWhale.yml}"
def process_scope_type(argument: str):
# TODO not sure what we are trying to achieve here
_, spoken_form = lookup_scope_type(argument)
return f'<*"{spoken_form}"/>'


def process_cursorless_command_step(argument: str):
print(f"{argument=}")
step_fixture = yaml.safe_load((tutorial_dir / argument).read_text())
print(f"{step_fixture['command']=}")
result = actions.user.private_cursorless_run_rpc_command_get(
"cursorless.tutorial.create",
{
"version": 0,
"stepFixture": step_fixture,
"yamlFilename": argument,
},
)
print(f"{result=}")
return f"<cmd@{cursorless_command_to_spoken_form(step_fixture['command'])}/>"
# return f"<cmd@{result}/>"


# TODO get this information from the extension
def cursorless_command_to_spoken_form(command: dict[str, str]):
return command["spokenForm"]


#_, spoken_form = lookup_scope_type(argument)
#return f'<*"{spoken_form}"/>'
return f'<*"SCOPETYPE_{argument}"/>'

# "When editing code, we often think in terms of statements, functions, etc. Let's clone a statement: {step:cloneStateInk.yml}",
#
# this builds a dictionary which has keys and values (each one is a spoken form)
# each value is built by calling the function with the argument being passed
# eg interpolation_processor_map["step"]()
interpolation_processor_map: dict[str, Callable[[str], str]] = {
# this will exist extension side
# nothing needed as not a Cursorless command
"literalStep": process_literal_step,
# import https://github.com/cursorless-dev/cursorless/blob/7341d0f707b1d0a0950a19894be3aebbb33582c8/packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/actions.ts#L7C1-L7C1
# hardcoded list of default spoken form for an action (not yet the customized one)
"action": process_action,
# scopeTypeType == "line", etc.
# generator.processScopeType({type: scopeTypeType})
"scopeType": process_scope_type,
"step": process_cursorless_command_step,
}


def process_tutorial_step(raw: str):
print(f"{raw=}")
current_index = 0
content = ""
for match in regex.finditer(raw):
content += raw[current_index : match.start()]
content += interpolation_processor_map[match.group(1)](match.group(2))
current_index = match.end()
content += raw[current_index : len(raw)]
print(f"{content=}")

return {
"content": content,
"restore_callback": print,
"modes": ["command"],
"app": "Code",
"context_hint": "Please open VSCode and enter command mode",
}


def get_basic_coding_walkthrough():
with open(tutorial_dir / "script.json") as f:
script = json.load(f)

return [
actions.user.hud_create_walkthrough_step(**process_tutorial_step(step))
for step in script
]


# TODO all the above will be deleted

def step_callback(x):
print(f"step_callback8: {x}")
yamlFilename = tutorial_content['yamlFilenames'][x]
if yamlFilename:
response = actions.user.private_cursorless_run_rpc_command_get(
"cursorless.tutorial.setupStep",
{
"version": 0,
"tutorialName": "unit-2-basic-coding",
"yamlFilename": yamlFilename
},
)

tutorial_content = None
def get_basic_coding_walkthrough():
global tutorial_content
print("get_basic_coding_walkthrough start")
tutorial_content = actions.user.private_cursorless_run_rpc_command_get(
"cursorless.tutorial.getContent",
{
"version": 0,
"tutorialName": "unit-2-basic-coding"
},
)
print(f"{tutorial_content=}")
walkthrough_steps = []
for content in tutorial_content['content']:
walkthrough_steps.append(actions.user.hud_create_walkthrough_step(
content=content,
restore_callback=step_callback,
modes=["command"],
app="Visual Studio Code", # Windows
# app="Code", # OS X?
context_hint="Please open VSCode and enter command mode"
))
print("get_basic_coding_walkthrough end")
return walkthrough_steps

# this is adding the menu to the hud
# by adding a list of HudWalkThroughStep
def on_ready():
actions.user.hud_add_lazy_walkthrough(
"Cursorless basic coding", get_basic_coding_walkthrough
12 changes: 8 additions & 4 deletions packages/common/src/cursorlessCommandIds.ts
Original file line number Diff line number Diff line change
@@ -47,7 +47,8 @@ export const cursorlessCommandIds = [
"cursorless.toggleDecorations",
"cursorless.showScopeVisualizer",
"cursorless.hideScopeVisualizer",
"cursorless.tutorial.create",
"cursorless.tutorial.getContent",
"cursorless.tutorial.setupStep",
] as const satisfies readonly `cursorless.${string}`[];

export type CursorlessCommandId = (typeof cursorlessCommandIds)[number];
@@ -77,6 +78,12 @@ export const cursorlessCommandDescriptions: Record<
["cursorless.hideScopeVisualizer"]: new VisibleCommand(
"Hide the scope visualizer",
),
["cursorless.tutorial.getContent"]: new VisibleCommand(
"Get the tutorial content based on Talon HUD",
),
["cursorless.tutorial.setupStep"]: new VisibleCommand(
"Setup the current step for the tutorial based on Talon HUD",
),

["cursorless.command"]: new HiddenCommand("The core cursorless command"),
["cursorless.showQuickPick"]: new HiddenCommand(
@@ -121,7 +128,4 @@ export const cursorlessCommandDescriptions: Record<
["cursorless.keyboard.modal.modeToggle"]: new HiddenCommand(
"Toggle the cursorless modal mode",
),
["cursorless.tutorial.create"]: new VisibleCommand(
"Create the tutorial based on Talon HUD",
),
};
153 changes: 133 additions & 20 deletions packages/cursorless-engine/src/core/Tutorial.ts
Original file line number Diff line number Diff line change
@@ -9,28 +9,59 @@ import path from "path";
import * as yaml from "js-yaml";
import { promises as fsp } from "node:fs";

import { TestCaseFixture } from "@cursorless/common";
import { SpokenForm, SpokenFormSuccess, TestCaseFixture } from "@cursorless/common";
import { Dictionary } from "lodash";
import { ide } from "../singletons/ide.singleton";
import { HatTokenMapImpl } from "./HatTokenMapImpl";
import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl";
import { canonicalizeAndValidateCommand } from "./commandVersionUpgrades/canonicalizeAndValidateCommand";

const tutorialDirectory =
"C:\\work\\tools\\voicecoding\\cursorless_fork\\packages\\cursorless-vscode-e2e\\src\\suite\\fixtures\\recorded\\tutorial\\unit-2-basic-coding";
const fs = require('node:fs');

// TODO use a relative path but I'm not sure where these source files are at running time
// and CWD is C:\Users\User\AppData\Local\Programs\Microsoft VS Code\
const tutorialRootDir =
"C:\\work\\tools\\voicecoding\\cursorless_fork\\packages\\cursorless-vscode-e2e\\src\\suite\\fixtures\\recorded\\tutorial\\";

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

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

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

/**
* The text content of the different steps of the current tutorial
*/
content: Array<string>;

/**
* The argument expected by the tutorial command.
*/
interface TutorialCommandArg {
/**
* The yaml files of the different steps of the current tutorial (if any)
*/
yamlFilenames: Array<string>;
}

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

/**
* A representation of the yaml file
* The name of the current tutorial
*/
stepFixture: Dictionary<string>;
tutorialName: string;

/**
* The yaml file for the current step
@@ -39,33 +70,115 @@ interface TutorialCommandArg {
}

export class Tutorial {
private customSpokenFormGenerator: CustomSpokenFormGeneratorImpl;

constructor(
hatTokenMap: HatTokenMapImpl,
customSpokenFormGenerator: CustomSpokenFormGeneratorImpl,
) {
this.create = this.create.bind(this);
this.getContent = this.getContent.bind(this);
this.setupStep = this.setupStep.bind(this);

this.customSpokenFormGenerator = customSpokenFormGenerator;
}

async create({ version, stepFixture, yamlFilename }: TutorialCommandArg) {
async getContent({ version, tutorialName }: TutorialGetContentArg) {
console.log("getContent(){ version, tutorialName, yamlFilename }: TutorialSetupStepArg", tutorialName);
if (version !== 0) {
throw new Error(`Unsupported tutorial api version: ${version}`);
}

// const fixture = stepFixture as TestCaseFixture;
this.createEnvironment(yamlFilename);
// TODO need to answer to the talon side only what is necessary
return stepFixture;
const tutorialDir = path.join(tutorialRootDir, tutorialName);
if (!fs.existsSync(tutorialDir)) {
throw new Error(`Invalid tutorial name: ${tutorialName}`);
}

const scriptFile = path.join(tutorialDir, "script.json");
if (!fs.existsSync(scriptFile)) {
throw new Error(`Can't file script file: ${scriptFile} in tutorial name: ${tutorialName}`);
}
const buffer = await fsp.readFile(scriptFile);
const contentList = JSON.parse(buffer.toString());
console.log(contentList);

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

var m;
var response : TutorialGetContentResponse = {
version: 0,
content: [],
yamlFilenames: [],
};
// we need to replace the {...} with the right content
for (var content of contentList) {
var yamlFilename = "";
do {
m = re.exec(content);
if (m) {
const name = m[1];
const arg = m[2];
console.log(name, arg);
if (name === "step")
{
const tutorialDir = path.join(tutorialRootDir, tutorialName);
if (!fs.existsSync(tutorialDir)) {
throw new Error(`Invalid tutorial name: ${tutorialName}`);
}

const yamlFile = path.join(tutorialDir, arg)
if (!fs.existsSync(yamlFile)) {
throw new Error(`Can't file yaml file: ${yamlFile} in tutorial name: ${tutorialName}`);
}
yamlFilename = arg;

const buffer = await fsp.readFile(yamlFile);
const fixture = yaml.load(buffer.toString()) as TestCaseFixture;

// command to be said for moving to the next step
const spoken_form = this.customSpokenFormGenerator.commandToSpokenForm(
canonicalizeAndValidateCommand(fixture.command)
) as SpokenFormSuccess;
console.log("\t", spoken_form.spokenForms[0]);
content = content.replace(m[0], `<cmd@${spoken_form.spokenForms[0]}/>`)
}
}
} while (m);
response.yamlFilenames.push(yamlFilename);
response.content.push(content);
}

// return to the talon side
return response;
}

async createEnvironment(yamlFilename: string) {
const buffer = await fsp.readFile(
path.join(tutorialDirectory, yamlFilename),
);
const fixture = yaml.load(buffer.toString()) as TestCaseFixture;
async setupStep({ version, tutorialName, yamlFilename }: TutorialSetupStepArg) {
console.log("setupStep()", tutorialName, yamlFilename);
if (version !== 0) {
throw new Error(`Unsupported tutorial api version: ${version}`);
}

const tutorialDir = path.join(tutorialRootDir, tutorialName);
if (!fs.existsSync(tutorialDir)) {
throw new Error(`Invalid tutorial name: ${tutorialName}`);
}

// TODO check for directory traversal?
const yamlFile = path.join(tutorialDir, yamlFilename);
if (!fs.existsSync(yamlFile)) {
throw new Error(`Can't file yaml file: ${yamlFile} in tutorial name: ${tutorialName}`);
}
const buffer = await fsp.readFile(yamlFile);
const fixture = yaml.load(buffer.toString()) as TestCaseFixture;

const editor = ide().openUntitledTextDocument({
content: fixture.initialState.documentContents,
language: fixture.languageId,
});

// TODO set up the right hats

// return to the talon side
return true;
}
}
Loading