Improve command system + add new commands

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

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

@ -27,7 +27,9 @@ export class ConnectionManager {
const con = config && this.connections.find(con => configMatches(con.config, config)); const con = config && this.connections.find(con => configMatches(con.config, config));
return con || (config ? undefined : this.connections.find(con => con.config.name === name)); 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 logging = Logging.scope(`createConnection(${name},${config ? 'config' : 'undefined'})`);
const con = this.getActiveConnection(name, config); const con = this.getActiveConnection(name, config);
if (con) return con; if (con) return con;

@ -1,10 +1,12 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { loadConfigs } from './config'; import { loadConfigs } from './config';
import type { Connection } from './connection';
import type { FileSystemConfig } from './fileSystemConfig'; import type { FileSystemConfig } from './fileSystemConfig';
import { FileSystemRouter } from './fileSystemRouter'; import { FileSystemRouter } from './fileSystemRouter';
import { Logging } from './logging'; import { Logging } from './logging';
import { Manager } from './manager'; import { Manager } from './manager';
import type { SSHPseudoTerminal } from './pseudoTerminal';
function generateDetail(config: FileSystemConfig): string | undefined { function generateDetail(config: FileSystemConfig): string | undefined {
const { username, host, putty } = config; const { username, host, putty } = config;
@ -43,6 +45,17 @@ function getVersion(): string | undefined {
return ext && ext.packageJSON && ext.packageJSON.version; 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) { export function activate(context: vscode.ExtensionContext) {
Logging.info(`Extension activated, version ${getVersion()}`); 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.window.createTreeView('sshfs-configs', { treeDataProvider: manager, showCollapseAll: true }));
subscribe(vscode.tasks.registerTaskProvider('ssh-shell', manager)); subscribe(vscode.tasks.registerTaskProvider('ssh-shell', manager));
async function pickAndClick(func: (name: string | FileSystemConfig) => void, name?: string | FileSystemConfig, activeOrNot?: boolean) { function registerCommandHandler(name: string, handler: CommandHandler) {
name = name || await pickConfig(manager, activeOrNot); const callback = async (arg?: string | FileSystemConfig | Connection | SSHPseudoTerminal | vscode.Uri) => {
if (name) func.call(manager, name); 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.new', () => manager.openSettings({ type: 'newconfig' }));
registerCommand('sshfs.settings', () => manager.openSettings());
registerCommand('sshfs.connect', (name?: string | FileSystemConfig) => pickAndClick(manager.commandConnect, name, false)); // sshfs.add(target?: string | FileSystemConfig)
registerCommand('sshfs.disconnect', (name?: string | FileSystemConfig) => pickAndClick(manager.commandDisconnect, name, true)); registerCommandHandler('sshfs.add', {
registerCommand('sshfs.reconnect', (name?: string | FileSystemConfig) => pickAndClick(manager.commandReconnect, name, true)); promptOptions: { promptConfigs: true },
registerCommand('sshfs.terminal', async (configOrUri?: string | FileSystemConfig | vscode.Uri) => { handleConfig: config => manager.commandConnect(config),
// SSH FS view context menu: [ config, null ] });
// Explorer context menu: [ uri, [uri] ]
// Command: [ ] // sshfs.disconnect(target: string | FileSystemConfig | Connection)
// And just in case, supporting [ configName ] too registerCommandHandler('sshfs.disconnect', {
let config = configOrUri; promptOptions: { promptConfigs: true, promptConnections: true },
let uri: vscode.Uri | undefined; handleString: name => manager.commandDisconnect(name),
if (config instanceof vscode.Uri) { handleConfig: config => manager.commandDisconnect(config.name),
uri = config; handleConnection: con => manager.commandDisconnect(con),
config = config.authority; });
}
config = config || await pickConfig(manager); // sshfs.termninal(target?: string | FileSystemConfig | Connection | vscode.Uri)
if (config) manager.commandTerminal(config, 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); 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]; 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 { interface SSHShellTaskOptions {
host: string; host: string;
command: 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) */ /* Commands (stuff for e.g. context menu for ssh-configs tree) */
public commandDisconnect(target: string | FileSystemConfig) { public commandConnect(config: FileSystemConfig) {
if (typeof target === 'object') target = target.name; Logging.info(`Command received to connect ${config.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];
const folders = vscode.workspace.workspaceFolders!; const folders = vscode.workspace.workspaceFolders!;
const index = folders.findIndex(f => f.uri.scheme === 'ssh' && f.uri.authority === target); const folder = folders && folders.find(f => f.uri.scheme === 'ssh' && f.uri.authority === config.name);
if (index !== -1) vscode.workspace.updateWorkspaceFolders(index, 1); if (folder) return vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer');
this.onDidChangeTreeDataEmitter.fire(null); 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) { for (const con of cons) this.connectionManager.closeConnection(con);
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}`);
const folders = vscode.workspace.workspaceFolders!; const folders = vscode.workspace.workspaceFolders!;
const folder = folders && folders.find(f => f.uri.scheme === 'ssh' && f.uri.authority === target); let start: number = folders.length;
if (folder) { let left: vscode.WorkspaceFolder[] = [];
this.onDidChangeTreeDataEmitter.fire(null); for (const folder of folders) {
const existing = this.fileSystems.find(fs => fs.config.name === target); if (folder.uri.scheme === 'ssh' && folder.uri.authority === target) {
if (existing) return vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer'); start = Math.min(folder.index, start);
return this.createFileSystem(target, config); } 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) { public async commandTerminal(target: FileSystemConfig | Connection, uri?: vscode.Uri) {
Logging.info(`Command received to open a terminal for ${typeof target === 'string' ? target : target.name}${uri ? ` in ${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 // 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://${config.name}/`, true);
uri = uri || vscode.Uri.parse(`ssh://${name}/`, true);
try { try {
if (typeof target === 'string') { await this.createTerminal(config.label || config.name, target, uri);
await this.createTerminal(target, undefined, uri);
} else {
await this.createTerminal(target.label || target.name, target, uri);
}
} catch (e) { } catch (e) {
const choice = await vscode.window.showErrorMessage<vscode.MessageItem>( 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 }); { title: 'Retry' }, { title: 'Ignore', isCloseAffordance: true });
if (choice && choice.title === 'Retry') return this.commandTerminal(target, uri); if (choice && choice.title === 'Retry') return this.commandTerminal(target, uri);
} }

Loading…
Cancel
Save