// 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 { }