├── LICENSE ├── README.md ├── autoload ├── fern │ └── scheme │ │ └── ssh │ │ └── provider.vim └── fern_ssh │ ├── buffer.vim │ └── connection.vim └── plugin └── fern_ssh.vim /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Alisue, hashnote.net 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌿 fern-ssh.vim 2 | 3 | [![fern plugin](https://img.shields.io/badge/🌿%20fern-plugin-yellowgreen)](https://github.com/lambdalisue/fern.vim) 4 | 5 | A scheme plugin for [fern.vim](https://github.com/lambdalisue/fern.vim) which show file system tree of a remote machine via SSH. 6 | 7 | **UNDER DEVELOPMENT** 8 | 9 | ## License 10 | 11 | The code in this plugin follows MIT license written in [LICENSE](./LICENSE). 12 | Contributors need to agree that any modifications sent in this repository follow the license. 13 | -------------------------------------------------------------------------------- /autoload/fern/scheme/ssh/provider.vim: -------------------------------------------------------------------------------- 1 | let s:Promise = vital#fern#import('Async.Promise') 2 | let s:AsyncLambda = vital#fern#import('Async.Lambda') 3 | 4 | function! fern#scheme#ssh#provider#new() abort 5 | return { 6 | \ 'get_root': funcref('s:provider_get_root'), 7 | \ 'get_parent' : funcref('s:provider_get_parent'), 8 | \ 'get_children' : funcref('s:provider_get_children'), 9 | \} 10 | endfunction 11 | 12 | function! s:provider_get_root(uri) abort 13 | call fern#logger#debug(a:uri) 14 | let fri = fern#fri#parse(a:uri) 15 | call fern#logger#debug(fri) 16 | let root = s:node(fri.authority, fri.path, 1) 17 | call fern#logger#debug(root) 18 | return root 19 | endfunction 20 | 21 | function! s:provider_get_parent(node, ...) abort 22 | if fern#internal#filepath#is_root(a:node._path) 23 | return s:Promise.reject('no parent node exists for the root') 24 | endif 25 | try 26 | let path = fern#internal#filepath#to_slash(a:node._path) 27 | let parent = fern#internal#path#dirname(path) 28 | let parent = fern#internal#filepath#from_slash(parent) 29 | return s:Promise.resolve(s:node(a:node._host, parent, 1)) 30 | catch 31 | return s:Promise.reject(v:exception) 32 | endtry 33 | endfunction 34 | 35 | function! s:provider_get_children(node, ...) abort 36 | let token = a:0 ? a:1 : s:CancellationToken.none 37 | let host = a:node._host 38 | let conn = fern_ssh#connection#new(host) 39 | return s:list_entries(conn, a:node._path, token) 40 | \.then(s:AsyncLambda.map_f({ v -> s:safe(funcref('s:node', [host] + v)) })) 41 | \.then(s:AsyncLambda.filter_f({ v -> !empty(v) })) 42 | endfunction 43 | 44 | function! s:node(host, path, isdir) abort 45 | let status = a:isdir 46 | let name = fern#internal#path#basename(fern#internal#filepath#to_slash(a:path)) 47 | let sshpath = fern#fri#format(fern#fri#new({ 48 | \ 'scheme': 'ssh', 49 | \ 'authority': a:host, 50 | \ 'path': a:path, 51 | \})) 52 | let bufname = status 53 | \ ? fern#fri#format(fern#fri#new({ 54 | \ 'scheme': 'fern', 55 | \ 'path': sshpath, 56 | \ })) 57 | \ : sshpath 58 | return { 59 | \ 'name': name, 60 | \ 'status': status, 61 | \ 'hidden': name[:0] ==# '.', 62 | \ 'bufname': bufname, 63 | \ '_path': a:path, 64 | \ '_host': a:host, 65 | \} 66 | endfunction 67 | 68 | function! s:safe(fn) abort 69 | try 70 | return a:fn() 71 | catch 72 | return v:null 73 | endtry 74 | endfunction 75 | 76 | function! s:list_entries(conn, path, token) abort 77 | " Use 'find' to follow symlinks and add trailing slash on directories so 78 | " that we can distinguish files and directories. 79 | " https://unix.stackexchange.com/a/4857 80 | let path = a:path ==# '' ? '.' : a:path 81 | return a:conn.start([ 82 | \ 'find', path, '-follow', '-maxdepth', '1', 83 | \ '-type', 'd', '-exec', 'sh', '-c', 'printf "%s/\n" "$0"', '{}', '\;', 84 | \ '-or', '-print', 85 | \], { 86 | \ 'token': a:token, 87 | \ 'reject_on_failure': 1, 88 | \}) 89 | \.catch({ v -> s:Promise.reject(join(v.stderr, "\n")) }) 90 | \.then({ v -> v.stdout }) 91 | \.then(s:AsyncLambda.filter_f({ v -> !empty(v) && v !=# path && v !=# '//' })) 92 | \.then(s:AsyncLambda.map_f({ v -> v[-1:] ==# '/' ? [v[:-2], 1] : [v, 0] })) 93 | endfunction 94 | -------------------------------------------------------------------------------- /autoload/fern_ssh/buffer.vim: -------------------------------------------------------------------------------- 1 | let s:Lambda = vital#fern#import('Lambda') 2 | let s:Promise = vital#fern#import('Async.Promise') 3 | 4 | function! fern_ssh#buffer#read() abort 5 | augroup fern_ssh_buffer_read 6 | autocmd! * 7 | autocmd BufReadCmd ++nested call s:BufReadCmd() 8 | autocmd BufWriteCmd ++nested call s:BufWriteCmd() 9 | augroup END 10 | 11 | setlocal buftype=acwrite 12 | filetype detect 13 | return s:BufReadCmd() 14 | endfunction 15 | 16 | function! fern_ssh#buffer#write() abort 17 | return s:BufWriteCmd() 18 | endfunction 19 | 20 | 21 | function! s:BufReadCmd() abort 22 | let fri = fern#fri#parse(expand('')) 23 | let conn = fern_ssh#connection#new(fri.authority) 24 | let bufnr = expand('') + 0 25 | return conn.start(['cat', fri.path], { 26 | \ 'reject_on_failure': v:true, 27 | \}) 28 | \.then({ r -> r.stdout }) 29 | \.then({ c -> fern#internal#buffer#replace(bufnr, c) }) 30 | \.catch({e -> fern#logger#error(e) }) 31 | endfunction 32 | 33 | function! s:BufWriteCmd() abort 34 | let fri = fern#fri#parse(expand('')) 35 | let conn = fern_ssh#connection#new(fri.authority) 36 | let bufnr = expand('') + 0 37 | let content = getbufline(bufnr, 1, '$') 38 | return conn.start([printf('cat > %s', escape(fri.path, '\'))], { 39 | \ 'stdin': s:Promise.resolve(content), 40 | \ 'reject_on_failure': v:true, 41 | \}) 42 | \.then({ -> setbufvar(bufnr, "&modified", 0) }) 43 | \.catch({e -> fern#logger#error(e) }) 44 | endfunction 45 | -------------------------------------------------------------------------------- /autoload/fern_ssh/connection.vim: -------------------------------------------------------------------------------- 1 | let s:Process = vital#fern#import("Async.Promise.Process") 2 | 3 | 4 | function! fern_ssh#connection#new(host) abort 5 | let conn = { 6 | \ "host": a:host, 7 | \ "start": funcref("s:connection_start"), 8 | \} 9 | return conn 10 | endfunction 11 | 12 | function! s:connection_start(args, ...) abort dict 13 | let options = copy(a:0 ? a:1 : {}) 14 | let args = ['ssh', '-T', '-x', self.host, s:cmdline(a:args)] 15 | call fern#logger#debug(args) 16 | return s:Process.start(args, options) 17 | endfunction 18 | 19 | function! s:cmdline(args) abort 20 | let args = copy(a:args) 21 | let args = map(args, { _, v -> s:shellescape(v) }) 22 | return join(args, ' ') 23 | endfunction 24 | 25 | function! s:shellescape(expr) abort 26 | if stridx(a:expr, ' ') isnot -1 27 | return printf("'%s'", escape(a:expr, "\\'")) 28 | else 29 | return a:expr 30 | endif 31 | endfunction 32 | -------------------------------------------------------------------------------- /plugin/fern_ssh.vim: -------------------------------------------------------------------------------- 1 | if exists('g:fern_ssh_loaded') 2 | finish 3 | endif 4 | let g:fern_ssh_loaded = 1 5 | 6 | 7 | function! s:BufReadCmd() abort 8 | call fern_ssh#buffer#read() 9 | endfunction 10 | 11 | function! s:BufWriteCmd() abort 12 | call fern_ssh#buffer#write() 13 | endfunction 14 | 15 | augroup fern_ssh_internal 16 | autocmd! * 17 | autocmd BufReadCmd ssh://* ++nested call s:BufReadCmd() 18 | autocmd BufWriteCmd ssh://* ++nested call s:BufWriteCmd() 19 | autocmd SessionLoadPost ssh://* ++nested call s:BufReadCmd() 20 | augroup END 21 | --------------------------------------------------------------------------------