atom-nuclide
Version:
A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.
568 lines (475 loc) • 20.9 kB
JavaScript
Object.defineProperty(exports, '__esModule', {
value: true
});
/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
var createRemoteConnection = _asyncToGenerator(function* (remoteProjectConfig) {
var host = remoteProjectConfig.host;
var cwd = remoteProjectConfig.cwd;
var displayTitle = remoteProjectConfig.displayTitle;
var connection = (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).RemoteConnection.getByHostnameAndPath(host, cwd);
if (connection != null) {
return connection;
}
connection = yield (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).RemoteConnection.createConnectionBySavedConfig(host, cwd, displayTitle);
if (connection != null) {
return connection;
}
// If connection fails using saved config, open connect dialog.
return (0, (_openConnection2 || _openConnection()).openConnectionDialog)({
initialServer: remoteProjectConfig.host,
initialCwd: remoteProjectConfig.cwd
});
});
/**
* The same TextEditor must be returned to prevent Atom from creating multiple tabs
* for the same file, because Atom doesn't cache pending opener promises.
*/
var createEditorForNuclide = _asyncToGenerator(function* (uri) {
try {
var buffer = undefined;
try {
buffer = yield (0, (_commonsAtomTextEditor2 || _commonsAtomTextEditor()).loadBufferForUri)(uri);
} catch (err) {
// Suppress ENOENT errors which occur if the file doesn't exist.
// This is the same thing Atom does when opening a file (given a URI) that doesn't exist.
if (err.code !== 'ENOENT') {
throw err;
}
// If `loadBufferForURI` fails, then the buffer is removed from Atom's list of buffers.
// `buffer.file` is marked as destroyed, making it useless. So we create
// a new `buffer` and call `finishLoading` so that the `buffer` is marked
// as `loaded` and the proper events are fired. The effect of all of this
// is that files that don't exist remotely anymore are shown as empty
// unsaved text editors.
buffer = (0, (_commonsAtomTextEditor2 || _commonsAtomTextEditor()).bufferForUri)(uri);
buffer.finishLoading();
}
// When in "large file mode", syntax highlighting and line wrapping are
// disabled (among other things). This makes large files more usable.
// Atom does this for local files.
// https://github.com/atom/atom/blob/v1.9.8/src/workspace.coffee#L547
var largeFileMode = buffer.getText().length > 2 * 1024 * 1024; // 2MB
var editor = atom.workspace.buildTextEditor({ buffer: buffer, largeFileMode: largeFileMode });
if (!atom.textEditors.editors.has(editor)) {
(function () {
// https://github.com/atom/atom/blob/v1.9.8/src/workspace.coffee#L559-L562
var disposable = atom.textEditors.add(editor);
editor.onDidDestroy(function () {
disposable.dispose();
});
})();
}
return editor;
} catch (err) {
logger.warn('buffer load issue:', err);
atom.notifications.addError('Failed to open ' + uri + ': ' + err.message);
throw err;
}
}
/**
* Check if the remote buffer has already been initialized in editor.
* This checks if the buffer is instance of NuclideTextBuffer.
*/
);
var reloadRemoteProjects = _asyncToGenerator(function* (remoteProjects) {
var _loop = function* (config) {
// eslint-disable-next-line babel/no-await-in-loop
var connection = yield createRemoteConnection(config);
if (!connection) {
logger.info('No RemoteConnection returned on restore state trial:', config.host, config.cwd);
atom.commands.dispatch(atom.views.getView(atom.workspace), 'nuclide-file-tree:force-refresh-roots');
// Atom restores remote files with a malformed URIs, which somewhat resemble local paths.
// If after an unsuccessful connection user modifies and saves them he's presented
// with a credential requesting dialog, as the file is attempted to be saved into
// /nuclide:/<hostname> folder. If the user will approve the elevation and actually save
// the file all kind of weird stuff happens (see t10842295) since the difference between the
// remote and the valid local path becomes less aparent.
// Anyway - these files better be closed.
atom.workspace.getTextEditors().forEach(function (textEditor) {
if (textEditor == null) {
return;
}
var path = textEditor.getPath();
if (path == null) {
return;
}
if (path.startsWith('nuclide:/' + config.host)) {
textEditor.destroy();
}
});
} else {
// It's fine the user connected to a different project on the same host:
// we should still be able to restore this using the new connection.
var _cwd = config.cwd;
var _host = config.host;
var _displayTitle = config.displayTitle;
if (connection.getPathForInitialWorkingDirectory() !== _cwd && connection.getRemoteHostname() === _host) {
// eslint-disable-next-line babel/no-await-in-loop
yield (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).RemoteConnection.createConnectionBySavedConfig(_host, _cwd, _displayTitle);
}
}
};
// This is intentionally serial.
// The 90% use case is to have multiple remote projects for a single connection;
// after the first one succeeds the rest should require no user action.
for (var config of remoteProjects) {
yield* _loop(config);
}
});
var shutdownServersAndRestartNuclide = _asyncToGenerator(function* () {
atom.confirm({
message: 'This will shutdown your Nuclide servers and restart Atom, ' + 'discarding all unsaved changes. Continue?',
buttons: {
'Shutdown & Restart': _asyncToGenerator(function* () {
try {
yield (0, (_nuclideAnalytics2 || _nuclideAnalytics()).trackImmediate)('nuclide-remote-projects:kill-and-restart');
} finally {
// This directly kills the servers without removing the RemoteConnections
// so that restarting Nuclide preserves the existing workspace state.
yield (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).ServerConnection.forceShutdownAllServers();
atom.reload();
}
}),
'Cancel': function Cancel() {}
}
});
});
exports.activate = activate;
exports.consumeStatusBar = consumeStatusBar;
exports.serialize = serialize;
exports.deactivate = deactivate;
exports.createRemoteDirectoryProvider = createRemoteDirectoryProvider;
exports.createRemoteDirectorySearcher = createRemoteDirectorySearcher;
exports.getHomeFragments = getHomeFragments;
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _commonsAtomTextEditor2;
function _commonsAtomTextEditor() {
return _commonsAtomTextEditor2 = require('../../commons-atom/text-editor');
}
var _nuclideLogging2;
function _nuclideLogging() {
return _nuclideLogging2 = require('../../nuclide-logging');
}
var _utils2;
function _utils() {
return _utils2 = require('./utils');
}
var _commonsAtomFeatureConfig2;
function _commonsAtomFeatureConfig() {
return _commonsAtomFeatureConfig2 = _interopRequireDefault(require('../../commons-atom/featureConfig'));
}
var _assert2;
function _assert() {
return _assert2 = _interopRequireDefault(require('assert'));
}
var _atom2;
function _atom() {
return _atom2 = require('atom');
}
var _nuclideRemoteConnection2;
function _nuclideRemoteConnection() {
return _nuclideRemoteConnection2 = require('../../nuclide-remote-connection');
}
var _nuclideAnalytics2;
function _nuclideAnalytics() {
return _nuclideAnalytics2 = require('../../nuclide-analytics');
}
var _openConnection2;
function _openConnection() {
return _openConnection2 = require('./open-connection');
}
var _commonsNodeNuclideUri2;
function _commonsNodeNuclideUri() {
return _commonsNodeNuclideUri2 = _interopRequireDefault(require('../../commons-node/nuclideUri'));
}
var _RemoteDirectorySearcher2;
function _RemoteDirectorySearcher() {
return _RemoteDirectorySearcher2 = _interopRequireDefault(require('./RemoteDirectorySearcher'));
}
var _RemoteDirectoryProvider2;
function _RemoteDirectoryProvider() {
return _RemoteDirectoryProvider2 = _interopRequireDefault(require('./RemoteDirectoryProvider'));
}
var _RemoteProjectsController2;
function _RemoteProjectsController() {
return _RemoteProjectsController2 = _interopRequireDefault(require('./RemoteProjectsController'));
}
var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)();
/**
* Stores the host and cwd of a remote connection.
*/
var packageSubscriptions = null;
var controller = null;
var CLOSE_PROJECT_DELAY_MS = 100;
var pendingFiles = {};
function createSerializableRemoteConnectionConfiguration(config) {
return {
host: config.host,
cwd: config.cwd,
displayTitle: config.displayTitle
};
}
function addRemoteFolderToProject(connection) {
var workingDirectoryUri = connection.getUriForInitialWorkingDirectory();
// If restoring state, then the project already exists with local directory and wrong repo
// instances. Hence, we remove it here, if existing, and add the new path for which we added a
// workspace opener handler.
atom.project.removePath(workingDirectoryUri);
atom.project.addPath(workingDirectoryUri);
var subscription = atom.project.onDidChangePaths(function () {
// When removing a project in an integration test, skip the cleanup.
if (atom.inSpecMode()) {
return;
}
// Delay closing the underlying socket connection until registered subscriptions have closed.
// We should never depend on the order of registration of the `onDidChangePaths` event,
// which also dispose consumed service's resources.
setTimeout(checkClosedProject, CLOSE_PROJECT_DELAY_MS);
});
function checkClosedProject() {
// The project paths may have changed during the delay time.
// Hence, the latest project paths are fetched here.
var paths = atom.project.getPaths();
if (paths.indexOf(workingDirectoryUri) !== -1) {
return;
}
// The project was removed from the tree.
subscription.dispose();
closeOpenFilesForRemoteProject(connection.getConfig());
var hostname = connection.getRemoteHostname();
var closeConnection = function closeConnection(shutdownIfLast) {
connection.close(shutdownIfLast);
};
if (!connection.isOnlyConnection()) {
logger.info('Remaining remote projects using Nuclide Server - no prompt to shutdown');
var shutdownIfLast = false;
closeConnection(shutdownIfLast);
return;
}
var confirmServerActionOnLastProject = (_commonsAtomFeatureConfig2 || _commonsAtomFeatureConfig()).default.get('nuclide-remote-projects.confirmServerActionOnLastProject');
(0, (_assert2 || _assert()).default)(typeof confirmServerActionOnLastProject === 'boolean');
var shutdownServerAfterDisconnection = (_commonsAtomFeatureConfig2 || _commonsAtomFeatureConfig()).default.get('nuclide-remote-projects.shutdownServerAfterDisconnection');
(0, (_assert2 || _assert()).default)(typeof shutdownServerAfterDisconnection === 'boolean');
if (!confirmServerActionOnLastProject) {
var shutdownIfLast = shutdownServerAfterDisconnection;
closeConnection(shutdownIfLast);
return;
}
var buttons = ['Keep It', 'Shutdown'];
var buttonToActions = new Map();
buttonToActions.set(buttons[0], function () {
return closeConnection( /* shutdownIfLast */false);
});
buttonToActions.set(buttons[1], function () {
return closeConnection( /* shutdownIfLast */true);
});
if (shutdownServerAfterDisconnection) {
// Atom takes the first button in the list as default option.
buttons.reverse();
}
var choice = global.atom.confirm({
message: 'No more remote projects on the host: \'' + hostname + '\'. Would you like to shutdown Nuclide server there?',
buttons: buttons
});
var action = buttonToActions.get(buttons[choice]);
(0, (_assert2 || _assert()).default)(action);
action();
}
}
function closeOpenFilesForRemoteProject(remoteProjectConfig) {
var openInstances = (0, (_utils2 || _utils()).getOpenFileEditorForRemoteProject)(remoteProjectConfig);
for (var openInstance of openInstances) {
var editor = openInstance.editor;
var pane = openInstance.pane;
pane.removeItem(editor);
editor.destroy();
}
}
function getRemoteRootDirectories() {
// TODO: Use nuclideUri instead.
return atom.project.getDirectories().filter(function (directory) {
return directory.getPath().startsWith('nuclide:');
});
}
/**
* Removes any Directory (not RemoteDirectory) objects that have Nuclide
* remote URIs.
*/
function deleteDummyRemoteRootDirectories() {
for (var directory of atom.project.getDirectories()) {
if ((_commonsNodeNuclideUri2 || _commonsNodeNuclideUri()).default.isRemote(directory.getPath()) && !(_nuclideRemoteConnection2 || _nuclideRemoteConnection()).RemoteDirectory.isRemoteDirectory(directory)) {
atom.project.removePath(directory.getPath());
}
}
}function isRemoteBufferInitialized(editor) {
var buffer = editor.getBuffer();
if (buffer && buffer.constructor.name === 'NuclideTextBuffer') {
return true;
}
return false;
}
function activate(state) {
var subscriptions = new (_atom2 || _atom()).CompositeDisposable();
controller = new (_RemoteProjectsController2 || _RemoteProjectsController()).default();
subscriptions.add((_nuclideRemoteConnection2 || _nuclideRemoteConnection()).RemoteConnection.onDidAddRemoteConnection(function (connection) {
addRemoteFolderToProject(connection);
// On Atom restart, it tries to open uri paths as local `TextEditor` pane items.
// Here, Nuclide reloads the remote project files that have empty text editors open.
var config = connection.getConfig();
var openInstances = (0, (_utils2 || _utils()).getOpenFileEditorForRemoteProject)(config);
var _loop2 = function (openInstance) {
// Keep the original open editor item with a unique name until the remote buffer is loaded,
// Then, we are ready to replace it with the remote tab in the same pane.
var pane = openInstance.pane;
var editor = openInstance.editor;
var uri = openInstance.uri;
var filePath = openInstance.filePath;
// Skip restoring the editer who has remote content loaded.
if (isRemoteBufferInitialized(editor)) {
return 'continue';
}
// Atom ensures that each pane only has one item per unique URI.
// Null out the existing pane item's URI so we can insert the new one
// without closing the pane.
/* $FlowFixMe */
editor.getURI = function () {
return null;
};
// Cleanup the old pane item on successful opening or when no connection could be
// established.
var cleanupBuffer = function cleanupBuffer() {
pane.removeItem(editor);
editor.destroy();
};
if (filePath === config.cwd) {
cleanupBuffer();
} else {
// If we clean up the buffer before the `openUriInPane` finishes,
// the pane will be closed, because it could have no other items.
// So we must clean up after.
atom.workspace.openURIInPane(uri, pane).then(cleanupBuffer, cleanupBuffer);
}
};
for (var openInstance of openInstances) {
var _ret3 = _loop2(openInstance);
if (_ret3 === 'continue') continue;
}
}));
subscriptions.add(atom.commands.add('atom-workspace', 'nuclide-remote-projects:connect', function () {
return (0, (_openConnection2 || _openConnection()).openConnectionDialog)();
}));
subscriptions.add(atom.commands.add('atom-workspace', 'nuclide-remote-projects:kill-and-restart', function () {
return shutdownServersAndRestartNuclide();
}));
// Subscribe opener before restoring the remote projects.
subscriptions.add(atom.workspace.addOpener(function () {
var uri = arguments.length <= 0 || arguments[0] === undefined ? '' : arguments[0];
if (uri.startsWith('nuclide:')) {
var serverConnection = (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).ServerConnection.getForUri(uri);
if (serverConnection == null) {
// It's possible that the URI opens before the remote connection has finished loading
// (or the remote connection cannot be restored for some reason).
//
// In this case, we can just let Atom open a blank editor. Once the connection
// is re-established, the `onDidAddRemoteConnection` logic above will restore the
// editor contents as appropriate.
return;
}
var connection = (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).RemoteConnection.getForUri(uri);
// On Atom restart, it tries to open the uri path as a file tab because it's not a local
// directory. We can't let that create a file with the initial working directory path.
if (connection != null && uri === connection.getUriForInitialWorkingDirectory()) {
var _ret4 = (function () {
var blankEditor = atom.workspace.buildTextEditor({});
// No matter what we do here, Atom is going to create a blank editor.
// We don't want the user to see this, so destroy it as soon as possible.
setImmediate(function () {
return blankEditor.destroy();
});
return {
v: blankEditor
};
})();
if (typeof _ret4 === 'object') return _ret4.v;
}
if (pendingFiles[uri]) {
return pendingFiles[uri];
}
var textEditorPromise = pendingFiles[uri] = createEditorForNuclide(uri);
var removeFromCache = function removeFromCache() {
return delete pendingFiles[uri];
};
textEditorPromise.then(removeFromCache, removeFromCache);
return textEditorPromise;
}
}));
// If RemoteDirectoryProvider is called before this, and it failed
// to provide a RemoteDirectory for a
// given URI, Atom will create a generic Directory to wrap that. We want
// to delete these instead, because those directories aren't valid/useful
// if they are not true RemoteDirectory objects (connected to a real
// real remote folder).
deleteDummyRemoteRootDirectories();
// Attempt to reload previously open projects.
var remoteProjectsConfig = state && state.remoteProjectsConfig;
if (remoteProjectsConfig != null) {
reloadRemoteProjects(remoteProjectsConfig);
}
packageSubscriptions = subscriptions;
}
function consumeStatusBar(statusBar) {
if (controller) {
controller.consumeStatusBar(statusBar);
}
}
// TODO: All of the elements of the array are non-null, but it does not seem possible to convince
// Flow of that.
function serialize() {
var remoteProjectsConfig = getRemoteRootDirectories().map(function (directory) {
var connection = (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).RemoteConnection.getForUri(directory.getPath());
return connection ? createSerializableRemoteConnectionConfiguration(connection.getConfig()) : null;
}).filter(function (config) {
return config != null;
});
return {
remoteProjectsConfig: remoteProjectsConfig
};
}
function deactivate() {
if (packageSubscriptions) {
packageSubscriptions.dispose();
packageSubscriptions = null;
}
if (controller != null) {
controller.destroy();
controller = null;
}
}
function createRemoteDirectoryProvider() {
return new (_RemoteDirectoryProvider2 || _RemoteDirectoryProvider()).default();
}
function createRemoteDirectorySearcher() {
return new (_RemoteDirectorySearcher2 || _RemoteDirectorySearcher()).default(function (dir) {
var service = (0, (_nuclideRemoteConnection2 || _nuclideRemoteConnection()).getServiceByNuclideUri)('GrepService', dir.getPath());
(0, (_assert2 || _assert()).default)(service);
return service;
});
}
function getHomeFragments() {
return {
feature: {
title: 'Remote Connection',
icon: 'cloud-upload',
description: 'Connect to a remote server to edit files.',
command: 'nuclide-remote-projects:connect'
},
priority: 8
};
}