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
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 _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 || ''
});
});
}
}