Improve command system + add new commands

feature/ssh-config
Kelvin Schoofs 4 years ago
parent c3d1bc8b29
commit 56b10b9c15

@ -15,12 +15,14 @@
"onFileSystemAccess:ssh",
"onView:sshfs-configs",
"onCommand:sshfs.new",
"onCommand:sshfs.connect",
"onCommand:sshfs.reconnect",
"onCommand:sshfs.add",
"onCommand:sshfs.disconnect",
"onCommand:sshfs.terminal",
"onCommand:sshfs.focusTerminal",
"onCommand:sshfs.closeTerminal",
"onCommand:sshfs.configure",
"onCommand:sshfs.reload",
"onCommand:sshfs.settings"
"onCommand:sshfs.settings",
],
"main": "./dist/extension.js",
"author": {
@ -58,42 +60,56 @@
{
"command": "sshfs.new",
"title": "Create a SSH FS configuration",
"category": "SSH FS"
"category": "SSH FS",
"icon": "$(new-file)"
},
{
"command": "sshfs.connect",
"title": "Connect as Workspace folder",
"category": "SSH FS"
},
{
"command": "sshfs.reconnect",
"title": "Reconnect Workspace folder",
"category": "SSH FS"
"command": "sshfs.add",
"title": "Add as Workspace folder",
"category": "SSH FS",
"icon": "$(new-folder)"
},
{
"command": "sshfs.disconnect",
"title": "Disconnect Workspace folder",
"category": "SSH FS"
"title": "Disconnect",
"category": "SSH FS",
"icon": "$(debug-disconnect)"
},
{
"command": "sshfs.terminal",
"title": "Open a remote SSH terminal",
"category": "SSH FS"
"title": "Open remote SSH terminal",
"category": "SSH FS",
"icon": "$(terminal)"
},
{
"command": "sshfs.configure",
"title": "Edit configuration",
"category": "SSH FS"
"category": "SSH FS",
"icon": "$(settings-gear)"
},
{
"command": "sshfs.reload",
"title": "Reload configurations",
"category": "SSH FS"
"category": "SSH FS",
"icon": "$(refresh)"
},
{
"command": "sshfs.settings",
"title": "Open settings and edit configurations",
"category": "SSH FS"
"category": "SSH FS",
"icon": "$(settings)"
},
{
"command": "sshfs.focusTerminal",
"title": "Focus terminal",
"category": "SSH FS",
"icon": "$(eye)"
},
{
"command": "sshfs.closeTerminal",
"title": "Close terminal",
"category": "SSH FS",
"icon": "$(close)"
}
],
"menus": {
@ -103,32 +119,36 @@
"group": "SSH FS@1"
},
{
"command": "sshfs.connect",
"command": "sshfs.add",
"group": "SSH FS@2"
},
{
"command": "sshfs.reconnect",
"command": "sshfs.disconnect",
"group": "SSH FS@3"
},
{
"command": "sshfs.disconnect",
"command": "sshfs.terminal",
"group": "SSH FS@4"
},
{
"command": "sshfs.terminal",
"command": "sshfs.configure",
"group": "SSH FS@5"
},
{
"command": "sshfs.configure",
"command": "sshfs.reload",
"group": "SSH FS@6"
},
{
"command": "sshfs.reload",
"command": "sshfs.settings",
"group": "SSH FS@7"
},
{
"command": "sshfs.settings",
"group": "SSH FS@8"
"command": "sshfs.focusTerminal",
"when": "false"
},
{
"command": "sshfs.closeTerminal",
"when": "false"
}
],
"view/title": [
@ -145,19 +165,24 @@
],
"view/item/context": [
{
"command": "sshfs.connect",
"when": "view == 'sshfs-configs' && viewItem == inactive",
"group": "SSH FS@1"
"command": "sshfs.add",
"when": "view == 'sshfs-configs' && viewItem == config",
"group": "inline@1"
},
{
"command": "sshfs.reconnect",
"when": "view == 'sshfs-configs' && viewItem == active",
"group": "SSH FS@2"
"command": "sshfs.terminal",
"when": "view == 'sshfs-configs' && viewItem == config",
"group": "inline@2"
},
{
"command": "sshfs.configure",
"when": "view == 'sshfs-configs' && viewItem == config",
"group": "inline@3"
},
{
"command": "sshfs.disconnect",
"when": "view == 'sshfs-configs' && viewItem == active",
"group": "SSH FS@3"
"when": "view == 'sshfs-configs' && viewItem == config",
"group": "inline@4"
},
{
"command": "sshfs.terminal",

@ -27,7 +27,9 @@ export class ConnectionManager {
const con = config && this.connections.find(con => configMatches(con.config, config));
return con || (config ? undefined : this.connections.find(con => con.config.name === name));
}
public async createConnection(name: string, config?: FileSystemConfig): Promise<Connection> {
public getActiveConnections(): Connection[] {
return [...this.connections];
}
const logging = Logging.scope(`createConnection(${name},${config ? 'config' : 'undefined'})`);
const con = this.getActiveConnection(name, config);
if (con) return con;

@ -1,10 +1,12 @@
import * as vscode from 'vscode';
import { loadConfigs } from './config';
import type { Connection } from './connection';
import type { FileSystemConfig } from './fileSystemConfig';
import { FileSystemRouter } from './fileSystemRouter';
import { Logging } from './logging';
import { Manager } from './manager';
import type { SSHPseudoTerminal } from './pseudoTerminal';
function generateDetail(config: FileSystemConfig): string | undefined {
const { username, host, putty } = config;
@ -43,6 +45,17 @@ function getVersion(): string | undefined {
return ext && ext.packageJSON && ext.packageJSON.version;
}
interface CommandHandler {
/** If set, a string/undefined prompts using the given options.
* If the input was a string, promptOptions.nameFilter is set to it */
promptOptions: PickComplexOptions;
handleString?(string: string): void;
handleUri?(uri: vscode.Uri): void;
handleConfig?(config: FileSystemConfig): void;
handleConnection?(connection: Connection): void;
handleTerminal?(terminal: SSHPseudoTerminal): void;
}
export function activate(context: vscode.ExtensionContext) {
Logging.info(`Extension activated, version ${getVersion()}`);
@ -56,32 +69,76 @@ export function activate(context: vscode.ExtensionContext) {
subscribe(vscode.window.createTreeView('sshfs-configs', { treeDataProvider: manager, showCollapseAll: true }));
subscribe(vscode.tasks.registerTaskProvider('ssh-shell', manager));
async function pickAndClick(func: (name: string | FileSystemConfig) => void, name?: string | FileSystemConfig, activeOrNot?: boolean) {
name = name || await pickConfig(manager, activeOrNot);
if (name) func.call(manager, name);
function registerCommandHandler(name: string, handler: CommandHandler) {
const callback = async (arg?: string | FileSystemConfig | Connection | SSHPseudoTerminal | vscode.Uri) => {
if (handler.promptOptions && (!arg || typeof arg === 'string')) {
arg = await pickComplex(manager, { ...handler.promptOptions, nameFilter: arg });
}
if (typeof arg === 'string') return handler.handleString?.(arg);
if (!arg) return;
if (arg instanceof vscode.Uri) {
return handler.handleUri?.(arg);
} else if ('handleInput' in arg) {
return handler.handleTerminal?.(arg);
} else if ('client' in arg) {
return handler.handleConnection?.(arg);
} else if ('name' in arg) {
return handler.handleConfig?.(arg);
}
Logging.warning(`CommandHandler for '${name}' could not handle input '${arg}'`);
};
registerCommand(name, callback);
}
// sshfs.new()
registerCommand('sshfs.new', () => manager.openSettings({ type: 'newconfig' }));
registerCommand('sshfs.settings', () => manager.openSettings());
registerCommand('sshfs.connect', (name?: string | FileSystemConfig) => pickAndClick(manager.commandConnect, name, false));
registerCommand('sshfs.disconnect', (name?: string | FileSystemConfig) => pickAndClick(manager.commandDisconnect, name, true));
registerCommand('sshfs.reconnect', (name?: string | FileSystemConfig) => pickAndClick(manager.commandReconnect, name, true));
registerCommand('sshfs.terminal', async (configOrUri?: string | FileSystemConfig | vscode.Uri) => {
// SSH FS view context menu: [ config, null ]
// Explorer context menu: [ uri, [uri] ]
// Command: [ ]
// And just in case, supporting [ configName ] too
let config = configOrUri;
let uri: vscode.Uri | undefined;
if (config instanceof vscode.Uri) {
uri = config;
config = config.authority;
}
config = config || await pickConfig(manager);
if (config) manager.commandTerminal(config, uri);
// sshfs.add(target?: string | FileSystemConfig)
registerCommandHandler('sshfs.add', {
promptOptions: { promptConfigs: true },
handleConfig: config => manager.commandConnect(config),
});
// sshfs.disconnect(target: string | FileSystemConfig | Connection)
registerCommandHandler('sshfs.disconnect', {
promptOptions: { promptConfigs: true, promptConnections: true },
handleString: name => manager.commandDisconnect(name),
handleConfig: config => manager.commandDisconnect(config.name),
handleConnection: con => manager.commandDisconnect(con),
});
// sshfs.termninal(target?: string | FileSystemConfig | Connection | vscode.Uri)
registerCommandHandler('sshfs.terminal', {
promptOptions: { promptConfigs: true, promptConnections: true },
handleConfig: config => manager.commandTerminal(config),
handleConnection: con => manager.commandTerminal(con),
handleUri: async uri => {
const con = await pickConnection(manager, uri.authority);
con && manager.commandTerminal(con, uri);
},
});
// sshfs.focusTerminal(target?: SSHPseudoTerminal)
registerCommandHandler('sshfs.focusTerminal', {
promptOptions: { promptTerminals: true },
handleTerminal: ({ terminal }) => terminal?.show(false),
});
// sshfs.closeTerminal(target?: SSHPseudoTerminal)
registerCommandHandler('sshfs.closeTerminal', {
promptOptions: { promptTerminals: true },
handleTerminal: terminal => terminal.close(),
});
registerCommand('sshfs.configure', (name?: string | FileSystemConfig) => pickAndClick(manager.commandConfigure, name));
// sshfs.configure(target?: string | FileSystemConfig)
registerCommandHandler('sshfs.configure', {
promptOptions: { promptConfigs: true },
handleConfig: config => manager.commandConfigure(config),
});
// sshfs.reload()
registerCommand('sshfs.reload', loadConfigs);
// sshfs.settings()
registerCommand('sshfs.settings', () => manager.openSettings());
}

@ -49,6 +49,13 @@ async function tryGetHome(ssh: Client): Promise<string | null> {
return mat[1];
}
function commandArgumentToName(arg?: string | FileSystemConfig | Connection): string {
if (!arg) return 'undefined';
if (typeof arg === 'string') return arg;
if ('client' in arg) return `Connection(${arg.actualConfig.name})`;
return `FileSystemConfig(${arg.name})`;
}
interface SSHShellTaskOptions {
host: string;
command: string;
@ -266,62 +273,49 @@ export class Manager implements vscode.TreeDataProvider<string | FileSystemConfi
)
}
/* Commands (stuff for e.g. context menu for ssh-configs tree) */
public commandDisconnect(target: string | FileSystemConfig) {
if (typeof target === 'object') target = target.name;
Logging.info(`Command received to disconnect ${target}`);
const fs = this.fileSystems.find(f => f.authority === target);
if (fs) {
fs.disconnect();
this.fileSystems.splice(this.fileSystems.indexOf(fs), 1);
}
delete this.creatingFileSystems[target];
public commandConnect(config: FileSystemConfig) {
Logging.info(`Command received to connect ${config.name}`);
const folders = vscode.workspace.workspaceFolders!;
const index = folders.findIndex(f => f.uri.scheme === 'ssh' && f.uri.authority === target);
if (index !== -1) vscode.workspace.updateWorkspaceFolders(index, 1);
this.onDidChangeTreeDataEmitter.fire(null);
const folder = folders && folders.find(f => f.uri.scheme === 'ssh' && f.uri.authority === config.name);
if (folder) return vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer');
vscode.workspace.updateWorkspaceFolders(folders ? folders.length : 0, 0, {
uri: vscode.Uri.parse(`ssh://${config.name}/`),
name: `SSH FS - ${config.label || config.name}`,
});
}
public commandDisconnect(target: string | Connection) {
Logging.info(`Command received to disconnect ${commandArgumentToName(target)}`);
let cons: Connection[];
if (typeof target === 'object' && 'client' in target) {
cons = [target];
target = target.actualConfig.name;
} else {
cons = this.connectionManager.getActiveConnections()
.filter(con => con.actualConfig.name === target);
}
public commandReconnect(target: string | FileSystemConfig) {
if (typeof target === 'object') target = target.name;
Logging.info(`Command received to reconnect ${target}`);
const fs = this.fileSystems.find(f => f.authority === target);
if (fs) {
fs.disconnect();
this.fileSystems.splice(this.fileSystems.indexOf(fs), 1);
}
delete this.creatingFileSystems[target];
// Even if we got an actual config object, we're passing on the name here
// This allows it to pick up config changes (which is why we usually reconnect)
this.commandConnect(target);
}
public commandConnect(target: string | FileSystemConfig) {
const config = typeof target === 'object' ? target : undefined;
if (typeof target === 'object') target = target.name;
Logging.info(`Command received to connect ${target}`);
for (const con of cons) this.connectionManager.closeConnection(con);
const folders = vscode.workspace.workspaceFolders!;
const folder = folders && folders.find(f => f.uri.scheme === 'ssh' && f.uri.authority === target);
if (folder) {
this.onDidChangeTreeDataEmitter.fire(null);
const existing = this.fileSystems.find(fs => fs.config.name === target);
if (existing) return vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer');
return this.createFileSystem(target, config);
let start: number = folders.length;
let left: vscode.WorkspaceFolder[] = [];
for (const folder of folders) {
if (folder.uri.scheme === 'ssh' && folder.uri.authority === target) {
start = Math.min(folder.index, start);
} else if (folder.index > start) {
left.push(folder);
}
vscode.workspace.updateWorkspaceFolders(folders ? folders.length : 0, 0, { uri: vscode.Uri.parse(`ssh://${target}/`), name: `SSH FS - ${target}` });
this.onDidChangeTreeDataEmitter.fire(null);
};
vscode.workspace.updateWorkspaceFolders(start, folders.length - start, ...left);
}
public async commandTerminal(target: string | FileSystemConfig, uri?: vscode.Uri) {
Logging.info(`Command received to open a terminal for ${typeof target === 'string' ? target : target.name}${uri ? ` in ${uri}` : ''}`);
public async commandTerminal(target: FileSystemConfig | Connection, uri?: vscode.Uri) {
Logging.info(`Command received to open a terminal for ${commandArgumentToName(target)}${uri ? ` in ${uri}` : ''}`);
const config = 'client' in target ? target.actualConfig : target;
// If no Uri is given, default to ssh://<target>/ which should respect config.root
const name = typeof target === 'string' ? target : target.name;
uri = uri || vscode.Uri.parse(`ssh://${name}/`, true);
uri = uri || vscode.Uri.parse(`ssh://${config.name}/`, true);
try {
if (typeof target === 'string') {
await this.createTerminal(target, undefined, uri);
} else {
await this.createTerminal(target.label || target.name, target, uri);
}
await this.createTerminal(config.label || config.name, target, uri);
} catch (e) {
const choice = await vscode.window.showErrorMessage<vscode.MessageItem>(
`Couldn't start a terminal for ${name}: ${e.message || e}`,
`Couldn't start a terminal for ${config.name}: ${e.message || e}`,
{ title: 'Retry' }, { title: 'Ignore', isCloseAffordance: true });
if (choice && choice.title === 'Retry') return this.commandTerminal(target, uri);
}

Loading…
Cancel
Save