UNPKG

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.

443 lines (388 loc) 16.7 kB
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); /* * 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. */ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _nuclideTextedit2; function _nuclideTextedit() { return _nuclideTextedit2 = _interopRequireDefault(require('../../nuclide-textedit')); } var _commonsNodeCollection2; function _commonsNodeCollection() { return _commonsNodeCollection2 = require('../../commons-node/collection'); } var _commonsNodeStream2; function _commonsNodeStream() { return _commonsNodeStream2 = require('../../commons-node/stream'); } var _MarkerTracker2; function _MarkerTracker() { return _MarkerTracker2 = require('./MarkerTracker'); } var _assert2; function _assert() { return _assert2 = _interopRequireDefault(require('assert')); } var _rxjsBundlesRxUmdMinJs2; function _rxjsBundlesRxUmdMinJs() { return _rxjsBundlesRxUmdMinJs2 = require('rxjs/bundles/Rx.umd.min.js'); } var DiagnosticStore = (function () { function DiagnosticStore() { _classCallCheck(this, DiagnosticStore); this._providerToFileToMessages = new Map(); this._fileToProviders = new (_commonsNodeCollection2 || _commonsNodeCollection()).MultiMap(); this._providerToProjectDiagnostics = new Map(); this._fileChanges = new (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).Subject(); this._projectChanges = new (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).BehaviorSubject([]); this._allChanges = new (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).BehaviorSubject([]); this._markerTracker = new (_MarkerTracker2 || _MarkerTracker()).MarkerTracker(); } _createClass(DiagnosticStore, [{ key: 'dispose', value: function dispose() { this._providerToFileToMessages.clear(); this._fileToProviders.clear(); this._providerToProjectDiagnostics.clear(); this._fileChanges.complete(); this._projectChanges.complete(); this._allChanges.complete(); } /** * Section: Methods to modify the store. */ /** * Update the messages from the given provider. * If the update contains messages at a scope that already has messages from * this provider in the store, the existing messages will be overwritten by the * new messages. * @param diagnosticProvider The diagnostic provider that these messages come from. * @param updates Set of updates to apply. */ }, { key: 'updateMessages', value: function updateMessages(diagnosticProvider, updates) { if (updates.filePathToMessages) { this._updateFileMessages(diagnosticProvider, updates.filePathToMessages); } if (updates.projectMessages) { this._updateProjectMessages(diagnosticProvider, updates.projectMessages); } if (updates.filePathToMessages || updates.projectMessages) { this._emitAllMessages(); } } }, { key: '_updateFileMessages', value: function _updateFileMessages(diagnosticProvider, newFilePathsToMessages) { var _this = this; var fileToMessages = this._providerToFileToMessages.get(diagnosticProvider); if (!fileToMessages) { fileToMessages = new Map(); this._providerToFileToMessages.set(diagnosticProvider, fileToMessages); } newFilePathsToMessages.forEach(function (newMessagesForPath, filePath) { // Flow naively thinks that since we are in a closure, fileToMessages could have been // reassigned to something null by the time this executes. (0, (_assert2 || _assert()).default)(fileToMessages != null); var messagesToRemove = fileToMessages.get(filePath); if (messagesToRemove != null) { _this._markerTracker.removeFileMessages(messagesToRemove); } _this._markerTracker.addFileMessages(newMessagesForPath); // Update _providerToFileToMessages. fileToMessages.set(filePath, newMessagesForPath); // Update _fileToProviders. _this._fileToProviders.add(filePath, diagnosticProvider); _this._emitFileMessages(filePath); }); } }, { key: '_updateProjectMessages', value: function _updateProjectMessages(diagnosticProvider, projectMessages) { this._providerToProjectDiagnostics.set(diagnosticProvider, projectMessages); this._emitProjectMessages(); } /** * Clear the messages from the given provider, according to the options. * @param options An Object of the form: * * scope: Can be 'file', 'project', or 'all'. * * 'file': The 'filePaths' option determines which files' messages to clear * * 'project': all 'project' scope messages are cleared. * * 'all': all messages are cleared. * * filePaths: Array of absolute file paths (NuclideUri) to clear messages for. */ }, { key: 'invalidateMessages', value: function invalidateMessages(diagnosticProvider, invalidationMessage) { if (invalidationMessage.scope === 'file') { this._invalidateFileMessagesForProvider(diagnosticProvider, invalidationMessage.filePaths); this._emitAllMessages(); } else if (invalidationMessage.scope === 'project') { this._invalidateProjectMessagesForProvider(diagnosticProvider); this._emitAllMessages(); } else if (invalidationMessage.scope === 'all') { this._invalidateAllMessagesForProvider(diagnosticProvider); } } }, { key: '_invalidateFileMessagesForProvider', value: function _invalidateFileMessagesForProvider(diagnosticProvider, pathsToRemove) { var fileToDiagnostics = this._providerToFileToMessages.get(diagnosticProvider); for (var filePath of pathsToRemove) { // Update _providerToFileToMessages. if (fileToDiagnostics) { var diagnosticsToRemove = fileToDiagnostics.get(filePath); if (diagnosticsToRemove != null) { this._markerTracker.removeFileMessages(diagnosticsToRemove); } fileToDiagnostics.delete(filePath); } // Update _fileToProviders. this._fileToProviders.delete(filePath, diagnosticProvider); this._emitFileMessages(filePath); } } }, { key: '_invalidateProjectMessagesForProvider', value: function _invalidateProjectMessagesForProvider(diagnosticProvider) { this._providerToProjectDiagnostics.delete(diagnosticProvider); this._emitProjectMessages(); } }, { key: '_invalidateAllMessagesForProvider', value: function _invalidateAllMessagesForProvider(diagnosticProvider) { // Invalidate all file messages. var filesToDiagnostics = this._providerToFileToMessages.get(diagnosticProvider); if (filesToDiagnostics) { var allFilePaths = filesToDiagnostics.keys(); this._invalidateFileMessagesForProvider(diagnosticProvider, allFilePaths); } // Invalidate all project messages. this._invalidateProjectMessagesForProvider(diagnosticProvider); this._emitAllMessages(); } }, { key: '_invalidateSingleMessage', value: function _invalidateSingleMessage(message) { this._markerTracker.removeFileMessages([message]); for (var fileToMessages of this._providerToFileToMessages.values()) { var fileMessages = fileToMessages.get(message.filePath); if (fileMessages != null) { (0, (_commonsNodeCollection2 || _commonsNodeCollection()).arrayRemove)(fileMessages, message); } } // Looks like emitAllMessages does not actually emit all messages. We need to do both for both // the gutter UI and the diagnostics table to get updated. this._emitFileMessages(message.filePath); this._emitAllMessages(); } /** * Section: Methods to read from the store. */ }, { key: 'getFileMessageUpdates', value: function getFileMessageUpdates(filePath) { var fileMessages = this._getFileMessages(filePath); var initialObservable = (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).Observable.of({ filePath: filePath, messages: fileMessages }); return (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).Observable.concat(initialObservable, this._fileChanges.filter(function (change) { return change.filePath === filePath; })); } }, { key: 'getProjectMessageUpdates', value: function getProjectMessageUpdates() { return this._projectChanges.asObservable(); } }, { key: 'getAllMessageUpdates', value: function getAllMessageUpdates() { return this._allChanges.asObservable(); } /** * Call the callback when the filePath's messages have changed. * In addition, the Store will immediately invoke the callback with the data * currently in the Store, iff there is any. * @param callback The function to message when any of the filePaths' messages * change. The array of messages is meant to completely replace any previous * messages for this file path. */ }, { key: 'onFileMessagesDidUpdate', value: function onFileMessagesDidUpdate(callback, filePath) { return new (_commonsNodeStream2 || _commonsNodeStream()).DisposableSubscription(this.getFileMessageUpdates(filePath).subscribe(callback)); } /** * Call the callback when project-scope messages change. * In addition, the Store will immediately invoke the callback with the data * currently in the Store, iff there is any. * @param callback The function to message when the project-scope messages * change. The array of messages is meant to completely replace any previous * project-scope messages. */ }, { key: 'onProjectMessagesDidUpdate', value: function onProjectMessagesDidUpdate(callback) { return new (_commonsNodeStream2 || _commonsNodeStream()).DisposableSubscription(this.getProjectMessageUpdates().subscribe(callback)); } /** * Call the callback when any messages change. * In addition, the Store will immediately invoke the callback with data * currently in the Store, iff there is any. * @param callback The function to message when any messages change. The array * of messages is meant to completely replace any previous messages. */ }, { key: 'onAllMessagesDidUpdate', value: function onAllMessagesDidUpdate(callback) { return new (_commonsNodeStream2 || _commonsNodeStream()).DisposableSubscription(this.getAllMessageUpdates().subscribe(callback)); } /** * Gets the current diagnostic messages for the file. * Prefer to get updates via ::onFileMessagesDidUpdate. */ }, { key: '_getFileMessages', value: function _getFileMessages(filePath) { var allFileMessages = []; var relevantProviders = this._fileToProviders.get(filePath); for (var provider of relevantProviders) { var fileToMessages = this._providerToFileToMessages.get(provider); (0, (_assert2 || _assert()).default)(fileToMessages != null); var _messages = fileToMessages.get(filePath); (0, (_assert2 || _assert()).default)(_messages != null); allFileMessages = allFileMessages.concat(_messages); } return allFileMessages; } /** * Gets the current project-scope diagnostic messages. * Prefer to get updates via ::onProjectMessagesDidUpdate. */ }, { key: '_getProjectMessages', value: function _getProjectMessages() { var allProjectMessages = []; for (var _messages2 of this._providerToProjectDiagnostics.values()) { allProjectMessages = allProjectMessages.concat(_messages2); } return allProjectMessages; } /** * Gets all current diagnostic messages. * Prefer to get updates via ::onAllMessagesDidUpdate. */ }, { key: '_getAllMessages', value: function _getAllMessages() { var allMessages = []; // Get all file messages. for (var fileToMessages of this._providerToFileToMessages.values()) { for (var _messages3 of fileToMessages.values()) { allMessages = allMessages.concat(_messages3); } } // Get all project messages. allMessages = allMessages.concat(this._getProjectMessages()); return allMessages; } /** * Section: Feedback from the UI */ }, { key: 'applyFix', value: function applyFix(message) { this._applyFixes(message.filePath, message); } }, { key: 'applyFixesForFile', value: function applyFixesForFile(file) { this._applyFixes.apply(this, [file].concat(_toConsumableArray(this._getFileMessages(file)))); } // Precondition: all messages have the given filePath }, { key: '_applyFixes', value: function _applyFixes(filePath) { for (var _len = arguments.length, messages = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { messages[_key - 1] = arguments[_key]; } var messagesWithFixes = messages.filter(function (msg) { return msg.fix != null; }); var fixes = []; for (var message of messagesWithFixes) { var fix = this._getUpdatedFix(message); if (fix == null) { notifyFixFailed(); return; } fixes.push(fix); } var succeeded = (0, (_nuclideTextedit2 || _nuclideTextedit()).default).apply(undefined, [filePath].concat(fixes)); if (succeeded) { for (var message of messagesWithFixes) { this._invalidateSingleMessage(message); } } else { notifyFixFailed(); } } /** * Return the fix for the given message with the updated range based on the stored markers. If the * range cannot be found (in particular, if edits have rendered it invalid), return null. * * Precondition: message.fix != null */ }, { key: '_getUpdatedFix', value: function _getUpdatedFix(message) { var fix = message.fix; (0, (_assert2 || _assert()).default)(fix != null); var actualRange = this._markerTracker.getCurrentRange(message); if (actualRange == null) { return null; } return _extends({}, fix, { oldRange: actualRange }); } /** * Section: Event Emitting */ }, { key: '_emitFileMessages', value: function _emitFileMessages(filePath) { this._fileChanges.next({ filePath: filePath, messages: this._getFileMessages(filePath) }); } }, { key: '_emitProjectMessages', value: function _emitProjectMessages() { this._projectChanges.next(this._getProjectMessages()); } }, { key: '_emitAllMessages', value: function _emitAllMessages() { this._allChanges.next(this._getAllMessages()); } }]); return DiagnosticStore; })(); function notifyFixFailed() { atom.notifications.addWarning('Failed to apply fix. Try saving to get fresh results and then try again.'); } module.exports = DiagnosticStore; // A map from each diagnostic provider to: // a map from each file it has messages for to the array of messages for that file. // A map from each file that has messages from any diagnostic provider // to the set of diagnostic providers that have messages for it. // A map from each diagnostic provider to the array of project messages from it.