diff --git a/package.json b/package.json index fce1e449d..1ccb464f8 100644 --- a/package.json +++ b/package.json @@ -278,7 +278,6 @@ "css-loader": "^1.0.0", "del": "^4.0.0", "event-stream": "^4.0.1", - "glob": "^7.1.4", "gulp": "^4.0.2", "gulp-cli": "^2.1.0", "gulp-filter": "^5.1.0", @@ -310,6 +309,7 @@ "compare-versions": "^3.5.1", "eventemitter2": "^5.0.1", "open": "^6.4.0", + "glob": "^7.1.4", "os": "^0.1.1", "react": "^16.8.6", "react-dom": "^16.8.6", diff --git a/src/extension.ts b/src/extension.ts index 23aa5b314..0ec570bf0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -41,6 +41,14 @@ function loadScript(context: vscode.ExtensionContext, scriptPath: string) { .toString()}">`; } +const setPathAndSendMessage = (currentPanel: vscode.WebviewPanel, newFilePath: string) => { + currentFileAbsPath = newFilePath; + currentPanel.webview.postMessage({ + command: "current-file", + state: { running_file: newFilePath } + }); +} + // Extension activation export async function activate(context: vscode.ExtensionContext) { console.info(CONSTANTS.INFO.EXTENSION_ACTIVATED); @@ -49,6 +57,7 @@ export async function activate(context: vscode.ExtensionContext) { let currentPanel: vscode.WebviewPanel | undefined; let childProcess: cp.ChildProcess | undefined; let messageListener: vscode.Disposable; + let activeEditorListener: vscode.Disposable; // Add our library path to settings.json for autocomplete functionality updatePythonExtraPaths(); @@ -70,20 +79,18 @@ export async function activate(context: vscode.ExtensionContext) { ); } - vscode.workspace.onDidSaveTextDocument( - async (document: vscode.TextDocument) => { - await updateCurrentFileIfPython(document); - } - ); + vscode.workspace.onDidSaveTextDocument(async (document: vscode.TextDocument) => { + await updateCurrentFileIfPython(document, currentPanel); + }); const openWebview = () => { if (currentPanel) { - currentPanel.reveal(vscode.ViewColumn.Two); + currentPanel.reveal(vscode.ViewColumn.Beside); } else { currentPanel = vscode.window.createWebviewPanel( "adafruitSimulator", CONSTANTS.LABEL.WEBVIEW_PANEL, - { preserveFocus: true, viewColumn: vscode.ViewColumn.Two }, + { preserveFocus: true, viewColumn: vscode.ViewColumn.Beside }, { // Only allow the webview to access resources in our extension's media directory localResourceRoots: [ @@ -103,6 +110,14 @@ export async function activate(context: vscode.ExtensionContext) { } } + if (activeEditorListener !== undefined) { + activeEditorListener.dispose(); + const index = context.subscriptions.indexOf(activeEditorListener); + if (index > -1) { + context.subscriptions.splice(index, 1); + } + } + if (currentPanel) { // Handle messages from webview messageListener = currentPanel.webview.onDidReceiveMessage( @@ -121,10 +136,15 @@ export async function activate(context: vscode.ExtensionContext) { break; case WebviewMessages.PLAY_SIMULATOR: console.log(`Play button ${messageJson} \n`); - if (message.text as boolean) { - telemetryAI.trackFeatureUsage( - TelemetryEventName.COMMAND_RUN_SIMULATOR_BUTTON - ); + if (message.text.state as boolean) { + setPathAndSendMessage(currentPanel, message.text.selected_file); + if (currentFileAbsPath) { + const foundDocument = utils.getActiveEditorFromPath(currentFileAbsPath); + if (foundDocument !== undefined) { + currentTextDocument = foundDocument; + } + } + telemetryAI.trackFeatureUsage(TelemetryEventName.COMMAND_RUN_SIMULATOR_BUTTON); runSimulatorCommand(); } else { killProcessIfRunning(); @@ -156,6 +176,9 @@ export async function activate(context: vscode.ExtensionContext) { undefined, context.subscriptions ); + + activeEditorListener = utils.addVisibleTextEditorCallback(currentPanel, context); + console.log("sent"); } currentPanel.onDidDispose( @@ -305,14 +328,15 @@ export async function activate(context: vscode.ExtensionContext) { killProcessIfRunning(); - await updateCurrentFileIfPython(vscode.window.activeTextEditor!.document); + await updateCurrentFileIfPython(vscode.window.activeTextEditor!.document, currentPanel); if (currentFileAbsPath === "") { - utils.logToOutputChannel( - outChannel, - CONSTANTS.ERROR.NO_FILE_TO_RUN, - true - ); + utils.logToOutputChannel(outChannel, CONSTANTS.ERROR.NO_FILE_TO_RUN, true); + vscode.window + .showErrorMessage( + CONSTANTS.ERROR.NO_FILE_TO_RUN, + DialogResponses.MESSAGE_UNDERSTOOD + ) } else { // Save on run await currentTextDocument.save(); @@ -389,7 +413,7 @@ export async function activate(context: vscode.ExtensionContext) { case "print": console.log( `Process print statement output = ${ - messageToWebview.data + messageToWebview.data }` ); utils.logToOutputChannel( @@ -456,14 +480,15 @@ export async function activate(context: vscode.ExtensionContext) { utils.logToOutputChannel(outChannel, CONSTANTS.INFO.DEPLOY_DEVICE); - await updateCurrentFileIfPython(vscode.window.activeTextEditor!.document); + await updateCurrentFileIfPython(vscode.window.activeTextEditor!.document, currentPanel); if (currentFileAbsPath === "") { - utils.logToOutputChannel( - outChannel, - CONSTANTS.ERROR.NO_FILE_TO_RUN, - true - ); + utils.logToOutputChannel(outChannel, CONSTANTS.ERROR.NO_FILE_TO_RUN, true); + vscode.window + .showErrorMessage( + CONSTANTS.ERROR.NO_FILE_TO_RUN, + DialogResponses.MESSAGE_UNDERSTOOD + ); } else if (!utils.validCodeFileName(currentFileAbsPath)) { // Save on run await currentTextDocument.save(); @@ -698,40 +723,19 @@ const getActivePythonFile = () => { return activeEditor ? activeEditor.document.fileName : ""; }; -const getFileFromFilePicker = () => { - const options: vscode.OpenDialogOptions = { - canSelectMany: false, - filters: { - "All files": ["*"], - "Python files": ["py"] - }, - openLabel: "Run File" - }; - - return vscode.window.showOpenDialog(options).then(async fileUri => { - if (fileUri && fileUri[0] && fileUri[0].fsPath.endsWith(".py")) { - console.log(`Selected file: ${fileUri[0].fsPath}`); - currentTextDocument = await vscode.workspace.openTextDocument(fileUri[0]); - return fileUri[0].fsPath; - } - }); -}; - const updateCurrentFileIfPython = async ( - activeTextDocument: vscode.TextDocument | undefined + activeTextDocument: vscode.TextDocument | undefined, + currentPanel: vscode.WebviewPanel ) => { if (activeTextDocument && activeTextDocument.languageId === "python") { - currentFileAbsPath = activeTextDocument.fileName; + setPathAndSendMessage(currentPanel, activeTextDocument.fileName); currentTextDocument = activeTextDocument; } else if (currentFileAbsPath === "") { - currentFileAbsPath = - getActivePythonFile() || (await getFileFromFilePicker()) || ""; + setPathAndSendMessage(currentPanel, + getActivePythonFile() || ""); } - if (currentFileAbsPath) { - await vscode.window.showTextDocument( - currentTextDocument, - vscode.ViewColumn.One - ); + if (utils.getActiveEditorFromPath(currentTextDocument.fileName) === undefined) { + await vscode.window.showTextDocument(currentTextDocument, vscode.ViewColumn.One); } }; diff --git a/src/extension_utils/utils.ts b/src/extension_utils/utils.ts index b37f9ea04..1e4d58af8 100644 --- a/src/extension_utils/utils.ts +++ b/src/extension_utils/utils.ts @@ -5,14 +5,7 @@ import * as fs from "fs"; import * as path from "path"; import { DependencyChecker } from "./dependencyChecker"; import { DeviceContext } from "../deviceContext"; -import { - ExtensionContext, - MessageItem, - OutputChannel, - Uri, - window, - workspace -} from "vscode"; +import * as vscode from "vscode"; import { CONSTANTS, CPX_CONFIG_FILE, @@ -24,11 +17,11 @@ import { CPXWorkspace } from "../cpxWorkspace"; // tslint:disable-next-line: export-name export const getPathToScript = ( - context: ExtensionContext, + context: vscode.ExtensionContext, folderName: string, fileName: string ) => { - const onDiskPath = Uri.file( + const onDiskPath = vscode.Uri.file( path.join(context.extensionPath, folderName, fileName) ); const scriptPath = onDiskPath.with({ scheme: "vscode-resource" }); @@ -43,13 +36,12 @@ export const validCodeFileName = (filePath: string) => { }; export const showPrivacyModal = (okAction: () => void) => { - window - .showInformationMessage( - `${CONSTANTS.INFO.THIRD_PARTY_WEBSITE}: ${CONSTANTS.LINKS.PRIVACY}`, - DialogResponses.AGREE_AND_PROCEED, - DialogResponses.CANCEL - ) - .then((privacySelection: MessageItem | undefined) => { + vscode.window.showInformationMessage( + `${CONSTANTS.INFO.THIRD_PARTY_WEBSITE}: ${CONSTANTS.LINKS.PRIVACY}`, + DialogResponses.AGREE_AND_PROCEED, + DialogResponses.CANCEL, + ) + .then((privacySelection: vscode.MessageItem | undefined) => { if (privacySelection === DialogResponses.AGREE_AND_PROCEED) { okAction(); } else if (privacySelection === DialogResponses.CANCEL) { @@ -59,7 +51,7 @@ export const showPrivacyModal = (okAction: () => void) => { }; export const logToOutputChannel = ( - outChannel: OutputChannel | undefined, + outChannel: vscode.OutputChannel | undefined, message: string, show: boolean = false ): void => { @@ -77,10 +69,10 @@ export function tryParseJSON(jsonString: string): any | boolean { if (jsonObj && typeof jsonObj === "object") { return jsonObj; } - } catch (exception) {} + } catch (exception) { } return false; -} +}; export function fileExistsSync(filePath: string): boolean { try { @@ -88,7 +80,7 @@ export function fileExistsSync(filePath: string): boolean { } catch (error) { return false; } -} +}; export function mkdirRecursivelySync(dirPath: string): void { if (directoryExistsSync(dirPath)) { @@ -103,7 +95,7 @@ export function mkdirRecursivelySync(dirPath: string): void { mkdirRecursivelySync(dirname); fs.mkdirSync(dirPath); } -} +}; export function directoryExistsSync(dirPath: string): boolean { try { @@ -111,7 +103,7 @@ export function directoryExistsSync(dirPath: string): boolean { } catch (e) { return false; } -} +}; /** * This method pads the current string with another string (repeated, if needed) @@ -142,11 +134,11 @@ export function padStart( } else { return (sourceString as any).padStart(targetLength, padString); } -} +}; export function convertToHex(num: number, width = 0): string { return padStart(num.toString(16), width, "0"); -} +}; export function generateCPXConfig(): void { const deviceContext: DeviceContext = DeviceContext.getInstance(); @@ -159,7 +151,7 @@ export function generateCPXConfig(): void { ); mkdirRecursivelySync(path.dirname(cpxConfigFilePath)); fs.writeFileSync(cpxConfigFilePath, JSON.stringify(cpxJson, null, 4)); -} +}; export const checkPythonDependency = async () => { const dependencyChecker: DependencyChecker = new DependencyChecker(); const result = await dependencyChecker.checkDependency( @@ -175,12 +167,9 @@ export const setPythonExectuableName = async () => { if (dependencyCheck.installed) { executableName = dependencyCheck.dependency; } else { - window - .showErrorMessage( - CONSTANTS.ERROR.NO_PYTHON_PATH, - DialogResponses.INSTALL_PYTHON - ) - .then((selection: MessageItem | undefined) => { + vscode.window.showErrorMessage(CONSTANTS.ERROR.NO_PYTHON_PATH, + DialogResponses.INSTALL_PYTHON) + .then((selection: vscode.MessageItem | undefined) => { if (selection === DialogResponses.INSTALL_PYTHON) { const okAction = () => { open(CONSTANTS.LINKS.DOWNLOAD_PYTHON); @@ -193,10 +182,36 @@ export const setPythonExectuableName = async () => { return executableName; }; +export const addVisibleTextEditorCallback = (currentPanel: vscode.WebviewPanel, context: vscode.ExtensionContext): vscode.Disposable => { + const initialPythonEditors = filterForPythonFiles(vscode.window.visibleTextEditors); + currentPanel.webview.postMessage({ + command: "visible-editors", + state: { activePythonEditors: initialPythonEditors } + }); + return vscode.window.onDidChangeVisibleTextEditors((textEditors: vscode.TextEditor[]) => { + const activePythonEditors = filterForPythonFiles(textEditors); + currentPanel.webview.postMessage({ + command: "visible-editors", + state: { activePythonEditors } + }); + }, {}, context.subscriptions) +}; + +export const filterForPythonFiles = (textEditors: vscode.TextEditor[]) => { + return textEditors.filter( + editor => editor.document.languageId === "python" + ).map(editor => editor.document.fileName); +}; + +export const getActiveEditorFromPath = (filePath: string): vscode.TextDocument => { + const activeEditor = vscode.window.visibleTextEditors.find((editor: vscode.TextEditor) => editor.document.fileName === filePath); + return activeEditor ? activeEditor.document : undefined; +}; + export const getServerPortConfig = (): number => { // tslint:disable: no-backbone-get-set-outside-model prefer-type-cast - if (workspace.getConfiguration().has(SERVER_INFO.SERVER_PORT_CONFIGURATION)) { - return workspace + if (vscode.workspace.getConfiguration().has(SERVER_INFO.SERVER_PORT_CONFIGURATION)) { + return vscode.workspace .getConfiguration() .get(SERVER_INFO.SERVER_PORT_CONFIGURATION) as number; } diff --git a/src/view/components/Dropdown.tsx b/src/view/components/Dropdown.tsx new file mode 100644 index 000000000..d84c09cf3 --- /dev/null +++ b/src/view/components/Dropdown.tsx @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as React from "react"; +import { CONSTANTS } from "../constants"; +import "../styles/Dropdown.css"; + +export interface IDropdownProps { + label: string; + textOptions: string[]; + lastChosen: string; + styleLabel: string; + width: number; + onBlur: (event: React.FocusEvent) => void; +} + +const Dropdown: React.FC = props => { + const parsedPath = parsePath(props.lastChosen); + const defaultText = + props.lastChosen !== "" + ? CONSTANTS.CURRENTLY_RUNNING(parsedPath[1]) + : CONSTANTS.NO_FILES_AVAILABLE; + return ( +
+ +
+ ); +}; + +const renderOptions = (options: string[]) => { + return options.map((name, index) => { + const key = `option-${index}`; + const parsedPath = parsePath(name); + return ( + + ); + }); +}; + +const parsePath = (filePath: string) => { + const lastSlash = + filePath.lastIndexOf("/") !== -1 + ? filePath.lastIndexOf("/") + : filePath.lastIndexOf("\\"); + return [filePath.slice(0, lastSlash), filePath.substr(lastSlash + 1)]; +}; + +export default Dropdown; diff --git a/src/view/components/Simulator.tsx b/src/view/components/Simulator.tsx index 0c9f498e4..29e25e5e5 100644 --- a/src/view/components/Simulator.tsx +++ b/src/view/components/Simulator.tsx @@ -5,6 +5,7 @@ import * as React from "react"; import { BUTTON_NEUTRAL, BUTTON_PRESSED } from "./cpx/Cpx_svg_style"; import Cpx, { updateSwitch, updatePinTouch } from "./cpx/Cpx"; import Button from "./Button"; +import Dropdown from "./Dropdown"; import { CONSTANTS } from "../constants"; import PlayLogo from "../svgs/play_svg"; import StopLogo from "../svgs/stop_svg"; @@ -24,6 +25,9 @@ interface ICpxState { } interface IState { + active_editors: string[]; + running_file: string; + selected_file: string; cpx: ICpxState; play_button: boolean; } @@ -53,8 +57,6 @@ const DEFAULT_CPX_STATE: ICpxState = { shake: false }; -const SIMULATOR_BUTTON_WIDTH = 60; - interface vscode { postMessage(message: any): void; } @@ -69,8 +71,11 @@ class Simulator extends React.Component { constructor(props: IMyProps) { super(props); this.state = { + active_editors: [], cpx: DEFAULT_CPX_STATE, - play_button: false + play_button: false, + running_file: "", + selected_file: "" }; this.handleClick = this.handleClick.bind(this); @@ -80,6 +85,7 @@ class Simulator extends React.Component { this.onMouseLeave = this.onMouseLeave.bind(this); this.togglePlayClick = this.togglePlayClick.bind(this); this.refreshSimulatorClick = this.refreshSimulatorClick.bind(this); + this.onSelectBlur = this.onSelectBlur.bind(this); } handleMessage = (event: any): void => { @@ -100,6 +106,23 @@ class Simulator extends React.Component { case "activate-play": this.setState({ ...this.state, play_button: !this.state.play_button }); break; + case "visible-editors": + console.log( + "Setting active editors", + message.state.activePythonEditors + ); + this.setState({ + ...this.state, + active_editors: message.state.activePythonEditors + }); + break; + case "current-file": + console.log("Setting current file", message.state.running_file); + this.setState({ + ...this.state, + running_file: message.state.running_file + }); + break; default: console.log("Invalid message received from the extension."); this.setState({ ...this.state, cpx: DEFAULT_CPX_STATE }); @@ -121,6 +144,16 @@ class Simulator extends React.Component { const image = this.state.play_button ? StopLogo : PlayLogo; return (
+
+ +
{ image={image} styleLabel="play" label="play" - width={SIMULATOR_BUTTON_WIDTH} + width={CONSTANTS.SIMULATOR_BUTTON_WIDTH} />
@@ -157,7 +190,10 @@ class Simulator extends React.Component { } protected togglePlayClick() { - sendMessage("play-simulator", !this.state.play_button); + sendMessage("play-simulator", { + selected_file: this.state.selected_file, + state: !this.state.play_button + }); const button = window.document.getElementById(CONSTANTS.ID_NAME.PLAY_BUTTON) || window.document.getElementById(CONSTANTS.ID_NAME.STOP_BUTTON); @@ -176,6 +212,9 @@ class Simulator extends React.Component { } } + protected onSelectBlur(event: React.FocusEvent) { + this.setState({ ...this.state, selected_file: event.currentTarget.value }); + } protected onKeyEvent(event: KeyboardEvent, active: boolean) { let element; const target = event.target as SVGElement; diff --git a/src/view/constants.ts b/src/view/constants.ts index 449911863..34a9b476d 100644 --- a/src/view/constants.ts +++ b/src/view/constants.ts @@ -3,6 +3,9 @@ // Key events export const CONSTANTS = { + CURRENTLY_RUNNING: (file: string) => { + return `Currently running: ${file}` + }, ID_NAME: { BUTTON_A: "BTN_A_OUTER", BUTTON_AB: "BTN_AB_OUTER", @@ -34,6 +37,7 @@ export const CONSTANTS = { NUMERIC_SIX: "Digit6", NUMERIC_SEVEN: "Digit7" }, + NO_FILES_AVAILABLE: "Choose a .py file to run on the Simulator", REDIRECT: { DESCRIPTION: 'By clicking "Agree and Proceed" you will be redirected to adafruit.com, a third party website not managed by Microsoft. Please note that your activity on adafruit.com is subject to Adafruit\'s privacy policy', @@ -41,7 +45,8 @@ export const CONSTANTS = { "https://learn.adafruit.com/adafruit-circuit-playground-express/circuitpython-quickstart", PRIVACY: "https://www.adafruit.com/privacy" }, - TOOLBAR_INFO: `Explore what's on the board:` + SIMULATOR_BUTTON_WIDTH: 60, + TOOLBAR_INFO: `Explore what's on the board:`, }; export default CONSTANTS; diff --git a/src/view/styles/Dropdown.css b/src/view/styles/Dropdown.css new file mode 100644 index 000000000..c1b355d26 --- /dev/null +++ b/src/view/styles/Dropdown.css @@ -0,0 +1,27 @@ +.dropdown { + background: var(--vscode-debugToolBar-background); + border-color: var(--vscode-highContrastButtonBorderOverride-color); + border-radius: 2px; + max-width: 300px; + min-width: 240px; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.22); + color: var(--vscode-foreground); + height: 32px; + padding-left: 8px; + padding-right: 4px; +} + +select.dropdown:hover, +select.dropdown:focus, +select.dropdown:active { + outline: 1px solid var(--vscode-button-background); + outline-offset: 1px; +} + +option { + height: 32px; + background: var(--vscode-debugToolBar-background); + align-items: center; + font-size: 14px; + color: var(--vscode-foreground); +} diff --git a/src/view/styles/Simulator.css b/src/view/styles/Simulator.css index 270415fb6..5a428910b 100644 --- a/src/view/styles/Simulator.css +++ b/src/view/styles/Simulator.css @@ -14,3 +14,7 @@ padding-top: 20px; justify-content: center; } + +.file-selector { + padding: 20px; +}