Merge branch 'feature/task-variables'

feature/ssh-config
Kelvin Schoofs 4 years ago
commit 7a4df82f20

@ -5,7 +5,7 @@ import * as vscode from 'vscode';
import { getConfig, loadConfigsRaw } from './config'; import { getConfig, loadConfigsRaw } from './config';
import { Connection, ConnectionManager } from './connection'; import { Connection, ConnectionManager } from './connection';
import type { FileSystemConfig } from './fileSystemConfig'; import type { FileSystemConfig } from './fileSystemConfig';
import { Logging } from './logging'; import { Logging, LOGGING_NO_STACKTRACE } from './logging';
import { isSSHPseudoTerminal } from './pseudoTerminal'; import { isSSHPseudoTerminal } from './pseudoTerminal';
import type { SSHFileSystem } from './sshFileSystem'; import type { SSHFileSystem } from './sshFileSystem';
import { catchingPromise, toPromise } from './toPromise'; import { catchingPromise, toPromise } from './toPromise';
@ -29,7 +29,7 @@ function commandArgumentToName(arg?: string | FileSystemConfig | Connection): st
return `FileSystemConfig(${arg.name})`; return `FileSystemConfig(${arg.name})`;
} }
interface SSHShellTaskOptions { interface SSHShellTaskOptions extends vscode.TaskDefinition {
host: string; host: string;
command: string; command: string;
workingDirectory?: string; workingDirectory?: string;
@ -134,7 +134,12 @@ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider
throw e; throw e;
}); });
} }
public getRemotePath(config: FileSystemConfig, relativePath: string) { public getRemotePath(config: FileSystemConfig, relativePath: string | vscode.Uri) {
if (relativePath instanceof vscode.Uri) {
if (relativePath.authority !== config.name)
throw new Error(`Uri authority for '${relativePath}' does not match config with name '${config.name}'`);
relativePath = relativePath.path;
}
if (relativePath.startsWith('/')) relativePath = relativePath.substr(1); if (relativePath.startsWith('/')) relativePath = relativePath.substr(1);
if (!config.root) return '/' + relativePath; if (!config.root) return '/' + relativePath;
const result = path.posix.join(config.root, relativePath); const result = path.posix.join(config.root, relativePath);
@ -147,8 +152,7 @@ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider
// Create connection (early so we have .actualConfig.root) // Create connection (early so we have .actualConfig.root)
const con = (config && 'client' in config) ? config : await this.connectionManager.createConnection(name, config); const con = (config && 'client' in config) ? config : await this.connectionManager.createConnection(name, config);
// Calculate working directory if applicable // Calculate working directory if applicable
let workingDirectory: string | undefined = uri && uri.path; const workingDirectory = uri && this.getRemotePath(con.actualConfig, uri);
if (workingDirectory) workingDirectory = this.getRemotePath(con.actualConfig, workingDirectory);
// Create pseudo terminal // Create pseudo terminal
this.connectionManager.update(con, con => con.pendingUserCount++); this.connectionManager.update(con, con => con.pendingUserCount++);
const pty = await createTerminal({ client: con.client, config: con.actualConfig, workingDirectory }); const pty = await createTerminal({ client: con.client, config: con.actualConfig, workingDirectory });
@ -169,41 +173,137 @@ export class Manager implements vscode.TaskProvider, vscode.TerminalLinkProvider
} }
public async promptReconnect(name: string) { public async promptReconnect(name: string) {
const config = getConfig(name); const config = getConfig(name);
console.log('config', name, config);
if (!config) return; if (!config) return;
const choice = await vscode.window.showWarningMessage(`SSH FS ${config.label || config.name} disconnected`, 'Ignore', 'Disconnect'); const choice = await vscode.window.showWarningMessage(`SSH FS ${config.label || config.name} disconnected`, 'Ignore', 'Disconnect');
if (choice === 'Disconnect') this.commandDisconnect(name); if (choice === 'Disconnect') this.commandDisconnect(name);
} }
/* TaskProvider */ /* TaskProvider */
protected async replaceTaskVariables(value: string, config: FileSystemConfig): Promise<string> {
return value.replace(/\$\{(.*?)\}/g, (str, match: string) => {
if (!match.startsWith('remote')) return str; // Our variables always start with "remote"
// https://github.com/microsoft/vscode/blob/bebd06640734c37f6d5f1a82b13297ce1d297dd1/src/vs/workbench/services/configurationResolver/common/variableResolver.ts#L156
const [key, argument] = match.split(':') as [string, string?];
const getFilePath = (): vscode.Uri => {
const uri = vscode.window.activeTextEditor?.document?.uri;
if (uri && uri.scheme === 'ssh') return uri;
if (uri) throw new Error(`Variable ${str}: Active editor is not a ssh:// file`);
throw new Error(`Variable ${str} can not be resolved. Please open an editor.`);
}
const getFolderPathForFile = (): vscode.Uri => {
const filePath = getFilePath();
const uri = vscode.workspace.getWorkspaceFolder(filePath)?.uri;
if (uri) return uri;
throw new Error(`Variable ${str}: can not find workspace folder of '${filePath}'.`);
}
const { workspaceFolders = [] } = vscode.workspace;
const sshFolders = workspaceFolders.filter(ws => ws.uri.scheme === 'ssh');
const sshFolder = sshFolders.length === 1 ? sshFolders[0] : undefined;
const getFolderUri = (): vscode.Uri => {
const { workspaceFolders = [] } = vscode.workspace;
if (argument) {
const uri = workspaceFolders.find(ws => ws.name === argument)?.uri;
if (uri && uri.scheme === 'ssh') return uri;
if (uri) throw new Error(`Variable ${str}: Workspace folder '${argument}' is not a ssh:// folder`);
throw new Error(`Variable ${str} can not be resolved. No such folder '${argument}'.`);
}
if (sshFolder) return sshFolder.uri;
if (sshFolders.length > 1) {
throw new Error(`Variable ${str} can not be resolved in a multi ssh:// folder workspace. Scope this variable using ':' and a workspace folder name.`);
}
throw new Error(`Variable ${str} can not be resolved. Please open an ssh:// folder.`);
};
switch (key) {
case 'remoteWorkspaceRoot':
case 'remoteWorkspaceFolder':
return this.getRemotePath(config, getFolderUri());
case 'remoteWorkspaceRootFolderName':
case 'remoteWorkspaceFolderBasename':
return path.basename(getFolderUri().path);
case 'remoteFile':
return this.getRemotePath(config, getFilePath());
case 'remoteFileWorkspaceFolder':
return this.getRemotePath(config, getFolderPathForFile());
case 'remoteRelativeFile':
if (sshFolder || argument)
return path.relative(getFolderUri().path, getFilePath().path);
return getFilePath().path;
case 'remoteRelativeFileDirname': {
const dirname = path.dirname(getFilePath().path);
if (sshFolder || argument) {
const relative = path.relative(getFolderUri().path, dirname);
return relative.length === 0 ? '.' : relative;
}
return dirname;
}
case 'remoteFileDirname':
return path.dirname(getFilePath().path);
case 'remoteFileExtname':
return path.extname(getFilePath().path);
case 'remoteFileBasename':
return path.basename(getFilePath().path);
case 'remoteFileBasenameNoExtension': {
const basename = path.basename(getFilePath().path);
return (basename.slice(0, basename.length - path.extname(basename).length));
}
case 'remoteFileDirnameBasename':
return path.basename(path.dirname(getFilePath().path));
case 'remotePathSeparator':
// Not sure if we even need/want this variable, but sure
return path.posix.sep;
default:
const msg = `Unrecognized task variable '${str}' starting with 'remote', ignoring`;
Logging.warning(msg, LOGGING_NO_STACKTRACE);
vscode.window.showWarningMessage(msg);
return str;
}
});
}
protected async replaceTaskVariablesRecursive<T>(object: T, handler: (value: string) => string | Promise<string>): Promise<T> {
if (typeof object === 'string') return handler(object) as any;
if (Array.isArray(object)) return object.map(v => this.replaceTaskVariablesRecursive(v, handler)) as any;
if (typeof object == 'object' && object !== null && !(object instanceof RegExp) && !(object instanceof Date)) {
// ^ Same requirements VS Code applies: https://github.com/microsoft/vscode/blob/bebd06640734c37f6d5f1a82b13297ce1d297dd1/src/vs/base/common/types.ts#L34
const result: any = {};
for (let key in object) {
const value = await this.replaceTaskVariablesRecursive(object[key], handler);
key = await this.replaceTaskVariablesRecursive(key, handler);
result[key] = value;
}
return result;
}
return object;
}
public provideTasks(token?: vscode.CancellationToken | undefined): vscode.ProviderResult<vscode.Task[]> { public provideTasks(token?: vscode.CancellationToken | undefined): vscode.ProviderResult<vscode.Task[]> {
return []; return [];
} }
public async resolveTask(task: vscode.Task, token?: vscode.CancellationToken | undefined): Promise<vscode.Task> { public async resolveTask(task: vscode.Task, token?: vscode.CancellationToken | undefined): Promise<vscode.Task> {
let { host, command, workingDirectory } = task.definition as unknown as SSHShellTaskOptions;
if (!host) throw new Error('Missing field \'host\' for ssh-shell task');
if (!command) throw new Error('Missing field \'command\' for ssh-shell task');
const config = getConfig(host);
if (!config) throw new Error(`No configuration with the name '${host}' found for ssh-shell task`);
// Calculate working directory if applicable
if (workingDirectory) workingDirectory = this.getRemotePath(config, workingDirectory);
return new vscode.Task( return new vscode.Task(
task.definition, task.definition, // Can't replace/modify this, otherwise we're not contributing to "this" task
vscode.TaskScope.Workspace, vscode.TaskScope.Workspace,
`SSH Task '${task.name}' for ${host}`, `SSH Task '${task.name}'`,
'ssh', 'ssh',
new vscode.CustomExecution(async () => { new vscode.CustomExecution(async (resolved: SSHShellTaskOptions) => {
const connection = await this.connectionManager.createConnection(host); const { createTerminal, createTextTerminal } = await import('./pseudoTerminal');
this.connectionManager.update(connection, con => con.pendingUserCount++); try {
const { createTerminal } = await import('./pseudoTerminal'); if (!resolved.host) throw new Error('Missing field \'host\' in task description');
const pty = await createTerminal({ if (!resolved.command) throw new Error('Missing field \'command\' in task description');
command, workingDirectory, const connection = await this.connectionManager.createConnection(resolved.host);
client: connection.client, resolved = await this.replaceTaskVariablesRecursive(resolved, value => this.replaceTaskVariables(value, connection.actualConfig));
config: connection.actualConfig, const { command, workingDirectory } = resolved;
}); //if (workingDirectory) workingDirectory = this.getRemotePath(config, workingDirectory);
this.connectionManager.update(connection, con => (con.pendingUserCount--, con.terminals.push(pty))); this.connectionManager.update(connection, con => con.pendingUserCount++);
pty.onDidClose(() => this.connectionManager.update(connection, const pty = await createTerminal({
con => con.terminals = con.terminals.filter(t => t !== pty))); command, workingDirectory,
return pty; client: connection.client,
config: connection.actualConfig,
});
this.connectionManager.update(connection, con => (con.pendingUserCount--, con.terminals.push(pty)));
pty.onDidClose(() => this.connectionManager.update(connection,
con => con.terminals = con.terminals.filter(t => t !== pty)));
return pty;
} catch (e) {
return createTextTerminal(`Error: ${e.message || e}`);
}
}) })
) )
} }

@ -116,3 +116,24 @@ export async function createTerminal(options: TerminalOptions): Promise<SSHPseud
}; };
return pseudo; return pseudo;
} }
export interface TextTerminal extends vscode.Pseudoterminal {
write(text: string): void;
close(code?: number): void;
onDidClose: vscode.Event<number>; // Redeclaring that it isn't undefined
onDidOpen: vscode.Event<void>;
}
export function createTextTerminal(initialText?: string): TextTerminal {
const onDidWrite = new vscode.EventEmitter<string>();
const onDidClose = new vscode.EventEmitter<number>();
const onDidOpen = new vscode.EventEmitter<void>();
return {
write: onDidWrite.fire.bind(onDidWrite),
close: onDidClose.fire.bind(onDidClose),
onDidWrite: onDidWrite.event,
onDidClose: onDidClose.event,
onDidOpen: onDidOpen.event,
open: () => initialText && (onDidWrite.fire(initialText + '\r\n'), onDidClose.fire(1)),
};
}

Loading…
Cancel
Save