diff --git a/.vscode/settings.json b/.vscode/settings.json index 53cf2003..a0dca7e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,6 @@ "source.fixAll": true, }, "tslint.enable": true, - "tslint.jsEnable": true, "[javascript]": { "editor.formatOnSave": true }, diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..14c5a713 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ +# Todos +- add to scripts when url fixed + +``` +"postinstall": "node ./node_modules/vscode/bin/install", +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb72b493..581dde5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,15 +25,15 @@ } }, "@types/mocha": { - "version": "2.2.48", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", - "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", "dev": true }, "@types/node": { - "version": "10.14.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.7.tgz", - "integrity": "sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.4.tgz", + "integrity": "sha512-j8YL2C0fXq7IONwl/Ud5Kt0PeXw22zGERt+HSSnwbKOJVsAGkEz3sFCYwaF9IOuoG1HOtE0vKCj6sXF7Q0+Vaw==", "dev": true }, "agent-base": { @@ -166,6 +166,17 @@ "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "color-convert": { @@ -767,15 +778,6 @@ "tweetnacl": "~0.14.0" } }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -801,9 +803,9 @@ "dev": true }, "tslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.16.0.tgz", - "integrity": "sha512-UxG2yNxJ5pgGwmMzPMYh/CCnCnh0HfPgtlVRDs1ykZklufFBL1ZoTlWFRz2NQjcoEiDoRp+JyT0lhBbbH/obyA==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.17.0.tgz", + "integrity": "sha512-pflx87WfVoYepTet3xLfDOLDm9Jqi61UXIKePOuca0qoAZyrGWonDG9VTbji58Fy+8gciUn8Bt7y69+KEVjc/w==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -812,7 +814,7 @@ "commander": "^2.12.1", "diff": "^3.2.0", "glob": "^7.1.1", - "js-yaml": "^3.13.0", + "js-yaml": "^3.13.1", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "resolve": "^1.3.2", @@ -852,9 +854,9 @@ "dev": true }, "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.1.tgz", + "integrity": "sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==", "dev": true }, "uri-js": { @@ -923,6 +925,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true + }, + "xstate": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.6.0.tgz", + "integrity": "sha512-1bPy5an0QLUX3GiqtJB68VgBrLIotxZwpItDBO3vbakgzEY0D8UG1hsbM4MJM3BN1Y43pnac3ChmPL5s+Bca0A==" } } } diff --git a/package.json b/package.json index db918883..4cd38b8b 100644 --- a/package.json +++ b/package.json @@ -11,68 +11,36 @@ ], "publisher": "Shawn McKay ", "activationEvents": [ - "onCommand:coderoad.tutorial_load" + "onCommand:coderoad.start" ], "main": "./out/extension.js", "contributes": { "commands": [ { - "command": "coderoad.tutorial_setup", - "title": "Tutorial Setup", - "category": "CodeRoad" - }, - { - "command": "coderoad.tutorial_load", - "title": "Load Tutorial", - "category": "CodeRoad" - }, - { - "command": "coderoad.test_run", - "title": "Run Test", - "category": "CodeRoad" - }, - { - "command": "coderoad.solution_load", - "title": "Load Solution", + "command": "coderoad.start", + "title": "Start", "category": "CodeRoad" } - ], - "viewsContainers": { - "activitybar": [ - { - "id": "coderoad-tutorial", - "title": "CodeRoad Tutorial", - "icon": "resources/icons/icon.svg" - } - ] - }, - "views": { - "coderoad-tutorial": [ - { - "id": "tutorial-summary", - "name": "Summary" - }, - { - "id": "tutorial-steps", - "name": "Instructions" - } - ] - } + ] }, "scripts": { "vscode:prepublish": "npm run compile", + "machine": "node ./out/state/index.js", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "postinstall": "node ./node_modules/vscode/bin/install", "test": "npm run compile && node ./node_modules/vscode/bin/test" }, "devDependencies": { - "@types/mocha": "^2.2.42", - "@types/node": "^10.12.21", + "@types/mocha": "^5.2.7", + "@types/node": "^12.0.4", "prettier": "^1.17.1", - "tslint": "^5.12.1", + "tslint": "^5.17.0", "tslint-config-prettier": "^1.18.0", - "typescript": "^3.3.1", + "typescript": "^3.5.1", "vscode": "^1.1.28" + }, + "dependencies": { + "xstate": "^4.6.0" } } diff --git a/src/commands/index.ts b/src/commands/index.ts deleted file mode 100644 index cd8623c6..00000000 --- a/src/commands/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as vscode from 'vscode' - -import runTest from './runTest' -import tutorialLoad from './tutorialLoad' -import loadSolution from './loadSolution' -// import quit from './quit' - -const COMMANDS = { - TUTORIAL_SETUP: 'coderoad.tutorial_setup', - TUTORIAL_LOAD: 'coderoad.tutorial_load', - RUN_TEST: 'coderoad.test_run', - LOAD_SOLUTION: 'coderoad.solution_load', - // QUIT: 'coderoad.quit', -} - -export default (context: vscode.ExtensionContext): void => { - const commands = { - [COMMANDS.TUTORIAL_LOAD](): void { - tutorialLoad(context) - }, - [COMMANDS.RUN_TEST]: runTest, - [COMMANDS.LOAD_SOLUTION]: loadSolution, - // [COMMANDS.QUIT]: () => quit(context.subscriptions), - } - - for (const cmd in commands) { - const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) - context.subscriptions.push(command) - } -} diff --git a/src/commands/tutorialLoad.ts b/src/commands/tutorialLoad.ts deleted file mode 100644 index 08c2bd32..00000000 --- a/src/commands/tutorialLoad.ts +++ /dev/null @@ -1,130 +0,0 @@ -import * as vscode from 'vscode' -import * as CR from 'typings' - -import fetch from '../utils/fetch' -import tutorialSetup from '../services/tutorialSetup' -import { loadProgressPosition } from '../services/position' -import * as storage from '../services/storage' -import rootSetup from '../services/rootSetup' -import { isEmptyWorkspace, openReadme } from '../utils/workspace' -import * as git from '../services/git' - -/* -new -if current workspace is empty, use it -if not, open a new folder then start -*/ - -async function continueTutorial() { - // TODO: verify that tutorial is loaded in workspace - // TODO: verify progress - // TODO: verify setup - await loadProgressPosition() - await openReadme() -} - -async function newTutorial(tutorial: CR.Tutorial) { - // if workspace isn't empty, clear it out if given permission - const isEmpty: boolean = await isEmptyWorkspace() - if (!isEmpty) { - // eslint-disable-next-line - const options = ['Open a new folder', 'I\'ll clear the files and folders myself'] - const shouldOpenFolder = await vscode.window.showQuickPick(options) - if (shouldOpenFolder === options[0]) { - await vscode.commands.executeCommand('vscode.openFolder') - } - } - - await tutorialSetup(tutorial) - await openReadme() -} - -function onSaveHook(languageIds: string[]) { - // trigger command on save - vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { - if (languageIds.includes(document.languageId) && document.uri.scheme === 'file') { - // do work - vscode.commands.executeCommand('coderoad.test_run') - } - }) -} - -async function validateCanContinue(): Promise { - // validate tutorial & progress found in local storage - // validate git is setup with a remote - const [tutorial, progress, hasGit, hasGitRemote] = await Promise.all([ - storage.getTutorial(), - storage.getProgress(), - git.gitVersion(), - git.gitCheckRemoteExists(), - ]) - return !!(tutorial && progress && hasGit && hasGitRemote) -} - -export default async function tutorialLoad(context: vscode.ExtensionContext): Promise { - // setup connection to workspace - await rootSetup(context) - - const modes = ['New'] - - const canContinue = await validateCanContinue() - if (canContinue) { - modes.push('Continue') - } - - const selectedMode: string | undefined = await vscode.window.showQuickPick(modes) - - if (!selectedMode) { - throw new Error('No mode selected') - return - } - - interface TutorialQuickPickItem extends vscode.QuickPickItem { - id: string - } - - // load tutorial summaries - const tutorialsData: { [id: string]: CR.TutorialSummary } = await fetch({ - resource: 'getTutorialsSummary', - }) - const selectableTutorials: TutorialQuickPickItem[] = Object.keys(tutorialsData).map(id => { - const tutorial = tutorialsData[id] - return { - label: tutorial.title, - description: tutorial.description, - // detail: '', // optional additional info - id, - } - }) - const selectedTutorial: TutorialQuickPickItem | undefined = await vscode.window.showQuickPick(selectableTutorials) - - if (!selectedTutorial) { - throw new Error('No tutorial selected') - } - - // load specific tutorial - const tutorial: CR.Tutorial | undefined = await fetch({ - resource: 'getTutorial', - params: { id: selectedTutorial.id }, - }) - - if (!tutorial) { - throw new Error('No tutorial found') - } - - switch (selectedMode) { - // new tutorial - case modes[0]: - await newTutorial(tutorial) - break - // continue - case modes[1]: - await continueTutorial() - break - } - - // setup hook to run tests on save - onSaveHook(tutorial.meta.languages) - - // TODO: start -} diff --git a/src/utils/channel.ts b/src/editor/channel.ts similarity index 100% rename from src/utils/channel.ts rename to src/editor/channel.ts diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts new file mode 100644 index 00000000..d923f11a --- /dev/null +++ b/src/editor/commands/index.ts @@ -0,0 +1,31 @@ +import * as vscode from 'vscode' +import start from './start' + +// import runTest from './runTest' +// import loadSolution from './loadSolution' +// import quit from './quit' + +const COMMANDS = { + // TUTORIAL_SETUP: 'coderoad.tutorial_setup', + START: 'coderoad.start', + // RUN_TEST: 'coderoad.test_run', + // LOAD_SOLUTION: 'coderoad.solution_load', + // QUIT: 'coderoad.quit', +} + + +export default (context: vscode.ExtensionContext): void => { + const commands = { + [COMMANDS.START]: async function startCommand(): Promise { + return start(context) + }, + // [COMMANDS.RUN_TEST]: runTest, + // [COMMANDS.LOAD_SOLUTION]: loadSolution, + // [COMMANDS.QUIT]: () => quit(context.subscriptions), + } + + for (const cmd in commands) { + const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) + context.subscriptions.push(command) + } +} diff --git a/src/commands/loadSolution.ts b/src/editor/commands/loadSolution.ts similarity index 82% rename from src/commands/loadSolution.ts rename to src/editor/commands/loadSolution.ts index 442f83b7..22d41257 100644 --- a/src/commands/loadSolution.ts +++ b/src/editor/commands/loadSolution.ts @@ -1,6 +1,6 @@ import * as CR from 'typings' -import * as storage from '../services/storage' -import { gitLoadCommits, gitClear } from '../services/git' +import * as storage from '../../services/storage' +import { gitLoadCommits, gitClear } from '../../services/git' export default async function loadSolution(): Promise { const [position, tutorial]: [CR.Position, CR.Tutorial | undefined] = await Promise.all([ diff --git a/src/commands/quit.ts b/src/editor/commands/quit.ts similarity index 100% rename from src/commands/quit.ts rename to src/editor/commands/quit.ts diff --git a/src/commands/runTest.ts b/src/editor/commands/runTest.ts similarity index 94% rename from src/commands/runTest.ts rename to src/editor/commands/runTest.ts index a3bd40f8..6e959433 100644 --- a/src/commands/runTest.ts +++ b/src/editor/commands/runTest.ts @@ -1,7 +1,7 @@ -import { getOutputChannel } from '../utils/channel' -import { exec } from '../utils/node' -import * as storage from '../services/storage' -import * as testResult from '../services/testResult' +import { getOutputChannel } from '../channel' +import { exec } from '../../services/node' +import * as storage from '../../services/storage' +import * as testResult from '../../services/testResult' // ensure only latest run_test action is taken let currentId = 0 diff --git a/src/editor/commands/start-old.ts b/src/editor/commands/start-old.ts new file mode 100644 index 00000000..5d172fbc --- /dev/null +++ b/src/editor/commands/start-old.ts @@ -0,0 +1,108 @@ +import * as vscode from 'vscode' +import * as CR from 'typings' + +import tutorialSetup from '../../services/tutorialSetup' +import { isEmptyWorkspace, openReadme } from '../workspace' +import { setWorkspaceRoot } from '../../services/node' +import { setStorage } from '../../editor/storage' + +/* +new +if current workspace is empty, use it +if not, open a new folder then start +*/ + +// async function continueTutorial() { +// // TODO: verify that tutorial is loaded in workspace +// // TODO: verify progress +// // TODO: verify setup +// await loadProgressPosition() +// await openReadme() +// } + +async function newTutorial(tutorial: CR.Tutorial) { + // if workspace isn't empty, clear it out if given permission + const isEmpty: boolean = await isEmptyWorkspace() + if (!isEmpty) { + // eslint-disable-next-line + const options = ['Open a new folder', 'I\'ll clear the files and folders myself'] + const shouldOpenFolder = await vscode.window.showQuickPick(options) + if (shouldOpenFolder === options[0]) { + await vscode.commands.executeCommand('vscode.openFolder') + } + } + + await tutorialSetup(tutorial) + await openReadme() +} + + +export default async function start(context: vscode.ExtensionContext): Promise { + console.log('start', context) + + + return; + + // const modes = ['New'] + + // const canContinue = await validateCanContinue() + // if (canContinue) { + // modes.push('Continue') + // } + + // const selectedMode: string | undefined = await vscode.window.showQuickPick(modes) + + // if (!selectedMode) { + // throw new Error('No mode selected') + // return + // } + + // interface TutorialQuickPickItem extends vscode.QuickPickItem { + // id: string + // } + + // // load tutorial summaries + // const tutorialsData: { [id: string]: CR.TutorialSummary } = await api({ + // resource: 'getTutorialsSummary', + // }) + // const selectableTutorials: TutorialQuickPickItem[] = Object.keys(tutorialsData).map(id => { + // const tutorial = tutorialsData[id] + // return { + // label: tutorial.title, + // description: tutorial.description, + // // detail: '', // optional additional info + // id, + // } + // }) + // const selectedTutorial: TutorialQuickPickItem | undefined = await vscode.window.showQuickPick(selectableTutorials) + + // if (!selectedTutorial) { + // throw new Error('No tutorial selected') + // } + + // // load specific tutorial + // const tutorial: CR.Tutorial | undefined = await fetch({ + // resource: 'getTutorial', + // params: { id: selectedTutorial.id }, + // }) + + // if (!tutorial) { + // throw new Error('No tutorial found') + // } + + // switch (selectedMode) { + // // new tutorial + // case modes[0]: + // await newTutorial(tutorial) + // break + // // continue + // case modes[1]: + // await continueTutorial() + // break + // } + + // // setup hook to run tests on save + + + // TODO: start +} diff --git a/src/editor/commands/start.ts b/src/editor/commands/start.ts new file mode 100644 index 00000000..84c4a77a --- /dev/null +++ b/src/editor/commands/start.ts @@ -0,0 +1,45 @@ +import * as vscode from 'vscode' +import { setWorkspaceRoot } from '../../services/node' +import { setStorage } from '../../editor/storage' +import { activate as activateMachine, default as machine } from '../../state' +import * as storage from '../../services/storage' +import * as git from '../../services/git' +import * as CR from 'typings' + +let initialTutorial: CR.Tutorial | undefined +let initialProgress: CR.Progress = { + levels: {}, + stages: {}, + steps: {}, + complete: false, +} + +export default async function start(context: vscode.ExtensionContext): Promise { + console.log('TUTORIAL_START') + + // setup connection to workspace + await setWorkspaceRoot() + // set workspace context path + await setStorage(context.workspaceState) + + // initialize state machine + activateMachine() + + console.log('ACTION: start') + + // verify that the user has a tutorial & progress + // verify git is setup with a coderoad remote + const [tutorial, progress, hasGit, hasGitRemote] = await Promise.all([ + storage.getTutorial(), + storage.getProgress(), + git.gitVersion(), + git.gitCheckRemoteExists(), + ]) + initialTutorial = tutorial + initialProgress = progress + const canContinue = !!(tutorial && progress && hasGit && hasGitRemote) + console.log('canContinue', canContinue) + // if a tutorial exists, "CONTINUE" + // otherwise start from "NEW" + machine.send(canContinue ? 'CONTINUE' : 'NEW') +} \ No newline at end of file diff --git a/src/editor/init.ts b/src/editor/init.ts new file mode 100644 index 00000000..dbc9f64a --- /dev/null +++ b/src/editor/init.ts @@ -0,0 +1,33 @@ +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode' + +import { deactivate as deactivateMachine } from '../state' +import createCommands from './commands' +import createViews from './views' + +// this method is called when your extension is activated +// your extension is activated the very first time the command is executed +export function activate(context: vscode.ExtensionContext) { + console.log('ACTIVATE!') + + // commands + createCommands(context) + + // views + createViews(context) + + // tasks + // add tasks here +} + +// this method is called when your extension is deactivated +export function deactivate(context: vscode.ExtensionContext): void { + // cleanup subscriptions/tasks + console.log('deactivate context', context) + for (const disposable of context.subscriptions) { + disposable.dispose() + } + // shut down state machine + deactivateMachine() +} diff --git a/src/editor/save.ts b/src/editor/save.ts new file mode 100644 index 00000000..5aee7a5f --- /dev/null +++ b/src/editor/save.ts @@ -0,0 +1,13 @@ +import * as vscode from 'vscode' + +function onSaveHook(languageIds: string[]) { + // trigger command on save + vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => { + if (languageIds.includes(document.languageId) && document.uri.scheme === 'file') { + // do work + vscode.commands.executeCommand('coderoad.test_run') + } + }) +} + +export default onSaveHook \ No newline at end of file diff --git a/src/editor/storage.ts b/src/editor/storage.ts new file mode 100644 index 00000000..a7d9f848 --- /dev/null +++ b/src/editor/storage.ts @@ -0,0 +1,18 @@ +import * as CR from 'typings' +import * as vscode from 'vscode' + +let storage: vscode.Memento + +// storage must be set initially +export function setStorage(workspaceState: vscode.Memento): void { + storage = workspaceState +} + +export function get(key: string): T | undefined { + return storage.get(key) +} + +export function update(key: string, value: string | Object): Thenable { + return storage.update(key, value) +} + diff --git a/src/utils/nonce.ts b/src/editor/utils/nonce.ts similarity index 100% rename from src/utils/nonce.ts rename to src/editor/utils/nonce.ts diff --git a/src/editor/views/index.ts b/src/editor/views/index.ts new file mode 100644 index 00000000..60a7d871 --- /dev/null +++ b/src/editor/views/index.ts @@ -0,0 +1,7 @@ +import * as vscode from 'vscode' + +const createViews = (context: vscode.ExtensionContext) => { + // TODO: create views +} + +export default createViews diff --git a/src/utils/workspace.ts b/src/editor/workspace.ts similarity index 96% rename from src/utils/workspace.ts rename to src/editor/workspace.ts index 599d19a9..ed1d456c 100644 --- a/src/utils/workspace.ts +++ b/src/editor/workspace.ts @@ -1,7 +1,7 @@ import * as fs from 'fs' import * as path from 'path' import * as vscode from 'vscode' -import { exec, exists } from './node' +import { exec, exists } from '../services/node' export async function isEmptyWorkspace(): Promise { const { stdout, stderr } = await exec('ls') diff --git a/src/extension.ts b/src/extension.ts index 2ef8c00a..40646445 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,30 +1,2 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below -import * as vscode from 'vscode' +export { activate, deactivate } from './editor/init' -import createCommands from './commands' -import createViews from './views' - -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { - console.log('ACTIVATE!') - - // commands - createCommands(context) - - // tasks - // add tasks here - - // views - createViews(context) -} - -// this method is called when your extension is deactivated -export function deactivate(context: vscode.ExtensionContext): void { - // cleanup subscriptions/tasks - console.log('deactivate context', context) - for (const disposable of context.subscriptions) { - disposable.dispose() - } -} diff --git a/src/utils/fetch.ts b/src/services/api/fetch.ts similarity index 92% rename from src/utils/fetch.ts rename to src/services/api/fetch.ts index d1a53b89..206ae0eb 100644 --- a/src/utils/fetch.ts +++ b/src/services/api/fetch.ts @@ -1,7 +1,7 @@ import * as CR from 'typings' // temporary tutorials -import basicTutorial from '../tutorials/basic' +import basicTutorial from '../../state/context/tutorials/basic' interface Options { resource: string diff --git a/src/services/api/index.ts b/src/services/api/index.ts new file mode 100644 index 00000000..206ae0eb --- /dev/null +++ b/src/services/api/index.ts @@ -0,0 +1,32 @@ +import * as CR from 'typings' + +// temporary tutorials +import basicTutorial from '../../state/context/tutorials/basic' + +interface Options { + resource: string + params?: any +} + +const tutorialsData: { [key: string]: CR.Tutorial } = { + tutorialId: basicTutorial, +} + +// TODO: replace with fetch resource +export default async function fetch(options: Options): Promise { + console.log('options', options) + switch (options.resource) { + case 'getTutorialsSummary': + // list of ids with summaries + let data: { [id: string]: CR.TutorialSummary } = {} + for (const tutorial of Object.values(tutorialsData)) { + data[tutorial.id] = tutorial.data.summary + } + return data + case 'getTutorial': + // specific tutorial by id + return tutorialsData[options.params.id] + default: + throw new Error('Resource not found') + } +} diff --git a/src/services/fs.ts b/src/services/fs.ts deleted file mode 100644 index 5bd52296..00000000 --- a/src/services/fs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { exec } from '../utils/node' - -export async function clear(): Promise { - // remove all files including ignored - // NOTE: Linux only - const command = 'ls -A1 | xargs rm -rf' - const { stderr } = await exec(command) - if (stderr) { - console.error(stderr) - throw new Error('Error removing all files & folders') - } -} diff --git a/src/services/git.ts b/src/services/git/index.ts similarity index 98% rename from src/services/git.ts rename to src/services/git/index.ts index f6cb33fc..347ef142 100644 --- a/src/services/git.ts +++ b/src/services/git/index.ts @@ -1,5 +1,5 @@ import * as CR from 'typings' -import { exec, exists, openFile } from '../utils/node' +import { exec, exists, openFile } from '../node' const gitOrigin = 'coderoad' diff --git a/src/utils/node.ts b/src/services/node/index.ts similarity index 79% rename from src/utils/node.ts rename to src/services/node/index.ts index 8d5c4c46..0a5234d9 100644 --- a/src/utils/node.ts +++ b/src/services/node/index.ts @@ -37,3 +37,15 @@ export const openFile = async (relativeFilePath: string): Promise => { console.log(`Failed to open file ${relativeFilePath}`, error) } } + + +// export async function clear(): Promise { +// // remove all files including ignored +// // NOTE: Linux only +// const command = 'ls -A1 | xargs rm -rf' +// const { stderr } = await exec(command) +// if (stderr) { +// console.error(stderr) +// throw new Error('Error removing all files & folders') +// } +// } \ No newline at end of file diff --git a/src/services/rootSetup.ts b/src/services/rootSetup.ts deleted file mode 100644 index 04c372a3..00000000 --- a/src/services/rootSetup.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as vscode from 'vscode' -import { setWorkspaceRoot } from '../utils/node' -import { setStorage } from './storage' - -export default async function setupRoot(context: vscode.ExtensionContext) { - await setWorkspaceRoot() - await setStorage(context.workspaceState) -} diff --git a/src/services/storage.ts b/src/services/storage.ts index b108edbd..397833bf 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -1,22 +1,15 @@ import * as CR from 'typings' -import * as vscode from 'vscode' - -let storage: vscode.Memento - -// storage must be set initially -export function setStorage(workspaceState: vscode.Memento): void { - storage = workspaceState -} +import * as storage from '../editor/storage' // TUTORIAL const STORE_TUTORIAL = 'coderoad:tutorial' export async function getTutorial(): Promise { - return storage.get(STORE_TUTORIAL) + return storage.get(STORE_TUTORIAL) } export async function setTutorial(tutorial: CR.Tutorial): Promise { - await storage.update(STORE_TUTORIAL, tutorial) + await storage.update(STORE_TUTORIAL, tutorial) } // POSITION @@ -25,12 +18,12 @@ const STORE_POSITION = 'coderoad:position' const defaultPosition = { levelId: '', stageId: '', stepId: '' } export async function getPosition(): Promise { - const position: CR.Position | undefined = storage.get(STORE_POSITION) - return position || defaultPosition + const position: CR.Position | undefined = storage.get(STORE_POSITION) + return position || defaultPosition } export async function setPosition(position: CR.Position): Promise { - await storage.update(STORE_POSITION, position) + await storage.update(STORE_POSITION, position) } // PROGRESS @@ -39,46 +32,46 @@ const STORE_PROGRESS = 'coderoad:progress' const defaultProgress = { levels: {}, stages: {}, steps: {}, hints: {}, complete: false } export async function getProgress(): Promise { - const progress: CR.Progress | undefined = await storage.get(STORE_PROGRESS) - return progress || defaultProgress + const progress: CR.Progress | undefined = await storage.get(STORE_PROGRESS) + return progress || defaultProgress } export async function resetProgress(): Promise { - await storage.update(STORE_PROGRESS, defaultProgress) + await storage.update(STORE_PROGRESS, defaultProgress) } interface ProgressUpdate { - levels?: { - [levelId: string]: boolean - } - stages?: { - [stageid: string]: boolean - } - steps?: { - [stepId: string]: boolean - } + levels?: { + [levelId: string]: boolean + } + stages?: { + [stageid: string]: boolean + } + steps?: { + [stepId: string]: boolean + } } export async function updateProgress(record: ProgressUpdate): Promise { - const progress = await getProgress() - if (record.levels) { - progress.levels = { - ...progress.levels, - ...record.levels, + const progress = await getProgress() + if (record.levels) { + progress.levels = { + ...progress.levels, + ...record.levels, + } } - } - if (record.stages) { - progress.stages = { - ...progress.stages, - ...record.stages, + if (record.stages) { + progress.stages = { + ...progress.stages, + ...record.stages, + } } - } - if (record.steps) { - progress.steps = { - ...progress.steps, - ...record.steps, + if (record.steps) { + progress.steps = { + ...progress.steps, + ...record.steps, + } } - } - await storage.update(STORE_PROGRESS, progress) + await storage.update(STORE_PROGRESS, progress) } diff --git a/src/services/tutorialSetup.ts b/src/services/tutorialSetup.ts index ea9b10ee..140604d7 100644 --- a/src/services/tutorialSetup.ts +++ b/src/services/tutorialSetup.ts @@ -1,7 +1,7 @@ import * as CR from 'typings' import * as position from '../services/position' import * as storage from '../services/storage' -import { isEmptyWorkspace } from '../utils/workspace' +import { isEmptyWorkspace } from '../editor/workspace' import { gitLoadCommits, gitInitIfNotExists, gitSetupRemote } from '../services/git' const testRepo = 'https://github.com/ShMcK/coderoad-tutorial-basic.git' diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts new file mode 100644 index 00000000..c3385df5 --- /dev/null +++ b/src/state/actions/index.ts @@ -0,0 +1,50 @@ +import { assign } from 'xstate' +import * as CR from 'typings' +import * as storage from '../../services/storage' +import * as git from '../../services/git' + +let initialTutorial: CR.Tutorial | undefined +let initialProgress: CR.Progress = { + levels: {}, + stages: {}, + steps: {}, + complete: false, +} + +export default { + tutorialLoad: assign({ + // load initial data, progress & position + data(): CR.TutorialData { + console.log('ACTION: tutorialLoad.data') + if (!initialTutorial) { + throw new Error('No Tutorial loaded') + } + return initialTutorial.data + + }, + progress(): CR.Progress { + console.log('ACTION: tutorialLoad.progress') + return initialProgress + }, + position() { + console.log('ACTION: tutorialLoad.position') + if (!initialTutorial) { + throw new Error('No Tutorial loaded') + } + const { data } = initialTutorial + + const levelId = data.summary.levelList[0] + const stageId = data.levels[levelId].stageList[0] + const stepId = data.stages[stageId].stepList[0] + + const position = { + levelId, + stageId, + stepId, + } + + return position + } + }), + +} \ No newline at end of file diff --git a/src/state/context/index.ts b/src/state/context/index.ts new file mode 100644 index 00000000..3b0443a4 --- /dev/null +++ b/src/state/context/index.ts @@ -0,0 +1,20 @@ +import basicTutorialData from './tutorials/basic' +import * as CR from 'typings' + +const tutorialContext: CR.MachineContext = { + position: { + levelId: '', + stageId: '', + stepId: '', + }, + progress: { + levels: {}, + stages: {}, + steps: {}, + complete: false, + }, + // TODO: load tutorial instead of preloading demo + data: basicTutorialData.data, +} + +export default tutorialContext \ No newline at end of file diff --git a/src/tutorials/basic.ts b/src/state/context/tutorials/basic.ts similarity index 100% rename from src/tutorials/basic.ts rename to src/state/context/tutorials/basic.ts diff --git a/src/state/guards/index.ts b/src/state/guards/index.ts new file mode 100644 index 00000000..563fac43 --- /dev/null +++ b/src/state/guards/index.ts @@ -0,0 +1,9 @@ +import * as CR from 'typings' + +export default { + // skip to the stage if the level has already been started + hasNoNextLevelProgress: (context: CR.MachineContext): boolean => { + console.log('GUARD: hasNoNextLevelProgress') + return false + }, +} diff --git a/src/state/index.ts b/src/state/index.ts new file mode 100644 index 00000000..376b66ef --- /dev/null +++ b/src/state/index.ts @@ -0,0 +1,31 @@ +import { interpret } from 'xstate' +import machine from './machine' + +const machineOptions = { + logger: console.log, + devTools: true, + deferEvents: true, + execute: true +} +// machine interpreter +// https://xstate.js.org/docs/guides/interpretation.html +const service = interpret(machine, machineOptions) + // logging + .onTransition(state => { + console.log('state', state) + if (state.changed) { + console.log('transition') + console.log(state.value) + } + }) + +export function activate() { + // initialize + service.start() +} + +export function deactivate() { + service.stop() +} + +export default service diff --git a/src/state/machine.ts b/src/state/machine.ts new file mode 100644 index 00000000..18cad205 --- /dev/null +++ b/src/state/machine.ts @@ -0,0 +1,159 @@ +import { Machine, send } from 'xstate' +import * as CR from 'typings' + +import actions from './actions' +import guards from './guards' +import initialContext from './context' + +export const machine = Machine< + CR.MachineContext, + CR.MachineStateSchema, + CR.MachineEvent +>( + { + id: 'tutorial', + context: initialContext, + initial: 'SelectTutorial', + states: { + SelectTutorial: { + initial: 'Initial', + states: { + Initial: { + on: { + CONTINUE: 'ContinueTutorial', + NEW: 'NewTutorial', + }, + }, + NewTutorial: { + initial: 'SelectTutorial', + states: { + SelectTutorial: { + on: { + TUTORIAL_START: 'InitializeTutorial', + }, + }, + InitializeTutorial: { + on: { + TUTORIAL_LOADED: 'Tutorial' + } + }, + } + + }, + ContinueTutorial: { + onEntry: 'tutorialLoad', + on: { + TUTORIAL_START: { + target: 'Tutorial.LoadNext', + } + } + }, + } + }, + Tutorial: { + initial: 'Summary', + states: { + LoadNext: { + onEntry: () => send('LOAD_NEXT'), + on: { + LOAD_NEXT: [ + { + target: 'Level', + cond: 'hasNoNextLevelProgress', + }, + { + target: 'Stage', + }, + ], + }, + }, + + Summary: { + on: { + NEXT: 'Level', + }, + }, + Level: { + onEntry: ['loadLevel'], + on: { + NEXT: 'Stage', + BACK: 'Summary', + }, + }, + Stage: { + onEntry: ['loadStage'], + initial: 'StageNormal', + states: { + StageNormal: { + on: { + TEST_RUN: 'TestRunning', + STEP_SOLUTION_LOAD: { + actions: ['callSolution'], + }, + }, + }, + TestRunning: { + on: { + TEST_SUCCESS: [ + { + target: 'StageComplete', + cond: 'tasksComplete', + }, + { + target: 'TestPass', + }, + ], + TEST_FAILURE: 'TestFail', + }, + }, + TestPass: { + onEntry: ['stepComplete'], + on: { + NEXT: [ + { + target: 'StageNormal', + cond: 'hasNextStep', + }, + { + target: 'StageComplete', + }, + ], + }, + }, + TestFail: { + on: { + RETURN: 'StageNormal', + }, + }, + StageComplete: { + on: { + NEXT: [ + { + target: 'Stage', + cond: 'hasNextStage', + }, + { + target: 'Level', + cond: 'hasNextLevel', + }, + { + target: 'EndTutorial', + }, + ], + }, + }, + }, + }, + EndTutorial: {}, + } + } + } + }, + { + actions, + guards, + activities: {}, + }, +) + +export default machine \ No newline at end of file diff --git a/src/typings/context.d.ts b/src/typings/context.d.ts new file mode 100644 index 00000000..66155638 --- /dev/null +++ b/src/typings/context.d.ts @@ -0,0 +1,21 @@ +import * as CR from './index' + +export interface Step extends Exclude { + status: { + complete: boolean + active: boolean + } +} + +export interface ReceivedEvent { + data: CR.Action +} + +export interface StageStepStatus { + active: boolean + complete: boolean +} + +export interface StageWithStatus extends CR.TutorialStage { + status: StageStepStatus +} diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index c4e5e07e..d98e8049 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -104,3 +104,59 @@ export interface Action { payload?: any meta?: any } + +export interface MachineContext { + position: Position + data: { + summary: TutorialSummary + levels: { + [levelId: string]: TutorialLevel + } + stages: { + [stageId: string]: TutorialStage + } + steps: { + [stepId: string]: TutorialStep + } + } + progress: Progress +} + +export interface MachineEvent { + type: string + payload?: any +} + +export interface MachineStateSchema { + states: { + SelectTutorial: { + states: { + Initial: {} + NewTutorial: { + states: { + SelectTutorial: {} + InitializeTutorial: {} + } + } + ContinueTutorial: {} + } + } + Tutorial: { + states: { + LoadNext: {} + Summary: {} + Level: {} + Stage: { + states: { + StageNormal: {} + TestRunning: {} + TestPass: {} + TestFail: {} + StageComplete: {} + } + } + EndTutorial: {} + } + } + } +} \ No newline at end of file diff --git a/src/views/index.ts b/src/views/index.ts deleted file mode 100644 index c550d756..00000000 --- a/src/views/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as vscode from 'vscode' - -const createViews = (context: vscode.ExtensionContext) => { - // TODO: level/stage select - // TODO: summary view - // TODO: instruction view - // docs: https://code.visualstudio.com/api/extension-guides/tree-view - // vscode.window.registerTreeDataProvider('nodeDependencies', new TreeDataProvider(context.workspace)) -} - -export default createViews