From 0be02a573bb48435f36e9517c4b69a2d7b633f96 Mon Sep 17 00:00:00 2001 From: Koby Hall <102518238+kobyhallx@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:15:11 +0100 Subject: [PATCH] feat(aztec): search for aztec-nargo on top of nargo bin (#67) Co-authored-by: Tom French Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- src/client.ts | 4 ++ src/extension.ts | 134 +++++++++++++++++++++++++++------------------- src/find-nargo.ts | 62 ++++++++++++++++++--- src/noir.ts | 41 ++++++++++++++ 4 files changed, 179 insertions(+), 62 deletions(-) diff --git a/src/client.ts b/src/client.ts index f114fcf..591a996 100644 --- a/src/client.ts +++ b/src/client.ts @@ -199,6 +199,10 @@ export default class Client extends LanguageClient { }); } + get command(): string { + return this.#command; + } + async refreshProfileInfo() { const response = await this.sendRequest('nargo/profile/run', { package: '' }); diff --git a/src/extension.ts b/src/extension.ts index 094f4ba..99bc7a6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -36,11 +36,13 @@ import { window, ProgressLocation, } from 'vscode'; +import os from 'os'; import { languageId } from './constants'; import Client from './client'; -import findNargo from './find-nargo'; +import findNargo, { findNargoBinaries } from './find-nargo'; import { lspClients, editorLineDecorationManager } from './noir'; +import { getNoirStatusBarItem, handleClientStartError } from './noir'; const activeCommands: Map = new Map(); @@ -168,6 +170,15 @@ function registerCommands(uri: Uri) { }); commands$.push(hideProfileInformationCommand$); + const selectNargoPathCommand$ = commands.registerCommand('nargo.config.path.select', async (..._args) => { + const homeDir = os.homedir(); + const foundNargoBinaries = findNargoBinaries(homeDir); + const result = await window.showQuickPick(foundNargoBinaries, { placeHolder: 'Select the Nargo binary to use' }); + const config = workspace.getConfiguration('noir', uri); + config.update('nargoPath', result); + }); + commands$.push(selectNargoPathCommand$); + activeCommands.set(file, Disposable.from(...commands$)); } @@ -246,67 +257,78 @@ async function didOpenTextDocument(document: TextDocument): Promise const uri = document.uri; let folder = workspace.getWorkspaceFolder(uri); let configHandler; - if (folder) { - // If we have nested workspace folders we only start a server on the outer most workspace folder. - folder = getOuterMostWorkspaceFolder(folder); - - await addWorkspaceClient(folder); - registerWorkspaceCommands(folder); - - configHandler = mutex(folder.uri.toString(), async (e: ConfigurationChangeEvent) => { - if (e.affectsConfiguration('noir.nargoFlags', folder.uri)) { - disposeWorkspaceCommands(folder); - await removeWorkspaceClient(folder); - await addWorkspaceClient(folder); - registerWorkspaceCommands(folder); - } - if (e.affectsConfiguration('noir.nargoPath', folder.uri)) { - disposeWorkspaceCommands(folder); - await removeWorkspaceClient(folder); - await addWorkspaceClient(folder); - registerWorkspaceCommands(folder); + try { + if (folder) { + // If we have nested workspace folders we only start a server on the outer most workspace folder. + folder = getOuterMostWorkspaceFolder(folder); + + await addWorkspaceClient(folder); + + const currentLspClient = lspClients.get(folder.uri.toString()); + const statusBarItem = getNoirStatusBarItem(); + statusBarItem.tooltip = currentLspClient.command; + statusBarItem.show(); + + registerWorkspaceCommands(folder); + + configHandler = mutex(folder.uri.toString(), async (e: ConfigurationChangeEvent) => { + if (e.affectsConfiguration('noir.nargoFlags', folder.uri)) { + disposeWorkspaceCommands(folder); + await removeWorkspaceClient(folder); + await addWorkspaceClient(folder); + registerWorkspaceCommands(folder); + } + + if (e.affectsConfiguration('noir.nargoPath', folder.uri)) { + disposeWorkspaceCommands(folder); + await removeWorkspaceClient(folder); + await addWorkspaceClient(folder); + registerWorkspaceCommands(folder); + } + + if (e.affectsConfiguration('noir.enableLSP', folder.uri)) { + await removeWorkspaceClient(folder); + await addWorkspaceClient(folder); + } + }); + } else { + // We only want to handle `file:` and `untitled:` schemes because + // vscode sends `output:` schemes for markdown responses from our LSP + if (uri.scheme !== 'file' && uri.scheme !== 'untitled') { + return Disposable.from(); } - if (e.affectsConfiguration('noir.enableLSP', folder.uri)) { - await removeWorkspaceClient(folder); - await addWorkspaceClient(folder); - } - }); - } else { - // We only want to handle `file:` and `untitled:` schemes because - // vscode sends `output:` schemes for markdown responses from our LSP - if (uri.scheme !== 'file' && uri.scheme !== 'untitled') { - return Disposable.from(); + // Each file outside of a workspace gets it's own client + await addFileClient(uri); + registerFileCommands(uri); + + configHandler = mutex(uri.toString(), async (e: ConfigurationChangeEvent) => { + if (e.affectsConfiguration('noir.nargoFlags', uri)) { + disposeFileCommands(uri); + await removeFileClient(uri); + await addFileClient(uri); + registerFileCommands(uri); + } + + if (e.affectsConfiguration('noir.nargoPath', uri)) { + disposeFileCommands(uri); + await removeFileClient(uri); + await addFileClient(uri); + registerFileCommands(uri); + } + + if (e.affectsConfiguration('noir.enableLSP', uri)) { + await removeFileClient(uri); + await addFileClient(uri); + } + }); } - // Each file outside of a workspace gets it's own client - await addFileClient(uri); - registerFileCommands(uri); - - configHandler = mutex(uri.toString(), async (e: ConfigurationChangeEvent) => { - if (e.affectsConfiguration('noir.nargoFlags', uri)) { - disposeFileCommands(uri); - await removeFileClient(uri); - await addFileClient(uri); - registerFileCommands(uri); - } - - if (e.affectsConfiguration('noir.nargoPath', uri)) { - disposeFileCommands(uri); - await removeFileClient(uri); - await addFileClient(uri); - registerFileCommands(uri); - } - - if (e.affectsConfiguration('noir.enableLSP', uri)) { - await removeFileClient(uri); - await addFileClient(uri); - } - }); + return workspace.onDidChangeConfiguration(configHandler); + } catch (e) { + handleClientStartError(e); } - - return workspace.onDidChangeConfiguration(configHandler); } async function didChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent) { diff --git a/src/find-nargo.ts b/src/find-nargo.ts index 04d40de..c3f7aa5 100644 --- a/src/find-nargo.ts +++ b/src/find-nargo.ts @@ -1,16 +1,66 @@ +import os from 'os'; +import path from 'path'; +import fs from 'fs'; import which from 'which'; +import { NargoNotFoundError } from './noir'; +import { MarkdownString } from 'vscode'; -const nargoBinaries = ['nargo']; +// List of possible nargo binaries to find on Path +// We prioritize 'nargo' as the more standard version. +const NARGO_BINARIES = ['nargo', 'aztec-nargo']; +// List of possible default installations in users folder +const NARGO_INSTALL_LOCATION_POSTFIXES = ['.nargo/bin/nargo', '.aztec/bin/aztec-nargo']; -export default function findNargo() { - for (const bin of nargoBinaries) { +function absoluteInstallLocationPaths(homeDir: string): string[] { + return NARGO_INSTALL_LOCATION_POSTFIXES.map((postfix) => path.join(homeDir, postfix)).filter((filePath) => + fs.existsSync(filePath), + ); +} + +export function findNargoBinaries(homeDir: string): string[] { + // Note that JS sets maintain insertion order. + const nargoBinaryPaths: Set = new Set(); + + for (const bin of NARGO_BINARIES) { try { - const nargo = which.sync(bin); + const path = which.sync(bin); // If it didn't throw, we found a nargo binary - return nargo; + nargoBinaryPaths.add(path); } catch (err) { // Not found } } - throw new Error('Unable to locate any nargo binary. Did you install it?'); + + // So far we have not found installations on path + // Let's check default installation locations + for (const filePath of absoluteInstallLocationPaths(homeDir)) { + nargoBinaryPaths.add(filePath); + } + + return [...nargoBinaryPaths]; +} + +export default function findNargo() { + const homeDir = os.homedir(); + const nargoBinaryPaths = findNargoBinaries(homeDir); + + if (nargoBinaryPaths.length > 0) { + return nargoBinaryPaths[0]; + } else { + const message = new MarkdownString(); + message.appendText(`Could not locate any of\n`); + for (const nargoBinary of NARGO_BINARIES) { + message.appendMarkdown(`\`${nargoBinary}\``); + message.appendText(`\n`); + } + + message.appendText(`on \`$PATH\`, or one of default installation locations\n`); + for (const postfix of NARGO_INSTALL_LOCATION_POSTFIXES) { + const filePath = path.join(homeDir, postfix); + message.appendMarkdown(`\`${filePath}\``); + message.appendText(`\n`); + } + + throw new NargoNotFoundError(message); + } } diff --git a/src/noir.ts b/src/noir.ts index 5829e77..bd267d5 100644 --- a/src/noir.ts +++ b/src/noir.ts @@ -1,6 +1,47 @@ +import { MarkdownString, StatusBarAlignment, StatusBarItem, ThemeColor, window } from 'vscode'; import { EditorLineDecorationManager } from './EditorLineDecorationManager'; import Client from './client'; export const lspClients: Map = new Map(); export const editorLineDecorationManager = new EditorLineDecorationManager(lspClients); + +let noirStatusBarItem: StatusBarItem; + +export function getNoirStatusBarItem() { + if (noirStatusBarItem) { + return noirStatusBarItem; + } + // Create Nargo Status bar item, we only need one for lifetime of plugin + // we will show/update it depending on file user is working with + noirStatusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 100); + noirStatusBarItem.text = 'Nargo'; + noirStatusBarItem.command = 'nargo.config.path.select'; + + return noirStatusBarItem; +} + +export class NargoNotFoundError extends Error { + constructor(markdownMessage?: MarkdownString) { + super(markdownMessage.value); + + this.markdownMessage = markdownMessage; + } + + markdownMessage: MarkdownString; +} + +export function handleClientStartError(err: Error) { + if (err instanceof NargoNotFoundError) { + const backgroundColor = new ThemeColor('statusBarItem.errorBackground'); + const foregroundColor = new ThemeColor('statusBarItem.errorForeground'); + + const statusBarItem = getNoirStatusBarItem(); + statusBarItem.backgroundColor = backgroundColor; + statusBarItem.color = foregroundColor; + statusBarItem.tooltip = err.markdownMessage; + statusBarItem.show(); + } else { + throw err; + } +}