From 6dad7f08aa66a9a77380d854e1930bf4ed112305 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:46:10 +0100 Subject: [PATCH] Remove node references --- .../src/ide/types/TutorialContentProvider.ts | 45 +++++++++++++++ packages/common/src/index.ts | 1 + .../cursorless-engine/src/api/Tutorial.ts | 17 ------ .../cursorless-engine/src/cursorlessEngine.ts | 25 +++++++-- .../disabledComponents/DisabledTutorial.ts | 41 ++++++++++++++ .../src/tutorial/TutorialImpl.ts | 56 ++++++------------- .../src/tutorial/TutorialScriptParser.ts | 18 +++--- .../src/tutorial/loadTutorialScript.ts | 17 ------ .../CursorlessCommandComponentParser.ts | 17 +++--- packages/cursorless-vscode/src/extension.ts | 26 ++++++--- .../src/FileSystemTutorialContentProvider.ts | 47 ++++++++++++++++ packages/node-common/src/index.ts | 1 + 12 files changed, 210 insertions(+), 101 deletions(-) create mode 100644 packages/common/src/ide/types/TutorialContentProvider.ts create mode 100644 packages/cursorless-engine/src/disabledComponents/DisabledTutorial.ts delete mode 100644 packages/cursorless-engine/src/tutorial/loadTutorialScript.ts create mode 100644 packages/node-common/src/FileSystemTutorialContentProvider.ts diff --git a/packages/common/src/ide/types/TutorialContentProvider.ts b/packages/common/src/ide/types/TutorialContentProvider.ts new file mode 100644 index 00000000000..9ee8226e582 --- /dev/null +++ b/packages/common/src/ide/types/TutorialContentProvider.ts @@ -0,0 +1,45 @@ +import { TestCaseFixtureLegacy } from "../../types/TestCaseFixture"; +import { TutorialId } from "../../types/tutorial.types"; + +export interface TutorialContentProvider { + getRawTutorials(): Promise; + + /** + * Load the "script.json" script for the current tutorial + */ + loadTutorialScript(tutorialId: string): Promise; + + /** + * Loads a fixture file from the tutorial directory, eg "takeNear.yml" + * + * @param tutorialId The tutorial id + * @param fixtureName The name of the fixture, eg "takeNear.yml" + * @returns A promise that resolves to the parsed fixture content + */ + loadFixture( + tutorialId: TutorialId, + 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/common/src/index.ts b/packages/common/src/index.ts index 586e906c7f5..4db673dcc95 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -29,6 +29,7 @@ export * from "./ide/types/events.types"; export * from "./ide/types/Paths"; export * from "./ide/types/CommandHistoryStorage"; export * from "./ide/types/RawTreeSitterQueryProvider"; +export * from "./ide/types/TutorialContentProvider"; export * from "./ide/types/FileSystem.types"; export * from "./types/RangeExpansionBehavior"; export * from "./types/InputBoxOptions"; diff --git a/packages/cursorless-engine/src/api/Tutorial.ts b/packages/cursorless-engine/src/api/Tutorial.ts index 8122e455292..1f784db7d63 100644 --- a/packages/cursorless-engine/src/api/Tutorial.ts +++ b/packages/cursorless-engine/src/api/Tutorial.ts @@ -25,23 +25,6 @@ export interface TutorialContent { steps: Array; } -export interface RawTutorialContent { - /** - * The title of the tutorial - */ - title: string; - - /** - * The version of the tutorial - */ - version: number; - - /** - * The steps of the current tutorial - */ - steps: string[]; -} - /** * Advance to the next step when the user completes a command */ diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 095e63ac4fd..f4a470edfeb 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -4,6 +4,7 @@ import { Hats, IDE, ScopeProvider, + TutorialContentProvider, ensureCommandShape, type RawTreeSitterQueryProvider, type TalonSpokenForms, @@ -41,14 +42,17 @@ import { ScopeRangeWatcher } from "./scopeProviders/ScopeRangeWatcher"; import { ScopeSupportChecker } from "./scopeProviders/ScopeSupportChecker"; import { ScopeSupportWatcher } from "./scopeProviders/ScopeSupportWatcher"; import { injectIde } from "./singletons/ide.singleton"; +import { DisabledTutorial } from "./disabledComponents/DisabledTutorial"; +import { Tutorial } from "./api/Tutorial"; -interface Props { +export interface EngineProps { ide: IDE; hats?: Hats; treeSitterQueryProvider?: RawTreeSitterQueryProvider; treeSitter?: TreeSitter; commandServerApi?: CommandServerApi; talonSpokenForms?: TalonSpokenForms; + tutorialContentProvider?: TutorialContentProvider; snippets?: Snippets; } @@ -59,8 +63,9 @@ export async function createCursorlessEngine({ treeSitter = new DisabledTreeSitter(), commandServerApi = new DisabledCommandServerApi(), talonSpokenForms = new DisabledTalonSpokenForms(), + tutorialContentProvider, snippets = new DisabledSnippets(), -}: Props): Promise { +}: EngineProps): Promise { injectIde(ide); const debug = new Debug(ide); @@ -91,8 +96,19 @@ export async function createCursorlessEngine({ commandRunnerDecorators.push(decorator); }; - const tutorial = new TutorialImpl(hatTokenMap, customSpokenFormGenerator); - addCommandRunnerDecorator(tutorial); + let tutorial: Tutorial; + if (tutorialContentProvider != null) { + const tutorialImpl = new TutorialImpl( + hatTokenMap, + customSpokenFormGenerator, + tutorialContentProvider, + ); + ide.disposeOnExit(tutorialImpl); + addCommandRunnerDecorator(tutorialImpl); + tutorial = tutorialImpl; + } else { + tutorial = new DisabledTutorial(); + } ide.disposeOnExit( debug, @@ -100,7 +116,6 @@ export async function createCursorlessEngine({ keyboardTargetUpdater, languageDefinitions, rangeUpdater, - tutorial, ); let previousCommand: Command | undefined = undefined; diff --git a/packages/cursorless-engine/src/disabledComponents/DisabledTutorial.ts b/packages/cursorless-engine/src/disabledComponents/DisabledTutorial.ts new file mode 100644 index 00000000000..500754891d1 --- /dev/null +++ b/packages/cursorless-engine/src/disabledComponents/DisabledTutorial.ts @@ -0,0 +1,41 @@ +import { + TutorialId, + TutorialState, + Disposable, + ScopeType, +} from "@cursorless/common"; +import { Tutorial } from "../api/Tutorial"; + +export class DisabledTutorial implements Tutorial { + onState(_callback: (state: TutorialState) => void): Disposable { + return { dispose: () => {} }; + } + + docsOpened(): void { + // Do nothing + } + scopeTypeVisualized(_scopeType: ScopeType | undefined): void { + // Do nothing + } + + start(_id: number | TutorialId): Promise { + throw new Error("Method not implemented."); + } + next(): Promise { + throw new Error("Method not implemented."); + } + previous(): Promise { + throw new Error("Method not implemented."); + } + restart(): Promise { + throw new Error("Method not implemented."); + } + resume(): Promise { + throw new Error("Method not implemented."); + } + list(): Promise { + throw new Error("Method not implemented."); + } + + readonly state: TutorialState = { type: "loading" }; +} diff --git a/packages/cursorless-engine/src/tutorial/TutorialImpl.ts b/packages/cursorless-engine/src/tutorial/TutorialImpl.ts index 4593ae980e4..4385f9e3f2c 100644 --- a/packages/cursorless-engine/src/tutorial/TutorialImpl.ts +++ b/packages/cursorless-engine/src/tutorial/TutorialImpl.ts @@ -6,6 +6,7 @@ import { ReadOnlyHatMap, ScopeType, TextEditor, + TutorialContentProvider, TutorialId, TutorialInfo, TutorialState, @@ -16,22 +17,18 @@ import { } from "@cursorless/common"; import { produce } from "immer"; import { isEqual } from "lodash-es"; -import { readdir } from "node:fs/promises"; -import path from "path"; import { CommandRunner } from "../CommandRunner"; import { CommandRunnerDecorator } from "../api/CursorlessEngineApi"; import { Tutorial, TutorialContent, TutorialStep } from "../api/Tutorial"; +import { Debouncer } from "../core/Debouncer"; import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl"; import { ide } from "../singletons/ide.singleton"; -import { Debouncer } from "../core/Debouncer"; import { TutorialError } from "./TutorialError"; import { TutorialScriptParser } from "./TutorialScriptParser"; -import { loadTutorialScript } from "./loadTutorialScript"; const HIGHLIGHT_COLOR = "highlight0"; export class TutorialImpl implements Tutorial, CommandRunnerDecorator { - private tutorialRootDir: string; private editor?: TextEditor; private state_: TutorialState = { type: "loading" }; private notifier: Notifier<[TutorialState]> = new Notifier(); @@ -42,13 +39,12 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { constructor( private hatTokenMap: HatTokenMap, private customSpokenFormGenerator: CustomSpokenFormGeneratorImpl, + private contentProvider: TutorialContentProvider, ) { this.setupStep = this.setupStep.bind(this); this.reparseCurrentTutorial = this.reparseCurrentTutorial.bind(this); const debouncer = new Debouncer(() => this.checkPreconditions(), 100); - this.tutorialRootDir = path.join(ide().assetsRoot, "tutorial"); - this.loadTutorialList(); this.disposables.push( @@ -85,30 +81,16 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { } async loadTutorialList() { - const tutorialDirs = await readdir(this.tutorialRootDir, { - withFileTypes: true, - }); - const tutorialProgress = ide().globalState.get("tutorialProgress"); - this.tutorials = await Promise.all( - tutorialDirs - .filter((dirent) => dirent.isDirectory()) - .map(async (dirent) => { - const tutorialId = dirent.name as TutorialId; - const rawContent = await loadTutorialScript( - this.tutorialRootDir, - tutorialId, - ); - - return { - id: tutorialId, - title: rawContent.title, - version: rawContent.version, - stepCount: rawContent.steps.length, - currentStep: tutorialProgress[tutorialId]?.currentStep ?? 0, - }; - }), + this.tutorials = (await this.contentProvider.getRawTutorials()).map( + (rawContent) => ({ + id: rawContent.id, + title: rawContent.title, + version: rawContent.version, + stepCount: rawContent.steps.length, + currentStep: tutorialProgress[rawContent.id]?.currentStep ?? 0, + }), ); this.setState({ @@ -163,13 +145,11 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { const tutorialId = this.state_.id; - const rawContent = await loadTutorialScript( - this.tutorialRootDir, - tutorialId, - ); + const rawContent = + await this.contentProvider.loadTutorialScript(tutorialId); const parser = new TutorialScriptParser( - this.tutorialRootDir, + this.contentProvider, tutorialId, this.customSpokenFormGenerator, ); @@ -207,13 +187,11 @@ export class TutorialImpl implements Tutorial, CommandRunnerDecorator { tutorialId = this.tutorials[tutorialId].id; } - const rawContent = await loadTutorialScript( - this.tutorialRootDir, - tutorialId, - ); + const rawContent = + await this.contentProvider.loadTutorialScript(tutorialId); const parser = new TutorialScriptParser( - this.tutorialRootDir, + this.contentProvider, tutorialId, this.customSpokenFormGenerator, ); diff --git a/packages/cursorless-engine/src/tutorial/TutorialScriptParser.ts b/packages/cursorless-engine/src/tutorial/TutorialScriptParser.ts index e3783a9100c..e8d77059066 100644 --- a/packages/cursorless-engine/src/tutorial/TutorialScriptParser.ts +++ b/packages/cursorless-engine/src/tutorial/TutorialScriptParser.ts @@ -1,15 +1,19 @@ -import { TutorialId, TutorialStepFragment } from "@cursorless/common"; +import { + TutorialContentProvider, + TutorialId, + TutorialStepFragment, +} from "@cursorless/common"; import { TutorialStep } from "../api/Tutorial"; import { parseScopeType } from "../customCommandGrammar/parseCommand"; import { CustomSpokenFormGeneratorImpl } from "../generateSpokenForm/CustomSpokenFormGeneratorImpl"; import { StepComponent } from "./StepComponent"; -import { CursorlessCommandComponentParser } from "./stepComponentParsers/CursorlessCommandComponentParser"; +import { getScopeTypeSpokenForm } from "./getScopeTypeSpokenForm"; +import { specialTerms } from "./specialTerms"; import { ActionComponentParser } from "./stepComponentParsers/ActionComponentParser"; -import { parseSpecialComponent } from "./stepComponentParsers/parseSpecialComponent"; +import { CursorlessCommandComponentParser } from "./stepComponentParsers/CursorlessCommandComponentParser"; import { GraphemeComponentParser } from "./stepComponentParsers/GraphemeComponentParser"; -import { getScopeTypeSpokenForm } from "./getScopeTypeSpokenForm"; +import { parseSpecialComponent } from "./stepComponentParsers/parseSpecialComponent"; import { parseVisualizeComponent } from "./stepComponentParsers/parseVisualizeComponent"; -import { specialTerms } from "./specialTerms"; /** * This is trying to catch occurrences of things like `{command:cloneStateInk.yml}` @@ -24,14 +28,14 @@ export class TutorialScriptParser { >; constructor( - tutorialRootDir: string, + contentProvider: TutorialContentProvider, tutorialId: TutorialId, customSpokenFormGenerator: CustomSpokenFormGeneratorImpl, ) { this.parseTutorialStep = this.parseTutorialStep.bind(this); const cursorlessCommandHandler = new CursorlessCommandComponentParser( - tutorialRootDir, + contentProvider, tutorialId, customSpokenFormGenerator, ); diff --git a/packages/cursorless-engine/src/tutorial/loadTutorialScript.ts b/packages/cursorless-engine/src/tutorial/loadTutorialScript.ts deleted file mode 100644 index 6ab7729af8f..00000000000 --- a/packages/cursorless-engine/src/tutorial/loadTutorialScript.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { readFile } from "node:fs/promises"; -import path from "path"; -import { RawTutorialContent } from "../api/Tutorial"; - -/** - * Load the "script.json" script for the current tutorial - */ -export async function loadTutorialScript( - tutorialRootDir: string, - tutorialName: string, -): Promise { - const buffer = await readFile( - path.join(tutorialRootDir, tutorialName, "script.json"), - ); - - return JSON.parse(buffer.toString()); -} diff --git a/packages/cursorless-engine/src/tutorial/stepComponentParsers/CursorlessCommandComponentParser.ts b/packages/cursorless-engine/src/tutorial/stepComponentParsers/CursorlessCommandComponentParser.ts index 6a5bc1b3376..731cc7b62f4 100644 --- a/packages/cursorless-engine/src/tutorial/stepComponentParsers/CursorlessCommandComponentParser.ts +++ b/packages/cursorless-engine/src/tutorial/stepComponentParsers/CursorlessCommandComponentParser.ts @@ -1,10 +1,12 @@ -import { CommandComplete, TutorialId } from "@cursorless/common"; -import { loadFixture } from "@cursorless/node-common"; -import path from "path"; +import { + CommandComplete, + TutorialContentProvider, + TutorialId, +} from "@cursorless/common"; +import { canonicalizeAndValidateCommand } from "../../core/commandVersionUpgrades/canonicalizeAndValidateCommand"; import { CustomSpokenFormGeneratorImpl } from "../../generateSpokenForm/CustomSpokenFormGeneratorImpl"; import { StepComponent, StepComponentParser } from "../StepComponent"; import { TutorialError } from "../TutorialError"; -import { canonicalizeAndValidateCommand } from "../../core/commandVersionUpgrades/canonicalizeAndValidateCommand"; /** * Parses components of the form `{command:takeNear.yml}`. The argument @@ -12,14 +14,15 @@ import { canonicalizeAndValidateCommand } from "../../core/commandVersionUpgrade */ export class CursorlessCommandComponentParser implements StepComponentParser { constructor( - private tutorialRootDir: string, + private contentProvider: TutorialContentProvider, private tutorialId: TutorialId, private customSpokenFormGenerator: CustomSpokenFormGeneratorImpl, ) {} async parse(arg: string): Promise { - const fixture = await loadFixture( - path.join(this.tutorialRootDir, this.tutorialId, arg), + const fixture = await this.contentProvider.loadFixture( + this.tutorialId, + arg, ); const command = canonicalizeAndValidateCommand(fixture.command); diff --git a/packages/cursorless-vscode/src/extension.ts b/packages/cursorless-vscode/src/extension.ts index ebd33e46371..50adc1ef7df 100644 --- a/packages/cursorless-vscode/src/extension.ts +++ b/packages/cursorless-vscode/src/extension.ts @@ -1,5 +1,6 @@ import { Disposable, + EnforceUndefined, FakeCommandServerApi, FakeIDE, IDE, @@ -12,12 +13,14 @@ import { } from "@cursorless/common"; import { CommandHistory, + EngineProps, createCursorlessEngine, } from "@cursorless/cursorless-engine"; import { FileSystemCommandHistoryStorage, FileSystemRawTreeSitterQueryProvider, FileSystemTalonSpokenForms, + FileSystemTutorialContentProvider, getFixturePath, } from "@cursorless/node-common"; import { @@ -104,6 +107,19 @@ export async function activate( ); context.subscriptions.push(treeSitterQueryProvider); + const engineProps: EnforceUndefined = { + ide: normalizedIde, + hats, + treeSitterQueryProvider, + treeSitter, + commandServerApi, + talonSpokenForms, + tutorialContentProvider: new FileSystemTutorialContentProvider( + normalizedIde.assetsRoot, + ), + snippets, + }; + const { commandApi, storedTargets, @@ -114,15 +130,7 @@ export async function activate( addCommandRunnerDecorator, customSpokenFormGenerator, tutorial, - } = await createCursorlessEngine({ - ide: normalizedIde, - hats, - treeSitterQueryProvider, - treeSitter, - commandServerApi, - talonSpokenForms, - snippets, - }); + } = await createCursorlessEngine(engineProps); const commandHistoryStorage = new FileSystemCommandHistoryStorage( fileSystem.cursorlessCommandHistoryDirPath, diff --git a/packages/node-common/src/FileSystemTutorialContentProvider.ts b/packages/node-common/src/FileSystemTutorialContentProvider.ts new file mode 100644 index 00000000000..a504c1bbd8b --- /dev/null +++ b/packages/node-common/src/FileSystemTutorialContentProvider.ts @@ -0,0 +1,47 @@ +import { + RawTutorialContent, + TutorialContentProvider, + TutorialId, +} from "@cursorless/common"; +import { readFile, readdir } from "node:fs/promises"; +import path from "path"; +import { loadFixture } from "./loadFixture"; + +export class FileSystemTutorialContentProvider + implements TutorialContentProvider +{ + private tutorialRootDir: string; + + constructor(assetsRoot: string) { + this.tutorialRootDir = path.join(assetsRoot, "tutorial"); + } + + async getRawTutorials() { + 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)), + ); + } + + async loadTutorialScript(tutorialId: string): Promise { + const buffer = await readFile( + path.join(this.tutorialRootDir, tutorialId, "script.json"), + ); + + return { + id: tutorialId, + ...JSON.parse(buffer.toString()), + }; + } + + async loadFixture(tutorialId: TutorialId, fixtureName: string) { + return loadFixture( + path.join(this.tutorialRootDir, tutorialId, fixtureName), + ); + } +} diff --git a/packages/node-common/src/index.ts b/packages/node-common/src/index.ts index 6eb6c2b07b7..9159de2a754 100644 --- a/packages/node-common/src/index.ts +++ b/packages/node-common/src/index.ts @@ -1,6 +1,7 @@ export * from "./FileSystemCommandHistoryStorage"; export * from "./FileSystemRawTreeSitterQueryProvider"; export * from "./FileSystemTalonSpokenForms"; +export * from "./FileSystemTutorialContentProvider"; export * from "./getCursorlessRepoRoot"; export * from "./getFixturePaths"; export * from "./getScopeTestPathsRecursively";