diff --git a/package.json b/package.json index 212f114..2a1ad69 100644 --- a/package.json +++ b/package.json @@ -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", @@ -258,4 +283,4 @@ "ssh2": "^0.8.9", "winreg": "^1.2.4" } -} +} \ No newline at end of file diff --git a/src/connection.ts b/src/connection.ts index 2b3b906..3292996 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -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 { + 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; diff --git a/src/extension.ts b/src/extension.ts index 4620d6d..a9c4b86 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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); + }, }); - registerCommand('sshfs.configure', (name?: string | FileSystemConfig) => pickAndClick(manager.commandConfigure, name)); + // 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(), + }); + + // 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()); } diff --git a/src/manager.ts b/src/manager.ts index 3e635d3..8efd8e5 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -49,6 +49,13 @@ async function tryGetHome(ssh: Client): Promise { 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 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 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); + 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); } - 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:/// 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( - `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); }