@ -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 } ` ) ;
}
} )
} )
)
)
}
}