diff --git a/vscode-wpilib/eslint.config.js b/vscode-wpilib/eslint.config.js new file mode 100644 index 00000000..bec74d70 --- /dev/null +++ b/vscode-wpilib/eslint.config.js @@ -0,0 +1,5 @@ +// eslint.config.js +module.exports = + { + ignores: ["media/", "out/", "resources/"] + }; diff --git a/vscode-wpilib/media/main.css b/vscode-wpilib/media/main.css new file mode 100644 index 00000000..e57c1808 --- /dev/null +++ b/vscode-wpilib/media/main.css @@ -0,0 +1,40 @@ +body { + background-color: transparent; +} + +.color-list { + list-style: none; + padding: 0; +} + +.color-entry { + width: 100%; + display: flex; + margin-bottom: 0.4em; + border: 1px solid var(--vscode-input-border); +} + +.color-preview { + width: 2em; + height: 2em; +} + +.color-preview:hover { + outline: inset white; +} + +.color-input { + display: block; + flex: 1; + width: 100%; + color: var(--vscode-input-foreground); + background-color: var(--vscode-input-background); + border: none; + padding: 0 0.6em; +} + +.add-color-button { + display: block; + border: none; + margin: 0 auto; +} diff --git a/vscode-wpilib/media/main.js b/vscode-wpilib/media/main.js new file mode 100644 index 00000000..26ba7538 --- /dev/null +++ b/vscode-wpilib/media/main.js @@ -0,0 +1,178 @@ +//@ts-check + +// This script will be run within the webview itself +// It cannot access the main VS Code APIs directly. +(function () { + const vscode = acquireVsCodeApi(); + let dropdowns; + let buttons; + let message; + let uninstalls; + let installs; + + function populateDropDowns(data) { + dropdowns.forEach((dropdown, index) => { + const versions = data[index].versionInfo; + versions.forEach((versionTuple, i) => { + const option = document.createElement('option'); + option.value = versionTuple.version; + option.textContent = versionTuple.version; + if (data[index].currentVersion === versionTuple.version) { + option.selected = true; + buttons[index].textContent = versionTuple.buttonText; + if (i === 0) { + //This is the first element of the version array thus the most current + buttons[index].setAttribute('disabled', 'true'); + } + } + dropdown.appendChild(option); + }); + }); + } + + // Function to generate HTML for installed dependencies + function generateInstalledHTML(installed) { + const trash = document.getElementById('trashicon')?.innerHTML + // Create HTML for installed dependencies + let installedHtml = '

Installed VendorDeps

'; + installed.forEach((dep, index) => { + installedHtml += ` +
+
+ ${dep.name}${dep.currentVersion} +
+
+ + + +
+
+ `; + }); + return installedHtml; + } + + // Function to generate HTML for available dependencies + function generateAvailableHTML(available) { + // Create HTML for available dependencies + let availableHtml = '

Available Dependencies

'; + available.forEach((dep, index) => { + availableHtml += ` +
+
+ ${dep.name} +
+
${dep.version} - ${dep.description}
+
+ `; + }); + return availableHtml; + } + + // Add event listeners to the buttons + function addEventListeners() { + // Handle messages sent from the extension to the webview + window.addEventListener('message', event => { + message = event.data; // The json data that the extension sent + switch (message.type) { + case 'updateDependencies': + { + const installedHTML = generateInstalledHTML(message.installed); + const availableHTML = generateAvailableHTML(message.available); + + const installedContainer = document.getElementById('installed-dependencies'); + const availableContainer = document.getElementById('available-dependencies'); + + if (installedContainer) { + installedContainer.innerHTML = installedHTML; + } else { + console.error('Element with ID "installed-dependencies" not found.'); + } + + if (availableContainer) { + availableContainer.innerHTML = availableHTML; + } else { + console.error('Element with ID "available-dependencies" not found.'); + } + + dropdowns = document.querySelectorAll('select[id^="version-select-"]'); + buttons = document.querySelectorAll('button[id^="version-action-"]'); + uninstalls = document.querySelectorAll('button[id^="uninstall-action-"]'); + installs = document.querySelectorAll('button[id^="install-action-"]'); + addDropdownListeners(); + populateDropDowns(message.installed); + break; + } + } + }); + + document.getElementById('updateall-action')?.addEventListener('click', () => { + vscode.postMessage({ type: 'updateall' }) + }); + + document.getElementById('refresh-action')?.addEventListener('click', () => { + vscode.postMessage({ type: 'refresh' }) + }); + } + + function addDropdownListeners() { + // Add event listener for the dropdown + dropdowns.forEach((dropdown, index) => + dropdown.addEventListener('change', () => { + const versions = message.installed[index].versionInfo; + var selectedText = dropdown.options[dropdown.selectedIndex].text; + const version = versions.find(versionTuple => versionTuple.version === selectedText); + // Change button text based on selected dropdown value + buttons[index].textContent = version.buttonText; + + if (dropdown.selectedIndex === 0 && version.version === message.installed[index].currentVersion) { + //This is the first element of the version array thus the most current + buttons[index].disabled = true; + } else { + buttons[index].disabled = false; + } + }) + ); + + buttons.forEach(button => { + button.addEventListener('click', () => { + const action = button.getAttribute('id'); + const index = getStringUntilFirstDashFromRight(action); + const drop = document.getElementById("version-select-" + index); + if (drop && drop instanceof HTMLSelectElement) { + var selectedText = drop.options[drop.selectedIndex].text; + // Handle update logic here + vscode.postMessage({ type: 'update', version: selectedText, index: index }); + } + }); + }); + + uninstalls.forEach(uninstall => { + uninstall.addEventListener('click', () => { + const action = uninstall.getAttribute('id'); + const index = getStringUntilFirstDashFromRight(action); + vscode.postMessage({ type: 'uninstall', index: index }); + }); + }); + + installs.forEach(install => { + install.addEventListener('click', () => { + const action = install.getAttribute('id'); + const index = getStringUntilFirstDashFromRight(action); + vscode.postMessage({ type: 'install', index: index }); + }); + }); + } + + function getStringUntilFirstDashFromRight(str) { + const index = str.lastIndexOf("-"); + if (index === -1) { + return str; // No dash found, return the whole string + } + return str.substring(index + 1); + } + + addEventListeners(); +}()); \ No newline at end of file diff --git a/vscode-wpilib/media/reset.css b/vscode-wpilib/media/reset.css new file mode 100644 index 00000000..92d02910 --- /dev/null +++ b/vscode-wpilib/media/reset.css @@ -0,0 +1,30 @@ +html { + box-sizing: border-box; + font-size: 13px; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body, +h1, +h2, +h3, +h4, +h5, +h6, +p, +ol, +ul { + margin: 0; + padding: 0; + font-weight: normal; +} + +img { + max-width: 100%; + height: auto; +} diff --git a/vscode-wpilib/media/trash-can-solid.png b/vscode-wpilib/media/trash-can-solid.png new file mode 100644 index 00000000..a2126a01 Binary files /dev/null and b/vscode-wpilib/media/trash-can-solid.png differ diff --git a/vscode-wpilib/media/vscode.css b/vscode-wpilib/media/vscode.css new file mode 100644 index 00000000..5d12b7e0 --- /dev/null +++ b/vscode-wpilib/media/vscode.css @@ -0,0 +1,91 @@ +:root { + --container-paddding: 20px; + --input-padding-vertical: 6px; + --input-padding-horizontal: 4px; + --input-margin-vertical: 4px; + --input-margin-horizontal: 0; +} + +body { + padding: 0 var(--container-paddding); + color: var(--vscode-foreground); + font-size: var(--vscode-font-size); + font-weight: var(--vscode-font-weight); + font-family: var(--vscode-font-family); + background-color: var(--vscode-editor-background); +} + +ol, +ul { + padding-left: var(--container-paddding); +} + +body > *, +form > * { + margin-block-start: var(--input-margin-vertical); + margin-block-end: var(--input-margin-vertical); +} + +*:focus { + outline-color: var(--vscode-focusBorder) !important; +} + +a { + color: var(--vscode-textLink-foreground); +} + +a:hover, +a:active { + color: var(--vscode-textLink-activeForeground); +} + +code { + font-size: var(--vscode-editor-font-size); + font-family: var(--vscode-editor-font-family); +} + +button { + border: none; + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + width: 100%; + text-align: center; + outline: 1px solid transparent; + outline-offset: 2px !important; + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); +} + +button:hover { + cursor: pointer; + background: var(--vscode-button-hoverBackground); +} + +button:focus { + outline-color: var(--vscode-focusBorder); +} + +button.secondary { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); +} + +button.secondary:hover { + background: var(--vscode-button-secondaryHoverBackground); +} + +input:not([type='checkbox']), +textarea { + display: block; + width: 100%; + border: none; + font-family: var(--vscode-font-family); + padding: var(--input-padding-vertical) var(--input-padding-horizontal); + color: var(--vscode-input-foreground); + outline-color: var(--vscode-input-border); + background-color: var(--vscode-input-background); +} + +input::placeholder, +textarea::placeholder { + color: var(--vscode-input-placeholderForeground); +} diff --git a/vscode-wpilib/media/wpilib.png b/vscode-wpilib/media/wpilib.png new file mode 100644 index 00000000..a1232673 Binary files /dev/null and b/vscode-wpilib/media/wpilib.png differ diff --git a/vscode-wpilib/package.json b/vscode-wpilib/package.json index 34bf41da..de2a3674 100644 --- a/vscode-wpilib/package.json +++ b/vscode-wpilib/package.json @@ -57,6 +57,15 @@ ], "main": "./out/extension", "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "wpilib-vendordeps", + "title": "WPILib Vendor Dependencies", + "icon": "media/wpilib.png" + } + ] + }, "untrustedWorkspaces": { "supported": "limited", "description": "%wpilibcore.untrustedWorkspace.title%" @@ -168,6 +177,17 @@ } }, "commands": [ + { + "command": "calicoColors.addColor", + "category": "Calico Colors", + "title": "Add Color" + }, + { + "command": "calicoColors.clearColors", + "category": "Calico Colors", + "title": "Clear Colors", + "icon": "$(clear-all)" + }, { "command": "wpilibcore.startRioLog", "title": "%wpilibcore.startRioLog.title%", @@ -405,7 +425,15 @@ "name": "C++ Dependencies", "when": "isWPILibProvidedCpp" } - ] + ], + "wpilib-vendordeps": [ + { + "type": "webview", + "id": "wpilib.dependencyView", + "name": "WPILib Vendor Dependencies", + "icon": "media/wpilib.png" + } + ] }, "keybindings": [ { diff --git a/vscode-wpilib/src/dependencyView.ts b/vscode-wpilib/src/dependencyView.ts new file mode 100644 index 00000000..29938861 --- /dev/null +++ b/vscode-wpilib/src/dependencyView.ts @@ -0,0 +1,435 @@ +import * as vscode from 'vscode'; +import * as fetch from 'node-fetch'; +import { ProjectInfoGatherer, IProjectInfo } from './projectinfo'; +import { VendorLibraries } from './vendorlibraries'; +import { IJsonDependency } from './shared/vendorlibrariesbase'; +import { IExternalAPI } from 'vscode-wpilibapi'; +import { isNewerVersion } from './versions'; +import { logger } from './logger'; +import { localize as i18n } from './locale'; + +export interface IJsonList { + path: string; + name: string; + version: string; + uuid: string; + description: string; + website: string; +} + +export interface IDepInstalled { name: string; currentVersion: string; versionInfo: { version: string, buttonText: string }[]; } + +export interface IJSMessage { type: string; version: string; index: string; } + +export class DependencyViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = 'wpilib.dependencyView'; + private projectInfo: ProjectInfoGatherer; + private vendorLibraries: VendorLibraries; + private viewInfo: IProjectInfo | undefined; + private disposables: vscode.Disposable[] = []; + private installedDeps: IJsonDependency[] = []; + private availableDeps: IJsonList[] = []; + private availableDepsList: IJsonList[] = []; + private onlineDeps: IJsonList[] = []; + private installedList: IDepInstalled[] = []; + private homeDeps: IJsonDependency[] = []; + private externalApi: IExternalAPI; + private ghURL = `https://raw.githubusercontent.com/wpilibsuite/vendor-json-repo/master/`; + private wp: vscode.WorkspaceFolder | undefined; + + private _view?: vscode.WebviewView; + + constructor( + private readonly _extensionUri: vscode.Uri, + projectInfo: ProjectInfoGatherer, + vendorLibraries: VendorLibraries, + externalAPI: IExternalAPI + ) { + this.projectInfo = projectInfo; + this.vendorLibraries = vendorLibraries; + this.externalApi = externalAPI; + } + + public async resolveWebviewView( + webviewView: vscode.WebviewView, + _context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + this._view = webviewView; + + webviewView.webview.options = { + // Allow scripts in the webview + enableScripts: true, + + localResourceRoots: [ + this._extensionUri, + vscode.Uri.joinPath(this._extensionUri, 'media') + ] + }; + + this.wp = await this.externalApi.getPreferencesAPI().getFirstOrSelectedWorkspace(); + if (this.wp === undefined) { + logger.warn('no workspace'); + return; + } + + if (this.projectInfo) { + this.viewInfo = await this.projectInfo.getViewInfo(); + } + + void this.refresh(this.wp); + webviewView.onDidChangeVisibility(() => { + if (this.wp) { + void this.refresh(this.wp); + } + }); + + this.viewInfo?.vendorLibraries.forEach(item => console.log(item.name.concat(' / ', item.version))); + + webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + + webviewView.webview.onDidReceiveMessage(data => { + if (this.isJSMessage(data)) { + switch (data.type) { + case 'install': + { + void this.install(data.index); + break; + } + case 'uninstall': + { + void this.uninstall(data.index); + break; + } + case 'update': + { + void this.update(data.version, data.index); + break; + } + case 'updateall': + { + void this.updateall(); + break; + } + case 'refresh': + { + if (this.wp) { + void this.refresh(this.wp); + } + break; + } + default: + { + break; + } + } + } + }); + } + + private isJSMessage(object: unknown): object is IJSMessage { + const maybeJSMessage = object as IJSMessage; + return maybeJSMessage && typeof maybeJSMessage.type === 'string'; + } + + private async update(version: string, indexString: string) { + const index = parseInt(indexString, 10); + // Make sure we have a workspace + if (this.wp) { + // If the version matches the current version then we want to update to latest + const versionToInstall = (version === this.installedList[index].currentVersion) + // Get the version of the first element of the array AKA the latest version + ? this.installedList[index].versionInfo[0].version + // It isn't the current version so user must have specified something else + : version; + + // Match both the name and the version + const avail = this.availableDeps.find(available => + (versionToInstall === available.version && this.installedList[index].name === available.name)); + await this.getURLInstallDep(avail); + await this.refresh(this.wp); + } + } + + private async updateall() { + if (this.wp) { + for (const installed of this.installedList) { + if (installed.versionInfo[0].version !== installed.currentVersion && this.wp) { + // Match both the name and the version + const avail = this.availableDeps.find(available => + (installed.versionInfo[0].version === available.version && installed.name === available.name)); + await this.getURLInstallDep(avail); + } + } + + await this.refresh(this.wp); + } + } + + private async install(index: string) { + const avail = this.availableDepsList[parseInt(index, 10)]; + if (avail && this.wp) { + await this.getURLInstallDep(avail); + await this.refresh(this.wp); + } + } + + private async uninstall(index: string) { + const uninstall = [this.installedDeps[parseInt(index, 10)]]; + if (this.wp) { + await this.vendorLibraries.uninstallVendorLibraries(uninstall, this.wp); + await this.refresh(this.wp); + } + } + + private async getURLInstallDep(avail: IJsonList | undefined) { + if (avail && this.wp) { + // Check to see if it is already a URL + let url = avail.path; + if (url.substring(0, 4) !== 'http') { + url = this.ghURL + url; + } + let dep; + try { + dep = await this.vendorLibraries.getJsonDepURL(url); + } catch { + dep = this.homeDeps.find(homdep => homdep.uuid === avail.uuid && homdep.version === avail.version); + } + + if (dep) { + const success = await this.vendorLibraries.installDependency(dep, this.vendorLibraries.getWpVendorFolder(this.wp), true); + + if (success) { + this.vendorLibraries.offerBuild(this.wp); + } + } + } + } + + public addDependency() { + if (this._view) { + this._view.webview.postMessage({ type: 'addDependency' }); + } + } + + public updateDependencies() { + if (this._view) { + this._view.webview.postMessage({ type: 'updateDependencies', installed: this.installedList, available: this.availableDepsList }); + } + } + + public dispose() { + for (const d of this.disposables) { + d.dispose(); + } + } + + public async refresh(workspace: vscode.WorkspaceFolder) { + this.installedDeps = await this.vendorLibraries.getCurrentlyInstalledLibraries(workspace); + this.installedList = []; + this.availableDepsList = []; + + this.availableDeps = await this.getAvailableDependencies(); + if (this.availableDeps.length !== 0) { + // Check Github for the VendorDep list + if (this.installedDeps.length !== 0) { + for (const id of this.installedDeps) { + let versionList = [{version: id.version, buttonText: i18n('ui', 'To Latest')}]; + for (const ad of this.availableDeps) { + if (id.uuid === ad.uuid) { + // Populate version array with version and button text + if (id.version !== ad.version) { + if (isNewerVersion(ad.version, id.version)) { + versionList.push({version: ad.version, buttonText: i18n('ui', 'Update')}); + } else { + versionList.push({version: ad.version, buttonText: i18n('ui', 'Downgrade')}); + } + } + } + } + // Now we need to sort the version list newest to oldest + versionList = this.sortVersions(versionList); + + this.installedList.push({ name: id.name, currentVersion: id.version, versionInfo: versionList }); + } + } + + // We need to group the available deps and filter out the installed ones + this.availableDeps.forEach(dep => { + // See if the dep is one of the installed deps if so don't add it + const installedDep = this.installedDeps.findIndex(depend => depend.uuid === dep.uuid); + if (installedDep < 0) { + // Check to see if it is already in the available list + const foundDep = this.availableDepsList.findIndex(depend => depend.uuid === dep.uuid); + if (foundDep < 0) { + // Not in the list so just add it + this.availableDepsList.push(dep); + } else if (isNewerVersion(dep.version, this.availableDepsList[foundDep].version)) { + // It was in the list but this version is newer so lets use that + this.availableDepsList[foundDep] = dep; + } + } + }); + + this.sortInstalled(); + this.sortAvailable(); + + this.updateDependencies(); + } + } + + private sortVersions(versionList: { version: string, buttonText: string }[]): { version: string, buttonText: string }[] { + versionList.sort((a, b) => { + if (isNewerVersion(a.version, b.version)) { + return -1; + } + else if (a.version === b.version) { + return 0; + } else { + return 1; + } + }); + return versionList; + } + + private sortInstalled() { + this.installedList.sort((a, b) => { + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + else if (a.name.toLowerCase() === b.name.toLowerCase()) { + return 0; + } else { + return -1; + } + }); + } + + private sortAvailable() { + this.availableDepsList.sort((a, b) => { + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + else if (a.name.toLowerCase() === b.name.toLowerCase()) { + return 0; + } else { + return -1; + } + }); + } + + public async getAvailableDependencies(): Promise { + this.homeDeps = []; + const listURL = this.ghURL + `${this.externalApi.getUtilitiesAPI().getFrcYear()}.json`; + try { + this.onlineDeps = await this.loadFileFromUrl(listURL); + } catch (err) { + logger.log('Error fetching file', err); + this.onlineDeps = []; + } + this.homeDeps = await this.vendorLibraries.getHomeDirDeps(); + this.homeDeps.forEach(homedep => { + const depList: IJsonList = { + path: i18n('ui', homedep.jsonUrl), + name: i18n('ui', homedep.name), + version: i18n('ui', homedep.version), + uuid: i18n('ui', homedep.uuid), + description: i18n('ui', 'Loaded from Local Copy'), + website: i18n('ui', 'Loaded from Local Copy') + }; + const found = this.onlineDeps.find(onlinedep => onlinedep.uuid === depList.uuid && onlinedep.version === depList.version); + if (!found) { + this.onlineDeps.push(depList); + } + }); + + return this.onlineDeps; + } + + protected async loadFileFromUrl(url: string): Promise { + const response = await fetch.default(url, { + timeout: 5000, + }); + if (response === undefined) { + throw new Error('Failed to fetch file'); + } + if (response.status >= 200 && response.status <= 300) { + const text = await response.text(); + const json = JSON.parse(text) as IJsonList[]; + if (this.isJsonList(json)) { + return json; + } else { + throw new Error('Incorrect JSON format'); + } + } else { + throw new Error('Bad status ' + response.status); + } + } + + private isJsonList(jsonDepList: IJsonList[]): jsonDepList is IJsonList[] { + return jsonDepList.every(jsonDep => { + return jsonDep.path !== undefined && jsonDep.name !== undefined + && jsonDep.uuid !== undefined && jsonDep.version !== undefined + && jsonDep.description !== undefined && jsonDep.website !== undefined; }); + } + + private _getHtmlForWebview(webview: vscode.Webview): string { + // Get the local path to main script run in the webview, then convert it to a uri we can use in the webview. + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'main.js')); + const trashUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'trash-can-solid.png')); + + // Return the complete HTML + return ` + + + + + + Vendor Dependencies + + + +
+ +
+
+
+
+ + + + + `; + } +} diff --git a/vscode-wpilib/src/extension.ts b/vscode-wpilib/src/extension.ts index 5d767dc7..56f45aac 100644 --- a/vscode-wpilib/src/extension.ts +++ b/vscode-wpilib/src/extension.ts @@ -35,6 +35,7 @@ import { Gradle2020Import } from './webviews/gradle2020import'; import { Help } from './webviews/help'; import { ProjectCreator } from './webviews/projectcreator'; import { WPILibUpdates } from './wpilibupdates'; +import { DependencyViewProvider } from './dependencyView'; // External API class to implement the IExternalAPI interface class ExternalAPI implements IExternalAPI { @@ -158,6 +159,39 @@ async function handleAfterTrusted(externalApi: ExternalAPI, context: vscode.Exte creationError = true; } + let depProvider: DependencyViewProvider | undefined; + + try { + if (projectInfo !== undefined && vendorLibs !== undefined) { + depProvider = new DependencyViewProvider(context.extensionUri, projectInfo, vendorLibs, externalApi); + + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(DependencyViewProvider.viewType, depProvider)); + + if (depProvider !== undefined) { + context.subscriptions.push( + vscode.commands.registerCommand('wpilib.addDependency', () => { + depProvider?.addDependency(); + })); + +/* context.subscriptions.push( + vscode.commands.registerCommand('wpilib.removeDependency', () => { + depProvider?.removeDependency(); + })) */ + + context.subscriptions.push( + vscode.commands.registerCommand('wpilib.updateDependencies', () => { + depProvider?.updateDependencies(); + })); + } + + context.subscriptions.push(depProvider); + } + } catch (err) { + logger.error('error creating dependency view', err); + creationError = true; + } + // Create all of our commands that the extension runs createVsCommands(context, externalApi); diff --git a/vscode-wpilib/src/projectinfo.ts b/vscode-wpilib/src/projectinfo.ts index de75b138..e7847ed5 100644 --- a/vscode-wpilib/src/projectinfo.ts +++ b/vscode-wpilib/src/projectinfo.ts @@ -108,6 +108,14 @@ Vendor Libraries: }); } + public async getViewInfo(): Promise { + const wp = await this.externalApi.getPreferencesAPI().getFirstOrSelectedWorkspace(); + if (wp === undefined) { + return wp; + } + return this.getProjectInfo(wp); + } + private async getProjectInfo(workspace: vscode.WorkspaceFolder): Promise { const vendorLibs = await this.vendorLibraries.getCurrentlyInstalledLibraries(workspace); diff --git a/vscode-wpilib/src/shared/vendorlibrariesbase.ts b/vscode-wpilib/src/shared/vendorlibrariesbase.ts index 7f52710a..8ec4aad8 100644 --- a/vscode-wpilib/src/shared/vendorlibrariesbase.ts +++ b/vscode-wpilib/src/shared/vendorlibrariesbase.ts @@ -71,7 +71,7 @@ export class VendorLibrariesBase { return true; } - protected getHomeDirDeps(): Promise { + public getHomeDirDeps(): Promise { return this.getDependencies(path.join(this.utilities.getWPILibHomeDir(), 'vendordeps')); } diff --git a/vscode-wpilib/src/vendorlibraries.ts b/vscode-wpilib/src/vendorlibraries.ts index 746677c4..abb00b34 100644 --- a/vscode-wpilib/src/vendorlibraries.ts +++ b/vscode-wpilib/src/vendorlibraries.ts @@ -98,8 +98,13 @@ export class VendorLibraries extends VendorLibrariesBase { return this.getInstalledDependencies(workspace); } + public async getJsonDepURL(url: string): Promise { + return this.loadFileFromUrl(url); + } + private async manageCurrentLibraries(workspace: vscode.WorkspaceFolder): Promise { const installedDeps = await this.getInstalledDependencies(workspace); + const deps: IJsonDependency[] = []; if (installedDeps.length !== 0) { const arr = installedDeps.map((jdep) => { @@ -111,25 +116,41 @@ export class VendorLibraries extends VendorLibrariesBase { }); if (toRemove !== undefined) { - const url = this.getWpVendorFolder(workspace); - const files = await readdirAsync(url); - for (const file of files) { - const fullPath = path.join(url, file); - const result = await this.readFile(fullPath); - if (result !== undefined) { - for (const ti of toRemove) { - if (ti.dep.uuid === result.uuid) { - await deleteFileAsync(fullPath); - } - } - } + for (const ti of toRemove) { + deps.push(ti.dep); } } + + void this.uninstallVendorLibraries(deps, workspace); } else { vscode.window.showInformationMessage(i18n('message', 'No dependencies installed')); } } + public async uninstallVendorLibraries(toRemove: IJsonDependency[] | undefined, workspace: vscode.WorkspaceFolder) { + if (toRemove !== undefined) { + let anySucceeded = false; + const url = this.getWpVendorFolder(workspace); + const files = await readdirAsync(url); + for (const file of files) { + const fullPath = path.join(url, file); + const result = await this.readFile(fullPath); + if (result !== undefined) { + for (const ti of toRemove) { + if (ti.uuid === result.uuid) { + await deleteFileAsync(fullPath); + anySucceeded = true; + } + } + } + } + + if (anySucceeded) { + this.offerBuild(workspace); + } + } + } + private async offlineUpdates(workspace: vscode.WorkspaceFolder): Promise { const installedDeps = await this.getInstalledDependencies(workspace); @@ -164,14 +185,7 @@ export class VendorLibraries extends VendorLibrariesBase { } } if (anySucceeded) { - const buildRes = await vscode.window.showInformationMessage(i18n('message', - 'It is recommended to run a "Build" after a vendor update to ensure dependencies are installed correctly. ' + - 'Would you like to do this now?'), { - modal: true, - }, {title: i18n('ui', 'Yes')}, {title: i18n('ui', 'No'), isCloseAffordance: true}); - if (buildRes?.title === i18n('ui', 'Yes')) { - await this.externalApi.getBuildTestAPI().buildCode(workspace, undefined); - } + this.offerBuild(workspace); } } } else { @@ -227,14 +241,7 @@ export class VendorLibraries extends VendorLibrariesBase { } } if (anySucceeded) { - const buildRes = await vscode.window.showInformationMessage(i18n('message', - 'It is recommended to run a "Build" after a vendor update to ensure dependencies are installed correctly. ' + - 'Would you like to do this now?'), { - modal: true, - }, {title: i18n('ui', 'Yes')}, {title: i18n('ui', 'No'), isCloseAffordance: true}); - if (buildRes?.title === i18n('ui', 'Yes')) { - await this.externalApi.getBuildTestAPI().buildCode(workspace, undefined); - } + this.offerBuild(workspace); } } } else { @@ -280,14 +287,7 @@ export class VendorLibraries extends VendorLibrariesBase { } } if (anySucceeded) { - const buildRes = await vscode.window.showInformationMessage(i18n('message', - 'It is recommended to run a "Build" after a vendor update to ensure dependencies are installed correctly. ' + - 'Would you like to do this now?'), { - modal: true, - }, {title: i18n('ui', 'Yes')}, {title: i18n('ui', 'No'), isCloseAffordance: true}); - if (buildRes?.title === i18n('ui', 'Yes')) { - await this.externalApi.getBuildTestAPI().buildCode(workspace, undefined); - } + this.offerBuild(workspace); } } } else { @@ -316,27 +316,31 @@ export class VendorLibraries extends VendorLibrariesBase { const success = await this.installDependency(file, this.getWpVendorFolder(workspace), true); if (success) { - const buildRes = await vscode.window.showInformationMessage(i18n('message', - 'It is recommended to run a "Build" after a vendor update to ensure dependencies are installed correctly. ' + - 'Would you like to do this now?'), { - modal: true, - }, {title: i18n('ui', 'Yes')}, {title: i18n('ui', 'No'), isCloseAffordance: true}); - if (buildRes?.title === i18n('ui', 'Yes')) { - await this.externalApi.getBuildTestAPI().buildCode(workspace, undefined); - } + this.offerBuild(workspace); } else { vscode.window.showErrorMessage(i18n('message', 'Failed to install {0}', file.name)); } } } - private getWpVendorFolder(workspace: vscode.WorkspaceFolder): string { + public getWpVendorFolder(workspace: vscode.WorkspaceFolder): string { return this.getVendorFolder(workspace.uri.fsPath); } private getInstalledDependencies(workspace: vscode.WorkspaceFolder): Promise { return this.getDependencies(this.getWpVendorFolder(workspace)); } + + public async offerBuild(workspace: vscode.WorkspaceFolder) { + const buildRes = await vscode.window.showInformationMessage(i18n('message', + 'It is recommended to run a "Build" after a vendor update. ' + + 'Would you like to do this now?'), { + modal: false, + }, {title: i18n('ui', 'Yes')}, {title: i18n('ui', 'No'), isCloseAffordance: true}); + if (buildRes?.title === i18n('ui', 'Yes')) { + await this.externalApi.getBuildTestAPI().buildCode(workspace, undefined); + } + } } const eventListener: vscode.EventEmitter = new vscode.EventEmitter();