// The module 'vscode' contains the VS Code extensibility API import * as cp from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import { CommandCentre } from './commands'; import { Pijul } from './pijul'; import { Repository } from './repository'; import { toDisposable } from './utils/disposableUtils'; export interface IPijul { path: string version: string } /** * Check for a Pijul installation at a specific location, asking the user to * select one if it cannot be found. * @param path A path that may already have been defined in the configuration file * @param config The extension configuration * @param outputChannel The output channel where logging information should be sent */ async function checkPijulInstallation (path: string | undefined, config: vscode.WorkspaceConfiguration, outputChannel: vscode.OutputChannel): Promise<IPijul> { if (!path) { // First, check if Pijul is on $PATH. path = 'pijul'; try { if (process.platform === 'win32') { path += '.exe'; } return await checkPijulExecutable(path); } catch (e) { // If not found, check if Pijul can be found in the .cargo directory try { path = await searchPijulCargoDirectory(outputChannel); if (path) { return await checkPijulExecutable(path); } } catch (_) { path = undefined; } } // TODO: Search in nix installation location // Since we need a path to proceed, ask the user to select one or stop activation if // Pijul wasn't found in the .cargo directory. if (!(config.get<boolean>('ignoreMissingInstallation') ?? false)) { const selectInstallation = 'Select a Pijul Executable'; const ignore = 'Don\'t Show this Warning Again'; const choice = await vscode.window.showWarningMessage( 'No Pijul installation has been configured for use with the extension. Select one to enable Pijul integration', selectInstallation, ignore ); if (choice === selectInstallation) { let fileFilters: { [name: string]: string[] } | undefined; if (process.platform === 'win32') { fileFilters = { svn: ['exe', 'bat'] }; } // Ask the user to select an executable location const pijulInstallation = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, filters: fileFilters }); if (pijulInstallation?.[0] != null) { const exePath = pijulInstallation[0].fsPath; await config.update('installationPath', exePath, vscode.ConfigurationTarget.Global); path = exePath; outputChannel.appendLine('Updated extension configuration to use Pijul installation at ' + exePath); } else { // Not sure if this case is possible, this is here for safety return await Promise.reject(new Error('No executable selected in file dialogue')); } } else { await config.update('ignoreMissingInstallation', true, vscode.ConfigurationTarget.Global); return await Promise.reject(new Error('Ignore')); } } else { // TODO: Wait for change to the configuration return await Promise.reject(new Error('Ignore')); } } return await checkPijulExecutable(path); } /** * Search for a pijul executable under the user's .cargo directory * @param outputChannel The output channel where logging information should be sent */ async function searchPijulCargoDirectory (outputChannel: vscode.OutputChannel): Promise<string | undefined> { outputChannel.appendLine('Looking for Pijul installation in .cargo...'); const cargoBin = path.join(os.homedir(), '.cargo', 'bin'); // I can't find an async version of this function if (fs.existsSync(cargoBin)) { let cargoPijul = path.join(cargoBin, 'pijul'); if (fs.existsSync(cargoPijul)) { return cargoPijul; } // Instead of checking if we're on windows or unix, just check if either exists cargoPijul = cargoPijul + '.exe'; if (fs.existsSync(cargoPijul)) { return cargoPijul; } } return undefined; } /** * Check if a Pijul executable is compatible with the extension by running `pijul --version` * @param path The path of the Pijul executable that will be checked */ async function checkPijulExecutable (path: string): Promise<IPijul> { return await new Promise<IPijul>((resolve, reject) => { const child = cp.spawn(path, ['--version']); const buffers: Buffer[] = []; child.stdout.on('data', (b: Buffer) => buffers.push(b)); child.on('error', () => reject(new Error(`Error checking version at ${path}`))); child.on('close', code => { const tokens = Buffer.concat(buffers).toString('utf8').trim().split(' '); if (code || tokens[0] !== 'pijul') { reject(new Error(`Error checking version at ${path}`)); } else { resolve({ path: path, version: tokens[1] }); } }); }); } /** * Nested activation function which is only run after the configuration has been checked and the extension * is confirmed to be enabled. * @param context The extension context * @param config The extension configuration * @param disposables An array of disposables which will be cleaned up when the extension is deactivated. */ async function _activate (_context: vscode.ExtensionContext, config: vscode.WorkspaceConfiguration, disposables: vscode.Disposable[]): Promise<void> { vscode.window.showInformationMessage('Pijul repository detected in workspace, extension has been activated.'); console.debug('Pijul VS Code Integration Activated'); const outputChannel = vscode.window.createOutputChannel('Pijul'); disposables.push(outputChannel); disposables.push(vscode.commands.registerCommand('pijul.showOutput', () => outputChannel.show())); const installationPath = config.get<string>('installationPath'); // Make sure that the Pijul installation is valid let identifiedInstallation = false; let pijulInfo; while (!identifiedInstallation) { try { pijulInfo = await checkPijulInstallation(installationPath, config, outputChannel); outputChannel.appendLine(`Using Pijul ${pijulInfo.version} at ${pijulInfo.path}`); identifiedInstallation = true; } catch (err) { if (err instanceof Error) { if (err.message === 'Ignore') { // Complete activation without finding the installation identifiedInstallation = true; return; } else { // Let the user know that the executable didn't work and try again await vscode.window.showErrorMessage(`Failed to find pijul installation at ${err.message?.split(' ')?.pop() ?? ''}, resetting...`); } } } } if (pijulInfo != null) { const pijul = new Pijul(pijulInfo); // Setup logging const onOutput = (str: string): void => { const lines = str.split(/\r?\n/mg); // Pop whitespace only lines off the end while (/^\s*$/.test(lines[lines.length - 1])) { lines.pop(); } outputChannel.appendLine(lines.join('\n')); }; pijul.onOutput.addListener('log', onOutput); disposables.push(toDisposable(() => pijul.onOutput.removeListener('log', onOutput))); const root = vscode.workspace.workspaceFolders?.[0]?.uri?.path ?? ''; const repository = new Repository(pijul.open(root), outputChannel); disposables.push(new CommandCentre(pijul, repository, outputChannel)); } } /** * Private instance of the extension context. */ let _context: vscode.ExtensionContext; /** * Provide a public function for other modules to access the Extension context. */ export function getExtensionContext (): vscode.ExtensionContext { return _context; } /** * Called when the extension is activated by the activation events. * @param context The extension context */ export async function activate (context: vscode.ExtensionContext): Promise<void> { _context = context; // Set a disposable array that disposable objects can be added to, guaranteeing // that they will be cleaned up when the extension is deactivated. const disposables: vscode.Disposable[] = []; context.subscriptions.push( new vscode.Disposable(() => vscode.Disposable.from(...disposables).dispose()) ); const config = vscode.workspace.getConfiguration('pijul'); const enabled = config.get<boolean>('enabled'); if (enabled ?? false) { await _activate(context, config, disposables).catch(err => console.error(err)); } else { // TODO: Wait for a configuration change which enables the extension } } /** * Deactivation function, which is used for disposing the extension resources. * Has no purpose at present. */ export function deactivate (): void { }