From 4862f650084d987d52fd4bd870edb178f50e286b Mon Sep 17 00:00:00 2001 From: Thad House Date: Thu, 28 Sep 2023 18:45:23 -0700 Subject: [PATCH 1/2] Start adding python support This is just the basic start, and only a code deployer to start with (and one that doesn't deploy at that, as I don't know the command) --- vscode-wpilib/src/extension.ts | 3 ++ vscode-wpilib/src/python/deploydebug.ts | 61 +++++++++++++++++++++++++ vscode-wpilib/src/python/python.ts | 20 ++++++++ 3 files changed, 84 insertions(+) create mode 100644 vscode-wpilib/src/python/deploydebug.ts create mode 100644 vscode-wpilib/src/python/python.ts diff --git a/vscode-wpilib/src/extension.ts b/vscode-wpilib/src/extension.ts index 7e17fdd4..57891610 100644 --- a/vscode-wpilib/src/extension.ts +++ b/vscode-wpilib/src/extension.ts @@ -36,6 +36,7 @@ import { Gradle2020Import } from './webviews/gradle2020import'; import { Help } from './webviews/help'; import { ProjectCreator } from './webviews/projectcreator'; import { WPILibUpdates } from './wpilibupdates'; +import { activatePython } from './python/python'; // External API class to implement the IExternalAPI interface class ExternalAPI implements IExternalAPI { @@ -117,6 +118,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..ad94f95a --- /dev/null +++ b/vscode-wpilib/src/python/deploydebug.ts @@ -0,0 +1,61 @@ +'use strict'; + +import * as jsonc from 'jsonc-parser'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { ICodeDeployer, IExecuteAPI, IExternalAPI, IPreferencesAPI } from 'vscode-wpilibapi'; +import { gradleRun, readFileAsync } from '../utilities'; + +class DeployCodeDeployer implements ICodeDeployer { + private preferences: IPreferencesAPI; + private executeApi: IExecuteAPI; + + constructor(externalApi: IExternalAPI) { + this.preferences = externalApi.getPreferencesAPI(); + this.executeApi = externalApi.getExecuteAPI(); + } + + public async getIsCurrentlyValid(workspace: vscode.WorkspaceFolder): Promise { + const prefs = this.preferences.getPreferences(workspace); + const currentLanguage = prefs.getCurrentLanguage(); + return currentLanguage === 'none' || currentLanguage === 'java'; + } + + public async runDeployer(teamNumber: number, workspace: vscode.WorkspaceFolder, + _: vscode.Uri | undefined, ...args: string[]): Promise { + // TODO actaully deploy + return false; + } + + public getDisplayName(): string { + return 'python'; + } + + public getDescription(): string { + return 'Python Deploy'; + } +} + +export class DeployDebug { + private deployDeployer: DeployCodeDeployer; + + constructor(externalApi: IExternalAPI) { + const deployDebugApi = externalApi.getDeployDebugAPI(); + deployDebugApi.addLanguageChoice('python'); + + //this.deployDebuger = new DebugCodeDeployer(externalApi); + this.deployDeployer = new DeployCodeDeployer(externalApi); + //this.simulator = new SimulateCodeDeployer(externalApi); + + deployDebugApi.registerCodeDeploy(this.deployDeployer); + + // if (allowDebug) { + // deployDebugApi.registerCodeDebug(this.deployDebuger); + // deployDebugApi.registerCodeSimulate(this.simulator); + // } + } + + public dispose() { + // + } + } diff --git a/vscode-wpilib/src/python/python.ts b/vscode-wpilib/src/python/python.ts new file mode 100644 index 00000000..6264933c --- /dev/null +++ b/vscode-wpilib/src/python/python.ts @@ -0,0 +1,20 @@ +'use strict'; + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { IExternalAPI } from 'vscode-wpilibapi'; +import { DeployDebug } from './deploydebug'; + +export async function activatePython(context: vscode.ExtensionContext, coreExports: IExternalAPI) { + + const extensionResourceLocation = path.join(context.extensionPath, 'resources', 'python'); + + const preferences = coreExports.getPreferencesAPI(); + const exampleTemplate = coreExports.getExampleTemplateAPI(); + const commandApi = coreExports.getCommandAPI(); + + const deployDebug = new DeployDebug(coreExports); + context.subscriptions.push(deployDebug); + + +} From fd3c14e81769f0ef7deb16116e64358d0cf5e6ba Mon Sep 17 00:00:00 2001 From: Thad House Date: Wed, 20 Dec 2023 11:50:30 -0800 Subject: [PATCH 2/2] Add more --- vscode-wpilib/src/executor.ts | 35 +++++- vscode-wpilib/src/extension.ts | 11 +- vscode-wpilib/src/python/deploydebug.ts | 87 ++++++++++---- vscode-wpilib/src/python/pypreferences.ts | 117 +++++++++++++++++++ vscode-wpilib/src/python/pypreferencesapi.ts | 94 +++++++++++++++ vscode-wpilib/src/python/python.ts | 16 ++- 6 files changed, 330 insertions(+), 30 deletions(-) create mode 100644 vscode-wpilib/src/python/pypreferences.ts create mode 100644 vscode-wpilib/src/python/pypreferencesapi.ts 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 1f24d472..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'; @@ -38,8 +38,12 @@ 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(); @@ -93,6 +97,9 @@ class ExternalAPI implements IExternalAPI { public getUtilitiesAPI(): UtilitiesAPI { return this.utilitiesApi; } + public getExecuteAPIEx(): IExecuteAPIEx { + return this.executeApi; + } } let updatePromptCount = 0; diff --git a/vscode-wpilib/src/python/deploydebug.ts b/vscode-wpilib/src/python/deploydebug.ts index ad94f95a..db516829 100644 --- a/vscode-wpilib/src/python/deploydebug.ts +++ b/vscode-wpilib/src/python/deploydebug.ts @@ -1,29 +1,68 @@ 'use strict'; -import * as jsonc from 'jsonc-parser'; -import * as path from 'path'; import * as vscode from 'vscode'; -import { ICodeDeployer, IExecuteAPI, IExternalAPI, IPreferencesAPI } from 'vscode-wpilibapi'; -import { gradleRun, readFileAsync } from '../utilities'; +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 executeApi: IExecuteAPI; + private pyPreferences : PyPreferencesAPI; + private executeApi: IExecuteAPIEx; - constructor(externalApi: IExternalAPI) { + constructor(externalApi: IExternalAPIEx, pyPreferences: PyPreferencesAPI) { this.preferences = externalApi.getPreferencesAPI(); - this.executeApi = externalApi.getExecuteAPI(); + 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 === 'java'; + return currentLanguage === 'none' || currentLanguage === 'python'; } - public async runDeployer(teamNumber: number, workspace: vscode.WorkspaceFolder, - _: vscode.Uri | undefined, ...args: string[]): Promise { - // TODO actaully deploy + 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; } @@ -39,23 +78,23 @@ class DeployCodeDeployer implements ICodeDeployer { export class DeployDebug { private deployDeployer: DeployCodeDeployer; - constructor(externalApi: IExternalAPI) { - const deployDebugApi = externalApi.getDeployDebugAPI(); - deployDebugApi.addLanguageChoice('python'); + constructor(externalApi: IExternalAPIEx, pyPreferences: PyPreferencesAPI) { + const deployDebugApi = externalApi.getDeployDebugAPI(); + deployDebugApi.addLanguageChoice('python'); - //this.deployDebuger = new DebugCodeDeployer(externalApi); - this.deployDeployer = new DeployCodeDeployer(externalApi); - //this.simulator = new SimulateCodeDeployer(externalApi); + // this.deployDebuger = new DebugCodeDeployer(externalApi); + this.deployDeployer = new DeployCodeDeployer(externalApi, pyPreferences); + // this.simulator = new SimulateCodeDeployer(externalApi); - deployDebugApi.registerCodeDeploy(this.deployDeployer); + deployDebugApi.registerCodeDeploy(this.deployDeployer); - // if (allowDebug) { - // deployDebugApi.registerCodeDebug(this.deployDebuger); - // deployDebugApi.registerCodeSimulate(this.simulator); - // } + // 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 index 6264933c..3371e424 100644 --- a/vscode-wpilib/src/python/python.ts +++ b/vscode-wpilib/src/python/python.ts @@ -2,10 +2,12 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import { IExternalAPI } from 'vscode-wpilibapi'; import { DeployDebug } from './deploydebug'; +import { logger } from '../logger'; +import { IExternalAPIEx } from '../extension'; +import { PyPreferencesAPI } from './pypreferencesapi'; -export async function activatePython(context: vscode.ExtensionContext, coreExports: IExternalAPI) { +export async function activatePython(context: vscode.ExtensionContext, coreExports: IExternalAPIEx) { const extensionResourceLocation = path.join(context.extensionPath, 'resources', 'python'); @@ -13,7 +15,15 @@ export async function activatePython(context: vscode.ExtensionContext, coreExpor const exampleTemplate = coreExports.getExampleTemplateAPI(); const commandApi = coreExports.getCommandAPI(); - const deployDebug = new DeployDebug(coreExports); + 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);