diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc6754..788b94e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.9.1 +* Started to convert package to Java Script + ## 1.7.0 * Convert package to conform to Atom 1.0 API diff --git a/lib/main.coffee b/lib/main.coffee deleted file mode 100644 index df0f9b3..0000000 --- a/lib/main.coffee +++ /dev/null @@ -1,165 +0,0 @@ -_ = require 'underscore-plus' -# Import needed to register deserializer -RemoteEditEditor = require './model/remote-edit-editor' - -# Deferred requirements -OpenFilesView = null -HostView = null -HostsView = null -Host = null -SftpHost = null -FtpHost = null -LocalFile = null -url = null -Q = null -InterProcessDataWatcher = null -fs = null - -module.exports = - config: - showHiddenFiles: - title: 'Show hidden files' - type: 'boolean' - default: false - uploadOnSave: - title: 'Upload on save' - description: 'When enabled, remote files will be automatically uploaded when saved' - type: 'boolean' - default: true - notifications: - title: 'Display notifications' - type: 'boolean' - default: true - sshPrivateKeyPath: - title: 'Path to private SSH key' - type: 'string' - default: '~/.ssh/id_rsa' - defaultSerializePath: - title: 'Default path to serialize remoteEdit data' - type: 'string' - default: '~/.atom/remoteEdit.json' - agentToUse: - title: 'SSH agent' - description: 'Overrides default SSH agent. See ssh2 docs for more info.' - type: 'string' - default: 'Default' - foldersOnTop: - title: 'Show folders on top' - type: 'boolean' - default: false - followLinks: - title: 'Follow symbolic links' - description: 'If set to true, symbolic links are treated as directories' - type: 'boolean' - default: true - clearFileList: - title: 'Clear file list' - description: 'When enabled, the open files list will be cleared on initialization' - type: 'boolean' - default: false - rememberLastOpenDirectory: - title: 'Remember last open directory' - description: 'When enabled, browsing a host will return you to the last directory you entered' - type: 'boolean' - default: false - storePasswordsUsingKeytar: - title: 'Store passwords using node-keytar' - description: 'When enabled, passwords and passphrases will be stored in system\'s keychain' - type: 'boolean' - default: false - filterHostsUsing: - type: 'object' - properties: - hostname: - type: 'boolean' - default: true - alias: - type: 'boolean' - default: false - username: - type: 'boolean' - default: false - port: - type: 'boolean' - default: false - - - activate: (state) -> - @setupOpeners() - @initializeIpdwIfNecessary() - - atom.commands.add('atom-workspace', 'remote-edit:show-open-files', => @showOpenFiles()) - atom.commands.add('atom-workspace', 'remote-edit:browse', => @browse()) - atom.commands.add('atom-workspace', 'remote-edit:new-host-sftp', => @newHostSftp()) - atom.commands.add('atom-workspace', 'remote-edit:new-host-ftp', => @newHostFtp()) - - deactivate: -> - @ipdw?.destroy() - - newHostSftp: -> - HostView ?= require './view/host-view' - SftpHost ?= require './model/sftp-host' - host = new SftpHost() - view = new HostView(host, @getOrCreateIpdw()) - view.toggle() - - newHostFtp: -> - HostView ?= require './view/host-view' - FtpHost ?= require './model/ftp-host' - host = new FtpHost() - view = new HostView(host, @getOrCreateIpdw()) - view.toggle() - - browse: -> - HostsView ?= require './view/hosts-view' - view = new HostsView(@getOrCreateIpdw()) - view.toggle() - - showOpenFiles: -> - OpenFilesView ?= require './view/open-files-view' - showOpenFilesView = new OpenFilesView(@getOrCreateIpdw()) - showOpenFilesView.toggle() - - initializeIpdwIfNecessary: -> - if atom.config.get 'remote-edit.notifications' - stop = false - for editor in atom.workspace.getTextEditors() when !stop - if editor instanceof RemoteEditEditor - @getOrCreateIpdw() - stop = true - - getOrCreateIpdw: -> - if @ipdw is undefined - InterProcessDataWatcher ?= require './model/inter-process-data-watcher' - fs = require 'fs-plus' - @ipdw = new InterProcessDataWatcher(fs.absolute(atom.config.get('remote-edit.defaultSerializePath'))) - else - @ipdw - - setupOpeners: -> - atom.workspace.addOpener (uriToOpen) -> - url ?= require 'url' - try - {protocol, host, query} = url.parse(uriToOpen, true) - catch error - return - return unless protocol is 'remote-edit:' - - if host is 'localfile' - Q ?= require 'q' - Host ?= require './model/host' - FtpHost ?= require './model/ftp-host' - SftpHost ?= require './model/sftp-host' - LocalFile ?= require './model/local-file' - localFile = LocalFile.deserialize(JSON.parse(decodeURIComponent(query.localFile))) - host = Host.deserialize(JSON.parse(decodeURIComponent(query.host))) - - atom.project.bufferForPath(localFile.path).then (buffer) -> - params = {buffer: buffer, registerEditor: true, host: host, localFile: localFile} - # copied from workspace.buildTextEditor - ws = atom.workspace - params = _.extend({ - config: ws.config, notificationManager: ws.notificationManager, packageManager: ws.packageManager, clipboard: ws.clipboard, viewRegistry: ws.viewRegistry, - grammarRegistry: ws.grammarRegistry, project: ws.project, assert: ws.assert, applicationDelegate: ws.applicationDelegate - }, params) - editor = new RemoteEditEditor(params) diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 0000000..ee43cb0 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,237 @@ +/* + * decaffeinated and cleaned up + */ +const _ = require('underscore-plus'); +// Import needed to register deserializer +const RemoteEditEditor = require('./model/remote-edit-editor'); + +// Deferred requirements +let OpenFilesView = null; +let HostView = null; +let HostsView = null; +let Host = null; +let SftpHost = null; +let FtpHost = null; +let LocalFile = null; +let url = null; +let Q = null; +let InterProcessDataWatcher = null; +let fs = null; + +module.exports = { + config: { + showHiddenFiles: { + title: 'Show hidden files', + type: 'boolean', + default: false + }, + uploadOnSave: { + title: 'Upload on save', + description: 'When enabled, remote files will be automatically uploaded when saved', + type: 'boolean', + default: true + }, + notifications: { + title: 'Display notifications', + type: 'boolean', + default: true + }, + sshPrivateKeyPath: { + title: 'Path to private SSH key', + type: 'string', + default: '~/.ssh/id_rsa' + }, + defaultSerializePath: { + title: 'Default path to serialize remoteEdit data', + type: 'string', + default: '~/.atom/remoteEdit.json' + }, + agentToUse: { + title: 'SSH agent', + description: 'Overrides default SSH agent. See ssh2 docs for more info.', + type: 'string', + default: 'Default' + }, + foldersOnTop: { + title: 'Show folders on top', + type: 'boolean', + default: false + }, + followLinks: { + title: 'Follow symbolic links', + description: 'If set to true, symbolic links are treated as directories', + type: 'boolean', + default: true + }, + clearFileList: { + title: 'Clear file list', + description: 'When enabled, the open files list will be cleared on initialization', + type: 'boolean', + default: false + }, + rememberLastOpenDirectory: { + title: 'Remember last open directory', + description: 'When enabled, browsing a host will return you to the last directory you entered', + type: 'boolean', + default: false + }, + storePasswordsUsingKeytar: { + title: 'Store passwords using node-keytar', + description: 'When enabled, passwords and passphrases will be stored in system\'s keychain', + type: 'boolean', + default: false + }, + filterHostsUsing: { + type: 'object', + properties: { + hostname: { + type: 'boolean', + default: true + }, + alias: { + type: 'boolean', + default: false + }, + username: { + type: 'boolean', + default: false + }, + port: { + type: 'boolean', + default: false + } + } + } + }, + + + activate(state) { + this.setupOpeners(); + this.initializeIpdwIfNecessary(); + + atom.commands.add('atom-workspace', 'remote-edit:show-open-files', () => this.showOpenFiles()); + atom.commands.add('atom-workspace', 'remote-edit:browse', () => this.browse()); + atom.commands.add('atom-workspace', 'remote-edit:new-host-sftp', () => this.newHostSftp()); + return atom.commands.add('atom-workspace', 'remote-edit:new-host-ftp', () => this.newHostFtp()); + }, + + deactivate() { + if (this.ipdw != null) { this.ipdw.destroy(); } + }, + + deserializeRemoteEditor() { + return data => new RemoteEditEditor(data); + }, + + newHostSftp() { + if (HostView == null) { HostView = require('./view/host-view'); } + if (SftpHost == null) { SftpHost = require('./model/sftp-host'); } + const host = new SftpHost(); + const view = new HostView(host, this.getOrCreateIpdw()); + view.toggle(); + }, + + newHostFtp() { + if (HostView == null) { HostView = require('./view/host-view'); } + if (FtpHost == null) { FtpHost = require('./model/ftp-host'); } + const host = new FtpHost(); + const view = new HostView(host, this.getOrCreateIpdw()); + view.toggle(); + }, + + browse() { + if (HostsView == null) { HostsView = require('./view/hosts-view'); } + const view = new HostsView(this.getOrCreateIpdw()); + view.toggle(); + }, + + showOpenFiles() { + if (OpenFilesView == null) { OpenFilesView = require('./view/open-files-view'); } + const showOpenFilesView = new OpenFilesView(this.getOrCreateIpdw()); + showOpenFilesView.toggle(); + }, + + initializeIpdwIfNecessary() { + if (atom.config.get('remote-edit.notifications')) { + let stop = false; + const result = []; + for (let editor of Array.from(atom.workspace.getTextEditors())) { + if (!stop) { + if (editor instanceof RemoteEditEditor) { + this.getOrCreateIpdw(); + result.push(stop = true); + } else { + result.push(undefined); + } + } + } + return result; + } + }, + + getOrCreateIpdw() { + if (this.ipdw === undefined) { + if (InterProcessDataWatcher == null) { InterProcessDataWatcher = require('./model/inter-process-data-watcher'); } + fs = require('fs-plus'); + return this.ipdw = new InterProcessDataWatcher(fs.absolute(atom.config.get('remote-edit.defaultSerializePath'))); + } else { + return this.ipdw; + } + }, + + setupOpeners() { + return atom.workspace.addOpener(function(uriToOpen) { + let host, protocol, query; + if (url == null) { url = require('url'); } + try { + ({protocol, host, query} = url.parse(uriToOpen, true)); + } catch (error) { + return; + } + if (protocol !== 'remote-edit:') { return; } + + if (host === 'localfile') { + if (Q == null) { Q = require('q'); } + if (Host == null) { Host = require('./model/host'); } + if (FtpHost == null) { FtpHost = require('./model/ftp-host'); } + if (SftpHost == null) { SftpHost = require('./model/sftp-host'); } + if (LocalFile == null) { LocalFile = require('./model/local-file'); } + const localFile = LocalFile.deserialize(JSON.parse(decodeURIComponent(query.localFile))); + host = Host.deserialize(JSON.parse(decodeURIComponent(query.host))); + + return atom.project.bufferForPath(localFile.path).then(function(buffer) { + let editor; + let params = {buffer, registerEditor: true, host, localFile}; + // copied from workspace.buildTextEditor + const ws = atom.workspace; + params = _.extend({ + config: ws.config, notificationManager: ws.notificationManager, packageManager: ws.packageManager, clipboard: ws.clipboard, viewRegistry: ws.viewRegistry, + grammarRegistry: ws.grammarRegistry, project: ws.project, assert: ws.assert, applicationDelegate: ws.applicationDelegate, autoHeight: false} + , params); + return editor = new RemoteEditEditor(params); + }); + } + }); + } +}; + + + +// params = Object.assign({assert: this.assert}, params) + +// let scope = null +// if (params.buffer) { +// const filePath = params.buffer.getPath() +// const headContent = params.buffer.getTextInRange(GRAMMAR_SELECTION_RANGE) +// params.grammar = ws.grammarRegistry +// scope = new ScopeDescriptor({scopes: [params.grammar.scopeName]}) +// } + +// Object.assign(params, this.textEditorParamsForScope(scope)) +// editor = new RemoteEditEditor(params) +// const subscriptions = new CompositeDisposable( +// this.textEditorRegistry.maintainGrammar(editor), +// this.textEditorRegistry.maintainConfig(editor) +// ) +// editor.onDidDestroy(() => { subscriptions.dispose() }) +// return editor diff --git a/lib/model/host.coffee b/lib/model/host.coffee index 35f2d57..518ba87 100644 --- a/lib/model/host.coffee +++ b/lib/model/host.coffee @@ -31,6 +31,7 @@ module.exports = getSearchKey: (searchKeySettings) -> toReturn = "" + return toReturn unless searchKeySettings toReturn = "#{toReturn} #{@alias}" if searchKeySettings["alias"] toReturn = "#{toReturn} #{@hostname}" if searchKeySettings["hostname"] toReturn = "#{toReturn} #{@username}" if searchKeySettings["username"] diff --git a/lib/model/remote-edit-editor.coffee b/lib/model/remote-edit-editor.coffee deleted file mode 100644 index 9eac971..0000000 --- a/lib/model/remote-edit-editor.coffee +++ /dev/null @@ -1,168 +0,0 @@ -path = require 'path' -resourcePath = atom.config.resourcePath -try - Editor = require path.resolve resourcePath, 'src', 'editor' -catch e - # Catch error -TextEditor = Editor ? require path.resolve resourcePath, 'src', 'text-editor' - - -# Defer requiring -Host = null -FtpHost = null -SftpHost = null -LocalFile = null -async = null -Dialog = null -_ = null - -module.exports = - class RemoteEditEditor extends TextEditor - atom.deserializers.add(this) - - constructor: (params = {}) -> - super(params) - if params.host - @host = params.host - if params.localFile - @localFile = params.localFile - - getIconName: -> - "globe" - - getTitle: -> - if @localFile? - @localFile.name - else if sessionPath = @getPath() - path.basename(sessionPath) - else - "undefined" - - getLongTitle: -> - Host ?= require './host' - FtpHost ?= require './ftp-host' - SftpHost ?= require './sftp-host' - - if i = @localFile.remoteFile.path.indexOf(@host.directory) > -1 - relativePath = @localFile.remoteFile.path[(i+@host.directory.length)..] - - fileName = @getTitle() - if @host instanceof SftpHost and @host? and @localFile? - directory = if relativePath? then relativePath else "sftp://#{@host.username}@#{@host.hostname}:#{@host.port}#{@localFile.remoteFile.path}" - else if @host instanceof FtpHost and @host? and @localFile? - directory = if relativePath? then relativePath else "ftp://#{@host.username}@#{@host.hostname}:#{@host.port}#{@localFile.remoteFile.path}" - else - directory = atom.project.relativize(path.dirname(sessionPath)) - directory = if directory.length > 0 then directory else path.basename(path.dirname(sessionPath)) - - "#{fileName} - #{directory}" - - onDidSaved: (callback) -> - @emitter.on 'did-saved', callback - - save: -> - @buffer.save() - @emitter.emit 'saved' - @initiateUpload() - - saveAs: (filePath) -> - @buffer.saveAs(filePath) - @localFile.path = filePath - @emitter.emit 'saved' - @initiateUpload() - - initiateUpload: -> - if atom.config.get 'remote-edit.uploadOnSave' - @upload() - else - Dialog ?= require '../view/dialog' - chosen = atom.confirm - message: "File has been saved. Do you want to upload changes to remote host?" - detailedMessage: "The changes exists on disk and can be uploaded later." - buttons: ["Upload", "Cancel"] - switch chosen - when 0 then @upload() - when 1 then return - - upload: (connectionOptions = {}) -> - async ?= require 'async' - _ ?= require 'underscore-plus' - if @localFile? and @host? - async.waterfall([ - (callback) => - if @host.usePassword and !connectionOptions.password? - if @host.password == "" or @host.password == '' or !@host.password? - async.waterfall([ - (callback) -> - Dialog ?= require '../view/dialog' - passwordDialog = new Dialog({prompt: "Enter password"}) - passwordDialog.toggle(callback) - ], (err, result) => - connectionOptions = _.extend({password: result}, connectionOptions) - callback(null) - ) - else - callback(null) - else - callback(null) - (callback) => - if !@host.isConnected() - @host.connect(callback, connectionOptions) - else - callback(null) - (callback) => - @host.writeFile(@localFile, callback) - ], (err) => - if err? and @host.usePassword - async.waterfall([ - (callback) -> - Dialog ?= require '../view/dialog' - passwordDialog = new Dialog({prompt: "Enter password"}) - passwordDialog.toggle(callback) - ], (err, result) => - @upload({password: result}) - ) - ) - else - console.error 'LocalFile and host not defined. Cannot upload file!' - - serialize: -> - data = super - data.deserializer = 'RemoteEditEditor' - data.localFile = @localFile?.serialize() - data.host = @host?.serialize() - return data - - # mostly copied from TextEditor.deserialize - @deserialize: (state, atomEnvironment) -> - try - displayBuffer = TextEditor.deserialize(state.displayBuffer, atomEnvironment) - catch error - if error.syscall is 'read' - return # error reading the file, dont deserialize an editor for it - else - throw error - - state.displayBuffer = displayBuffer - state.registerEditor = true - if state.localFile? - LocalFile = require '../model/local-file' - state.localFile = LocalFile.deserialize(state.localFile) - if state.host? - Host = require '../model/host' - FtpHost = require '../model/ftp-host' - SftpHost = require '../model/sftp-host' - state.host = Host.deserialize(state.host) - # displayBuffer has no getMarkerLayer - #state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) - state.config = atomEnvironment.config - state.notificationManager = atomEnvironment.notifications - state.packageManager = atomEnvironment.packages - state.clipboard = atomEnvironment.clipboard - state.viewRegistry = atomEnvironment.views - state.grammarRegistry = atomEnvironment.grammars - state.project = atomEnvironment.project - state.assert = atomEnvironment.assert.bind(atomEnvironment) - state.applicationDelegate = atomEnvironment.applicationDelegate - new this(state) - diff --git a/lib/model/remote-edit-editor.js b/lib/model/remote-edit-editor.js new file mode 100644 index 0000000..70f5933 --- /dev/null +++ b/lib/model/remote-edit-editor.js @@ -0,0 +1,213 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Editor, RemoteEditEditor; +const path = require('path'); +const { resourcePath } = atom.config; +try { + Editor = require(path.resolve(resourcePath, 'src', 'editor')); +} catch (e) {} +// Catch error +const TextEditor = Editor != null ? Editor : require(path.resolve(resourcePath, 'src', 'text-editor')); + +// Defer requiring +let Host = null; +let FtpHost = null; +let SftpHost = null; +let LocalFile = null; +let async = null; +let Dialog = null; +let _ = null; + +module.exports = + class RemoteEditEditor extends TextEditor { + + constructor(params) { + if (params == null) { params = {}; } + super(params); + if (params.host) { + this.host = params.host; + } + if (params.localFile) { + this.localFile = params.localFile; + } + } + + getIconName() { + return "globe"; + } + + getTitle() { + let sessionPath; + if (this.localFile != null) { + return this.localFile.name; + } else if ((sessionPath = this.getPath())) { + return path.basename(sessionPath); + } else { + return "undefined"; + } + } + + getLongTitle() { + let directory, i, relativePath; + if (Host == null) { Host = require('./host'); } + if (FtpHost == null) { FtpHost = require('./ftp-host'); } + if (SftpHost == null) { SftpHost = require('./sftp-host'); } + + if (i = this.localFile.remoteFile.path.indexOf(this.host.directory) > -1) { + relativePath = this.localFile.remoteFile.path.slice((i+this.host.directory.length)); + } + + const fileName = this.getTitle(); + if (this.host instanceof SftpHost && (this.host != null) && (this.localFile != null)) { + directory = (relativePath != null) ? relativePath : `sftp://${this.host.username}@${this.host.hostname}:${this.host.port}${this.localFile.remoteFile.path}`; + } else if (this.host instanceof FtpHost && (this.host != null) && (this.localFile != null)) { + directory = (relativePath != null) ? relativePath : `ftp://${this.host.username}@${this.host.hostname}:${this.host.port}${this.localFile.remoteFile.path}`; + } else { + directory = atom.project.relativize(path.dirname(sessionPath)); + directory = directory.length > 0 ? directory : path.basename(path.dirname(sessionPath)); + } + + return `${fileName} - ${directory}`; + } + + onDidSaved(callback) { + return this.emitter.on('did-saved', callback); + } + + save() { + this.buffer.save(); + this.emitter.emit('saved'); + return this.initiateUpload(); + } + + saveAs(filePath) { + this.buffer.saveAs(filePath); + this.localFile.path = filePath; + this.emitter.emit('saved'); + return this.initiateUpload(); + } + + initiateUpload() { + if (atom.config.get('remote-edit.uploadOnSave')) { + return this.upload(); + } else { + if (Dialog == null) { Dialog = require('../view/dialog'); } + const chosen = atom.confirm({ + message: "File has been saved. Do you want to upload changes to remote host?", + detailedMessage: "The changes exists on disk and can be uploaded later.", + buttons: ["Upload", "Cancel"]}); + switch (chosen) { + case 0: return this.upload(); + case 1: return; + } + } + } + + upload(connectionOptions) { + if (connectionOptions == null) { connectionOptions = {}; } + if (async == null) { async = require('async'); } + if (_ == null) { _ = require('underscore-plus'); } + if ((this.localFile != null) && (this.host != null)) { + return async.waterfall([ + callback => { + if (this.host.usePassword && (connectionOptions.password == null)) { + if ((this.host.password === "") || (this.host.password === '') || (this.host.password == null)) { + return async.waterfall([ + function(callback) { + if (Dialog == null) { Dialog = require('../view/dialog'); } + const passwordDialog = new Dialog({prompt: "Enter password"}); + return passwordDialog.toggle(callback); + } + ], (err, result) => { + connectionOptions = _.extend({password: result}, connectionOptions); + return callback(null); + }); + } else { + return callback(null); + } + } else { + return callback(null); + } + }, + callback => { + if (!this.host.isConnected()) { + return this.host.connect(callback, connectionOptions); + } else { + return callback(null); + } + }, + callback => { + return this.host.writeFile(this.localFile, callback); + } + ], err => { + if ((err != null) && this.host.usePassword) { + return async.waterfall([ + function(callback) { + if (Dialog == null) { Dialog = require('../view/dialog'); } + const passwordDialog = new Dialog({prompt: "Enter password"}); + return passwordDialog.toggle(callback); + } + ], (err, result) => { + return this.upload({password: result}); + }); + } + }); + } else { + return console.error('LocalFile and host not defined. Cannot upload file!'); + } + } + + serialize() { + const data = super.serialize(...arguments); + data.deserializer = 'RemoteEditEditor'; + data.localFile = this.localFile != null ? this.localFile.serialize() : undefined; + data.host = this.host != null ? this.host.serialize() : undefined; + return data; + } + + // mostly copied from TextEditor.deserialize + static deserialize(state, atomEnvironment) { + try { + //console.error state + //displayBuffer = TextEditor.deserialize(state.displayBuffer, atomEnvironment) + state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment); + state.tabLength = state.tokenizedBuffer.getTabLength(); + } catch (error) { + if (error.syscall === 'read') { + return; // error reading the file, dont deserialize an editor for it + } else { + throw error; + } + } + //state.displayBuffer = displayBuffer + state.buffer = state.tokenizedBuffer.buffer; + state.registerEditor = true; + if (state.localFile != null) { + LocalFile = require('../model/local-file'); + state.localFile = LocalFile.deserialize(state.localFile); + } + if (state.host != null) { + Host = require('../model/host'); + FtpHost = require('../model/ftp-host'); + SftpHost = require('../model/sftp-host'); + state.host = Host.deserialize(state.host); + } + // displayBuffer has no getMarkerLayer + //state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) + state.config = atomEnvironment.config; + state.notificationManager = atomEnvironment.notifications; + state.packageManager = atomEnvironment.packages; + state.clipboard = atomEnvironment.clipboard; + state.viewRegistry = atomEnvironment.views; + state.grammarRegistry = atomEnvironment.grammars; + state.project = atomEnvironment.project; + state.assert = atomEnvironment.assert.bind(atomEnvironment); + state.applicationDelegate = atomEnvironment.applicationDelegate; + state.autoHeight = false; + return new (this)(state); + } + } diff --git a/lib/view/dialog.coffee b/lib/view/dialog.coffee deleted file mode 100644 index 60c1da2..0000000 --- a/lib/view/dialog.coffee +++ /dev/null @@ -1,64 +0,0 @@ -{$, $$, View, TextEditorView} = require 'atom-space-pen-views' -{CompositeDisposable} = require 'atom' - -module.exports = -class Dialog extends View - @content: ({prompt} = {}) -> - @div class: 'dialog', => - @label prompt, class: 'icon', outlet: 'promptText' - @subview 'miniEditor', new TextEditorView(mini: true) - @div class: 'error-message', outlet: 'errorMessage' - - initialize: ({iconClass} = {}) -> - @promptText.addClass(iconClass) if iconClass - - @disposables = new CompositeDisposable - @disposables.add atom.commands.add 'atom-workspace', - 'core:confirm': => @onConfirm(@miniEditor.getText()) - 'core:cancel': (event) => - @cancel() - event.stopPropagation() - - @miniEditor.getModel().onDidChange => @showError() - @miniEditor.on 'blur', => @cancel() - - onConfirm: (value) -> - @callback?(undefined, value) - @cancel() - value - - showError: (message='') -> - @errorMessage.text(message) - @flashError() if message - - destroy: -> - @disposables.dispose() - - cancel: -> - @cancelled() - @restoreFocus() - @destroy() - - cancelled: -> - @hide() - - toggle: (@callback) -> - if @panel?.isVisible() - @cancel() - else - @show() - - show: () -> - @panel ?= atom.workspace.addModalPanel(item: this) - @panel.show() - @storeFocusedElement() - @miniEditor.focus() - - hide: -> - @panel?.hide() - - storeFocusedElement: -> - @previouslyFocusedElement = $(document.activeElement) - - restoreFocus: -> - @previouslyFocusedElement?.focus() diff --git a/lib/view/dialog.js b/lib/view/dialog.js new file mode 100644 index 0000000..96566dc --- /dev/null +++ b/lib/view/dialog.js @@ -0,0 +1,98 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let Dialog; +const {$, $$, View, TextEditorView} = require('atom-space-pen-views'); +const {CompositeDisposable} = require('atom'); + +module.exports = + (Dialog = class Dialog extends View { + static content(param) { + if (param == null) { param = {}; } + const {prompt} = param; + return this.div({class: 'dialog'}, () => { + this.label(prompt, {class: 'icon', outlet: 'promptText'}); + this.subview('miniEditor', new TextEditorView({mini: true})); + return this.div({class: 'error-message', outlet: 'errorMessage'}); + }); + } + + initialize(param) { + if (param == null) { param = {}; } + const {iconClass} = param; + if (iconClass) { this.promptText.addClass(iconClass); } + + this.disposables = new CompositeDisposable; + this.disposables.add(atom.commands.add('atom-workspace', { + 'core:confirm': () => this.onConfirm(this.miniEditor.getText()), + 'core:cancel': event => { + this.cancel(); + return event.stopPropagation(); + } + } + ) + ); + + this.miniEditor.getModel().onDidChange(() => this.showError()); + return this.miniEditor.on('blur', () => this.cancel()); + } + + onConfirm(value) { + if (typeof this.callback === 'function') { + this.callback(undefined, value); + } + this.cancel(); + return value; + } + + showError(message) { + if (message == null) { message = ''; } + this.errorMessage.text(message); + if (message) { return this.flashError(); } + } + + destroy() { + return this.disposables.dispose(); + } + + cancel() { + this.cancelled(); + this.restoreFocus(); + return this.destroy(); + } + + cancelled() { + return this.hide(); + } + + toggle(callback) { + this.callback = callback; + if ((this.panel != null ? this.panel.isVisible() : undefined)) { + return this.cancel(); + } else { + return this.show(); + } + } + + show() { + if (this.panel == null) { this.panel = atom.workspace.addModalPanel({item: this}); } + this.panel.show(); + this.storeFocusedElement(); + return this.miniEditor.focus(); + } + + hide() { + return (this.panel != null ? this.panel.hide() : undefined); + } + + storeFocusedElement() { + return this.previouslyFocusedElement = $(document.activeElement); + } + + restoreFocus() { + return (this.previouslyFocusedElement != null ? this.previouslyFocusedElement.focus() : undefined); + } + }); diff --git a/lib/view/files-view.coffee b/lib/view/files-view.coffee deleted file mode 100644 index f7fc509..0000000 --- a/lib/view/files-view.coffee +++ /dev/null @@ -1,226 +0,0 @@ -{$, $$, SelectListView} = require 'atom-space-pen-views' -{CompositeDisposable} = require 'atom' -LocalFile = require '../model/local-file' - -Dialog = require './dialog' - -fs = require 'fs' -os = require 'os' -async = require 'async' -util = require 'util' -path = require 'path' -Q = require 'q' -_ = require 'underscore-plus' -mkdirp = require 'mkdirp' -moment = require 'moment' -upath = require 'upath' - -module.exports = - class FilesView extends SelectListView - initialize: (@host) -> - super - @addClass('filesview') - - @disposables = new CompositeDisposable - @listenForEvents() - - connect: (connectionOptions = {}) -> - @path = if atom.config.get('remote-edit.rememberLastOpenDirectory') and @host.lastOpenDirectory? then @host.lastOpenDirectory else @host.directory - async.waterfall([ - (callback) => - if @host.usePassword and !connectionOptions.password? - if @host.password == "" or @host.password == '' or !@host.password? - async.waterfall([ - (callback) -> - passwordDialog = new Dialog({prompt: "Enter password"}) - passwordDialog.toggle(callback) - ], (err, result) => - connectionOptions = _.extend({password: result}, connectionOptions) - @toggle() - callback(null) - ) - else - callback(null) - else - callback(null) - (callback) => - if !@host.isConnected() - @setLoading("Connecting...") - @host.connect(callback, connectionOptions) - else - callback(null) - (callback) => - @populate(callback) - ], (err, result) => - if err? - console.error err - if err.code == 450 or err.type == "PERMISSION_DENIED" - @setError("You do not have read permission to what you've specified as the default directory! See the console for more info.") - else if err.code is 2 and @path is @host.lastOpenDirectory - # no such file, can occur if lastOpenDirectory is used and the dir has been removed - @host.lastOpenDirectory = undefined - @connect(connectionOptions) - else if @host.usePassword and (err.code == 530 or err.level == "connection-ssh") - async.waterfall([ - (callback) -> - passwordDialog = new Dialog({prompt: "Enter password"}) - passwordDialog.toggle(callback) - ], (err, result) => - @toggle() - @connect({password: result}) - ) - else - @setError(err) - ) - - getFilterKey: -> - return "name" - - destroy: -> - @panel.destroy() if @panel? - @disposables.dispose() - - cancelled: -> - @hide() - @host?.close() - @destroy() - - toggle: -> - if @panel?.isVisible() - @cancel() - else - @show() - - show: -> - @panel?.destroy() - @panel = atom.workspace.addModalPanel(item: this) - @panel.show() - @storeFocusedElement() - @focusFilterEditor() - - hide: -> - @panel?.hide() - - viewForItem: (item) -> - $$ -> - @li class: 'two-lines', => - if item.isFile - @div class: 'primary-line icon icon-file-text', item.name - else if item.isDir - @div class: 'primary-line icon icon-file-directory', item.name - else if item.isLink - @div class: 'primary-line icon icon-file-symlink-file', item.name - - @div class: 'secondary-line no-icon text-subtle', "Size: #{item.size}, Mtime: #{item.lastModified}, Permissions: #{item.permissions}" - - - - populate: (callback) -> - async.waterfall([ - (callback) => - @setLoading("Loading...") - @host.getFilesMetadata(@path, callback) - (items, callback) => - items = _.sortBy(items, 'isFile') if atom.config.get 'remote-edit.foldersOnTop' - @setItems(items) - callback(undefined, undefined) - ], (err, result) => - @setError(err) if err? - callback?(err, result) - ) - - populateList: -> - super - @setError path.resolve @path - - getNewPath: (next) -> - if (@path[@path.length - 1] == "/") - @path + next - else - @path + "/" + next - - updatePath: (next) => - @path = upath.normalize(@getNewPath(next)) - - getDefaultSaveDirForHostAndFile: (file, callback) -> - async.waterfall([ - (callback) -> - fs.realpath(os.tmpDir(), callback) - (tmpDir, callback) -> - tmpDir = tmpDir + path.sep + "remote-edit" - fs.mkdir(tmpDir, ((err) -> - if err? && err.code == 'EEXIST' - callback(null, tmpDir) - else - callback(err, tmpDir) - ) - ) - (tmpDir, callback) => - tmpDir = tmpDir + path.sep + @host.hashCode() + '_' + @host.username + "-" + @host.hostname + file.dirName - mkdirp(tmpDir, ((err) -> - if err? && err.code == 'EEXIST' - callback(null, tmpDir) - else - callback(err, tmpDir) - ) - ) - ], (err, savePath) -> - callback(err, savePath) - ) - - openFile: (file) => - @setLoading("Downloading file...") - dtime = moment().format("HH:mm:ss DD/MM/YY") - async.waterfall([ - (callback) => - @getDefaultSaveDirForHostAndFile(file, callback) - (savePath, callback) => - savePath = savePath + path.sep + dtime.replace(/([^a-z0-9\s]+)/gi, '').replace(/([\s]+)/gi, '-') + "_" + file.name - localFile = new LocalFile(savePath, file, dtime, @host) - @host.getFile(localFile, callback) - ], (err, localFile) => - if err? - @setError(err) - console.error err - else - @host.addLocalFile(localFile) - uri = "remote-edit://localFile/?localFile=#{encodeURIComponent(JSON.stringify(localFile.serialize()))}&host=#{encodeURIComponent(JSON.stringify(localFile.host.serialize()))}" - atom.workspace.open(uri, split: 'left') - - @host.close() - @cancel() - ) - - openDirectory: (dir) => - @setLoading("Opening directory...") - throw new Error("Not implemented yet!") - - confirmed: (item) -> - if item.isFile - @openFile(item) - else if item.isDir - @filterEditorView.setText('') - @setItems() - @updatePath(item.name) - @host.lastOpenDirectory = upath.normalize(item.path) - @host.invalidate() - @populate() - else if item.isLink - if atom.config.get('remote-edit.followLinks') - @filterEditorView.setText('') - @setItems() - @updatePath(item.name) - @populate() - else - @openFile(item) - - else - @setError("Selected item is neither a file, directory or link!") - - listenForEvents: -> - @disposables.add atom.commands.add 'atom-workspace', 'filesview:open', => - item = @getSelectedItem() - if item.isFile - @openFile(item) - else if item.isDir - @openDirectory(item) diff --git a/lib/view/files-view.js b/lib/view/files-view.js new file mode 100644 index 0000000..552578a --- /dev/null +++ b/lib/view/files-view.js @@ -0,0 +1,290 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FilesView; +const {$, $$, SelectListView} = require('atom-space-pen-views'); +const {CompositeDisposable} = require('atom'); +const LocalFile = require('../model/local-file'); + +const Dialog = require('./dialog'); + +const fs = require('fs'); +const os = require('os'); +const async = require('async'); +const util = require('util'); +const path = require('path'); +const Q = require('q'); +const _ = require('underscore-plus'); +const mkdirp = require('mkdirp'); +const moment = require('moment'); +const upath = require('upath'); + +module.exports = + (FilesView = class FilesView extends SelectListView { + constructor(...args) { + super(...args); + this.updatePath = this.updatePath.bind(this); + this.openFile = this.openFile.bind(this); + this.openDirectory = this.openDirectory.bind(this); + } + + initialize(host) { + this.host = host; + super.initialize(...arguments); + this.addClass('filesview'); + + this.disposables = new CompositeDisposable; + return this.listenForEvents(); + } + + connect(connectionOptions) { + if (connectionOptions == null) { connectionOptions = {}; } + this.path = atom.config.get('remote-edit.rememberLastOpenDirectory') && (this.host.lastOpenDirectory != null) ? this.host.lastOpenDirectory : this.host.directory; + return async.waterfall([ + callback => { + if (this.host.usePassword && (connectionOptions.password == null)) { + if ((this.host.password === "") || (this.host.password === '') || (this.host.password == null)) { + return async.waterfall([ + function(callback) { + const passwordDialog = new Dialog({prompt: "Enter password"}); + return passwordDialog.toggle(callback); + } + ], (err, result) => { + connectionOptions = _.extend({password: result}, connectionOptions); + this.toggle(); + return callback(null); + }); + } else { + return callback(null); + } + } else { + return callback(null); + } + }, + callback => { + if (!this.host.isConnected()) { + this.setLoading("Connecting..."); + return this.host.connect(callback, connectionOptions); + } else { + return callback(null); + } + }, + callback => { + return this.populate(callback); + } + ], (err, result) => { + if (err != null) { + console.error(err); + if ((err.code === 450) || (err.type === "PERMISSION_DENIED")) { + return this.setError("You do not have read permission to what you've specified as the default directory! See the console for more info."); + } else if ((err.code === 2) && (this.path === this.host.lastOpenDirectory)) { + // no such file, can occur if lastOpenDirectory is used and the dir has been removed + this.host.lastOpenDirectory = undefined; + return this.connect(connectionOptions); + } else if (this.host.usePassword && ((err.code === 530) || (err.level === "connection-ssh"))) { + return async.waterfall([ + function(callback) { + const passwordDialog = new Dialog({prompt: "Enter password"}); + return passwordDialog.toggle(callback); + } + ], (err, result) => { + this.toggle(); + return this.connect({password: result}); + }); + } else { + return this.setError(err); + } + } + }); + } + + getFilterKey() { + return "name"; + } + + destroy() { + if (this.panel != null) { this.panel.destroy(); } + return this.disposables.dispose(); + } + + cancelled() { + this.hide(); + if (this.host != null) { + this.host.close(); + } + return this.destroy(); + } + + toggle() { + if ((this.panel != null ? this.panel.isVisible() : undefined)) { + return this.cancel(); + } else { + return this.show(); + } + } + + show() { + if (this.panel != null) { + this.panel.destroy(); + } + this.panel = atom.workspace.addModalPanel({item: this}); + this.panel.show(); + this.storeFocusedElement(); + return this.focusFilterEditor(); + } + + hide() { + return (this.panel != null ? this.panel.hide() : undefined); + } + + viewForItem(item) { + return $$(function() { + return this.li({class: 'two-lines'}, () => { + if (item.isFile) { + this.div({class: 'primary-line icon icon-file-text'}, item.name); + } else if (item.isDir) { + this.div({class: 'primary-line icon icon-file-directory'}, item.name); + } else if (item.isLink) { + this.div({class: 'primary-line icon icon-file-symlink-file'}, item.name); + } + + return this.div({class: 'secondary-line no-icon text-subtle'}, `Size: ${item.size}, Mtime: ${item.lastModified}, Permissions: ${item.permissions}`); + }); + }); + } + + + + populate(callback) { + return async.waterfall([ + callback => { + this.setLoading("Loading..."); + return this.host.getFilesMetadata(this.path, callback); + }, + (items, callback) => { + if (atom.config.get('remote-edit.foldersOnTop')) { items = _.sortBy(items, 'isFile'); } + this.setItems(items); + return callback(undefined, undefined); + } + ], (err, result) => { + if (err != null) { this.setError(err); } + return (typeof callback === 'function' ? callback(err, result) : undefined); + }); + } + + populateList() { + super.populateList(...arguments); + return this.setError(path.resolve(this.path)); + } + + getNewPath(next) { + if (this.path[this.path.length - 1] === "/") { + return this.path + next; + } else { + return this.path + "/" + next; + } + } + + updatePath(next) { + return this.path = upath.normalize(this.getNewPath(next)); + } + + getDefaultSaveDirForHostAndFile(file, callback) { + return async.waterfall([ + callback => fs.realpath(os.tmpDir(), callback), + function(tmpDir, callback) { + tmpDir = tmpDir + path.sep + "remote-edit"; + return fs.mkdir(tmpDir, (function(err) { + if ((err != null) && (err.code === 'EEXIST')) { + return callback(null, tmpDir); + } else { + return callback(err, tmpDir); + } + }) + ); + }, + (tmpDir, callback) => { + tmpDir = tmpDir + path.sep + this.host.hashCode() + '_' + this.host.username + "-" + this.host.hostname + file.dirName; + return mkdirp(tmpDir, (function(err) { + if ((err != null) && (err.code === 'EEXIST')) { + return callback(null, tmpDir); + } else { + return callback(err, tmpDir); + } + }) + ); + } + ], (err, savePath) => callback(err, savePath)); + } + + openFile(file) { + this.setLoading("Downloading file..."); + const dtime = moment().format("HH:mm:ss DD/MM/YY"); + return async.waterfall([ + callback => { + return this.getDefaultSaveDirForHostAndFile(file, callback); + }, + (savePath, callback) => { + savePath = savePath + path.sep + dtime.replace(/([^a-z0-9\s]+)/gi, '').replace(/([\s]+)/gi, '-') + "_" + file.name; + const localFile = new LocalFile(savePath, file, dtime, this.host); + return this.host.getFile(localFile, callback); + } + ], (err, localFile) => { + if (err != null) { + this.setError(err); + return console.error(err); + } else { + this.host.addLocalFile(localFile); + const uri = `remote-edit://localFile/?localFile=${encodeURIComponent(JSON.stringify(localFile.serialize()))}&host=${encodeURIComponent(JSON.stringify(localFile.host.serialize()))}`; + const text = atom.workspace.open(uri, {split: 'left'}); + this.host.close(); + return this.cancel(); + } + }); + } + + openDirectory(dir) { + this.setLoading("Opening directory..."); + throw new Error("Not implemented yet!"); + } + + confirmed(item) { + if (item.isFile) { + return this.openFile(item); + } else if (item.isDir) { + this.filterEditorView.setText(''); + this.setItems(); + this.updatePath(item.name); + this.host.lastOpenDirectory = upath.normalize(item.path); + this.host.invalidate(); + return this.populate(); + } else if (item.isLink) { + if (atom.config.get('remote-edit.followLinks')) { + this.filterEditorView.setText(''); + this.setItems(); + this.updatePath(item.name); + return this.populate(); + } else { + return this.openFile(item); + } + + } else { + return this.setError("Selected item is neither a file, directory or link!"); + } + } + + listenForEvents() { + return this.disposables.add(atom.commands.add('atom-workspace', 'filesview:open', () => { + const item = this.getSelectedItem(); + if (item.isFile) { + return this.openFile(item); + } else if (item.isDir) { + return this.openDirectory(item); + } + }) + ); + } + }); diff --git a/lib/view/host-view.coffee b/lib/view/host-view.coffee deleted file mode 100644 index fe8a7a5..0000000 --- a/lib/view/host-view.coffee +++ /dev/null @@ -1,221 +0,0 @@ -{$, View, TextEditorView} = require 'atom-space-pen-views' -{CompositeDisposable} = require 'atom' - -Host = require '../model/host' -SftpHost = require '../model/sftp-host' -FtpHost = require '../model/ftp-host' - -fs = require 'fs-plus' - -try - keytar = require 'keytar' -catch err - console.debug 'Keytar could not be loaded! Passwords will be stored in cleartext to remoteEdit.json!' - keytar = undefined - -module.exports = - class HostView extends View - @content: -> - @div class: 'host-view', => - @h2 "Connection settings", class: "host-header" - @label 'Hostname:' - @subview 'hostname', new TextEditorView(mini: true) - - @label 'Default directory:' - @subview 'directory', new TextEditorView(mini: true) - - @label 'Username:' - @subview 'username', new TextEditorView(mini: true) - - @label 'Port:' - @subview 'port', new TextEditorView(mini: true) - - - @h2 "Authentication settings", class: "host-header" - @div class: 'block', outlet: 'authenticationButtonsBlock', => - @div class: 'btn-group', => - @button class: 'btn selected', outlet: 'userAgentButton', click: 'userAgentButtonClick', 'User agent' - @button class: 'btn', outlet: 'privateKeyButton', click: 'privateKeyButtonClick', 'Private key' - @button class: 'btn', outlet: 'passwordButton', click: 'passwordButtonClick', 'Password' - - @div class: 'block', outlet: 'passwordBlock', => - @label 'Password:' - @subview 'password', new TextEditorView(mini: true) - @label 'Passwords are by default stored in cleartext! Leave password field empty if you want to be prompted.', class: 'text-warning' - @label 'Passwords can be saved to default system keychain by enabling option in settings.', class: 'text-warning' - - @div class: 'block', outlet: 'privateKeyBlock', => - @label 'Private key path:' - @subview 'privateKeyPath', new TextEditorView(mini: true) - @label 'Private key passphrase:' - @subview 'privateKeyPassphrase', new TextEditorView(mini: true) - @label 'Passphrases are by default stored in cleartext! Leave Passphrases field empty if you want to be prompted.', class: 'text-warning' - @label 'Passphrases can be saved to default system keychain by enabling option in settings.', class: 'text-warning' - - @h2 "Additional settings", class: "host-header" - @label 'Alias:' - @subview 'alias', new TextEditorView(mini: true) - - - - @div class: 'block', outlet: 'buttonBlock', => - @button class: 'inline-block btn pull-right', outlet: 'cancelButton', click: 'cancel', 'Cancel' - @button class: 'inline-block btn pull-right', outlet: 'saveButton', click: 'confirm','Save' - - initialize: (@host, @ipdw) -> - throw new Error("Parameter \"host\" undefined!") if !@host? - - @disposables = new CompositeDisposable - @disposables.add atom.commands.add 'atom-workspace', - 'core:confirm': => @confirm() - 'core:cancel': (event) => - @cancel() - event.stopPropagation() - - @alias.setText(@host.alias ? "") - @hostname.setText(@host.hostname ? "") - @directory.setText(@host.directory ? "/") - @username.setText(@host.username ? "") - - @port.setText(@host.port ? "") - - if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (keytar?) - keytarPassword = keytar.getPassword(@host.getServiceNamePassword(), @host.getServiceAccount()) - @password.setText(keytarPassword ? "") - else - @password.setText(@host.password ? "") - - @privateKeyPath.setText(@host.privateKeyPath ? atom.config.get('remote-edit.sshPrivateKeyPath')) - if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (@host instanceof SftpHost) and (keytar?) - keytarPassphrase = keytar.getPassword(@host.getServiceNamePassphrase(), @host.getServiceAccount()) - @privateKeyPassphrase.setText(keytarPassphrase ? "") - else - @privateKeyPassphrase.setText(@host.passphrase ? "") - - userAgentButtonClick: -> - @privateKeyButton.toggleClass('selected', false) - @userAgentButton.toggleClass('selected', true) - @passwordButton.toggleClass('selected', false) - @passwordBlock.hide() - @privateKeyBlock.hide() - - privateKeyButtonClick: -> - @privateKeyButton.toggleClass('selected', true) - @userAgentButton.toggleClass('selected', false) - @passwordButton.toggleClass('selected', false) - @passwordBlock.hide() - @privateKeyBlock.show() - @privateKeyPath.focus() - - passwordButtonClick: -> - @privateKeyButton.toggleClass('selected', false) - @userAgentButton.toggleClass('selected', false) - @passwordButton.toggleClass('selected', true) - @privateKeyBlock.hide() - @passwordBlock.show() - @password.focus() - - - confirm: -> - @cancel() - - @host.alias = @alias.getText() - @host.hostname = @hostname.getText() - @host.directory = @directory.getText() - @host.username = @username.getText() - @host.port = @port.getText() - - if @host instanceof SftpHost - @host.useAgent = @userAgentButton.hasClass('selected') - @host.usePrivateKey = @privateKeyButton.hasClass('selected') - @host.usePassword = @passwordButton.hasClass('selected') - - if @privateKeyButton.hasClass('selected') - @host.privateKeyPath = fs.absolute(@privateKeyPath.getText()) - if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (@privateKeyPassphrase.getText().length > 0) and (keytar?) - keytar.replacePassword(@host.getServiceNamePassphrase(), @host.getServiceAccount(), @privateKeyPassphrase.getText()) - @host.passphrase = "***** keytar *****" - else if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (@privateKeyPassphrase.getText().length is 0) - keytar.deletePassword(@host.getServiceNamePassphrase(), @host.getServiceAccount()) - @host.passphrase = "" - else - @host.passphrase = @privateKeyPassphrase.getText() - if @passwordButton.hasClass('selected') - if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (@password.getText().length > 0) and (keytar?) - keytarResult = keytar.replacePassword(@host.getServiceNamePassword(), @host.getServiceAccount(), @password.getText()) - @host.password = "***** keytar *****" - else if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (@password.getText().length is 0) and (keytar?) - keytar.deletePassword(@host.getServiceNamePassword(), @host.getServiceAccount()) - @host.password = "" - else - @host.password = @password.getText() - else if @host instanceof FtpHost - @host.usePassword = true - if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (@password.getText().length > 0) and (keytar?) - keytarResult = keytar.replacePassword(@host.getServiceNamePassword(), @host.getServiceAccount(), @password.getText()) - @host.password = "***** keytar *****" - else if atom.config.get('remote-edit.storePasswordsUsingKeytar') and (@password.getText().length is 0) and (keytar?) - keytar.deletePassword(@host.getServiceNamePassword(), @host.getServiceAccount()) - @host.password = "" - else - @host.password = @password.getText() - else - throw new Error("\"host\" is not valid type!", @host) - - - - if @ipdw? - @ipdw.getData().then((data) => - data.addNewHost(@host) - ) - else - @host.invalidate() - - destroy: -> - @panel.destroy() if @panel? - @disposables.dispose() - - cancel: -> - @cancelled() - @restoreFocus() - @destroy() - - cancelled: -> - @hide() - - toggle: -> - if @panel?.isVisible() - @cancel() - else - @show() - - show: -> - if (@host instanceof SftpHost) - @authenticationButtonsBlock.show() - if @host.usePassword - @passwordButton.click() - else if @host.usePrivateKey - @privateKeyButton.click() - else if @host.useAgent - @userAgentButton.click() - else if (@host instanceof FtpHost) - @authenticationButtonsBlock.hide() - @passwordBlock.show() - @privateKeyBlock.hide() - else - throw new Error("\"host\" is unknown!", @host) - - @panel ?= atom.workspace.addModalPanel(item: this) - @panel.show() - - @storeFocusedElement() - @hostname.focus() - - hide: -> - @panel?.hide() - - storeFocusedElement: -> - @previouslyFocusedElement = $(document.activeElement) - - restoreFocus: -> - @previouslyFocusedElement?.focus() diff --git a/lib/view/host-view.js b/lib/view/host-view.js new file mode 100644 index 0000000..fe00ecc --- /dev/null +++ b/lib/view/host-view.js @@ -0,0 +1,269 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HostView, keytar; +const {$, View, TextEditorView} = require('atom-space-pen-views'); +const {CompositeDisposable} = require('atom'); + +const Host = require('../model/host'); +const SftpHost = require('../model/sftp-host'); +const FtpHost = require('../model/ftp-host'); + +const fs = require('fs-plus'); + +try { + keytar = require('keytar'); +} catch (err) { + console.debug('Keytar could not be loaded! Passwords will be stored in cleartext to remoteEdit.json!'); + keytar = undefined; +} + +module.exports = + (HostView = class HostView extends View { + static content() { + return this.div({class: 'host-view'}, () => { + this.h2("Connection settings", {class: "host-header"}); + this.label('Hostname:'); + this.subview('hostname', new TextEditorView({mini: true})); + + this.label('Default directory:'); + this.subview('directory', new TextEditorView({mini: true})); + + this.label('Username:'); + this.subview('username', new TextEditorView({mini: true})); + + this.label('Port:'); + this.subview('port', new TextEditorView({mini: true})); + + + this.h2("Authentication settings", {class: "host-header"}); + this.div({class: 'block', outlet: 'authenticationButtonsBlock'}, () => { + return this.div({class: 'btn-group'}, () => { + this.button({class: 'btn selected', outlet: 'userAgentButton', click: 'userAgentButtonClick'}, 'User agent'); + this.button({class: 'btn', outlet: 'privateKeyButton', click: 'privateKeyButtonClick'}, 'Private key'); + return this.button({class: 'btn', outlet: 'passwordButton', click: 'passwordButtonClick'}, 'Password'); + }); + }); + + this.div({class: 'block', outlet: 'passwordBlock'}, () => { + this.label('Password:'); + this.subview('password', new TextEditorView({mini: true})); + this.label('Passwords are by default stored in cleartext! Leave password field empty if you want to be prompted.', {class: 'text-warning'}); + return this.label('Passwords can be saved to default system keychain by enabling option in settings.', {class: 'text-warning'}); + }); + + this.div({class: 'block', outlet: 'privateKeyBlock'}, () => { + this.label('Private key path:'); + this.subview('privateKeyPath', new TextEditorView({mini: true})); + this.label('Private key passphrase:'); + this.subview('privateKeyPassphrase', new TextEditorView({mini: true})); + this.label('Passphrases are by default stored in cleartext! Leave Passphrases field empty if you want to be prompted.', {class: 'text-warning'}); + return this.label('Passphrases can be saved to default system keychain by enabling option in settings.', {class: 'text-warning'}); + }); + + this.h2("Additional settings", {class: "host-header"}); + this.label('Alias:'); + this.subview('alias', new TextEditorView({mini: true})); + + + + return this.div({class: 'block', outlet: 'buttonBlock'}, () => { + this.button({class: 'inline-block btn pull-right', outlet: 'cancelButton', click: 'cancel'}, 'Cancel'); + return this.button({class: 'inline-block btn pull-right', outlet: 'saveButton', click: 'confirm'},'Save'); + }); + }); + } + + initialize(host, ipdw) { + this.host = host; + this.ipdw = ipdw; + if ((this.host == null)) { throw new Error("Parameter \"host\" undefined!"); } + + this.disposables = new CompositeDisposable; + this.disposables.add(atom.commands.add('atom-workspace', { + 'core:confirm': () => this.confirm(), + 'core:cancel': event => { + this.cancel(); + return event.stopPropagation(); + } + } + ) + ); + + this.alias.setText(this.host.alias != null ? this.host.alias : ""); + this.hostname.setText(this.host.hostname != null ? this.host.hostname : ""); + this.directory.setText(this.host.directory != null ? this.host.directory : "/"); + this.username.setText(this.host.username != null ? this.host.username : ""); + + this.port.setText(this.host.port != null ? this.host.port : ""); + + if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (keytar != null)) { + const keytarPassword = keytar.getPassword(this.host.getServiceNamePassword(), this.host.getServiceAccount()); + this.password.setText(keytarPassword != null ? keytarPassword : ""); + } else { + this.password.setText(this.host.password != null ? this.host.password : ""); + } + + this.privateKeyPath.setText(this.host.privateKeyPath != null ? this.host.privateKeyPath : atom.config.get('remote-edit.sshPrivateKeyPath')); + if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (this.host instanceof SftpHost) && (keytar != null)) { + const keytarPassphrase = keytar.getPassword(this.host.getServiceNamePassphrase(), this.host.getServiceAccount()); + return this.privateKeyPassphrase.setText(keytarPassphrase != null ? keytarPassphrase : ""); + } else { + return this.privateKeyPassphrase.setText(this.host.passphrase != null ? this.host.passphrase : ""); + } + } + + userAgentButtonClick() { + this.privateKeyButton.toggleClass('selected', false); + this.userAgentButton.toggleClass('selected', true); + this.passwordButton.toggleClass('selected', false); + this.passwordBlock.hide(); + return this.privateKeyBlock.hide(); + } + + privateKeyButtonClick() { + this.privateKeyButton.toggleClass('selected', true); + this.userAgentButton.toggleClass('selected', false); + this.passwordButton.toggleClass('selected', false); + this.passwordBlock.hide(); + this.privateKeyBlock.show(); + return this.privateKeyPath.focus(); + } + + passwordButtonClick() { + this.privateKeyButton.toggleClass('selected', false); + this.userAgentButton.toggleClass('selected', false); + this.passwordButton.toggleClass('selected', true); + this.privateKeyBlock.hide(); + this.passwordBlock.show(); + return this.password.focus(); + } + + + confirm() { + let keytarResult; + this.cancel(); + + this.host.alias = this.alias.getText(); + this.host.hostname = this.hostname.getText(); + this.host.directory = this.directory.getText(); + this.host.username = this.username.getText(); + this.host.port = this.port.getText(); + + if (this.host instanceof SftpHost) { + this.host.useAgent = this.userAgentButton.hasClass('selected'); + this.host.usePrivateKey = this.privateKeyButton.hasClass('selected'); + this.host.usePassword = this.passwordButton.hasClass('selected'); + + if (this.privateKeyButton.hasClass('selected')) { + this.host.privateKeyPath = fs.absolute(this.privateKeyPath.getText()); + if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (this.privateKeyPassphrase.getText().length > 0) && (keytar != null)) { + keytar.replacePassword(this.host.getServiceNamePassphrase(), this.host.getServiceAccount(), this.privateKeyPassphrase.getText()); + this.host.passphrase = "***** keytar *****"; + } else if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (this.privateKeyPassphrase.getText().length === 0)) { + keytar.deletePassword(this.host.getServiceNamePassphrase(), this.host.getServiceAccount()); + this.host.passphrase = ""; + } else { + this.host.passphrase = this.privateKeyPassphrase.getText(); + } + } + if (this.passwordButton.hasClass('selected')) { + if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (this.password.getText().length > 0) && (keytar != null)) { + keytarResult = keytar.replacePassword(this.host.getServiceNamePassword(), this.host.getServiceAccount(), this.password.getText()); + this.host.password = "***** keytar *****"; + } else if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (this.password.getText().length === 0) && (keytar != null)) { + keytar.deletePassword(this.host.getServiceNamePassword(), this.host.getServiceAccount()); + this.host.password = ""; + } else { + this.host.password = this.password.getText(); + } + } + } else if (this.host instanceof FtpHost) { + this.host.usePassword = true; + if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (this.password.getText().length > 0) && (keytar != null)) { + keytarResult = keytar.replacePassword(this.host.getServiceNamePassword(), this.host.getServiceAccount(), this.password.getText()); + this.host.password = "***** keytar *****"; + } else if (atom.config.get('remote-edit.storePasswordsUsingKeytar') && (this.password.getText().length === 0) && (keytar != null)) { + keytar.deletePassword(this.host.getServiceNamePassword(), this.host.getServiceAccount()); + this.host.password = ""; + } else { + this.host.password = this.password.getText(); + } + } else { + throw new Error("\"host\" is not valid type!", this.host); + } + + + + if (this.ipdw != null) { + return this.ipdw.getData().then(data => { + return data.addNewHost(this.host); + }); + } else { + return this.host.invalidate(); + } + } + + destroy() { + if (this.panel != null) { this.panel.destroy(); } + return this.disposables.dispose(); + } + + cancel() { + this.cancelled(); + this.restoreFocus(); + return this.destroy(); + } + + cancelled() { + return this.hide(); + } + + toggle() { + if ((this.panel != null ? this.panel.isVisible() : undefined)) { + return this.cancel(); + } else { + return this.show(); + } + } + + show() { + if (this.host instanceof SftpHost) { + this.authenticationButtonsBlock.show(); + if (this.host.usePassword) { + this.passwordButton.click(); + } else if (this.host.usePrivateKey) { + this.privateKeyButton.click(); + } else if (this.host.useAgent) { + this.userAgentButton.click(); + } + } else if (this.host instanceof FtpHost) { + this.authenticationButtonsBlock.hide(); + this.passwordBlock.show(); + this.privateKeyBlock.hide(); + } else { + throw new Error("\"host\" is unknown!", this.host); + } + + if (this.panel == null) { this.panel = atom.workspace.addModalPanel({item: this}); } + this.panel.show(); + + this.storeFocusedElement(); + return this.hostname.focus(); + } + + hide() { + return (this.panel != null ? this.panel.hide() : undefined); + } + + storeFocusedElement() { + return this.previouslyFocusedElement = $(document.activeElement); + } + + restoreFocus() { + return (this.previouslyFocusedElement != null ? this.previouslyFocusedElement.focus() : undefined); + } + }); diff --git a/lib/view/hosts-view.coffee b/lib/view/hosts-view.coffee deleted file mode 100644 index d0f95a8..0000000 --- a/lib/view/hosts-view.coffee +++ /dev/null @@ -1,100 +0,0 @@ -{$, $$, SelectListView} = require 'atom-space-pen-views' -{CompositeDisposable, Emitter} = require 'atom' -_ = require 'underscore-plus' - -FilesView = require './files-view' -HostView = require './host-view' - -SftpHost = require '../model/sftp-host' -FtpHost = require '../model/ftp-host' - -module.exports = - class HostsView extends SelectListView - initialize: (@ipdw) -> - super - @createItemsFromIpdw() - @addClass('hosts-view') - - @disposables = new CompositeDisposable - @disposables.add @ipdw.onDidChange => @createItemsFromIpdw() - - @listenForEvents() - - destroy: -> - @panel.destroy() if @panel? - @disposables.dispose() - - cancelled: -> - @hide() - @destroy() - - toggle: -> - if @panel?.isVisible() - @cancel() - else - @show() - - show: -> - @panel ?= atom.workspace.addModalPanel(item: this) - @panel.show() - - @storeFocusedElement() - - @focusFilterEditor() - - hide: -> - @panel?.hide() - - getFilterKey: -> - return "searchKey" - - viewForItem: (item) -> - keyBindings = @keyBindings - - $$ -> - @li class: 'two-lines', => - @div class: 'primary-line', => - @span class: 'inline-block highlight', "#{item.alias}" if item.alias? - @span class: 'inline-block', "#{item.username}@#{item.hostname}:#{item.port}:#{item.directory}" - if item instanceof SftpHost - authType = "not set" - if item.usePassword and (item.password == "" or item.password == '' or !item.password?) - authType = "password (not set)" - else if item.usePassword - authType = "password (set)" - else if item.usePrivateKey - authType = "key" - else if item.useAgent - authType = "agent" - @div class: "secondary-line", "Type: SFTP, Open files: #{item.localFiles.length}, Auth: " + authType - else if item instanceof FtpHost - authType = "not set" - if item.usePassword and (item.password == "" or item.password == '' or !item.password?) - authType = "password (not set)" - else - authType = "password (set)" - @div class: "secondary-line", "Type: FTP, Open files: #{item.localFiles.length}, Auth: " + authType - else - @div class: "secondary-line", "Type: UNDEFINED" - - confirmed: (item) -> - @cancel() - filesView = new FilesView(item) - filesView.connect() - filesView.toggle() - - listenForEvents: -> - @disposables.add atom.commands.add 'atom-workspace', 'hostview:delete', => - item = @getSelectedItem() - if item? - item.delete() - @setLoading() - @disposables.add atom.commands.add 'atom-workspace', 'hostview:edit', => - item = @getSelectedItem() - if item? - hostView = new HostView(item) - hostView.toggle() - @cancel() - - createItemsFromIpdw: -> - @ipdw.getData().then((resolved) => @setItems(resolved.hostList)) diff --git a/lib/view/hosts-view.js b/lib/view/hosts-view.js new file mode 100644 index 0000000..205ec38 --- /dev/null +++ b/lib/view/hosts-view.js @@ -0,0 +1,134 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HostsView; +const {$, $$, SelectListView} = require('atom-space-pen-views'); +const {CompositeDisposable, Emitter} = require('atom'); +const _ = require('underscore-plus'); + +const FilesView = require('./files-view'); +const HostView = require('./host-view'); + +const SftpHost = require('../model/sftp-host'); +const FtpHost = require('../model/ftp-host'); + +module.exports = + (HostsView = class HostsView extends SelectListView { + initialize(ipdw) { + this.ipdw = ipdw; + super.initialize(...arguments); + this.createItemsFromIpdw(); + this.addClass('hosts-view'); + + this.disposables = new CompositeDisposable; + this.disposables.add(this.ipdw.onDidChange(() => this.createItemsFromIpdw())); + + return this.listenForEvents(); + } + + destroy() { + if (this.panel != null) { this.panel.destroy(); } + return this.disposables.dispose(); + } + + cancelled() { + this.hide(); + return this.destroy(); + } + + toggle() { + if ((this.panel != null ? this.panel.isVisible() : undefined)) { + return this.cancel(); + } else { + return this.show(); + } + } + + show() { + if (this.panel == null) { this.panel = atom.workspace.addModalPanel({item: this}); } + this.panel.show(); + + this.storeFocusedElement(); + + return this.focusFilterEditor(); + } + + hide() { + return (this.panel != null ? this.panel.hide() : undefined); + } + + getFilterKey() { + return "searchKey"; + } + + viewForItem(item) { + const { keyBindings } = this; + + return $$(function() { + return this.li({class: 'two-lines'}, () => { + let authType; + this.div({class: 'primary-line'}, () => { + if (item.alias != null) { this.span({class: 'inline-block highlight'}, `${item.alias}`); } + return this.span({class: 'inline-block'}, `${item.username}@${item.hostname}:${item.port}:${item.directory}`); + }); + if (item instanceof SftpHost) { + authType = "not set"; + if (item.usePassword && ((item.password === "") || (item.password === '') || (item.password == null))) { + authType = "password (not set)"; + } else if (item.usePassword) { + authType = "password (set)"; + } else if (item.usePrivateKey) { + authType = "key"; + } else if (item.useAgent) { + authType = "agent"; + } + return this.div({class: "secondary-line"}, `Type: SFTP, Open files: ${item.localFiles.length}, Auth: ` + authType); + } else if (item instanceof FtpHost) { + authType = "not set"; + if (item.usePassword && ((item.password === "") || (item.password === '') || (item.password == null))) { + authType = "password (not set)"; + } else { + authType = "password (set)"; + } + return this.div({class: "secondary-line"}, `Type: FTP, Open files: ${item.localFiles.length}, Auth: ` + authType); + } else { + return this.div({class: "secondary-line"}, "Type: UNDEFINED"); + } + }); + }); + } + + confirmed(item) { + this.cancel(); + const filesView = new FilesView(item); + filesView.connect(); + return filesView.toggle(); + } + + listenForEvents() { + this.disposables.add(atom.commands.add('atom-workspace', 'hostview:delete', () => { + const item = this.getSelectedItem(); + if (item != null) { + item.delete(); + return this.setLoading(); + } + }) + ); + return this.disposables.add(atom.commands.add('atom-workspace', 'hostview:edit', () => { + const item = this.getSelectedItem(); + if (item != null) { + const hostView = new HostView(item); + hostView.toggle(); + return this.cancel(); + } + }) + ); + } + + createItemsFromIpdw() { + return this.ipdw.getData().then(resolved => this.setItems(resolved.hostList)); + } + }); diff --git a/lib/view/open-files-view.coffee b/lib/view/open-files-view.coffee deleted file mode 100644 index 5759ddf..0000000 --- a/lib/view/open-files-view.coffee +++ /dev/null @@ -1,83 +0,0 @@ -{$$, SelectListView} = require 'atom-space-pen-views' -{CompositeDisposable} = require 'atom' - -async = require 'async' -Q = require 'q' -_ = require 'underscore-plus' -fs = require 'fs-plus' -moment = require 'moment' - -LocalFile = require '../model/local-file' - -module.exports = - class OpenFilesView extends SelectListView - initialize: (@ipdw) -> - super - @addClass('open-files-view') - @createItemsFromIpdw() - - @disposables = new CompositeDisposable - @disposables.add @ipdw.onDidChange => @createItemsFromIpdw() - - @listenForEvents() - - destroy: -> - @panel.destroy() if @panel? - @disposables.dispose() - - cancelled: -> - @hide() - @destroy() - - toggle: -> - if @panel?.isVisible() - @cancel() - else - @show() - - show: -> - @panel ?= atom.workspace.addModalPanel(item: this) - @panel.show() - - @storeFocusedElement() - - @focusFilterEditor() - - hide: -> - @panel?.hide() - - getFilterKey: -> - return "name" - - viewForItem: (localFile) -> - $$ -> - @li class: 'two-lines', => - @div class: 'primary-line icon globe', "#{localFile.host.protocol}://#{localFile.host.username}@#{localFile.host.hostname}:#{localFile.host.port}#{localFile.remoteFile.path}" - #mtime = moment(fs.statSync(localFile.path).mtime.getTime()).format("HH:mm:ss DD/MM/YY") - mtime = moment(fs.stat(localFile.path, (stat) => stat?.mtime?.getTime())).format("HH:mm:ss DD/MM/YY") - @div class: 'secondary-line no-icon text-subtle', "Downloaded: #{localFile.dtime}, Mtime: #{mtime}" - - confirmed: (localFile) -> - uri = "remote-edit://localFile/?localFile=#{encodeURIComponent(JSON.stringify(localFile.serialize()))}&host=#{encodeURIComponent(JSON.stringify(localFile.host.serialize()))}" - atom.workspace.open(uri, split: 'left') - @cancel() - - listenForEvents: -> - @disposables.add atom.commands.add 'atom-workspace', 'openfilesview:delete', => - item = @getSelectedItem() - if item? - @items = _.reject(@items, ((val) -> val == item)) - item.delete() - @setLoading() - - createItemsFromIpdw: -> - @ipdw.getData().then((data) => - localFiles = [] - async.each(data.hostList, ((host, callback) -> - async.each(host.localFiles, ((file, callback) -> - file.host = host - localFiles.push(file) - ), ((err) -> console.error err if err?)) - ), ((err) -> console.error err if err?)) - @setItems(localFiles) - ) diff --git a/lib/view/open-files-view.js b/lib/view/open-files-view.js new file mode 100644 index 0000000..8462931 --- /dev/null +++ b/lib/view/open-files-view.js @@ -0,0 +1,109 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let OpenFilesView; +const {$$, SelectListView} = require('atom-space-pen-views'); +const {CompositeDisposable} = require('atom'); + +const async = require('async'); +const Q = require('q'); +const _ = require('underscore-plus'); +const fs = require('fs-plus'); +const moment = require('moment'); + +const LocalFile = require('../model/local-file'); + +module.exports = + (OpenFilesView = class OpenFilesView extends SelectListView { + initialize(ipdw) { + this.ipdw = ipdw; + super.initialize(...arguments); + this.addClass('open-files-view'); + this.createItemsFromIpdw(); + + this.disposables = new CompositeDisposable; + this.disposables.add(this.ipdw.onDidChange(() => this.createItemsFromIpdw())); + + this.listenForEvents(); + } + + destroy() { + if (this.panel != null) { this.panel.destroy(); } + this.disposables.dispose(); + } + + cancelled() { + this.hide(); + return this.destroy(); + } + + toggle() { + if (this.panel && this.panel.isVisible()) { + this.cancel(); + } else { + this.show(); + } + } + + show() { + if (this.panel == null) { this.panel = atom.workspace.addModalPanel({item: this}); } + this.panel.show(); + + this.storeFocusedElement(); + + this.focusFilterEditor(); + } + + hide() { + this.panel && this.panel.hide(); + } + + getFilterKey() { + return "name"; + } + + viewForItem(localFile) { + return $$(function() { + return this.li({class: 'two-lines'}, () => { + this.div({class: 'primary-line icon globe'}, `${localFile.host.protocol}://${localFile.host.username}@${localFile.host.hostname}:${localFile.host.port}${localFile.remoteFile.path}`); + //mtime = moment(fs.statSync(localFile.path).mtime.getTime()).format("HH:mm:ss DD/MM/YY") + const mtime = moment(fs.stat(localFile.path, stat => stat && stat.mtime && stat.mtime.getTime())).format("HH:mm:ss DD/MM/YY"); + return this.div({class: 'secondary-line no-icon text-subtle'}, `Downloaded: ${localFile.dtime}, Mtime: ${mtime}`); + }); + }); + } + + confirmed(localFile) { + const uri = `remote-edit://localFile/?localFile=${encodeURIComponent(JSON.stringify(localFile.serialize()))}&host=${encodeURIComponent(JSON.stringify(localFile.host.serialize()))}`; + atom.workspace.open(uri, {split: 'left'}); + return this.cancel(); + } + + listenForEvents() { + return this.disposables.add(atom.commands.add('atom-workspace', 'openfilesview:delete', () => { + const item = this.getSelectedItem(); + if (item != null) { + this.items = _.reject(this.items, (val => val === item)); + item.delete(); + return this.setLoading(); + } + }) + ); + } + + createItemsFromIpdw() { + return this.ipdw.getData().then(data => { + const localFiles = []; + async.each(data.hostList, ((host, callback) => + async.each(host.localFiles, (function(file, callback) { + file.host = host; + return localFiles.push(file); + }), (function(err) { if (err != null) { return console.error(err); } })) + ), (function(err) { if (err != null) { return console.error(err); } })); + return this.setItems(localFiles); + }); + } + }); diff --git a/package.json b/package.json index dbf1ffa..2e075e0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "remote-edit", - "main": "./lib/main.coffee", - "version": "1.9.0", + "main": "./lib/main.js", + "version": "1.9.1", "description": "Browse and edit remote files using SFTP and FTP", "homepage": "https://github.com/sveale/remote-edit", "repository": { @@ -37,11 +37,14 @@ "temp": ">=0.8.3" }, "optionalDependencies": { - "keytar": "~3.0.0" + "keytar": "~4.1.0" }, "devDependencies": { "temp": "*" }, + "deserializers": { + "RemoteEditEditor": "deserializeRemoteEditor" + }, "keywords": [ "atom", "package",