diff --git a/vscode-wpilib/src/executor.ts b/vscode-wpilib/src/executor.ts index 78a2010a..731c1953 100644 --- a/vscode-wpilib/src/executor.ts +++ b/vscode-wpilib/src/executor.ts @@ -17,7 +17,12 @@ interface ITaskRunnerQuickPick { taskRunner: ITaskRunner; } -export class ExecuteAPI implements IExecuteAPI { +export interface IExecuteAPIEx { + executeProcessCommand(command: string, name: string, args: string[], rootDir: string, workspace: vscode.WorkspaceFolder): Promise; + executePythonCommand(args: string[], rootDir: string, workspace: vscode.WorkspaceFolder, name: string): Promise; +} + +export class ExecuteAPI implements IExecuteAPI, IExecuteAPIEx { private runners: ITaskRunner[] = []; constructor() { @@ -39,6 +44,34 @@ export class ExecuteAPI implements IExecuteAPI { }); } + public async executeProcessCommand(command: string, name: string, args: string[], rootDir: string, + workspace: vscode.WorkspaceFolder): Promise { + const process = new vscode.ProcessExecution(command, args, { + cwd: rootDir + }); + logger.log('executing process in workspace', process, workspace.uri.fsPath); + + const task = new vscode.Task({ type: 'wpilibprocexec'}, workspace, name, 'wpilib', process); + task.presentationOptions.echo = true; + task.presentationOptions.clear = true; + const execution = await vscode.tasks.executeTask(task); + const runner: ITaskRunner = { + cancelled: false, + condition: new PromiseCondition(-1), + execution, + + }; + this.runners.push(runner); + return runner.condition.wait(); + } + + public async executePythonCommand(args: string[], rootDir: string, workspace: vscode.WorkspaceFolder, name: string): Promise { + const configuration = vscode.workspace.getConfiguration('python', workspace.uri); + const interpreter: string = configuration.get('pythonPath', 'python'); + + return this.executeProcessCommand(interpreter, name, args, rootDir, workspace); + } + public async executeCommand(command: string, name: string, rootDir: string, workspace: vscode.WorkspaceFolder, env?: { [key: string]: string }): Promise { const shell = new vscode.ShellExecution(command, { diff --git a/vscode-wpilib/src/extension.ts b/vscode-wpilib/src/extension.ts index 1777cb08..ec817e81 100644 --- a/vscode-wpilib/src/extension.ts +++ b/vscode-wpilib/src/extension.ts @@ -15,7 +15,7 @@ import { CommandAPI } from './commandapi'; import { activateCpp } from './cpp/cpp'; import { ApiProvider } from './cppprovider/apiprovider'; import { DeployDebugAPI } from './deploydebugapi'; -import { ExecuteAPI } from './executor'; +import { ExecuteAPI, IExecuteAPIEx } from './executor'; import { activateJava } from './java/java'; import { findJdkPath } from './jdkdetector'; import { localize as i18n } from './locale'; @@ -36,9 +36,14 @@ import { Gradle2020Import } from './webviews/gradle2020import'; import { Help } from './webviews/help'; import { ProjectCreator } from './webviews/projectcreator'; import { WPILibUpdates } from './wpilibupdates'; +import { activatePython } from './python/python'; + +export interface IExternalAPIEx extends IExternalAPI { + getExecuteAPIEx(): IExecuteAPIEx; +} // External API class to implement the IExternalAPI interface -class ExternalAPI implements IExternalAPI { +class ExternalAPI implements IExternalAPI, IExternalAPIEx { // Create method is used because constructors cannot be async. public static async Create(resourceFolder: string): Promise { const preferencesApi = await PreferencesAPI.Create(); @@ -92,6 +97,9 @@ class ExternalAPI implements IExternalAPI { public getUtilitiesAPI(): UtilitiesAPI { return this.utilitiesApi; } + public getExecuteAPIEx(): IExecuteAPIEx { + return this.executeApi; + } } let updatePromptCount = 0; @@ -117,6 +125,8 @@ async function handleAfterTrusted(externalApi: ExternalAPI, context: vscode.Exte await activateCpp(context, externalApi); // Active the java parts of the extension await activateJava(context, externalApi); + // Activate the python parts of the extension + await activatePython(context, externalApi); try { // Add built in tools diff --git a/vscode-wpilib/src/python/deploydebug.ts b/vscode-wpilib/src/python/deploydebug.ts new file mode 100644 index 00000000..db516829 --- /dev/null +++ b/vscode-wpilib/src/python/deploydebug.ts @@ -0,0 +1,100 @@ +'use strict'; + +import * as vscode from 'vscode'; +import { ICodeDeployer, IPreferencesAPI } from 'vscode-wpilibapi'; +import { IExternalAPIEx } from '../extension'; +import { PyPreferencesAPI } from './pypreferencesapi'; +import { IExecuteAPIEx } from '../executor'; + +function getCurrentFileIfPython(): string | undefined { + const currentEditor = vscode.window.activeTextEditor; + if (currentEditor === undefined) { + return undefined; + } + if (currentEditor.document.fileName.endsWith('.py')) { + return currentEditor.document.fileName; + } + return undefined; +} + +class DeployCodeDeployer implements ICodeDeployer { + private preferences: IPreferencesAPI; + private pyPreferences : PyPreferencesAPI; + private executeApi: IExecuteAPIEx; + + constructor(externalApi: IExternalAPIEx, pyPreferences: PyPreferencesAPI) { + this.preferences = externalApi.getPreferencesAPI(); + this.executeApi = externalApi.getExecuteAPIEx(); + this.pyPreferences = pyPreferences; + } + + public async getIsCurrentlyValid(workspace: vscode.WorkspaceFolder): Promise { + const prefs = this.preferences.getPreferences(workspace); + const currentLanguage = prefs.getCurrentLanguage(); + return currentLanguage === 'none' || currentLanguage === 'python'; + } + + public async runDeployer(_teamNumber: number, workspace: vscode.WorkspaceFolder, + source: vscode.Uri | undefined, ..._args: string[]): Promise { + let file: string = ''; + if (source === undefined) { + const cFile = getCurrentFileIfPython(); + if (cFile !== undefined) { + file = cFile; + } else { + const mFile = await this.pyPreferences.getPreferences(workspace).getMainFile(); + if (mFile === undefined) { + return false; + } + file = mFile; + } + } else { + file = source.fsPath; + } + + const prefs = this.preferences.getPreferences(workspace); + + const deploy = [file, 'deploy', `--team=${await prefs.getTeamNumber()}`]; + + if (prefs.getSkipTests()) { + deploy.push('--skip-tests'); + } + + const result = await this.executeApi.executePythonCommand(deploy, workspace.uri.fsPath, workspace, 'Python Deploy'); + + return result === 0; + return false; + } + + public getDisplayName(): string { + return 'python'; + } + + public getDescription(): string { + return 'Python Deploy'; + } +} + +export class DeployDebug { + private deployDeployer: DeployCodeDeployer; + + constructor(externalApi: IExternalAPIEx, pyPreferences: PyPreferencesAPI) { + const deployDebugApi = externalApi.getDeployDebugAPI(); + deployDebugApi.addLanguageChoice('python'); + + // this.deployDebuger = new DebugCodeDeployer(externalApi); + this.deployDeployer = new DeployCodeDeployer(externalApi, pyPreferences); + // this.simulator = new SimulateCodeDeployer(externalApi); + + deployDebugApi.registerCodeDeploy(this.deployDeployer); + + // if (allowDebug) { + // deployDebugApi.registerCodeDebug(this.deployDebuger); + // deployDebugApi.registerCodeSimulate(this.simulator); + // } + } + + // tslint:disable-next-line:no-empty + public dispose() { + } +} diff --git a/vscode-wpilib/src/python/pypreferences.ts b/vscode-wpilib/src/python/pypreferences.ts new file mode 100644 index 00000000..3279a31a --- /dev/null +++ b/vscode-wpilib/src/python/pypreferences.ts @@ -0,0 +1,117 @@ +'use strict'; +import * as fs from 'fs'; +import * as jsonc from 'jsonc-parser'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { mkdirAsync, writeFileAsync } from '../utilities'; + +interface IPreferencesJson { + mainFile?: string; +} + +const defaultPreferences: IPreferencesJson = { +}; + +export class PyPreferences { + public workspace: vscode.WorkspaceFolder; + private preferencesFile?: vscode.Uri; + private readonly configFolder: string; + private readonly preferenceFileName: string = 'python_preferences.json'; + private preferencesJson: IPreferencesJson; + private configFileWatcher: vscode.FileSystemWatcher; + private readonly preferencesGlob: string = '**/' + this.preferenceFileName; + private disposables: vscode.Disposable[] = []; + + constructor(workspace: vscode.WorkspaceFolder) { + this.workspace = workspace; + this.configFolder = path.join(workspace.uri.fsPath, '.wpilib'); + + const configFilePath = path.join(this.configFolder, this.preferenceFileName); + + if (fs.existsSync(configFilePath)) { + this.preferencesFile = vscode.Uri.file(configFilePath); + this.preferencesJson = defaultPreferences; + this.updatePreferences(); + } else { + // Set up defaults, and create + this.preferencesJson = defaultPreferences; + } + + const rp = new vscode.RelativePattern(workspace, this.preferencesGlob); + + this.configFileWatcher = vscode.workspace.createFileSystemWatcher(rp); + this.disposables.push(this.configFileWatcher); + + this.configFileWatcher.onDidCreate((uri) => { + this.preferencesFile = uri; + this.updatePreferences(); + }); + + this.configFileWatcher.onDidDelete(() => { + this.preferencesFile = undefined; + this.updatePreferences(); + }); + + this.configFileWatcher.onDidChange(() => { + this.updatePreferences(); + }); + } + + public async getMainFile(): Promise { + if (this.preferencesJson.mainFile === undefined) { + const selection = await this.requestMainFile(); + if (selection !== undefined) { + await this.setMainFile(selection); + return selection; + } + } + return this.preferencesJson.mainFile; + } + + public async setMainFile(file: string): Promise { + this.preferencesJson.mainFile = file; + await this.writePreferences(); + } + + public dispose() { + for (const d of this.disposables) { + d.dispose(); + } + } + + private async requestMainFile(): Promise { + const glob = await vscode.workspace.findFiles(new vscode.RelativePattern(this.workspace, '*.py')); + if (glob.length === 0) { + return undefined; + } + + const map = glob.map((v) => { + return path.basename(v.fsPath); + }); + + const selection = await vscode.window.showQuickPick(map, { + placeHolder: 'Pick a file to be your main file', + }); + + return selection; + } + + private updatePreferences() { + if (this.preferencesFile === undefined) { + this.preferencesJson = defaultPreferences; + return; + } + + const results = fs.readFileSync(this.preferencesFile.fsPath, 'utf8'); + this.preferencesJson = jsonc.parse(results) as IPreferencesJson; + } + + private async writePreferences(): Promise { + if (this.preferencesFile === undefined) { + const configFilePath = path.join(this.configFolder, this.preferenceFileName); + this.preferencesFile = vscode.Uri.file(configFilePath); + await mkdirAsync(path.dirname(this.preferencesFile.fsPath)); + } + await writeFileAsync(this.preferencesFile.fsPath, JSON.stringify(this.preferencesJson, null, 4)); + } +} diff --git a/vscode-wpilib/src/python/pypreferencesapi.ts b/vscode-wpilib/src/python/pypreferencesapi.ts new file mode 100644 index 00000000..e64cfcd6 --- /dev/null +++ b/vscode-wpilib/src/python/pypreferencesapi.ts @@ -0,0 +1,94 @@ +'use strict'; +import * as vscode from 'vscode'; +import { PyPreferences } from './pypreferences'; + +export interface IPyPreferencesChangedPair { + workspace: vscode.WorkspaceFolder; + preference: PyPreferences; +} + +export class PyPreferencesAPI { + public onDidPreferencesFolderChanged: vscode.Event; + private preferences: PyPreferences[] = []; + private preferencesEmitter: vscode.EventEmitter = new vscode.EventEmitter(); + private disposables: vscode.Disposable[] = []; + + constructor() { + this.onDidPreferencesFolderChanged = this.preferencesEmitter.event; + + const workspaces = vscode.workspace.workspaceFolders; + if (workspaces !== undefined) { + for (const w of workspaces) { + this.preferences.push(new PyPreferences(w)); + } + } + this.disposables.push(this.preferencesEmitter); + + this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => { + // Nuke and reset + // TODO: Remove existing preferences from the extension context + for (const p of this.preferences) { + p.dispose(); + } + + const wp = vscode.workspace.workspaceFolders; + + if (wp === undefined) { + return; + } + + const pairArr: IPyPreferencesChangedPair[] = []; + this.preferences = []; + + for (const w of wp) { + const p = new PyPreferences(w); + this.preferences.push(p); + const pair: IPyPreferencesChangedPair = { + preference: p, + workspace: w, + }; + pairArr.push(pair); + } + + this.preferencesEmitter.fire(pairArr); + + this.disposables.push(...this.preferences); + })); + this.disposables.push(...this.preferences); + + } + + public getPreferences(workspace: vscode.WorkspaceFolder): PyPreferences { + for (const p of this.preferences) { + if (p.workspace.uri === workspace.uri) { + return p; + } + } + return this.preferences[0]; + } + + public async getFirstOrSelectedWorkspace(): Promise { + const wp = vscode.workspace.workspaceFolders; + if (wp === undefined) { + return undefined; + } + + if (wp.length > 1) { + const res = await vscode.window.showWorkspaceFolderPick(); + if (res !== undefined) { + return res; + } + return undefined; + } else if (wp.length === 1) { + return wp[0]; + } else { + return undefined; + } + } + + public dispose() { + for (const d of this.disposables) { + d.dispose(); + } + } +} diff --git a/vscode-wpilib/src/python/python.ts b/vscode-wpilib/src/python/python.ts new file mode 100644 index 00000000..3371e424 --- /dev/null +++ b/vscode-wpilib/src/python/python.ts @@ -0,0 +1,30 @@ +'use strict'; + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { DeployDebug } from './deploydebug'; +import { logger } from '../logger'; +import { IExternalAPIEx } from '../extension'; +import { PyPreferencesAPI } from './pypreferencesapi'; + +export async function activatePython(context: vscode.ExtensionContext, coreExports: IExternalAPIEx) { + + const extensionResourceLocation = path.join(context.extensionPath, 'resources', 'python'); + + const preferences = coreExports.getPreferencesAPI(); + const exampleTemplate = coreExports.getExampleTemplateAPI(); + const commandApi = coreExports.getCommandAPI(); + + const pythonExtension = vscode.extensions.getExtension('ms-python.python'); + if (pythonExtension === undefined) { + logger.log('Could not find python extension. Python deployment and debugging is disabled'); + return; + } + + const pyPrefs: PyPreferencesAPI = new PyPreferencesAPI(); + + const deployDebug = new DeployDebug(coreExports, pyPrefs); + context.subscriptions.push(deployDebug); + + +}