From 481807c47cf46fa72107a91e24a9a179615412ad Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sat, 8 Jun 2019 11:10:29 -0700 Subject: [PATCH 01/12] run test action --- src/editor/commands/index.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index 2d000bb9..ad8f39be 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -2,15 +2,15 @@ import * as vscode from 'vscode' import start from './start' import ReactPanel from '../views/createWebview' -// import runTest from './runTest' +import runTest from './runTest' // import loadSolution from './loadSolution' // import quit from './quit' const COMMANDS = { // TUTORIAL_SETUP: 'coderoad.tutorial_setup', START: 'coderoad.start', - OPEN_WEBVIEW: 'coderoad.open_webview' - // RUN_TEST: 'coderoad.test_run', + OPEN_WEBVIEW: 'coderoad.open_webview', + RUN_TEST: 'coderoad.test_run', // LOAD_SOLUTION: 'coderoad.solution_load', // QUIT: 'coderoad.quit', } @@ -18,13 +18,15 @@ const COMMANDS = { export default (context: vscode.ExtensionContext): void => { const commands = { - [COMMANDS.START]: async function startCommand(): Promise<void> { - return start(context) + [COMMANDS.START]: () => { + start(context) }, [COMMANDS.OPEN_WEBVIEW]: () => { ReactPanel.createOrShow(context.extensionPath); }, - // [COMMANDS.RUN_TEST]: runTest, + [COMMANDS.RUN_TEST]: () => { + runTest() + }, // [COMMANDS.LOAD_SOLUTION]: loadSolution, // [COMMANDS.QUIT]: () => quit(context.subscriptions), } From d00a485520f4e121797b8531695aa02e47d3d28a Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sat, 8 Jun 2019 12:06:17 -0700 Subject: [PATCH 02/12] progress refactoring webview, editor, machine --- src/editor/ReactWebView.ts | 146 ++++++++++++++++++++++++++++++ src/editor/commands/start.ts | 4 +- src/editor/index.ts | 75 +++++++++++++++ src/editor/init.ts | 33 ------- src/editor/views/createWebview.ts | 141 ----------------------------- src/editor/views/index.ts | 7 -- src/editor/views/onReceive.ts | 6 -- src/editor/views/utils/nonce.ts | 8 -- src/extension.ts | 11 ++- src/state/index.ts | 57 +++++++----- src/state/message.ts | 13 +++ src/typings/index.d.ts | 10 +- 12 files changed, 288 insertions(+), 223 deletions(-) create mode 100644 src/editor/ReactWebView.ts create mode 100644 src/editor/index.ts delete mode 100644 src/editor/init.ts delete mode 100644 src/editor/views/createWebview.ts delete mode 100644 src/editor/views/index.ts delete mode 100644 src/editor/views/onReceive.ts delete mode 100644 src/editor/views/utils/nonce.ts create mode 100644 src/state/message.ts diff --git a/src/editor/ReactWebView.ts b/src/editor/ReactWebView.ts new file mode 100644 index 00000000..56247edc --- /dev/null +++ b/src/editor/ReactWebView.ts @@ -0,0 +1,146 @@ +import * as vscode from 'vscode' +import * as CR from 'typings' +import * as path from 'path' + +function getNonce(): string { + let text = '' + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) + } + return text +} + +// TODO: move column into createOrShow + + +/** + * Manages React webview panels + */ +class ReactWebView { + /** + * Track the currently panel. Only allow a single panel to exist at a time. + */ + public static currentPanel: ReactWebView | undefined = undefined + + // @ts-ignore + private panel: vscode.WebviewPanel + private extensionPath: string + private disposables: vscode.Disposable[] = [] + private onReceive: any // TODO: properly type + + public constructor(extensionPath: string, onReceive: any) { + this.extensionPath = extensionPath + this.onReceive = onReceive + } + + public async createOrShow(extensionPath: string, column: number = vscode.ViewColumn.One): Promise<void> { + const hasActiveEditor = vscode.window.activeTextEditor + + if (!hasActiveEditor) { + throw new Error('Should have an open file on launch') + } + + // If we already have a panel, show it. + // Otherwise, create a new panel. + if (ReactWebView.currentPanel && ReactWebView.currentPanel.panel) { + ReactWebView.currentPanel.panel.reveal(column) + } else { + const viewType = 'CodeRoad' + const title = 'CodeRoad' + const config = { + // Enable javascript in the webview + enableScripts: true, + + // And restric the webview to only loading content from our extension's `media` directory. + localResourceRoots: [vscode.Uri.file(path.join(this.extensionPath, 'build'))], + + // prevents destroying the window when it is in the background + retainContextWhenHidden: true, + } + // Create and show a new webview panel + this.panel = vscode.window.createWebviewPanel(viewType, title, column, config) + + // Set the webview's initial html content + this.panel.webview.html = this.getHtmlForWebview() + + // Listen for when the panel is disposed + // This happens when the user closes the panel or when the panel is closed programatically + this.panel.onDidDispose(() => this.dispose(), null, this.disposables) + + // Handle messages from the webview + this.panel.webview.onDidReceiveMessage(this.onReceive, null, this.disposables) + } + } + + public async postMessage(action: CR.Action): Promise<void> { + // Send a message to the webview webview. + // You can send any JSON serializable data. + const success = await this.panel.webview.postMessage(action) + if (!success) { + throw new Error(`Message post failure: ${JSON.stringify(action)}`) + } + } + + public dispose(): void { + ReactWebView.currentPanel = undefined + + // Clean up our resources + this.panel.dispose() + + while (this.disposables.length) { + const x = this.disposables.pop() + if (x) { + x.dispose() + } + } + } + + private getHtmlForWebview(): string { + + // eslint-disable-next-line + const manifest = require(path.join(this.extensionPath, 'build', 'asset-manifest.json')) + const mainScript = manifest.files['main.js'] + // grab first chunk + const chunk = Object.keys(manifest.files).filter(f => f.match(/^static\/js\/.+\.js$/))[0] + const chunkScript = manifest.files[chunk] + const mainStyle = manifest.files['main.css'] + + const scriptPathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'build', mainScript)) + const scriptUri = scriptPathOnDisk.with({ scheme: 'vscode-resource' }) + const chunkPathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'build', chunkScript)) + const chunkUri = chunkPathOnDisk.with({ scheme: 'vscode-resource' }) + const stylePathOnDisk = vscode.Uri.file(path.join(this.extensionPath, 'build', mainStyle)) + const styleUri = stylePathOnDisk.with({ scheme: 'vscode-resource' }) + + // Use a nonce to whitelist which scripts can be run + const nonce = getNonce() + const nonce2 = getNonce() + const nonce3 = getNonce() + + return `<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> + <meta name="theme-color" content="#000000"> + <title>React App</title> + <link rel="manifest" href="./manifest.json" /> + <link rel="stylesheet" type="text/css" href="${styleUri}"> + <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'nonce-${nonce}' 'nonce-${nonce2}' 'nonce-${nonce3}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;"> + <base href="${vscode.Uri.file(path.join(this.extensionPath, 'build')).with({ scheme: 'vscode-resource' })}/"> + <style></style> + </head> + + <body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root">Loading...</div> + <script nonce=${nonce} src="./webpackBuild.js"></script> + <script nonce=${nonce2} src="${chunkUri}"></script> + <script nonce="${nonce3}" src="${scriptUri}"></script> + </body> + </html>` + } +} + +export default ReactWebView diff --git a/src/editor/commands/start.ts b/src/editor/commands/start.ts index 84c4a77a..48b44ddb 100644 --- a/src/editor/commands/start.ts +++ b/src/editor/commands/start.ts @@ -18,9 +18,9 @@ export default async function start(context: vscode.ExtensionContext): Promise<v console.log('TUTORIAL_START') // setup connection to workspace - await setWorkspaceRoot() + // await setWorkspaceRoot() // set workspace context path - await setStorage(context.workspaceState) + // await setStorage(context.workspaceState) // initialize state machine activateMachine() diff --git a/src/editor/index.ts b/src/editor/index.ts new file mode 100644 index 00000000..3b9b40d1 --- /dev/null +++ b/src/editor/index.ts @@ -0,0 +1,75 @@ +import * as vscode from 'vscode' +import * as CR from '../typings' +import { setStorage } from './storage' +import ReactWebView from './ReactWebView' + +class Editor { + // extension context set on activation + // @ts-ignore + private context: vscode.ExtensionContext + private workspaceRoot: string | undefined + private machine: CR.StateMachine + private webview: any + + private COMMANDS = { + START: 'coderoad.start', + OPEN_WEBVIEW: 'coderoad.open_webview', + RUN_TEST: 'coderoad.test_run', + } + + constructor(machine: CR.StateMachine) { + this.machine = machine + } + + private commandStart() { + // set workspace root + const { rootPath } = vscode.workspace + if (!rootPath) { + throw new Error('Requires a workspace. Please open a folder') + } + this.workspaceRoot = rootPath + + // set local storage workspace + setStorage(this.context.workspaceState) + + // activate machine + this.machine.activate() + this.webview = new ReactWebView(this.context.extensionPath, this.machine.onReceive) + } + + private activateCommands() { + const { COMMANDS } = this + const commands = { + [COMMANDS.START]: () => { + this.commandStart() + }, + [COMMANDS.OPEN_WEBVIEW]: () => { + this.webview.createOrShow(this.context.extensionPath); + }, + } + for (const cmd in commands) { + const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) + this.context.subscriptions.push(command) + } + } + public activate(context: vscode.ExtensionContext): void { + console.log('ACTIVATE!') + this.context = context + // commands + this.activateCommands() + + // setup tasks or views here + + } + public deactivate(): void { + console.log('DEACTIVATE!') + // cleanup subscriptions/tasks + for (const disposable of this.context.subscriptions) { + disposable.dispose() + } + // shut down state machine + this.machine.deactivate() + } +} + +export default Editor \ No newline at end of file diff --git a/src/editor/init.ts b/src/editor/init.ts deleted file mode 100644 index dbc9f64a..00000000 --- a/src/editor/init.ts +++ /dev/null @@ -1,33 +0,0 @@ -// 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/views/createWebview.ts b/src/editor/views/createWebview.ts deleted file mode 100644 index 321403a0..00000000 --- a/src/editor/views/createWebview.ts +++ /dev/null @@ -1,141 +0,0 @@ -import * as vscode from 'vscode' -import * as CR from 'typings' -import * as path from 'path' - -import getNonce from './utils/nonce' -import onReceive from './onReceive' - -/** - * Manages React webview panels - */ -class ReactPanel { - /** - * Track the currently panel. Only allow a single panel to exist at a time. - */ - public static currentPanel: ReactPanel | undefined = undefined - - private readonly _panel: vscode.WebviewPanel - private readonly _extensionPath: string - private _disposables: vscode.Disposable[] = [] - - public static async createOrShow(extensionPath: string): Promise<void> { - // const hasActiveEditor = vscode.window.activeTextEditor - - // if (!hasActiveEditor) { - // throw new Error('Should have an open file on launch') - // } - const column = vscode.ViewColumn.One - - // If we already have a panel, show it. - // Otherwise, create a new panel. - if (ReactPanel.currentPanel) { - console.log('--- HAS CURRENT PANEL ---') - ReactPanel.currentPanel._panel.reveal(column) - } else { - ReactPanel.currentPanel = new ReactPanel(extensionPath, column) - } - } - - private constructor(extensionPath: string, column: vscode.ViewColumn) { - this._extensionPath = extensionPath - - const viewType = 'CodeRoad' - const title = 'CodeRoad' - const config = { - // Enable javascript in the webview - enableScripts: true, - - // And restric the webview to only loading content from our extension's `media` directory. - localResourceRoots: [vscode.Uri.file(path.join(this._extensionPath, 'build'))], - - // prevents destroying the window when it is in the background - retainContextWhenHidden: true, - } - - // Create and show a new webview panel - this._panel = vscode.window.createWebviewPanel(viewType, title, column, config) - - // Set the webview's initial html content - this._panel.webview.html = this._getHtmlForWebview() - - // Listen for when the panel is disposed - // This happens when the user closes the panel or when the panel is closed programatically - this._panel.onDidDispose(() => this.dispose(), null, this._disposables) - - // Handle messages from the webview - this._panel.webview.onDidReceiveMessage(onReceive, null, this._disposables) - } - - public async postMessage(action: CR.Action): Promise<void> { - // Send a message to the webview webview. - // You can send any JSON serializable data. - const success = await this._panel.webview.postMessage(action) - if (!success) { - throw new Error(`Message post failure: ${JSON.stringify(action)}`) - } - } - - public dispose(): void { - ReactPanel.currentPanel = undefined - - // Clean up our resources - this._panel.dispose() - - while (this._disposables.length) { - const x = this._disposables.pop() - if (x) { - x.dispose() - } - } - } - - private _getHtmlForWebview(): string { - - // eslint-disable-next-line - const manifest = require(path.join(this._extensionPath, 'build', 'asset-manifest.json')) - const mainScript = manifest.files['main.js'] - // grab first chunk - const chunk = Object.keys(manifest.files).filter(f => f.match(/^static\/js\/.+\.js$/))[0] - const chunkScript = manifest.files[chunk] - const mainStyle = manifest.files['main.css'] - - const scriptPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'build', mainScript)) - const scriptUri = scriptPathOnDisk.with({ scheme: 'vscode-resource' }) - const chunkPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'build', chunkScript)) - const chunkUri = chunkPathOnDisk.with({ scheme: 'vscode-resource' }) - const stylePathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'build', mainStyle)) - const styleUri = stylePathOnDisk.with({ scheme: 'vscode-resource' }) - - // Use a nonce to whitelist which scripts can be run - const nonce = getNonce() - const nonce2 = getNonce() - const nonce3 = getNonce() - - const output = `<!DOCTYPE html> - <html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> - <meta name="theme-color" content="#000000"> - <title>React App</title> - <link rel="manifest" href="./manifest.json" /> - <link rel="stylesheet" type="text/css" href="${styleUri}"> - <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'nonce-${nonce}' 'nonce-${nonce2}' 'nonce-${nonce3}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;"> - <base href="${vscode.Uri.file(path.join(this._extensionPath, 'build')).with({ scheme: 'vscode-resource' })}/"> - <style></style> - </head> - - <body> - <noscript>You need to enable JavaScript to run this app.</noscript> - <div id="root">Loading...</div> - <script nonce=${nonce} src="./webpackBuild.js"></script> - <script nonce=${nonce2} src="${chunkUri}"></script> - <script nonce="${nonce3}" src="${scriptUri}"></script> - </body> - </html>` - console.log(output) - return output - } -} - -export default ReactPanel diff --git a/src/editor/views/index.ts b/src/editor/views/index.ts deleted file mode 100644 index 60a7d871..00000000 --- a/src/editor/views/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as vscode from 'vscode' - -const createViews = (context: vscode.ExtensionContext) => { - // TODO: create views -} - -export default createViews diff --git a/src/editor/views/onReceive.ts b/src/editor/views/onReceive.ts deleted file mode 100644 index ccd69eea..00000000 --- a/src/editor/views/onReceive.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as CR from 'typings' - -export default async (action: CR.Action): Promise<void> => { - console.log('action', action) - // TODO: call state machine -} diff --git a/src/editor/views/utils/nonce.ts b/src/editor/views/utils/nonce.ts deleted file mode 100644 index 313819e1..00000000 --- a/src/editor/views/utils/nonce.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function getNonce(): string { - let text = '' - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)) - } - return text -} diff --git a/src/extension.ts b/src/extension.ts index 40646445..80fd9c64 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,2 +1,11 @@ -export { activate, deactivate } from './editor/init' +import StateMachine from './state' +import Editor from './editor' + +// state machine that governs application logic +const Machine = new StateMachine() +// vscode editor +const VSCodeEditor = new Editor(Machine) + +export const activate = VSCodeEditor.activate +export const deactivate = VSCodeEditor.deactivate diff --git a/src/state/index.ts b/src/state/index.ts index 376b66ef..ab5f7686 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,31 +1,40 @@ -import { interpret } from 'xstate' +import { interpret, Interpreter } from 'xstate' +import * as CR from '../typings' 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() +class StateMachine { + private machineOptions = { + logger: console.log, + devTools: true, + deferEvents: true, + execute: true + } + private service: Interpreter<CR.MachineContext, CR.MachineStateSchema, CR.MachineEvent> + constructor() { + this.service = interpret(machine, this.machineOptions) + // logging + .onTransition(state => { + console.log('state', state) + if (state.changed) { + console.log('transition') + console.log(state.value) + } + }) + } + activate() { + // initialize + this.service.start() + } + deactivate() { + this.service.stop() + } + onReceive(action: CR.Action) { + console.log('RECEIVED ACTION') + console.log(action) + } } -export default service +export default StateMachine diff --git a/src/state/message.ts b/src/state/message.ts new file mode 100644 index 00000000..2393abde --- /dev/null +++ b/src/state/message.ts @@ -0,0 +1,13 @@ +import panel from '../views/Panel' +import * as CR from '../typings' + +export const onSend = (action: CR.Action) => { + if (!panel || !panel.currentPanel) { + throw new Error('No valid panel available') + } + panel.currentPanel.postMessage(action) +} + +export const onReceive = (action: CR.Action) => { + console.log(`RECEIVED: ${JSON.stringify(action)}`) +} diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index 0c16d246..4c8a9bf3 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -1,3 +1,5 @@ +import { onReceive } from "state/message"; + export interface TutorialLevel { stageList: string[] content: { @@ -159,4 +161,10 @@ export interface MachineStateSchema { } } } -} \ No newline at end of file +} + +export interface StateMachine { + activate(): void + deactivate(): void + onReceive(action: Action): void +} From 3261c339f5fa50d74b7152d6933fd32cf7088262 Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sat, 8 Jun 2019 19:39:41 -0700 Subject: [PATCH 03/12] refactor order of launches --- src/editor/ReactWebView.ts | 8 +--- src/editor/commands/index.ts | 66 +++++++++++++++--------------- src/editor/commands/start.ts | 78 ++++++++++++++++++------------------ src/editor/index.ts | 24 ++++++----- src/state/message.ts | 10 ++--- 5 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/editor/ReactWebView.ts b/src/editor/ReactWebView.ts index 56247edc..e19c8f56 100644 --- a/src/editor/ReactWebView.ts +++ b/src/editor/ReactWebView.ts @@ -34,13 +34,7 @@ class ReactWebView { this.onReceive = onReceive } - public async createOrShow(extensionPath: string, column: number = vscode.ViewColumn.One): Promise<void> { - const hasActiveEditor = vscode.window.activeTextEditor - - if (!hasActiveEditor) { - throw new Error('Should have an open file on launch') - } - + public async createOrShow(column: number = vscode.ViewColumn.One): Promise<void> { // If we already have a panel, show it. // Otherwise, create a new panel. if (ReactWebView.currentPanel && ReactWebView.currentPanel.panel) { diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index ad8f39be..5fabd1fe 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -1,38 +1,38 @@ -import * as vscode from 'vscode' -import start from './start' -import ReactPanel from '../views/createWebview' +// import * as vscode from 'vscode' +// import start from './start' +// // import ReactPanel from '../views/createWebview' -import runTest from './runTest' -// import loadSolution from './loadSolution' -// import quit from './quit' +// import runTest from './runTest' +// // import loadSolution from './loadSolution' +// // import quit from './quit' -const COMMANDS = { - // TUTORIAL_SETUP: 'coderoad.tutorial_setup', - START: 'coderoad.start', - OPEN_WEBVIEW: 'coderoad.open_webview', - RUN_TEST: 'coderoad.test_run', - // LOAD_SOLUTION: 'coderoad.solution_load', - // QUIT: 'coderoad.quit', -} +// const COMMANDS = { +// // TUTORIAL_SETUP: 'coderoad.tutorial_setup', +// START: 'coderoad.start', +// OPEN_WEBVIEW: 'coderoad.open_webview', +// RUN_TEST: 'coderoad.test_run', +// // LOAD_SOLUTION: 'coderoad.solution_load', +// // QUIT: 'coderoad.quit', +// } -export default (context: vscode.ExtensionContext): void => { - const commands = { - [COMMANDS.START]: () => { - start(context) - }, - [COMMANDS.OPEN_WEBVIEW]: () => { - ReactPanel.createOrShow(context.extensionPath); - }, - [COMMANDS.RUN_TEST]: () => { - runTest() - }, - // [COMMANDS.LOAD_SOLUTION]: loadSolution, - // [COMMANDS.QUIT]: () => quit(context.subscriptions), - } +// export default (context: vscode.ExtensionContext): void => { +// const commands = { +// [COMMANDS.START]: () => { +// start(context) +// }, +// [COMMANDS.OPEN_WEBVIEW]: () => { +// // ReactPanel.createOrShow(context.extensionPath); +// }, +// [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) - } -} +// for (const cmd in commands) { +// const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) +// context.subscriptions.push(command) +// } +// } diff --git a/src/editor/commands/start.ts b/src/editor/commands/start.ts index 48b44ddb..a7e80ae3 100644 --- a/src/editor/commands/start.ts +++ b/src/editor/commands/start.ts @@ -1,45 +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' +// 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, -} +// let initialTutorial: CR.Tutorial | undefined +// let initialProgress: CR.Progress = { +// levels: {}, +// stages: {}, +// steps: {}, +// complete: false, +// } -export default async function start(context: vscode.ExtensionContext): Promise<void> { - console.log('TUTORIAL_START') +// export default async function start(context: vscode.ExtensionContext): Promise<void> { +// console.log('TUTORIAL_START') - // setup connection to workspace - // await setWorkspaceRoot() - // set workspace context path - // await setStorage(context.workspaceState) +// // setup connection to workspace +// // await setWorkspaceRoot() +// // set workspace context path +// // await setStorage(context.workspaceState) - // initialize state machine - activateMachine() +// // initialize state machine +// activateMachine() - console.log('ACTION: start') +// 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 +// // 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/index.ts b/src/editor/index.ts index 3b9b40d1..870ea187 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -21,7 +21,7 @@ class Editor { this.machine = machine } - private commandStart() { + private commandStart = (): void => { // set workspace root const { rootPath } = vscode.workspace if (!rootPath) { @@ -33,18 +33,24 @@ class Editor { setStorage(this.context.workspaceState) // activate machine - this.machine.activate() this.webview = new ReactWebView(this.context.extensionPath, this.machine.onReceive) + this.machine.activate() + + console.log('command start webview') + console.log(this.webview) } - private activateCommands() { - const { COMMANDS } = this + private activateCommands = (): void => { + console.log('this.COMMANDS', this.COMMANDS) const commands = { - [COMMANDS.START]: () => { + [this.COMMANDS.START]: () => { + console.log('start') this.commandStart() }, - [COMMANDS.OPEN_WEBVIEW]: () => { - this.webview.createOrShow(this.context.extensionPath); + [this.COMMANDS.OPEN_WEBVIEW]: () => { + console.log('open webview') + console.log(this.webview) + this.webview.createOrShow(); }, } for (const cmd in commands) { @@ -52,7 +58,7 @@ class Editor { this.context.subscriptions.push(command) } } - public activate(context: vscode.ExtensionContext): void { + public activate = (context: vscode.ExtensionContext): void => { console.log('ACTIVATE!') this.context = context // commands @@ -61,7 +67,7 @@ class Editor { // setup tasks or views here } - public deactivate(): void { + public deactivate = (): void => { console.log('DEACTIVATE!') // cleanup subscriptions/tasks for (const disposable of this.context.subscriptions) { diff --git a/src/state/message.ts b/src/state/message.ts index 2393abde..70c2909f 100644 --- a/src/state/message.ts +++ b/src/state/message.ts @@ -1,11 +1,11 @@ -import panel from '../views/Panel' +// import panel from '../views/Panel' import * as CR from '../typings' export const onSend = (action: CR.Action) => { - if (!panel || !panel.currentPanel) { - throw new Error('No valid panel available') - } - panel.currentPanel.postMessage(action) + // if (!panel || !panel.currentPanel) { + // throw new Error('No valid panel available') + // } + // panel.currentPanel.postMessage(action) } export const onReceive = (action: CR.Action) => { From 6d8b993d332134b8ab6b10ec364cc9ab86bf026d Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sat, 8 Jun 2019 20:25:21 -0700 Subject: [PATCH 04/12] further editor refactoring --- src/editor/commands/index.ts | 75 ++++++++++++++++++++---------------- src/editor/commands/quit.ts | 5 --- src/editor/index.ts | 53 ++++++++----------------- src/extension.ts | 14 ++++--- src/services/git/index.ts | 5 ++- src/services/node/index.ts | 19 +-------- src/services/testResult.ts | 1 - src/state/index.ts | 3 +- 8 files changed, 71 insertions(+), 104 deletions(-) delete mode 100644 src/editor/commands/quit.ts diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index 5fabd1fe..94f0c7d9 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -1,38 +1,47 @@ -// import * as vscode from 'vscode' -// import start from './start' -// // import ReactPanel from '../views/createWebview' +import * as vscode from 'vscode' +import { join } from 'path' +import { setStorage } from '../storage' +import ReactWebView from '../ReactWebView' +import * as CR from '../../typings' -// import runTest from './runTest' -// // import loadSolution from './loadSolution' -// // import quit from './quit' +const COMMANDS = { + START: 'coderoad.start', + OPEN_WEBVIEW: 'coderoad.open_webview', + OPEN_FILE: 'coderoad.open_file', + RUN_TEST: 'coderoad.test_run', +} -// const COMMANDS = { -// // TUTORIAL_SETUP: 'coderoad.tutorial_setup', -// START: 'coderoad.start', -// OPEN_WEBVIEW: 'coderoad.open_webview', -// RUN_TEST: 'coderoad.test_run', -// // LOAD_SOLUTION: 'coderoad.solution_load', -// // QUIT: 'coderoad.quit', -// } +interface CreateCommandProps { + context: vscode.ExtensionContext, + machine: CR.StateMachine +} +// React panel webview +let webview: any; -// export default (context: vscode.ExtensionContext): void => { -// const commands = { -// [COMMANDS.START]: () => { -// start(context) -// }, -// [COMMANDS.OPEN_WEBVIEW]: () => { -// // ReactPanel.createOrShow(context.extensionPath); -// }, -// [COMMANDS.RUN_TEST]: () => { -// runTest() -// }, -// // [COMMANDS.LOAD_SOLUTION]: loadSolution, -// // [COMMANDS.QUIT]: () => quit(context.subscriptions), -// } +export const createCommands = ({ context, machine }: CreateCommandProps) => ({ + [COMMANDS.START]: () => { + // set local storage workspace + setStorage(context.workspaceState) -// for (const cmd in commands) { -// const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) -// context.subscriptions.push(command) -// } -// } + // activate machine + webview = new ReactWebView(context.extensionPath, machine.onReceive) + machine.activate() + }, + [COMMANDS.OPEN_WEBVIEW]: () => { + webview.createOrShow(); + }, + [COMMANDS.OPEN_FILE]: async (relativeFilePath: string) => { + try { + const workspaceRoot = vscode.workspace.rootPath + if (!workspaceRoot) { + throw new Error('No workspace root path') + } + const absoluteFilePath = join(workspaceRoot, relativeFilePath) + const doc = await vscode.workspace.openTextDocument(absoluteFilePath) + await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) + } catch (error) { + console.log(`Failed to open file ${relativeFilePath}`, error) + } + } +}) \ No newline at end of file diff --git a/src/editor/commands/quit.ts b/src/editor/commands/quit.ts deleted file mode 100644 index 06c2254b..00000000 --- a/src/editor/commands/quit.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as vscode from 'vscode' - -export default () => { - // TODO: quit -} diff --git a/src/editor/index.ts b/src/editor/index.ts index 870ea187..5bacfebf 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -1,58 +1,35 @@ import * as vscode from 'vscode' import * as CR from '../typings' -import { setStorage } from './storage' -import ReactWebView from './ReactWebView' +import { createCommands } from './commands' + +interface Props { + machine: CR.StateMachine, + setWorkspaceRoot(rootPath: string): void +} class Editor { // extension context set on activation // @ts-ignore private context: vscode.ExtensionContext - private workspaceRoot: string | undefined private machine: CR.StateMachine - private webview: any - private COMMANDS = { - START: 'coderoad.start', - OPEN_WEBVIEW: 'coderoad.open_webview', - RUN_TEST: 'coderoad.test_run', - } - - constructor(machine: CR.StateMachine) { + constructor({ machine, setWorkspaceRoot }: Props) { this.machine = machine - } - private commandStart = (): void => { - // set workspace root - const { rootPath } = vscode.workspace + // set workspace root for node executions + const { workspace } = vscode + const { rootPath } = workspace if (!rootPath) { throw new Error('Requires a workspace. Please open a folder') } - this.workspaceRoot = rootPath - - // set local storage workspace - setStorage(this.context.workspaceState) - - // activate machine - this.webview = new ReactWebView(this.context.extensionPath, this.machine.onReceive) - this.machine.activate() - - console.log('command start webview') - console.log(this.webview) + setWorkspaceRoot(rootPath) } private activateCommands = (): void => { - console.log('this.COMMANDS', this.COMMANDS) - const commands = { - [this.COMMANDS.START]: () => { - console.log('start') - this.commandStart() - }, - [this.COMMANDS.OPEN_WEBVIEW]: () => { - console.log('open webview') - console.log(this.webview) - this.webview.createOrShow(); - }, - } + const commands = createCommands({ + context: this.context, + machine: this.machine, + }) for (const cmd in commands) { const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) this.context.subscriptions.push(command) diff --git a/src/extension.ts b/src/extension.ts index 80fd9c64..62a91e02 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,15 @@ +import { setWorkspaceRoot } from './services/node' import StateMachine from './state' import Editor from './editor' // state machine that governs application logic -const Machine = new StateMachine() -// vscode editor -const VSCodeEditor = new Editor(Machine) +export const machine = new StateMachine() -export const activate = VSCodeEditor.activate -export const deactivate = VSCodeEditor.deactivate +// vscode editor +export const editor = new Editor({ + machine, + setWorkspaceRoot, +}) +export const activate = editor.activate +export const deactivate = editor.deactivate diff --git a/src/services/git/index.ts b/src/services/git/index.ts index 347ef142..46cc654f 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -1,5 +1,6 @@ +import * as vscode from 'vscode' import * as CR from 'typings' -import { exec, exists, openFile } from '../node' +import { exec, exists } from '../node' const gitOrigin = 'coderoad' @@ -40,7 +41,7 @@ export async function gitLoadCommits(actions: CR.TutorialAction): Promise<void> if (files) { for (const filePath of files) { - openFile(filePath) + vscode.commands.executeCommand('coderoad.open_webview', filePath) } } } diff --git a/src/services/node/index.ts b/src/services/node/index.ts index 0a5234d9..04aa976e 100644 --- a/src/services/node/index.ts +++ b/src/services/node/index.ts @@ -1,6 +1,4 @@ -import { workspace } from 'vscode' import * as fs from 'fs' -import * as vscode from 'vscode' import { join } from 'path' import { exec as cpExec } from 'child_process' import { promisify } from 'util' @@ -11,11 +9,7 @@ let workspaceRoot: string // set workspace root // other function will use this to target the correct cwd -export async function setWorkspaceRoot(): Promise<void> { - const { rootPath } = workspace - if (!rootPath) { - throw new Error('Requires a workspace. Please open a folder') - } +export function setWorkspaceRoot(rootPath: string): void { workspaceRoot = rootPath } @@ -28,17 +22,6 @@ export const exec = (cmd: string): Promise<{ stdout: string; stderr: string }> = // collect all paths together export const exists = (...paths: string[]): boolean => fs.existsSync(join(workspaceRoot, ...paths)) -export const openFile = async (relativeFilePath: string): Promise<void> => { - try { - const absoluteFilePath = join(workspaceRoot, relativeFilePath) - const doc = await vscode.workspace.openTextDocument(absoluteFilePath) - await vscode.window.showTextDocument(doc, vscode.ViewColumn.One) - } catch (error) { - console.log(`Failed to open file ${relativeFilePath}`, error) - } -} - - // export async function clear(): Promise<void> { // // remove all files including ignored // // NOTE: Linux only diff --git a/src/services/testResult.ts b/src/services/testResult.ts index b2e3fb0f..056646d1 100644 --- a/src/services/testResult.ts +++ b/src/services/testResult.ts @@ -2,7 +2,6 @@ import * as CR from 'typings' import * as vscode from 'vscode' import * as storage from './storage' - export async function onSuccess(position: CR.Position) { console.log('onSuccess', position) vscode.window.showInformationMessage('SUCCESS') diff --git a/src/state/index.ts b/src/state/index.ts index ab5f7686..516f1358 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -17,9 +17,8 @@ class StateMachine { this.service = interpret(machine, this.machineOptions) // logging .onTransition(state => { - console.log('state', state) if (state.changed) { - console.log('transition') + console.log('next state') console.log(state.value) } }) From 0a3e3956adbb02e8ad5093b1165cd3a6e5f88f23 Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sat, 8 Jun 2019 20:30:26 -0700 Subject: [PATCH 05/12] cleanup component structure --- web-app/src/App.css | 33 ------------------------------ web-app/src/App.test.tsx | 9 -------- web-app/src/App.tsx | 14 ------------- web-app/src/Routes.tsx | 5 +++++ web-app/src/index.tsx | 7 ++++--- web-app/src/logo.svg | 7 ------- web-app/src/{ => styles}/index.css | 0 7 files changed, 9 insertions(+), 66 deletions(-) delete mode 100644 web-app/src/App.css delete mode 100644 web-app/src/App.test.tsx delete mode 100644 web-app/src/App.tsx create mode 100644 web-app/src/Routes.tsx delete mode 100644 web-app/src/logo.svg rename web-app/src/{ => styles}/index.css (100%) diff --git a/web-app/src/App.css b/web-app/src/App.css deleted file mode 100644 index b41d297c..00000000 --- a/web-app/src/App.css +++ /dev/null @@ -1,33 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 40vmin; - pointer-events: none; -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/web-app/src/App.test.tsx b/web-app/src/App.test.tsx deleted file mode 100644 index a754b201..00000000 --- a/web-app/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(<App />, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/web-app/src/App.tsx b/web-app/src/App.tsx deleted file mode 100644 index 3b8aacc4..00000000 --- a/web-app/src/App.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import './App.css'; - -const App: React.FC = () => { - return ( - <div className="App"> - <header className="App-header"> - Hello World - </header> - </div> - ); -} - -export default App; diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx new file mode 100644 index 00000000..a9953b31 --- /dev/null +++ b/web-app/src/Routes.tsx @@ -0,0 +1,5 @@ +import * as React from 'react' + +export default () => { + return <div>Hello World</div> +} \ No newline at end of file diff --git a/web-app/src/index.tsx b/web-app/src/index.tsx index b7e93595..ba003317 100644 --- a/web-app/src/index.tsx +++ b/web-app/src/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; +import Routes from './Routes'; -ReactDOM.render(<App />, document.getElementById('root') as HTMLElement) +import './styles/index.css'; + +ReactDOM.render(<Routes />, document.getElementById('root') as HTMLElement) diff --git a/web-app/src/logo.svg b/web-app/src/logo.svg deleted file mode 100644 index 6b60c104..00000000 --- a/web-app/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> - <g fill="#61DAFB"> - <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> - <circle cx="420.9" cy="296.5" r="45.7"/> - <path d="M520.5 78.1z"/> - </g> -</svg> diff --git a/web-app/src/index.css b/web-app/src/styles/index.css similarity index 100% rename from web-app/src/index.css rename to web-app/src/styles/index.css From ec2e0e0d6a428d1ca6c101315127dff3b480b31b Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sun, 9 Jun 2019 10:56:51 -0700 Subject: [PATCH 06/12] absolute path typings --- src/editor/commands/index.ts | 2 +- src/editor/index.ts | 2 +- src/state/message.ts | 2 +- tsconfig.json | 2 +- {src/typings => typings}/context.d.ts | 0 {src/typings => typings}/index.d.ts | 2 -- web-app/src/components/Continue/index.tsx | 2 +- web-app/src/components/Level/index.tsx | 2 +- web-app/src/components/Stage/index.tsx | 2 +- web-app/src/components/Step/index.tsx | 2 +- web-app/src/components/Summary/index.tsx | 2 +- web-app/tsconfig.json | 18 ++++-------------- web-app/tsconfig.paths.json | 21 +++++++++++++++++++++ 13 files changed, 34 insertions(+), 25 deletions(-) rename {src/typings => typings}/context.d.ts (100%) rename {src/typings => typings}/index.d.ts (98%) create mode 100644 web-app/tsconfig.paths.json diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index 94f0c7d9..2b96940e 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode' import { join } from 'path' import { setStorage } from '../storage' import ReactWebView from '../ReactWebView' -import * as CR from '../../typings' +import * as CR from 'typings' const COMMANDS = { START: 'coderoad.start', diff --git a/src/editor/index.ts b/src/editor/index.ts index 5bacfebf..db128230 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode' -import * as CR from '../typings' +import * as CR from 'typings' import { createCommands } from './commands' interface Props { diff --git a/src/state/message.ts b/src/state/message.ts index 70c2909f..571b2100 100644 --- a/src/state/message.ts +++ b/src/state/message.ts @@ -1,5 +1,5 @@ // import panel from '../views/Panel' -import * as CR from '../typings' +import * as CR from 'typings' export const onSend = (action: CR.Action) => { // if (!panel || !panel.currentPanel) { diff --git a/tsconfig.json b/tsconfig.json index 8cd5646e..cef79727 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,7 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "paths": { - "typings": ["./typings/index.d.ts"], + "typings": ["../typings/index.d.ts"], }, }, "exclude": [ diff --git a/src/typings/context.d.ts b/typings/context.d.ts similarity index 100% rename from src/typings/context.d.ts rename to typings/context.d.ts diff --git a/src/typings/index.d.ts b/typings/index.d.ts similarity index 98% rename from src/typings/index.d.ts rename to typings/index.d.ts index 4c8a9bf3..73b2b99f 100644 --- a/src/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,5 +1,3 @@ -import { onReceive } from "state/message"; - export interface TutorialLevel { stageList: string[] content: { diff --git a/web-app/src/components/Continue/index.tsx b/web-app/src/components/Continue/index.tsx index c99195df..ea65eaac 100644 --- a/web-app/src/components/Continue/index.tsx +++ b/web-app/src/components/Continue/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import CR from '../../../../src/typings' +import CR from 'typings' import ContinueItem from './ContinueItem' diff --git a/web-app/src/components/Level/index.tsx b/web-app/src/components/Level/index.tsx index 403c9f3d..a48d8ee5 100644 --- a/web-app/src/components/Level/index.tsx +++ b/web-app/src/components/Level/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Button, Card } from '@alifd/next' -import CR from '../../../../src/typings' +import CR from 'typings' const styles = { card: { diff --git a/web-app/src/components/Stage/index.tsx b/web-app/src/components/Stage/index.tsx index ac45304c..16b7a104 100644 --- a/web-app/src/components/Stage/index.tsx +++ b/web-app/src/components/Stage/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Button, Card } from '@alifd/next' -import CR from '../../../../src/typings' +import CR from 'typings' import Step from '../Step' diff --git a/web-app/src/components/Step/index.tsx b/web-app/src/components/Step/index.tsx index 1e764e17..372c77d4 100644 --- a/web-app/src/components/Step/index.tsx +++ b/web-app/src/components/Step/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { Checkbox } from '@alifd/next' // import CC from '../../typings/client' -import CR from '../../../../src/typings' +import CR from 'typings' const styles = { card: { diff --git a/web-app/src/components/Summary/index.tsx b/web-app/src/components/Summary/index.tsx index 00509b27..e00ac952 100644 --- a/web-app/src/components/Summary/index.tsx +++ b/web-app/src/components/Summary/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { Button, Card } from '@alifd/next' -import CR from '../../../../src/typings' +import CR from 'typings' const styles = { card: { diff --git a/web-app/tsconfig.json b/web-app/tsconfig.json index e68470ec..04664a04 100644 --- a/web-app/tsconfig.json +++ b/web-app/tsconfig.json @@ -1,8 +1,10 @@ { + "extends": "./tsconfig.paths.json", "compilerOptions": { "target": "es5", "lib": [ "dom", + "dom.iterable", "esnext" ], "allowJs": true, @@ -16,21 +18,9 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "preserve", - "sourceMap": true, - "rootDirs": ["src", "stories"], - "baseUrl": "src", - "outDir": "build" + "jsx": "preserve" }, "include": [ - "src", - "../src/typings" - ], - "exclude": [ - "node_modules", - "build", - "scripts", - "jest", - "public" + "src" ] } diff --git a/web-app/tsconfig.paths.json b/web-app/tsconfig.paths.json new file mode 100644 index 00000000..89a64aa3 --- /dev/null +++ b/web-app/tsconfig.paths.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "rootDirs": [ + "src", + "stories" + ], + "paths": { + "typings": [ + "../../typings/index.d.ts" + ], + } + }, + "exclude": [ + "node_modules", + "build", + "scripts", + "jest", + "public" + ] +} \ No newline at end of file From b50e2c095bb37b21d05d0bdd3058e71fbe401c3a Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sun, 9 Jun 2019 14:25:14 -0700 Subject: [PATCH 07/12] support passing of messages to view --- src/editor/ReactWebView.ts | 93 ++++++++++++++++++------------------ src/editor/commands/index.ts | 53 ++++++++++++++++++-- src/editor/commands/start.ts | 45 ----------------- src/editor/index.ts | 9 ++++ src/extension.ts | 1 + src/services/git/index.ts | 2 +- src/state/actions/index.ts | 3 ++ src/state/index.ts | 10 +++- src/state/machine.ts | 8 +++- typings/index.d.ts | 4 ++ web-app/package.json | 1 - web-app/src/Routes.tsx | 34 +++++++++++-- 12 files changed, 161 insertions(+), 102 deletions(-) delete mode 100644 src/editor/commands/start.ts diff --git a/src/editor/ReactWebView.ts b/src/editor/ReactWebView.ts index e19c8f56..ca941fcd 100644 --- a/src/editor/ReactWebView.ts +++ b/src/editor/ReactWebView.ts @@ -2,27 +2,10 @@ import * as vscode from 'vscode' import * as CR from 'typings' import * as path from 'path' -function getNonce(): string { - let text = '' - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)) - } - return text -} - -// TODO: move column into createOrShow - - /** * Manages React webview panels */ class ReactWebView { - /** - * Track the currently panel. Only allow a single panel to exist at a time. - */ - public static currentPanel: ReactWebView | undefined = undefined - // @ts-ignore private panel: vscode.WebviewPanel private extensionPath: string @@ -32,53 +15,71 @@ class ReactWebView { public constructor(extensionPath: string, onReceive: any) { this.extensionPath = extensionPath this.onReceive = onReceive + + // Create and show a new webview panel + this.panel = this.createWebviewPanel(vscode.ViewColumn.One) + + // Set the webview's initial html content + this.panel.webview.html = this.getHtmlForWebview() + + // Listen for when the panel is disposed + // This happens when the user closes the panel or when the panel is closed programatically + this.panel.onDidDispose(() => this.dispose(), null, this.disposables) + + // Handle messages from the webview + this.panel.webview.onDidReceiveMessage(this.onReceive, null, this.disposables) + console.log('webview loaded') } - public async createOrShow(column: number = vscode.ViewColumn.One): Promise<void> { + public async createOrShow(column: number): Promise<void> { // If we already have a panel, show it. // Otherwise, create a new panel. - if (ReactWebView.currentPanel && ReactWebView.currentPanel.panel) { - ReactWebView.currentPanel.panel.reveal(column) + if (this.panel && this.panel.webview) { + console.log('reveal') + this.panel.reveal(column) } else { - const viewType = 'CodeRoad' - const title = 'CodeRoad' - const config = { - // Enable javascript in the webview - enableScripts: true, - - // And restric the webview to only loading content from our extension's `media` directory. - localResourceRoots: [vscode.Uri.file(path.join(this.extensionPath, 'build'))], + console.log('make new panel') + this.panel = this.createWebviewPanel(column) - // prevents destroying the window when it is in the background - retainContextWhenHidden: true, - } - // Create and show a new webview panel - this.panel = vscode.window.createWebviewPanel(viewType, title, column, config) - - // Set the webview's initial html content - this.panel.webview.html = this.getHtmlForWebview() + } + } - // Listen for when the panel is disposed - // This happens when the user closes the panel or when the panel is closed programatically - this.panel.onDidDispose(() => this.dispose(), null, this.disposables) + private createWebviewPanel(column: number): vscode.WebviewPanel { + const viewType = 'CodeRoad' + const title = 'CodeRoad' + const config = { + // Enable javascript in the webview + enableScripts: true, + // And restric the webview to only loading content from our extension's `media` directory. + localResourceRoots: [vscode.Uri.file(path.join(this.extensionPath, 'build'))], + // prevents destroying the window when it is in the background + retainContextWhenHidden: true, + } + return vscode.window.createWebviewPanel(viewType, title, column, config) + } - // Handle messages from the webview - this.panel.webview.onDidReceiveMessage(this.onReceive, null, this.disposables) + private getNonce(): string { + let text = '' + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)) } + return text } public async postMessage(action: CR.Action): Promise<void> { + console.log('webview postMessage') + console.log(action) // Send a message to the webview webview. // You can send any JSON serializable data. const success = await this.panel.webview.postMessage(action) if (!success) { throw new Error(`Message post failure: ${JSON.stringify(action)}`) } + console.log('postMessage sent') } public dispose(): void { - ReactWebView.currentPanel = undefined - // Clean up our resources this.panel.dispose() @@ -108,9 +109,9 @@ class ReactWebView { const styleUri = stylePathOnDisk.with({ scheme: 'vscode-resource' }) // Use a nonce to whitelist which scripts can be run - const nonce = getNonce() - const nonce2 = getNonce() - const nonce3 = getNonce() + const nonce = this.getNonce() + const nonce2 = this.getNonce() + const nonce3 = this.getNonce() return `<!DOCTYPE html> <html lang="en"> diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index 2b96940e..ec1c41e7 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -6,32 +6,65 @@ import * as CR from 'typings' const COMMANDS = { START: 'coderoad.start', + NEW_OR_CONTINUE: 'coderoad.new_or_continue', OPEN_WEBVIEW: 'coderoad.open_webview', + SEND_STATE: 'coderoad.send_state', OPEN_FILE: 'coderoad.open_file', RUN_TEST: 'coderoad.test_run', } interface CreateCommandProps { context: vscode.ExtensionContext, - machine: CR.StateMachine + machine: CR.StateMachine, + storage: any, + git: any } // React panel webview let webview: any; +let initialTutorial: CR.Tutorial | undefined +let initialProgress: CR.Progress = { + levels: {}, + stages: {}, + steps: {}, + complete: false, +} -export const createCommands = ({ context, machine }: CreateCommandProps) => ({ +export const createCommands = ({ context, machine, storage, git }: CreateCommandProps) => ({ + // initialize [COMMANDS.START]: () => { // set local storage workspace setStorage(context.workspaceState) // activate machine webview = new ReactWebView(context.extensionPath, machine.onReceive) + console.log('webview', webview.panel.webview.postMessage) machine.activate() }, - [COMMANDS.OPEN_WEBVIEW]: () => { - webview.createOrShow(); + [COMMANDS.NEW_OR_CONTINUE]: async () => { + // 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') + }, + // open React webview + [COMMANDS.OPEN_WEBVIEW]: (column: number = vscode.ViewColumn.One) => { + webview.createOrShow(column); }, + // open a file [COMMANDS.OPEN_FILE]: async (relativeFilePath: string) => { + console.log(`OPEN_FILE ${JSON.stringify(relativeFilePath)}`) try { const workspaceRoot = vscode.workspace.rootPath if (!workspaceRoot) { @@ -43,5 +76,17 @@ export const createCommands = ({ context, machine }: CreateCommandProps) => ({ } catch (error) { console.log(`Failed to open file ${relativeFilePath}`, error) } + }, + // send messages to webview + [COMMANDS.SEND_STATE]: (action: CR.Action) => { + console.log(`SEND ${JSON.stringify(action)}`) + console.log('webview') + console.log(webview) + // console.log(webview.currentPanel) + // if (!webview || !webview.currentPanel) { + // throw new Error('No valid panel available') + // } + webview.postMessage(action) + } }) \ No newline at end of file diff --git a/src/editor/commands/start.ts b/src/editor/commands/start.ts deleted file mode 100644 index a7e80ae3..00000000 --- a/src/editor/commands/start.ts +++ /dev/null @@ -1,45 +0,0 @@ -// 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<void> { -// 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/index.ts b/src/editor/index.ts index db128230..26504478 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode' import * as CR from 'typings' import { createCommands } from './commands' +import * as storage from '../services/storage' +import * as git from '../services/git' interface Props { machine: CR.StateMachine, @@ -29,6 +31,8 @@ class Editor { const commands = createCommands({ context: this.context, machine: this.machine, + storage, + git, }) for (const cmd in commands) { const command: vscode.Disposable = vscode.commands.registerCommand(cmd, commands[cmd]) @@ -53,6 +57,11 @@ class Editor { // shut down state machine this.machine.deactivate() } + + // execute vscode command + public dispatch = (type: string, payload: any) => { + vscode.commands.executeCommand(type, payload) + } } export default Editor \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 62a91e02..ad32b1d8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import { setWorkspaceRoot } from './services/node' import StateMachine from './state' import Editor from './editor' + // state machine that governs application logic export const machine = new StateMachine() diff --git a/src/services/git/index.ts b/src/services/git/index.ts index 46cc654f..4cd7e34a 100644 --- a/src/services/git/index.ts +++ b/src/services/git/index.ts @@ -41,7 +41,7 @@ export async function gitLoadCommits(actions: CR.TutorialAction): Promise<void> if (files) { for (const filePath of files) { - vscode.commands.executeCommand('coderoad.open_webview', filePath) + vscode.commands.executeCommand('coderoad.open_file', filePath) } } } diff --git a/src/state/actions/index.ts b/src/state/actions/index.ts index 3c435d38..c2abb20b 100644 --- a/src/state/actions/index.ts +++ b/src/state/actions/index.ts @@ -48,5 +48,8 @@ export default { createWebview() { console.log('execute coderoad.open_webview') vscode.commands.executeCommand('coderoad.open_webview') + }, + newOrContinue() { + vscode.commands.executeCommand('coderoad.new_or_continue') } } \ No newline at end of file diff --git a/src/state/index.ts b/src/state/index.ts index 516f1358..4d2905b4 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,6 +1,7 @@ import { interpret, Interpreter } from 'xstate' -import * as CR from '../typings' +import * as CR from 'typings' import machine from './machine' +import * as vscode from 'vscode' // machine interpreter // https://xstate.js.org/docs/guides/interpretation.html @@ -17,9 +18,11 @@ class StateMachine { this.service = interpret(machine, this.machineOptions) // logging .onTransition(state => { + console.log('onTransition', state.changed) if (state.changed) { console.log('next state') console.log(state.value) + vscode.commands.executeCommand('coderoad.send_state', state.value) } }) } @@ -30,6 +33,11 @@ class StateMachine { deactivate() { this.service.stop() } + send(action: string | CR.Action) { + console.log('machine.send') + console.log(action) + this.service.send(action) + } onReceive(action: CR.Action) { console.log('RECEIVED ACTION') console.log(action) diff --git a/src/state/machine.ts b/src/state/machine.ts index 49f6008b..273a915e 100644 --- a/src/state/machine.ts +++ b/src/state/machine.ts @@ -16,10 +16,16 @@ export const machine = Machine< initial: 'SelectTutorial', states: { SelectTutorial: { + onEntry: ['createWebview'], initial: 'Initial', states: { Initial: { - onEntry: ['createWebview'], + after: { + 1000: 'Startup' + } + }, + Startup: { + onEntry: ['newOrContinue'], on: { CONTINUE: 'ContinueTutorial', NEW: 'NewTutorial', diff --git a/typings/index.d.ts b/typings/index.d.ts index 73b2b99f..84b30f12 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,3 +1,5 @@ +import { send } from "xstate"; + export interface TutorialLevel { stageList: string[] content: { @@ -132,6 +134,7 @@ export interface MachineStateSchema { SelectTutorial: { states: { Initial: {} + Startup: {} NewTutorial: { states: { SelectTutorial: {} @@ -164,5 +167,6 @@ export interface MachineStateSchema { export interface StateMachine { activate(): void deactivate(): void + send(action: string | Action): void onReceive(action: Action): void } diff --git a/web-app/package.json b/web-app/package.json index f2e82aec..4ac6c449 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -19,7 +19,6 @@ "build": "react-scripts build", "postbuild": "cp -R ./build/ ../build/ && cp public/webpackBuild.js ../build/webpackBuild.js", "test": "react-scripts test", - "eject": "react-scripts eject", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook" }, diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index a9953b31..5d7c9a34 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -1,5 +1,33 @@ import * as React from 'react' +import * as CR from 'typings' -export default () => { - return <div>Hello World</div> -} \ No newline at end of file +interface ReceivedEvent { + data: CR.Action +} + +const Routes = () => { + const [state, setState] = React.useState({ SelectTutorial: 'Initial' }) + const handleEvent = (event: ReceivedEvent): void => { + console.log('--- HANDLE EVENT ---') + const message = event.data + console.log(`RECEIVED: ${JSON.stringify(message)}`) + // messages from core + if (message.type === 'SET_STATE') { + setState(message.payload) + } + } + + // event bus listener + React.useEffect(() => { + const listener = 'message' + window.addEventListener(listener, handleEvent) + return () => { + window.removeEventListener(listener, handleEvent) + } + }) + return ( + <div>State: {JSON.stringify(state)}</div> + ) +} + +export default Routes \ No newline at end of file From 9c530c95b403e18b0ad0d7e1aaecea6abfee9ead Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sun, 9 Jun 2019 14:29:43 -0700 Subject: [PATCH 08/12] update state in client --- src/editor/commands/index.ts | 12 +++++------- web-app/src/Routes.tsx | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index ec1c41e7..05dd3939 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -54,8 +54,8 @@ export const createCommands = ({ context, machine, storage, git }: CreateCommand initialProgress = progress const canContinue = !!(tutorial && progress && hasGit && hasGitRemote) console.log('canContinue', canContinue) - // if a tutorial exists, "CONTINUE" - // otherwise start from "NEW" + // if a tutorial exists, 'CONTINUE' + // otherwise start from 'NEW' machine.send(canContinue ? 'CONTINUE' : 'NEW') }, // open React webview @@ -78,15 +78,13 @@ export const createCommands = ({ context, machine, storage, git }: CreateCommand } }, // send messages to webview - [COMMANDS.SEND_STATE]: (action: CR.Action) => { - console.log(`SEND ${JSON.stringify(action)}`) - console.log('webview') - console.log(webview) + [COMMANDS.SEND_STATE]: (payload: any) => { + console.log(`SEND ${JSON.stringify(payload)}`) // console.log(webview.currentPanel) // if (!webview || !webview.currentPanel) { // throw new Error('No valid panel available') // } - webview.postMessage(action) + webview.postMessage({ type: 'SET_STATE', payload }) } }) \ No newline at end of file diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 5d7c9a34..1d65c837 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -8,7 +8,6 @@ interface ReceivedEvent { const Routes = () => { const [state, setState] = React.useState({ SelectTutorial: 'Initial' }) const handleEvent = (event: ReceivedEvent): void => { - console.log('--- HANDLE EVENT ---') const message = event.data console.log(`RECEIVED: ${JSON.stringify(message)}`) // messages from core From 864a26ed7732f8ad05c2c7a692d2db820d09f5f5 Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sun, 9 Jun 2019 15:00:19 -0700 Subject: [PATCH 09/12] setup cond rendering route --- web-app/src/Routes.tsx | 13 ++++++++++++- web-app/src/components/Cond/index.tsx | 17 +++++++++++++++++ web-app/src/components/Cond/utils/state.ts | 12 ++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 web-app/src/components/Cond/index.tsx create mode 100644 web-app/src/components/Cond/utils/state.ts diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 1d65c837..f83e0363 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -1,5 +1,8 @@ import * as React from 'react' import * as CR from 'typings' +import NewPage from './components/New' +import ContinuePage from './components/Continue' +import Cond from './components/Cond' interface ReceivedEvent { data: CR.Action @@ -24,8 +27,16 @@ const Routes = () => { window.removeEventListener(listener, handleEvent) } }) + return ( - <div>State: {JSON.stringify(state)}</div> + <div> + <Cond state={state} path="SelectTutorial.NewTutorial"> + <NewPage onNew={() => console.log('new!')} /> + </Cond> + <Cond state={state} path="SelectTutorial.ContinueTutorial"> + <ContinuePage onContinue={() => console.log('continue!')} tutorials={[]} /> + </Cond> + </div> ) } diff --git a/web-app/src/components/Cond/index.tsx b/web-app/src/components/Cond/index.tsx new file mode 100644 index 00000000..9eed24ba --- /dev/null +++ b/web-app/src/components/Cond/index.tsx @@ -0,0 +1,17 @@ +import * as React from 'react' +import { stateMatch } from './utils/state' + +interface Props { + state: any + path: string + children: React.ReactElement +} + +const Cond = (props: Props) => { + if (!stateMatch(props.state, props.path)) { + return null + } + return props.children +} + +export default Cond diff --git a/web-app/src/components/Cond/utils/state.ts b/web-app/src/components/Cond/utils/state.ts new file mode 100644 index 00000000..5706cc57 --- /dev/null +++ b/web-app/src/components/Cond/utils/state.ts @@ -0,0 +1,12 @@ +export function stateMatch(state: any, statePath: string) { + let current = state + let paths = statePath.split('.') + try { + for (const p of paths) { + current = current[p] + } + } catch (error) { + return false + } + return current !== undefined +} \ No newline at end of file From 08b28afd3d66695d663a5dcc0afbef93735f1ef6 Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sun, 9 Jun 2019 15:43:15 -0700 Subject: [PATCH 10/12] run received actions through state machine --- src/editor/ReactWebView.ts | 6 +++--- src/editor/commands/index.ts | 16 +++++----------- src/state/index.ts | 4 ++-- typings/index.d.ts | 2 +- web-app/src/Routes.tsx | 11 ++++++++++- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/editor/ReactWebView.ts b/src/editor/ReactWebView.ts index ca941fcd..de113d6b 100644 --- a/src/editor/ReactWebView.ts +++ b/src/editor/ReactWebView.ts @@ -12,9 +12,8 @@ class ReactWebView { private disposables: vscode.Disposable[] = [] private onReceive: any // TODO: properly type - public constructor(extensionPath: string, onReceive: any) { + public constructor(extensionPath: string) { this.extensionPath = extensionPath - this.onReceive = onReceive // Create and show a new webview panel this.panel = this.createWebviewPanel(vscode.ViewColumn.One) @@ -27,7 +26,8 @@ class ReactWebView { this.panel.onDidDispose(() => this.dispose(), null, this.disposables) // Handle messages from the webview - this.panel.webview.onDidReceiveMessage(this.onReceive, null, this.disposables) + const onReceive = (action: string | CR.Action) => vscode.commands.executeCommand('coderoad.receive_action', action) + this.panel.webview.onDidReceiveMessage(onReceive, null, this.disposables) console.log('webview loaded') } diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index 05dd3939..45d45a47 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -9,6 +9,7 @@ const COMMANDS = { NEW_OR_CONTINUE: 'coderoad.new_or_continue', OPEN_WEBVIEW: 'coderoad.open_webview', SEND_STATE: 'coderoad.send_state', + RECEIVE_ACTION: 'coderoad.receive_action', OPEN_FILE: 'coderoad.open_file', RUN_TEST: 'coderoad.test_run', } @@ -22,13 +23,6 @@ interface CreateCommandProps { // React panel webview let webview: any; -let initialTutorial: CR.Tutorial | undefined -let initialProgress: CR.Progress = { - levels: {}, - stages: {}, - steps: {}, - complete: false, -} export const createCommands = ({ context, machine, storage, git }: CreateCommandProps) => ({ // initialize @@ -37,7 +31,7 @@ export const createCommands = ({ context, machine, storage, git }: CreateCommand setStorage(context.workspaceState) // activate machine - webview = new ReactWebView(context.extensionPath, machine.onReceive) + webview = new ReactWebView(context.extensionPath) console.log('webview', webview.panel.webview.postMessage) machine.activate() }, @@ -50,8 +44,6 @@ export const createCommands = ({ context, machine, storage, git }: CreateCommand git.gitVersion(), git.gitCheckRemoteExists(), ]) - initialTutorial = tutorial - initialProgress = progress const canContinue = !!(tutorial && progress && hasGit && hasGitRemote) console.log('canContinue', canContinue) // if a tutorial exists, 'CONTINUE' @@ -85,6 +77,8 @@ export const createCommands = ({ context, machine, storage, git }: CreateCommand // throw new Error('No valid panel available') // } webview.postMessage({ type: 'SET_STATE', payload }) - + }, + [COMMANDS.RECEIVE_ACTION]: (action: string | CR.Action) => { + machine.onReceive(action) } }) \ No newline at end of file diff --git a/src/state/index.ts b/src/state/index.ts index 4d2905b4..d7f0e827 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -38,9 +38,9 @@ class StateMachine { console.log(action) this.service.send(action) } - onReceive(action: CR.Action) { - console.log('RECEIVED ACTION') + onReceive(action: string | CR.Action) { console.log(action) + this.service.send(action) } } diff --git a/typings/index.d.ts b/typings/index.d.ts index 84b30f12..044be525 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -168,5 +168,5 @@ export interface StateMachine { activate(): void deactivate(): void send(action: string | Action): void - onReceive(action: Action): void + onReceive(action: string | Action): void } diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index f83e0363..5822eea5 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -8,6 +8,15 @@ interface ReceivedEvent { data: CR.Action } +declare var acquireVsCodeApi: any + +const vscode = acquireVsCodeApi() + +function send(event: string|CR.Action) { + return vscode.postMessage(event) +} + + const Routes = () => { const [state, setState] = React.useState({ SelectTutorial: 'Initial' }) const handleEvent = (event: ReceivedEvent): void => { @@ -31,7 +40,7 @@ const Routes = () => { return ( <div> <Cond state={state} path="SelectTutorial.NewTutorial"> - <NewPage onNew={() => console.log('new!')} /> + <NewPage onNew={() => send('TUTORIAL_START')} /> </Cond> <Cond state={state} path="SelectTutorial.ContinueTutorial"> <ContinuePage onContinue={() => console.log('continue!')} tutorials={[]} /> From 3cdce4d9aa088558b0956273c0de0229e1bf8292 Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sun, 9 Jun 2019 16:02:09 -0700 Subject: [PATCH 11/12] setup state/data --- src/editor/commands/index.ts | 12 ++++++------ src/state/index.ts | 6 ++++-- web-app/src/Routes.tsx | 25 ++++++++++++++----------- web-app/src/utils/vscode.ts | 9 +++++++++ 4 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 web-app/src/utils/vscode.ts diff --git a/src/editor/commands/index.ts b/src/editor/commands/index.ts index 45d45a47..9ea0b92e 100644 --- a/src/editor/commands/index.ts +++ b/src/editor/commands/index.ts @@ -9,6 +9,7 @@ const COMMANDS = { NEW_OR_CONTINUE: 'coderoad.new_or_continue', OPEN_WEBVIEW: 'coderoad.open_webview', SEND_STATE: 'coderoad.send_state', + SEND_DATA: 'coderoad.send_data', RECEIVE_ACTION: 'coderoad.receive_action', OPEN_FILE: 'coderoad.open_file', RUN_TEST: 'coderoad.test_run', @@ -70,15 +71,14 @@ export const createCommands = ({ context, machine, storage, git }: CreateCommand } }, // send messages to webview - [COMMANDS.SEND_STATE]: (payload: any) => { - console.log(`SEND ${JSON.stringify(payload)}`) - // console.log(webview.currentPanel) - // if (!webview || !webview.currentPanel) { - // throw new Error('No valid panel available') - // } + [COMMANDS.SEND_STATE]: (payload: { data: any, state: any }) => { webview.postMessage({ type: 'SET_STATE', payload }) }, + [COMMANDS.SEND_DATA]: (payload: { data: any }) => { + webview.postMessage({ type: 'SET_DATA', payload }) + }, [COMMANDS.RECEIVE_ACTION]: (action: string | CR.Action) => { + console.log('onReceiveAction', action) machine.onReceive(action) } }) \ No newline at end of file diff --git a/src/state/index.ts b/src/state/index.ts index d7f0e827..6a7cb3bb 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -18,11 +18,13 @@ class StateMachine { this.service = interpret(machine, this.machineOptions) // logging .onTransition(state => { - console.log('onTransition', state.changed) + console.log('onTransition', state) if (state.changed) { console.log('next state') console.log(state.value) - vscode.commands.executeCommand('coderoad.send_state', state.value) + vscode.commands.executeCommand('coderoad.send_state', { state: state.value, data: state.context }) + } else { + vscode.commands.executeCommand('coderoad.send_data', { data: state.context }) } }) } diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 5822eea5..6e0c9e88 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -1,5 +1,7 @@ import * as React from 'react' import * as CR from 'typings' +import { send } from './utils/vscode' + import NewPage from './components/New' import ContinuePage from './components/Continue' import Cond from './components/Cond' @@ -8,23 +10,21 @@ interface ReceivedEvent { data: CR.Action } -declare var acquireVsCodeApi: any - -const vscode = acquireVsCodeApi() - -function send(event: string|CR.Action) { - return vscode.postMessage(event) -} - - const Routes = () => { const [state, setState] = React.useState({ SelectTutorial: 'Initial' }) + const [data, setData] = React.useState({}) + + const handleEvent = (event: ReceivedEvent): void => { const message = event.data - console.log(`RECEIVED: ${JSON.stringify(message)}`) + console.log('RECEIVED') + console.log(message) // messages from core if (message.type === 'SET_STATE') { - setState(message.payload) + setState(message.payload.state) + setData(message.payload.data) + } else if (message.type === 'SET_DATA') { + setData(message.payload.data) } } @@ -37,8 +37,11 @@ const Routes = () => { } }) + // TODO: refactor cond to user <Router><Route> and accept first route as if/else if return ( <div> + <h5>state: {JSON.stringify(state)}</h5> + <p>data:{JSON.stringify(data)}</p> <Cond state={state} path="SelectTutorial.NewTutorial"> <NewPage onNew={() => send('TUTORIAL_START')} /> </Cond> diff --git a/web-app/src/utils/vscode.ts b/web-app/src/utils/vscode.ts new file mode 100644 index 00000000..c53a5ebf --- /dev/null +++ b/web-app/src/utils/vscode.ts @@ -0,0 +1,9 @@ +import { Action } from 'typings' + +declare var acquireVsCodeApi: any + +const vscode = acquireVsCodeApi() + +export function send(event: string | Action) { + return vscode.postMessage(event) +} From 99372b4e9ac156dc3836b1727eba2d8a1e5fbd36 Mon Sep 17 00:00:00 2001 From: shmck <shawn.j.mckay@gmail.com> Date: Sun, 9 Jun 2019 16:03:47 -0700 Subject: [PATCH 12/12] move components into containers --- web-app/src/Routes.tsx | 5 +++-- .../src/{components => containers}/Continue/ContinueItem.tsx | 0 web-app/src/{components => containers}/Continue/index.tsx | 0 web-app/src/{components => containers}/New/index.tsx | 0 web-app/stories/Continue.stories.tsx | 2 +- web-app/stories/New.stories.tsx | 2 +- 6 files changed, 5 insertions(+), 4 deletions(-) rename web-app/src/{components => containers}/Continue/ContinueItem.tsx (100%) rename web-app/src/{components => containers}/Continue/index.tsx (100%) rename web-app/src/{components => containers}/New/index.tsx (100%) diff --git a/web-app/src/Routes.tsx b/web-app/src/Routes.tsx index 6e0c9e88..ca1d1512 100644 --- a/web-app/src/Routes.tsx +++ b/web-app/src/Routes.tsx @@ -2,9 +2,10 @@ import * as React from 'react' import * as CR from 'typings' import { send } from './utils/vscode' -import NewPage from './components/New' -import ContinuePage from './components/Continue' import Cond from './components/Cond' +import NewPage from './containers/New' +import ContinuePage from './containers/Continue' + interface ReceivedEvent { data: CR.Action diff --git a/web-app/src/components/Continue/ContinueItem.tsx b/web-app/src/containers/Continue/ContinueItem.tsx similarity index 100% rename from web-app/src/components/Continue/ContinueItem.tsx rename to web-app/src/containers/Continue/ContinueItem.tsx diff --git a/web-app/src/components/Continue/index.tsx b/web-app/src/containers/Continue/index.tsx similarity index 100% rename from web-app/src/components/Continue/index.tsx rename to web-app/src/containers/Continue/index.tsx diff --git a/web-app/src/components/New/index.tsx b/web-app/src/containers/New/index.tsx similarity index 100% rename from web-app/src/components/New/index.tsx rename to web-app/src/containers/New/index.tsx diff --git a/web-app/stories/Continue.stories.tsx b/web-app/stories/Continue.stories.tsx index 6845b72c..1f4df9d0 100644 --- a/web-app/stories/Continue.stories.tsx +++ b/web-app/stories/Continue.stories.tsx @@ -3,7 +3,7 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' -import Continue from '../src/components/Continue' +import Continue from '../src/containers/Continue' import demo from './data/basic' storiesOf('Continue', module).add('Page', () => <Continue tutorials={[demo]} onContinue={action('onContinue')} />) diff --git a/web-app/stories/New.stories.tsx b/web-app/stories/New.stories.tsx index 643311d0..5ade6701 100644 --- a/web-app/stories/New.stories.tsx +++ b/web-app/stories/New.stories.tsx @@ -3,6 +3,6 @@ import React from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' -import New from '../src/components/New' +import New from '../src/containers/New' storiesOf('New', module).add('Page', () => <New onNew={action('onNew')} />)