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.

299 lines (248 loc) 11.6 kB
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 _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })(); exports.applyUpdateToEditor = applyUpdateToEditor; function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _assert2; function _assert() { return _assert2 = _interopRequireDefault(require('assert')); } var _reactForAtom2; function _reactForAtom() { return _reactForAtom2 = require('react-for-atom'); } var _commonsAtomGoToLocation2; function _commonsAtomGoToLocation() { return _commonsAtomGoToLocation2 = require('../../commons-atom/go-to-location'); } var _nuclideAnalytics2; function _nuclideAnalytics() { return _nuclideAnalytics2 = require('../../nuclide-analytics'); } var _DiagnosticsPopup2; function _DiagnosticsPopup() { return _DiagnosticsPopup2 = require('./DiagnosticsPopup'); } var GUTTER_ID = 'nuclide-diagnostics-gutter'; // Needs to be the same as glyph-height in gutter.atom-text-editor.less. var GLYPH_HEIGHT = 15; // px var POPUP_DISPOSE_TIMEOUT = 100; // TODO(mbolin): Make it so that when mousing over an element with this CSS class (or specifically, // the child element with the "region" CSS class), we also do a showPopupFor(). This seems to be // tricky given how the DOM of a TextEditor works today. There are div.tile elements, each of which // has its own div.highlights element and many div.line elements. The div.highlights element has 0 // or more children, each child being a div.highlight with a child div.region. The div.region // element is defined to be {position: absolute; pointer-events: none; z-index: -1}. The absolute // positioning and negative z-index make it so it isn't eligible for mouseover events, so we // might have to listen for mouseover events on TextEditor and then use its own APIs, such as // decorationsForScreenRowRange(), to see if there is a hit target instead. Since this will be // happening onmousemove, we also have to be careful to make sure this is not expensive. var HIGHLIGHT_CSS = 'nuclide-diagnostics-gutter-ui-highlight'; var ERROR_HIGHLIGHT_CSS = 'nuclide-diagnostics-gutter-ui-highlight-error'; var WARNING_HIGHLIGHT_CSS = 'nuclide-diagnostics-gutter-ui-highlight-warning'; var ERROR_GUTTER_CSS = 'nuclide-diagnostics-gutter-ui-gutter-error'; var WARNING_GUTTER_CSS = 'nuclide-diagnostics-gutter-ui-gutter-warning'; var editorToMarkers = new WeakMap(); var itemToEditor = new WeakMap(); function applyUpdateToEditor(editor, update, fixer) { var gutter = editor.gutterWithName(GUTTER_ID); if (!gutter) { // TODO(jessicalin): Determine an appropriate priority so that the gutter: // (1) Shows up to the right of the line numbers. // (2) Shows the items that are added to it right away. // Using a value of 10 fixes (1), but breaks (2). This seems like it is likely a bug in Atom. // By default, a gutter will be destroyed when its editor is destroyed, // so there is no need to register a callback via onDidDestroy(). gutter = editor.addGutter({ name: GUTTER_ID, visible: false }); } var marker = undefined; var markers = editorToMarkers.get(editor); // TODO: Consider a more efficient strategy that does not blindly destroy all of the // existing markers. if (markers) { for (marker of markers) { marker.destroy(); } markers.clear(); } else { markers = new Set(); } var rowToMessage = new Map(); function addMessageForRow(message, row) { var messages = rowToMessage.get(row); if (!messages) { messages = []; rowToMessage.set(row, messages); } messages.push(message); } for (var _message of update.messages) { var range = _message.range; var highlightMarker = undefined; if (range) { addMessageForRow(_message, range.start.row); highlightMarker = editor.markBufferRange(range); } else { addMessageForRow(_message, 0); } var highlightCssClass = undefined; if (_message.type === 'Error') { highlightCssClass = HIGHLIGHT_CSS + ' ' + ERROR_HIGHLIGHT_CSS; } else { highlightCssClass = HIGHLIGHT_CSS + ' ' + WARNING_HIGHLIGHT_CSS; } // This marker underlines text. if (highlightMarker) { editor.decorateMarker(highlightMarker, { type: 'highlight', 'class': highlightCssClass }); markers.add(highlightMarker); } } // Find all of the gutter markers for the same row and combine them into one marker/popup. for (var _ref3 of rowToMessage.entries()) { var _ref2 = _slicedToArray(_ref3, 2); var row = _ref2[0]; var messages = _ref2[1]; // If at least one of the diagnostics is an error rather than the warning, // display the glyph in the gutter to represent an error rather than a warning. var gutterMarkerCssClass = messages.some(function (msg) { return msg.type === 'Error'; }) ? ERROR_GUTTER_CSS : WARNING_GUTTER_CSS; // This marker adds some UI to the gutter. var _createGutterItem = createGutterItem(messages, gutterMarkerCssClass, fixer); var item = _createGutterItem.item; var dispose = _createGutterItem.dispose; itemToEditor.set(item, editor); var gutterMarker = editor.markBufferPosition([row, 0]); gutter.decorateMarker(gutterMarker, { item: item }); gutterMarker.onDidDestroy(dispose); markers.add(gutterMarker); } editorToMarkers.set(editor, markers); // Once the gutter is shown for the first time, it is displayed for the lifetime of the // TextEditor. if (update.messages.length > 0) { gutter.show(); } } function createGutterItem(messages, gutterMarkerCssClass, fixer) { var item = window.document.createElement('span'); item.innerText = ''; // The triangle-right icon in the octicon font. item.className = gutterMarkerCssClass; var popupElement = null; var paneItemSubscription = null; var disposeTimeout = null; var clearDisposeTimeout = function clearDisposeTimeout() { if (disposeTimeout) { clearTimeout(disposeTimeout); } }; var dispose = function dispose() { if (popupElement) { (_reactForAtom2 || _reactForAtom()).ReactDOM.unmountComponentAtNode(popupElement); (0, (_assert2 || _assert()).default)(popupElement.parentNode != null); popupElement.parentNode.removeChild(popupElement); popupElement = null; } if (paneItemSubscription) { paneItemSubscription.dispose(); paneItemSubscription = null; } clearDisposeTimeout(); }; var goToLocation = function goToLocation(path, line) { // Before we jump to the location, we want to close the popup. dispose(); var column = 0; (0, (_commonsAtomGoToLocation2 || _commonsAtomGoToLocation()).goToLocation)(path, line, column); }; item.addEventListener('mouseenter', function (event) { // If there was somehow another popup for this gutter item, dispose it. This can happen if the // user manages to scroll and escape disposal. dispose(); popupElement = showPopupFor(messages, item, goToLocation, fixer); popupElement.addEventListener('mouseleave', dispose); popupElement.addEventListener('mouseenter', clearDisposeTimeout); // This makes sure that the popup disappears when you ctrl+tab to switch tabs. paneItemSubscription = atom.workspace.onDidChangeActivePaneItem(dispose); }); item.addEventListener('mouseleave', function (event) { // When the popup is shown, we want to dispose it if the user manages to move the cursor off of // the gutter glyph without moving it onto the popup. Even though the popup appears above (as in // Z-index above) the gutter glyph, if you move the cursor such that it is only above the glyph // for one frame you can cause the popup to appear without the mouse ever entering it. disposeTimeout = setTimeout(dispose, POPUP_DISPOSE_TIMEOUT); }); return { item: item, dispose: dispose }; } /** * Shows a popup for the diagnostic just below the specified item. */ function showPopupFor(messages, item, goToLocation, fixer) { // The popup will be an absolutely positioned child element of <atom-workspace> so that it appears // on top of everything. var workspaceElement = atom.views.getView(atom.workspace); var hostElement = window.document.createElement('div'); // $FlowFixMe check parentNode for null workspaceElement.parentNode.appendChild(hostElement); // Move it down vertically so it does not end up under the mouse pointer. var _item$getBoundingClientRect = item.getBoundingClientRect(); var top = _item$getBoundingClientRect.top; var left = _item$getBoundingClientRect.left; var trackedFixer = function trackedFixer() { fixer.apply(undefined, arguments); (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('diagnostics-gutter-autofix'); }; var trackedGoToLocation = function trackedGoToLocation(filePath, line) { goToLocation(filePath, line); (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('diagnostics-gutter-goto-location'); }; (_reactForAtom2 || _reactForAtom()).ReactDOM.render((_reactForAtom2 || _reactForAtom()).React.createElement((_DiagnosticsPopup2 || _DiagnosticsPopup()).DiagnosticsPopup, { left: left, top: top, messages: messages, fixer: trackedFixer, goToLocation: trackedGoToLocation }), hostElement); // Check to see whether the popup is within the bounds of the TextEditor. If not, display it above // the glyph rather than below it. var editor = itemToEditor.get(item); var editorElement = atom.views.getView(editor); var _editorElement$getBoundingClientRect = editorElement.getBoundingClientRect(); var editorTop = _editorElement$getBoundingClientRect.top; var editorHeight = _editorElement$getBoundingClientRect.height; var _item$getBoundingClientRect2 = item.getBoundingClientRect(); var itemTop = _item$getBoundingClientRect2.top; var itemHeight = _item$getBoundingClientRect2.height; var popupHeight = hostElement.firstElementChild.clientHeight; if (itemTop + itemHeight + popupHeight > editorTop + editorHeight) { var popupElement = hostElement.firstElementChild; // Shift the popup back down by GLYPH_HEIGHT, so that the bottom padding overlaps with the // glyph. An additional 4 px is needed to make it look the same way it does when it shows up // below. I don't know why. popupElement.style.top = String(itemTop - popupHeight + GLYPH_HEIGHT + 4) + 'px'; } try { return hostElement; } finally { messages.forEach(function (message) { (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('diagnostics-gutter-show-popup', { 'diagnostics-provider': message.providerName, 'diagnostics-message': message.text || message.html || '' }); }); } }