/
githubmirror
/
atom
Обзор
Документация
Войти
/
githubmirror
/
atom
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/text-editor-component.js
5 211 строк
157 KB
sadick254
Use custom element on text editor element
18 авг 2021, 12:55
Не верифицирован
18 авг 2021, 12:55
510d04b
Код
Авторство
О чём код?
/* global ResizeObserver */ const etch = require('etch'); const { Point, Range } = require('text-buffer'); const LineTopIndex = require('line-top-index'); const TextEditor = require('./text-editor'); const { isPairedCharacter } = require('./text-utils'); const electron = require('electron'); const clipboard = electron.clipboard; const $ = etch.dom; let TextEditorElement; const DEFAULT_ROWS_PER_TILE = 6; const NORMAL_WIDTH_CHARACTER = 'x'; const DOUBLE_WIDTH_CHARACTER = '我'; const HALF_WIDTH_CHARACTER = 'ハ'; const KOREAN_CHARACTER = '세'; const NBSP_CHARACTER = '\u00a0'; const ZERO_WIDTH_NBSP_CHARACTER = '\ufeff'; const MOUSE_DRAG_AUTOSCROLL_MARGIN = 40; const CURSOR_BLINK_RESUME_DELAY = 300; const CURSOR_BLINK_PERIOD = 800; function scaleMouseDragAutoscrollDelta(delta) { return Math.pow(delta / 3, 3) / 280; } module.exports = class TextEditorComponent { static setScheduler(scheduler) { etch.setScheduler(scheduler); } static getScheduler() { return etch.getScheduler(); } static didUpdateStyles() { if (this.attachedComponents) { this.attachedComponents.forEach(component => { component.didUpdateStyles(); }); } } static didUpdateScrollbarStyles() { if (this.attachedComponents) { this.attachedComponents.forEach(component => { component.didUpdateScrollbarStyles(); }); } } constructor(props) { this.props = props; if (!props.model) { props.model = new TextEditor({ mini: props.mini, readOnly: props.readOnly }); } this.props.model.component = this; if (props.element) { this.element = props.element; } else { if (!TextEditorElement) TextEditorElement = require('./text-editor-element'); this.element = TextEditorElement.createTextEditorElement(); } this.element.initialize(this); this.virtualNode = $('atom-text-editor'); this.virtualNode.domNode = this.element; this.refs = {}; this.updateSync = this.updateSync.bind(this); this.didBlurHiddenInput = this.didBlurHiddenInput.bind(this); this.didFocusHiddenInput = this.didFocusHiddenInput.bind(this); this.didPaste = this.didPaste.bind(this); this.didTextInput = this.didTextInput.bind(this); this.didKeydown = this.didKeydown.bind(this); this.didKeyup = this.didKeyup.bind(this); this.didKeypress = this.didKeypress.bind(this); this.didCompositionStart = this.didCompositionStart.bind(this); this.didCompositionUpdate = this.didCompositionUpdate.bind(this); this.didCompositionEnd = this.didCompositionEnd.bind(this); this.updatedSynchronously = this.props.updatedSynchronously; this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this); this.didMouseDownOnContent = this.didMouseDownOnContent.bind(this); this.debouncedResumeCursorBlinking = debounce( this.resumeCursorBlinking.bind(this), this.props.cursorBlinkResumeDelay || CURSOR_BLINK_RESUME_DELAY ); this.lineTopIndex = new LineTopIndex(); this.lineNodesPool = new NodePool(); this.updateScheduled = false; this.suppressUpdates = false; this.hasInitialMeasurements = false; this.measurements = { lineHeight: 0, baseCharacterWidth: 0, doubleWidthCharacterWidth: 0, halfWidthCharacterWidth: 0, koreanCharacterWidth: 0, gutterContainerWidth: 0, lineNumberGutterWidth: 0, clientContainerHeight: 0, clientContainerWidth: 0, verticalScrollbarWidth: 0, horizontalScrollbarHeight: 0, longestLineWidth: 0 }; this.derivedDimensionsCache = {}; this.visible = false; this.cursorsBlinking = false; this.cursorsBlinkedOff = false; this.nextUpdateOnlyBlinksCursors = null; this.linesToMeasure = new Map(); this.extraRenderedScreenLines = new Map(); this.horizontalPositionsToMeasure = new Map(); // Keys are rows with positions we want to measure, values are arrays of columns to measure this.horizontalPixelPositionsByScreenLineId = new Map(); // Values are maps from column to horizontal pixel positions this.blockDecorationsToMeasure = new Set(); this.blockDecorationsByElement = new WeakMap(); this.blockDecorationSentinel = document.createElement('div'); this.blockDecorationSentinel.style.height = '1px'; this.heightsByBlockDecoration = new WeakMap(); this.blockDecorationResizeObserver = new ResizeObserver( this.didResizeBlockDecorations.bind(this) ); this.lineComponentsByScreenLineId = new Map(); this.overlayComponents = new Set(); this.shouldRenderDummyScrollbars = true; this.remeasureScrollbars = false; this.pendingAutoscroll = null; this.scrollTopPending = false; this.scrollLeftPending = false; this.scrollTop = 0; this.scrollLeft = 0; this.previousScrollWidth = 0; this.previousScrollHeight = 0; this.lastKeydown = null; this.lastKeydownBeforeKeypress = null; this.accentedCharacterMenuIsOpen = false; this.remeasureGutterDimensions = false; this.guttersToRender = [this.props.model.getLineNumberGutter()]; this.guttersVisibility = [this.guttersToRender[0].visible]; this.idsByTileStartRow = new Map(); this.nextTileId = 0; this.renderedTileStartRows = []; this.showLineNumbers = this.props.model.doesShowLineNumbers(); this.lineNumbersToRender = { maxDigits: 2, bufferRows: [], screenRows: [], keys: [], softWrappedFlags: [], foldableFlags: [] }; this.decorationsToRender = { lineNumbers: new Map(), lines: null, highlights: [], cursors: [], overlays: [], customGutter: new Map(), blocks: new Map(), text: [] }; this.decorationsToMeasure = { highlights: [], cursors: new Map() }; this.textDecorationsByMarker = new Map(); this.textDecorationBoundaries = []; this.pendingScrollTopRow = this.props.initialScrollTopRow; this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn; this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1; this.measuredContent = false; this.queryGuttersToRender(); this.queryMaxLineNumberDigits(); this.observeBlockDecorations(); this.updateClassList(); etch.updateSync(this); } update(props) { if (props.model !== this.props.model) { this.props.model.component = null; props.model.component = this; } this.props = props; this.scheduleUpdate(); } pixelPositionForScreenPosition({ row, column }) { const top = this.pixelPositionAfterBlocksForRow(row); let left = column === 0 ? 0 : this.pixelLeftForRowAndColumn(row, column); if (left == null) { this.requestHorizontalMeasurement(row, column); this.updateSync(); left = this.pixelLeftForRowAndColumn(row, column); } return { top, left }; } scheduleUpdate(nextUpdateOnlyBlinksCursors = false) { if (!this.visible) return; if (this.suppressUpdates) return; this.nextUpdateOnlyBlinksCursors = this.nextUpdateOnlyBlinksCursors !== false && nextUpdateOnlyBlinksCursors === true; if (this.updatedSynchronously) { this.updateSync(); } else if (!this.updateScheduled) { this.updateScheduled = true; etch.getScheduler().updateDocument(() => { if (this.updateScheduled) this.updateSync(true); }); } } updateSync(useScheduler = false) { // Don't proceed if we know we are not visible if (!this.visible) { this.updateScheduled = false; return; } // Don't proceed if we have to pay for a measurement anyway and detect // that we are no longer visible. if ( (this.remeasureCharacterDimensions || this.remeasureAllBlockDecorations) && !this.isVisible() ) { if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise(); this.updateScheduled = false; return; } const onlyBlinkingCursors = this.nextUpdateOnlyBlinksCursors; this.nextUpdateOnlyBlinksCursors = null; if (useScheduler && onlyBlinkingCursors) { this.refs.cursorsAndInput.updateCursorBlinkSync(this.cursorsBlinkedOff); if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise(); this.updateScheduled = false; return; } if (this.remeasureCharacterDimensions) { const originalLineHeight = this.getLineHeight(); const originalBaseCharacterWidth = this.getBaseCharacterWidth(); const scrollTopRow = this.getScrollTopRow(); const scrollLeftColumn = this.getScrollLeftColumn(); this.measureCharacterDimensions(); this.measureGutterDimensions(); this.queryLongestLine(); if (this.getLineHeight() !== originalLineHeight) { this.setScrollTopRow(scrollTopRow); } if (this.getBaseCharacterWidth() !== originalBaseCharacterWidth) { this.setScrollLeftColumn(scrollLeftColumn); } this.remeasureCharacterDimensions = false; } this.measureBlockDecorations(); this.updateSyncBeforeMeasuringContent(); if (useScheduler === true) { const scheduler = etch.getScheduler(); scheduler.readDocument(() => { const restartFrame = this.measureContentDuringUpdateSync(); scheduler.updateDocument(() => { if (restartFrame) { this.updateSync(true); } else { this.updateSyncAfterMeasuringContent(); } }); }); } else { const restartFrame = this.measureContentDuringUpdateSync(); if (restartFrame) { this.updateSync(false); } else { this.updateSyncAfterMeasuringContent(); } } this.updateScheduled = false; } measureBlockDecorations() { if (this.remeasureAllBlockDecorations) { this.remeasureAllBlockDecorations = false; const decorations = this.props.model.getDecorations(); for (let i = 0; i < decorations.length; i++) { const decoration = decorations[i]; const marker = decoration.getMarker(); if (marker.isValid() && decoration.getProperties().type === 'block') { this.blockDecorationsToMeasure.add(decoration); } } // Update the width of the line tiles to ensure block decorations are // measured with the most recent width. if (this.blockDecorationsToMeasure.size > 0) { this.updateSyncBeforeMeasuringContent(); } } if (this.blockDecorationsToMeasure.size > 0) { const { blockDecorationMeasurementArea } = this.refs; const sentinelElements = new Set(); blockDecorationMeasurementArea.appendChild(document.createElement('div')); this.blockDecorationsToMeasure.forEach(decoration => { const { item } = decoration.getProperties(); const decorationElement = TextEditor.viewForItem(item); if (document.contains(decorationElement)) { const parentElement = decorationElement.parentElement; if (!decorationElement.previousSibling) { const sentinelElement = this.blockDecorationSentinel.cloneNode(); parentElement.insertBefore(sentinelElement, decorationElement); sentinelElements.add(sentinelElement); } if (!decorationElement.nextSibling) { const sentinelElement = this.blockDecorationSentinel.cloneNode(); parentElement.appendChild(sentinelElement); sentinelElements.add(sentinelElement); } this.didMeasureVisibleBlockDecoration = true; } else { blockDecorationMeasurementArea.appendChild( this.blockDecorationSentinel.cloneNode() ); blockDecorationMeasurementArea.appendChild(decorationElement); blockDecorationMeasurementArea.appendChild( this.blockDecorationSentinel.cloneNode() ); } }); if (this.resizeBlockDecorationMeasurementsArea) { this.resizeBlockDecorationMeasurementsArea = false; this.refs.blockDecorationMeasurementArea.style.width = this.getScrollWidth() + 'px'; } this.blockDecorationsToMeasure.forEach(decoration => { const { item } = decoration.getProperties(); const decorationElement = TextEditor.viewForItem(item); const { previousSibling, nextSibling } = decorationElement; const height = nextSibling.getBoundingClientRect().top - previousSibling.getBoundingClientRect().bottom; this.heightsByBlockDecoration.set(decoration, height); this.lineTopIndex.resizeBlock(decoration, height); }); sentinelElements.forEach(sentinelElement => sentinelElement.remove()); while (blockDecorationMeasurementArea.firstChild) { blockDecorationMeasurementArea.firstChild.remove(); } this.blockDecorationsToMeasure.clear(); } } updateSyncBeforeMeasuringContent() { this.measuredContent = false; this.derivedDimensionsCache = {}; this.updateModelSoftWrapColumn(); if (this.pendingAutoscroll) { let { screenRange, options } = this.pendingAutoscroll; this.autoscrollVertically(screenRange, options); this.requestHorizontalMeasurement( screenRange.start.row, screenRange.start.column ); this.requestHorizontalMeasurement( screenRange.end.row, screenRange.end.column ); } this.populateVisibleRowRange(this.getRenderedStartRow()); this.populateVisibleTiles(); this.queryScreenLinesToRender(); this.queryLongestLine(); this.queryLineNumbersToRender(); this.queryGuttersToRender(); this.queryDecorationsToRender(); this.queryExtraScreenLinesToRender(); this.shouldRenderDummyScrollbars = !this.remeasureScrollbars; etch.updateSync(this); this.updateClassList(); this.shouldRenderDummyScrollbars = true; this.didMeasureVisibleBlockDecoration = false; } measureContentDuringUpdateSync() { let gutterDimensionsChanged = false; if (this.remeasureGutterDimensions) { gutterDimensionsChanged = this.measureGutterDimensions(); this.remeasureGutterDimensions = false; } const wasHorizontalScrollbarVisible = this.canScrollHorizontally() && this.getHorizontalScrollbarHeight() > 0; this.measureLongestLineWidth(); this.measureHorizontalPositions(); this.updateAbsolutePositionedDecorations(); const isHorizontalScrollbarVisible = this.canScrollHorizontally() && this.getHorizontalScrollbarHeight() > 0; if (this.pendingAutoscroll) { this.derivedDimensionsCache = {}; const { screenRange, options } = this.pendingAutoscroll; this.autoscrollHorizontally(screenRange, options); if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { this.autoscrollVertically(screenRange, options); } this.pendingAutoscroll = null; } this.linesToMeasure.clear(); this.measuredContent = true; return ( gutterDimensionsChanged || wasHorizontalScrollbarVisible !== isHorizontalScrollbarVisible ); } updateSyncAfterMeasuringContent() { this.derivedDimensionsCache = {}; etch.updateSync(this); this.currentFrameLineNumberGutterProps = null; this.scrollTopPending = false; this.scrollLeftPending = false; if (this.remeasureScrollbars) { // Flush stored scroll positions to the vertical and the horizontal // scrollbars. This is because they have just been destroyed and recreated // as a result of their remeasurement, but we could not assign the scroll // top while they were initialized because they were not attached to the // DOM yet. this.refs.verticalScrollbar.flushScrollPosition(); this.refs.horizontalScrollbar.flushScrollPosition(); this.measureScrollbarDimensions(); this.remeasureScrollbars = false; etch.updateSync(this); } this.derivedDimensionsCache = {}; if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise(); } render() { const { model } = this.props; const style = {}; if (!model.getAutoHeight() && !model.getAutoWidth()) { style.contain = 'size'; } let clientContainerHeight = '100%'; let clientContainerWidth = '100%'; if (this.hasInitialMeasurements) { if (model.getAutoHeight()) { clientContainerHeight = this.getContentHeight() + this.getHorizontalScrollbarHeight() + 'px'; } if (model.getAutoWidth()) { style.width = 'min-content'; clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() + this.getVerticalScrollbarWidth() + 'px'; } else { style.width = this.element.style.width; } } let attributes = {}; if (model.isMini()) { attributes.mini = ''; } if (model.isReadOnly()) { attributes.readonly = ''; } const dataset = { encoding: model.getEncoding() }; const grammar = model.getGrammar(); if (grammar && grammar.scopeName) { dataset.grammar = grammar.scopeName.replace(/\./g, ' '); } return $( 'atom-text-editor', { // See this.updateClassList() for construction of the class name style, attributes, dataset, tabIndex: -1, on: { mousewheel: this.didMouseWheel } }, $.div( { ref: 'clientContainer', style: { position: 'relative', contain: 'strict', overflow: 'hidden', backgroundColor: 'inherit', height: clientContainerHeight, width: clientContainerWidth } }, this.renderGutterContainer(), this.renderScrollContainer() ), this.renderOverlayDecorations() ); } renderGutterContainer() { if (this.props.model.isMini()) { return null; } else { return $(GutterContainerComponent, { ref: 'gutterContainer', key: 'gutterContainer', rootComponent: this, hasInitialMeasurements: this.hasInitialMeasurements, measuredContent: this.measuredContent, scrollTop: this.getScrollTop(), scrollHeight: this.getScrollHeight(), lineNumberGutterWidth: this.getLineNumberGutterWidth(), lineHeight: this.getLineHeight(), renderedStartRow: this.getRenderedStartRow(), renderedEndRow: this.getRenderedEndRow(), rowsPerTile: this.getRowsPerTile(), guttersToRender: this.guttersToRender, decorationsToRender: this.decorationsToRender, isLineNumberGutterVisible: this.props.model.isLineNumberGutterVisible(), showLineNumbers: this.showLineNumbers, lineNumbersToRender: this.lineNumbersToRender, didMeasureVisibleBlockDecoration: this.didMeasureVisibleBlockDecoration }); } } renderScrollContainer() { const style = { position: 'absolute', contain: 'strict', overflow: 'hidden', top: 0, bottom: 0, backgroundColor: 'inherit' }; if (this.hasInitialMeasurements) { style.left = this.getGutterContainerWidth() + 'px'; style.width = this.getScrollContainerWidth() + 'px'; } return $.div( { ref: 'scrollContainer', key: 'scrollContainer', className: 'scroll-view', style }, this.renderContent(), this.renderDummyScrollbars() ); } renderContent() { let style = { contain: 'strict', overflow: 'hidden', backgroundColor: 'inherit' }; if (this.hasInitialMeasurements) { style.width = ceilToPhysicalPixelBoundary(this.getScrollWidth()) + 'px'; style.height = ceilToPhysicalPixelBoundary(this.getScrollHeight()) + 'px'; style.willChange = 'transform'; style.transform = `translate(${-roundToPhysicalPixelBoundary( this.getScrollLeft() )}px, ${-roundToPhysicalPixelBoundary(this.getScrollTop())}px)`; } return $.div( { ref: 'content', on: { mousedown: this.didMouseDownOnContent }, style }, this.renderLineTiles(), this.renderBlockDecorationMeasurementArea(), this.renderCharacterMeasurementLine() ); } renderHighlightDecorations() { return $(HighlightsComponent, { hasInitialMeasurements: this.hasInitialMeasurements, highlightDecorations: this.decorationsToRender.highlights.slice(), width: this.getScrollWidth(), height: this.getScrollHeight(), lineHeight: this.getLineHeight() }); } renderLineTiles() { const style = { position: 'absolute', contain: 'strict', overflow: 'hidden' }; const children = []; children.push(this.renderHighlightDecorations()); if (this.hasInitialMeasurements) { const { lineComponentsByScreenLineId } = this; const startRow = this.getRenderedStartRow(); const endRow = this.getRenderedEndRow(); const rowsPerTile = this.getRowsPerTile(); const tileWidth = this.getScrollWidth(); for (let i = 0; i < this.renderedTileStartRows.length; i++) { const tileStartRow = this.renderedTileStartRows[i]; const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile); const tileHeight = this.pixelPositionBeforeBlocksForRow(tileEndRow) - this.pixelPositionBeforeBlocksForRow(tileStartRow); children.push( $(LinesTileComponent, { key: this.idsByTileStartRow.get(tileStartRow), measuredContent: this.measuredContent, height: tileHeight, width: tileWidth, top: this.pixelPositionBeforeBlocksForRow(tileStartRow), lineHeight: this.getLineHeight(), renderedStartRow: startRow, tileStartRow, tileEndRow, screenLines: this.renderedScreenLines.slice( tileStartRow - startRow, tileEndRow - startRow ), lineDecorations: this.decorationsToRender.lines.slice( tileStartRow - startRow, tileEndRow - startRow ), textDecorations: this.decorationsToRender.text.slice( tileStartRow - startRow, tileEndRow - startRow ), blockDecorations: this.decorationsToRender.blocks.get(tileStartRow), displayLayer: this.props.model.displayLayer, nodePool: this.lineNodesPool, lineComponentsByScreenLineId }) ); } this.extraRenderedScreenLines.forEach((screenLine, screenRow) => { if (screenRow < startRow || screenRow >= endRow) { children.push( $(LineComponent, { key: 'extra-' + screenLine.id, offScreen: true, screenLine, screenRow, displayLayer: this.props.model.displayLayer, nodePool: this.lineNodesPool, lineComponentsByScreenLineId }) ); } }); style.width = this.getScrollWidth() + 'px'; style.height = this.getScrollHeight() + 'px'; } children.push(this.renderPlaceholderText()); children.push(this.renderCursorsAndInput()); return $.div( { key: 'lineTiles', ref: 'lineTiles', className: 'lines', style }, children ); } renderCursorsAndInput() { return $(CursorsAndInputComponent, { ref: 'cursorsAndInput', key: 'cursorsAndInput', didBlurHiddenInput: this.didBlurHiddenInput, didFocusHiddenInput: this.didFocusHiddenInput, didTextInput: this.didTextInput, didPaste: this.didPaste, didKeydown: this.didKeydown, didKeyup: this.didKeyup, didKeypress: this.didKeypress, didCompositionStart: this.didCompositionStart, didCompositionUpdate: this.didCompositionUpdate, didCompositionEnd: this.didCompositionEnd, measuredContent: this.measuredContent, lineHeight: this.getLineHeight(), scrollHeight: this.getScrollHeight(), scrollWidth: this.getScrollWidth(), decorationsToRender: this.decorationsToRender, cursorsBlinkedOff: this.cursorsBlinkedOff, hiddenInputPosition: this.hiddenInputPosition, tabIndex: this.tabIndex }); } renderPlaceholderText() { const { model } = this.props; if (model.isEmpty()) { const placeholderText = model.getPlaceholderText(); if (placeholderText != null) { return $.div({ className: 'placeholder-text' }, placeholderText); } } return null; } renderCharacterMeasurementLine() { return $.div( { key: 'characterMeasurementLine', ref: 'characterMeasurementLine', className: 'line dummy', style: { position: 'absolute', visibility: 'hidden' } }, $.span({ ref: 'normalWidthCharacterSpan' }, NORMAL_WIDTH_CHARACTER), $.span({ ref: 'doubleWidthCharacterSpan' }, DOUBLE_WIDTH_CHARACTER), $.span({ ref: 'halfWidthCharacterSpan' }, HALF_WIDTH_CHARACTER), $.span({ ref: 'koreanCharacterSpan' }, KOREAN_CHARACTER) ); } renderBlockDecorationMeasurementArea() { return $.div({ ref: 'blockDecorationMeasurementArea', key: 'blockDecorationMeasurementArea', style: { contain: 'strict', position: 'absolute', visibility: 'hidden', width: this.getScrollWidth() + 'px' } }); } renderDummyScrollbars() { if (this.shouldRenderDummyScrollbars && !this.props.model.isMini()) { let scrollHeight, scrollTop, horizontalScrollbarHeight; let scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible; let canScrollHorizontally, canScrollVertically; if (this.hasInitialMeasurements) { scrollHeight = this.getScrollHeight(); scrollWidth = this.getScrollWidth(); scrollTop = this.getScrollTop(); scrollLeft = this.getScrollLeft(); canScrollHorizontally = this.canScrollHorizontally(); canScrollVertically = this.canScrollVertically(); horizontalScrollbarHeight = this.getHorizontalScrollbarHeight(); verticalScrollbarWidth = this.getVerticalScrollbarWidth(); forceScrollbarVisible = this.remeasureScrollbars; } else { forceScrollbarVisible = true; } return [ $(DummyScrollbarComponent, { ref: 'verticalScrollbar', orientation: 'vertical', didScroll: this.didScrollDummyScrollbar, didMouseDown: this.didMouseDownOnContent, canScroll: canScrollVertically, scrollHeight, scrollTop, horizontalScrollbarHeight, forceScrollbarVisible }), $(DummyScrollbarComponent, { ref: 'horizontalScrollbar', orientation: 'horizontal', didScroll: this.didScrollDummyScrollbar, didMouseDown: this.didMouseDownOnContent, canScroll: canScrollHorizontally, scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible }), // Force a "corner" to render where the two scrollbars meet at the lower right $.div({ ref: 'scrollbarCorner', className: 'scrollbar-corner', style: { position: 'absolute', height: '20px', width: '20px', bottom: 0, right: 0, overflow: 'scroll' } }) ]; } else { return null; } } renderOverlayDecorations() { return this.decorationsToRender.overlays.map(overlayProps => $( OverlayComponent, Object.assign( { key: overlayProps.element, overlayComponents: this.overlayComponents, didResize: overlayComponent => { this.updateOverlayToRender(overlayProps); overlayComponent.update(overlayProps); } }, overlayProps ) ) ); } // Imperatively manipulate the class list of the root element to avoid // clearing classes assigned by package authors. updateClassList() { const { model } = this.props; const oldClassList = this.classList; const newClassList = ['editor']; if (this.focused) newClassList.push('is-focused'); if (model.isMini()) newClassList.push('mini'); for (var i = 0; i < model.selections.length; i++) { if (!model.selections[i].isEmpty()) { newClassList.push('has-selection'); break; } } if (oldClassList) { for (let i = 0; i < oldClassList.length; i++) { const className = oldClassList[i]; if (!newClassList.includes(className)) { this.element.classList.remove(className); } } } for (let i = 0; i < newClassList.length; i++) { this.element.classList.add(newClassList[i]); } this.classList = newClassList; } queryScreenLinesToRender() { const { model } = this.props; this.renderedScreenLines = model.displayLayer.getScreenLines( this.getRenderedStartRow(), this.getRenderedEndRow() ); } queryLongestLine() { const { model } = this.props; const longestLineRow = model.getApproximateLongestScreenRow(); const longestLine = model.screenLineForScreenRow(longestLineRow); if ( longestLine !== this.previousLongestLine || this.remeasureCharacterDimensions ) { this.requestLineToMeasure(longestLineRow, longestLine); this.longestLineToMeasure = longestLine; this.previousLongestLine = longestLine; } } queryExtraScreenLinesToRender() { this.extraRenderedScreenLines.clear(); this.linesToMeasure.forEach((screenLine, row) => { if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) { this.extraRenderedScreenLines.set(row, screenLine); } }); } queryLineNumbersToRender() { const { model } = this.props; if (!model.anyLineNumberGutterVisible()) return; if (this.showLineNumbers !== model.doesShowLineNumbers()) { this.remeasureGutterDimensions = true; this.showLineNumbers = model.doesShowLineNumbers(); } this.queryMaxLineNumberDigits(); const startRow = this.getRenderedStartRow(); const endRow = this.getRenderedEndRow(); const renderedRowCount = this.getRenderedRowCount(); const bufferRows = model.bufferRowsForScreenRows(startRow, endRow); const screenRows = new Array(renderedRowCount); const keys = new Array(renderedRowCount); const foldableFlags = new Array(renderedRowCount); const softWrappedFlags = new Array(renderedRowCount); let previousBufferRow = startRow > 0 ? model.bufferRowForScreenRow(startRow - 1) : -1; let softWrapCount = 0; for (let row = startRow; row < endRow; row++) { const i = row - startRow; const bufferRow = bufferRows[i]; if (bufferRow === previousBufferRow) { softWrapCount++; softWrappedFlags[i] = true; keys[i] = bufferRow + '-' + softWrapCount; } else { softWrapCount = 0; softWrappedFlags[i] = false; keys[i] = bufferRow; } const nextBufferRow = bufferRows[i + 1]; if (bufferRow !== nextBufferRow) { foldableFlags[i] = model.isFoldableAtBufferRow(bufferRow); } else { foldableFlags[i] = false; } screenRows[i] = row; previousBufferRow = bufferRow; } // Delete extra buffer row at the end because it's not currently on screen. bufferRows.pop(); this.lineNumbersToRender.bufferRows = bufferRows; this.lineNumbersToRender.screenRows = screenRows; this.lineNumbersToRender.keys = keys; this.lineNumbersToRender.foldableFlags = foldableFlags; this.lineNumbersToRender.softWrappedFlags = softWrappedFlags; } queryMaxLineNumberDigits() { const { model } = this.props; if (model.anyLineNumberGutterVisible()) { const maxDigits = Math.max(2, model.getLineCount().toString().length); if (maxDigits !== this.lineNumbersToRender.maxDigits) { this.remeasureGutterDimensions = true; this.lineNumbersToRender.maxDigits = maxDigits; } } } renderedScreenLineForRow(row) { return ( this.renderedScreenLines[row - this.getRenderedStartRow()] || this.extraRenderedScreenLines.get(row) ); } queryGuttersToRender() { const oldGuttersToRender = this.guttersToRender; const oldGuttersVisibility = this.guttersVisibility; this.guttersToRender = this.props.model.getGutters(); this.guttersVisibility = this.guttersToRender.map(g => g.visible); if ( !oldGuttersToRender || oldGuttersToRender.length !== this.guttersToRender.length ) { this.remeasureGutterDimensions = true; } else { for (let i = 0, length = this.guttersToRender.length; i < length; i++) { if ( this.guttersToRender[i] !== oldGuttersToRender[i] || this.guttersVisibility[i] !== oldGuttersVisibility[i] ) { this.remeasureGutterDimensions = true; break; } } } } queryDecorationsToRender() { this.decorationsToRender.lineNumbers.clear(); this.decorationsToRender.lines = []; this.decorationsToRender.overlays.length = 0; this.decorationsToRender.customGutter.clear(); this.decorationsToRender.blocks = new Map(); this.decorationsToRender.text = []; this.decorationsToMeasure.highlights.length = 0; this.decorationsToMeasure.cursors.clear(); this.textDecorationsByMarker.clear(); this.textDecorationBoundaries.length = 0; const decorationsByMarker = this.props.model.decorationManager.decorationPropertiesByMarkerForScreenRowRange( this.getRenderedStartRow(), this.getRenderedEndRow() ); decorationsByMarker.forEach((decorations, marker) => { const screenRange = marker.getScreenRange(); const reversed = marker.isReversed(); for (let i = 0; i < decorations.length; i++) { const decoration = decorations[i]; this.addDecorationToRender( decoration.type, decoration, marker, screenRange, reversed ); } }); this.populateTextDecorationsToRender(); } addDecorationToRender(type, decoration, marker, screenRange, reversed) { if (Array.isArray(type)) { for (let i = 0, length = type.length; i < length; i++) { this.addDecorationToRender( type[i], decoration, marker, screenRange, reversed ); } } else { switch (type) { case 'line': case 'line-number': this.addLineDecorationToRender( type, decoration, screenRange, reversed ); break; case 'highlight': this.addHighlightDecorationToMeasure( decoration, screenRange, marker.id ); break; case 'cursor': this.addCursorDecorationToMeasure( decoration, marker, screenRange, reversed ); break; case 'overlay': this.addOverlayDecorationToRender(decoration, marker); break; case 'gutter': this.addCustomGutterDecorationToRender(decoration, screenRange); break; case 'block': this.addBlockDecorationToRender(decoration, screenRange, reversed); break; case 'text': this.addTextDecorationToRender(decoration, screenRange, marker); break; } } } addLineDecorationToRender(type, decoration, screenRange, reversed) { let decorationsToRender; if (type === 'line') { decorationsToRender = this.decorationsToRender.lines; } else { const gutterName = decoration.gutterName || 'line-number'; decorationsToRender = this.decorationsToRender.lineNumbers.get( gutterName ); if (!decorationsToRender) { decorationsToRender = []; this.decorationsToRender.lineNumbers.set( gutterName, decorationsToRender ); } } let omitLastRow = false; if (screenRange.isEmpty()) { if (decoration.onlyNonEmpty) return; } else { if (decoration.onlyEmpty) return; if (decoration.omitEmptyLastRow !== false) { omitLastRow = screenRange.end.column === 0; } } const renderedStartRow = this.getRenderedStartRow(); let rangeStartRow = screenRange.start.row; let rangeEndRow = screenRange.end.row; if (decoration.onlyHead) { if (reversed) { rangeEndRow = rangeStartRow; } else { rangeStartRow = rangeEndRow; } } rangeStartRow = Math.max(rangeStartRow, this.getRenderedStartRow()); rangeEndRow = Math.min(rangeEndRow, this.getRenderedEndRow() - 1); for (let row = rangeStartRow; row <= rangeEndRow; row++) { if (omitLastRow && row === screenRange.end.row) break; const currentClassName = decorationsToRender[row - renderedStartRow]; const newClassName = currentClassName ? currentClassName + ' ' + decoration.class : decoration.class; decorationsToRender[row - renderedStartRow] = newClassName; } } addHighlightDecorationToMeasure(decoration, screenRange, key) { screenRange = constrainRangeToRows( screenRange, this.getRenderedStartRow(), this.getRenderedEndRow() ); if (screenRange.isEmpty()) return; const { class: className, flashRequested, flashClass, flashDuration } = decoration; decoration.flashRequested = false; this.decorationsToMeasure.highlights.push({ screenRange, key, className, flashRequested, flashClass, flashDuration }); this.requestHorizontalMeasurement( screenRange.start.row, screenRange.start.column ); this.requestHorizontalMeasurement( screenRange.end.row, screenRange.end.column ); } addCursorDecorationToMeasure(decoration, marker, screenRange, reversed) { const { model } = this.props; if (!model.getShowCursorOnSelection() && !screenRange.isEmpty()) return; let decorationToMeasure = this.decorationsToMeasure.cursors.get(marker); if (!decorationToMeasure) { const isLastCursor = model.getLastCursor().getMarker() === marker; const screenPosition = reversed ? screenRange.start : screenRange.end; const { row, column } = screenPosition; if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return; this.requestHorizontalMeasurement(row, column); let columnWidth = 0; if (model.lineLengthForScreenRow(row) > column) { columnWidth = 1; this.requestHorizontalMeasurement(row, column + 1); } decorationToMeasure = { screenPosition, columnWidth, isLastCursor }; this.decorationsToMeasure.cursors.set(marker, decorationToMeasure); } if (decoration.class) { if (decorationToMeasure.className) { decorationToMeasure.className += ' ' + decoration.class; } else { decorationToMeasure.className = decoration.class; } } if (decoration.style) { if (decorationToMeasure.style) { Object.assign(decorationToMeasure.style, decoration.style); } else { decorationToMeasure.style = Object.assign({}, decoration.style); } } } addOverlayDecorationToRender(decoration, marker) { const { class: className, item, position, avoidOverflow } = decoration; const element = TextEditor.viewForItem(item); const screenPosition = position === 'tail' ? marker.getTailScreenPosition() : marker.getHeadScreenPosition(); this.requestHorizontalMeasurement( screenPosition.row, screenPosition.column ); this.decorationsToRender.overlays.push({ className, element, avoidOverflow, screenPosition }); } addCustomGutterDecorationToRender(decoration, screenRange) { let decorations = this.decorationsToRender.customGutter.get( decoration.gutterName ); if (!decorations) { decorations = []; this.decorationsToRender.customGutter.set( decoration.gutterName, decorations ); } const top = this.pixelPositionAfterBlocksForRow(screenRange.start.row); const height = this.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) - top; decorations.push({ className: 'decoration' + (decoration.class ? ' ' + decoration.class : ''), element: TextEditor.viewForItem(decoration.item), top, height }); } addBlockDecorationToRender(decoration, screenRange, reversed) { const { row } = reversed ? screenRange.start : screenRange.end; if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) return; const tileStartRow = this.tileStartRowForRow(row); const screenLine = this.renderedScreenLines[ row - this.getRenderedStartRow() ]; let decorationsByScreenLine = this.decorationsToRender.blocks.get( tileStartRow ); if (!decorationsByScreenLine) { decorationsByScreenLine = new Map(); this.decorationsToRender.blocks.set( tileStartRow, decorationsByScreenLine ); } let decorations = decorationsByScreenLine.get(screenLine.id); if (!decorations) { decorations = []; decorationsByScreenLine.set(screenLine.id, decorations); } decorations.push(decoration); // Order block decorations by increasing values of their "order" property. Break ties with "id", which mirrors // their creation sequence. decorations.sort((a, b) => a.order !== b.order ? a.order - b.order : a.id - b.id ); } addTextDecorationToRender(decoration, screenRange, marker) { if (screenRange.isEmpty()) return; let decorationsForMarker = this.textDecorationsByMarker.get(marker); if (!decorationsForMarker) { decorationsForMarker = []; this.textDecorationsByMarker.set(marker, decorationsForMarker); this.textDecorationBoundaries.push({ position: screenRange.start, starting: [marker] }); this.textDecorationBoundaries.push({ position: screenRange.end, ending: [marker] }); } decorationsForMarker.push(decoration); } populateTextDecorationsToRender() { // Sort all boundaries in ascending order of position this.textDecorationBoundaries.sort((a, b) => a.position.compare(b.position) ); // Combine adjacent boundaries with the same position for (let i = 0; i < this.textDecorationBoundaries.length; ) { const boundary = this.textDecorationBoundaries[i]; const nextBoundary = this.textDecorationBoundaries[i + 1]; if (nextBoundary && nextBoundary.position.isEqual(boundary.position)) { if (nextBoundary.starting) { if (boundary.starting) { boundary.starting.push(...nextBoundary.starting); } else { boundary.starting = nextBoundary.starting; } } if (nextBoundary.ending) { if (boundary.ending) { boundary.ending.push(...nextBoundary.ending); } else { boundary.ending = nextBoundary.ending; } } this.textDecorationBoundaries.splice(i + 1, 1); } else { i++; } } const renderedStartRow = this.getRenderedStartRow(); const renderedEndRow = this.getRenderedEndRow(); const containingMarkers = []; // Iterate over boundaries to build up text decorations. for (let i = 0; i < this.textDecorationBoundaries.length; i++) { const boundary = this.textDecorationBoundaries[i]; // If multiple markers start here, sort them by order of nesting (markers ending later come first) if (boundary.starting && boundary.starting.length > 1) { boundary.starting.sort((a, b) => a.compare(b)); } // If multiple markers start here, sort them by order of nesting (markers starting earlier come first) if (boundary.ending && boundary.ending.length > 1) { boundary.ending.sort((a, b) => b.compare(a)); } // Remove markers ending here from containing markers array if (boundary.ending) { for (let j = boundary.ending.length - 1; j >= 0; j--) { containingMarkers.splice( containingMarkers.lastIndexOf(boundary.ending[j]), 1 ); } } // Add markers starting here to containing markers array if (boundary.starting) containingMarkers.push(...boundary.starting); // Determine desired className and style based on containing markers let className, style; for (let j = 0; j < containingMarkers.length; j++) { const marker = containingMarkers[j]; const decorations = this.textDecorationsByMarker.get(marker); for (let k = 0; k < decorations.length; k++) { const decoration = decorations[k]; if (decoration.class) { if (className) { className += ' ' + decoration.class; } else { className = decoration.class; } } if (decoration.style) { if (style) { Object.assign(style, decoration.style); } else { style = Object.assign({}, decoration.style); } } } } // Add decoration start with className/style for current position's column, // and also for the start of every row up until the next decoration boundary if (boundary.position.row >= renderedStartRow) { this.addTextDecorationStart( boundary.position.row, boundary.position.column, className, style ); } const nextBoundary = this.textDecorationBoundaries[i + 1]; if (nextBoundary) { let row = Math.max(boundary.position.row + 1, renderedStartRow); const endRow = Math.min(nextBoundary.position.row, renderedEndRow); for (; row < endRow; row++) { this.addTextDecorationStart(row, 0, className, style); } if ( row === nextBoundary.position.row && nextBoundary.position.column !== 0 ) { this.addTextDecorationStart(row, 0, className, style); } } } } addTextDecorationStart(row, column, className, style) { const renderedStartRow = this.getRenderedStartRow(); let decorationStarts = this.decorationsToRender.text[ row - renderedStartRow ]; if (!decorationStarts) { decorationStarts = []; this.decorationsToRender.text[row - renderedStartRow] = decorationStarts; } decorationStarts.push({ column, className, style }); } updateAbsolutePositionedDecorations() { this.updateHighlightsToRender(); this.updateCursorsToRender(); this.updateOverlaysToRender(); } updateHighlightsToRender() { this.decorationsToRender.highlights.length = 0; for (let i = 0; i < this.decorationsToMeasure.highlights.length; i++) { const highlight = this.decorationsToMeasure.highlights[i]; const { start, end } = highlight.screenRange; highlight.startPixelTop = this.pixelPositionAfterBlocksForRow(start.row); highlight.startPixelLeft = this.pixelLeftForRowAndColumn( start.row, start.column ); highlight.endPixelTop = this.pixelPositionAfterBlocksForRow(end.row) + this.getLineHeight(); highlight.endPixelLeft = this.pixelLeftForRowAndColumn( end.row, end.column ); this.decorationsToRender.highlights.push(highlight); } } updateCursorsToRender() { this.decorationsToRender.cursors.length = 0; this.decorationsToMeasure.cursors.forEach(cursor => { const { screenPosition, className, style } = cursor; const { row, column } = screenPosition; const pixelTop = this.pixelPositionAfterBlocksForRow(row); const pixelLeft = this.pixelLeftForRowAndColumn(row, column); let pixelWidth; if (cursor.columnWidth === 0) { pixelWidth = this.getBaseCharacterWidth(); } else { pixelWidth = this.pixelLeftForRowAndColumn(row, column + 1) - pixelLeft; } const cursorPosition = { pixelTop, pixelLeft, pixelWidth, className, style }; this.decorationsToRender.cursors.push(cursorPosition); if (cursor.isLastCursor) this.hiddenInputPosition = cursorPosition; }); } updateOverlayToRender(decoration) { const windowInnerHeight = this.getWindowInnerHeight(); const windowInnerWidth = this.getWindowInnerWidth(); const contentClientRect = this.refs.content.getBoundingClientRect(); const { element, screenPosition, avoidOverflow } = decoration; const { row, column } = screenPosition; let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight(); let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column); const clientRect = element.getBoundingClientRect(); if (avoidOverflow !== false) { const computedStyle = window.getComputedStyle(element); const elementTop = wrapperTop + parseInt(computedStyle.marginTop); const elementBottom = elementTop + clientRect.height; const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom); const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft); const elementRight = elementLeft + clientRect.width; if (elementBottom > windowInnerHeight && flippedElementTop >= 0) { wrapperTop -= elementTop - flippedElementTop; } if (elementLeft < 0) { wrapperLeft -= elementLeft; } else if (elementRight > windowInnerWidth) { wrapperLeft -= elementRight - windowInnerWidth; } } decoration.pixelTop = Math.round(wrapperTop); decoration.pixelLeft = Math.round(wrapperLeft); } updateOverlaysToRender() { const overlayCount = this.decorationsToRender.overlays.length; if (overlayCount === 0) return null; for (let i = 0; i < overlayCount; i++) { const decoration = this.decorationsToRender.overlays[i]; this.updateOverlayToRender(decoration); } } didAttach() { if (!this.attached) { this.attached = true; this.intersectionObserver = new IntersectionObserver(entries => { const { intersectionRect } = entries[entries.length - 1]; if (intersectionRect.width > 0 || intersectionRect.height > 0) { this.didShow(); } else { this.didHide(); } }); this.intersectionObserver.observe(this.element); this.resizeObserver = new ResizeObserver(this.didResize.bind(this)); this.resizeObserver.observe(this.element); if (this.refs.gutterContainer) { this.gutterContainerResizeObserver = new ResizeObserver( this.didResizeGutterContainer.bind(this) ); this.gutterContainerResizeObserver.observe( this.refs.gutterContainer.element ); } this.overlayComponents.forEach(component => component.didAttach()); if (this.isVisible()) { this.didShow(); if (this.refs.verticalScrollbar) this.refs.verticalScrollbar.flushScrollPosition(); if (this.refs.horizontalScrollbar) this.refs.horizontalScrollbar.flushScrollPosition(); } else { this.didHide(); } if (!this.constructor.attachedComponents) { this.constructor.attachedComponents = new Set(); } this.constructor.attachedComponents.add(this); } } didDetach() { if (this.attached) { this.intersectionObserver.disconnect(); this.resizeObserver.disconnect(); if (this.gutterContainerResizeObserver) this.gutterContainerResizeObserver.disconnect(); this.overlayComponents.forEach(component => component.didDetach()); this.didHide(); this.attached = false; this.constructor.attachedComponents.delete(this); } } didShow() { if (!this.visible && this.isVisible()) { if (!this.hasInitialMeasurements) this.measureDimensions(); this.visible = true; this.props.model.setVisible(true); this.resizeBlockDecorationMeasurementsArea = true; this.updateSync(); this.flushPendingLogicalScrollPosition(); } } didHide() { if (this.visible) { this.visible = false; this.props.model.setVisible(false); } } // Called by TextEditorElement so that focus events can be handled before // the element is attached to the DOM. didFocus() { if (!this.visible) this.didShow(); if (!this.focused) { this.focused = true; this.startCursorBlinking(); this.scheduleUpdate(); } this.getHiddenInput().focus({ preventScroll: true }); } // Called by TextEditorElement so that this function is always the first // listener to be fired, even if other listeners are bound before creating // the component. didBlur(event) { if (event.relatedTarget === this.getHiddenInput()) { event.stopImmediatePropagation(); } } didBlurHiddenInput(event) { if ( this.element !== event.relatedTarget && !this.element.contains(event.relatedTarget) ) { this.focused = false; this.stopCursorBlinking(); this.scheduleUpdate(); this.element.dispatchEvent(new FocusEvent(event.type, event)); } } didFocusHiddenInput() { if (!this.focused) { this.focused = true; this.startCursorBlinking(); this.scheduleUpdate(); } } didMouseWheel(event) { const scrollSensitivity = this.props.model.getScrollSensitivity() / 100; let { wheelDeltaX, wheelDeltaY } = event; if (Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)) { wheelDeltaX = wheelDeltaX * scrollSensitivity; wheelDeltaY = 0; } else { wheelDeltaX = 0; wheelDeltaY = wheelDeltaY * scrollSensitivity; } if (this.getPlatform() !== 'darwin' && event.shiftKey) { let temp = wheelDeltaX; wheelDeltaX = wheelDeltaY; wheelDeltaY = temp; } const scrollLeftChanged = wheelDeltaX !== 0 && this.setScrollLeft(this.getScrollLeft() - wheelDeltaX); const scrollTopChanged = wheelDeltaY !== 0 && this.setScrollTop(this.getScrollTop() - wheelDeltaY); if (scrollLeftChanged || scrollTopChanged) { event.preventDefault(); this.updateSync(); } } didResize() { // Prevent the component from measuring the client container dimensions when // getting spurious resize events. if (this.isVisible()) { const clientContainerWidthChanged = this.measureClientContainerWidth(); const clientContainerHeightChanged = this.measureClientContainerHeight(); if (clientContainerWidthChanged || clientContainerHeightChanged) { if (clientContainerWidthChanged) { this.remeasureAllBlockDecorations = true; } this.resizeObserver.disconnect(); this.scheduleUpdate(); process.nextTick(() => { this.resizeObserver.observe(this.element); }); } } } didResizeGutterContainer() { // Prevent the component from measuring the gutter dimensions when getting // spurious resize events. if (this.isVisible() && this.measureGutterDimensions()) { this.gutterContainerResizeObserver.disconnect(); this.scheduleUpdate(); process.nextTick(() => { this.gutterContainerResizeObserver.observe( this.refs.gutterContainer.element ); }); } } didScrollDummyScrollbar() { let scrollTopChanged = false; let scrollLeftChanged = false; if (!this.scrollTopPending) { scrollTopChanged = this.setScrollTop( this.refs.verticalScrollbar.element.scrollTop ); } if (!this.scrollLeftPending) { scrollLeftChanged = this.setScrollLeft( this.refs.horizontalScrollbar.element.scrollLeft ); } if (scrollTopChanged || scrollLeftChanged) this.updateSync(); } didUpdateStyles() { this.remeasureCharacterDimensions = true; this.horizontalPixelPositionsByScreenLineId.clear(); this.scheduleUpdate(); } didUpdateScrollbarStyles() { if (!this.props.model.isMini()) { this.remeasureScrollbars = true; this.scheduleUpdate(); } } didPaste(event) { // On Linux, Chromium translates a middle-button mouse click into a // mousedown event *and* a paste event. Since Atom supports the middle mouse // click as a way of closing a tab, we only want the mousedown event, not // the paste event. And since we don't use the `paste` event for any // behavior in Atom, we can no-op the event to eliminate this issue. // See https://github.com/atom/atom/pull/15183#issue-248432413. if (this.getPlatform() === 'linux') event.preventDefault(); } didTextInput(event) { if (this.compositionCheckpoint) { this.props.model.revertToCheckpoint(this.compositionCheckpoint); this.compositionCheckpoint = null; } if (this.isInputEnabled()) { event.stopPropagation(); // WARNING: If we call preventDefault on the input of a space // character, then the browser interprets the spacebar keypress as a // page-down command, causing spaces to scroll elements containing // editors. This means typing space will actually change the contents // of the hidden input, which will cause the browser to autoscroll the // scroll container to reveal the input if it is off screen (See // https://github.com/atom/atom/issues/16046). To correct for this // situation, we automatically reset the scroll position to 0,0 after // typing a space. None of this can really be tested. if (event.data === ' ') { window.setImmediate(() => { this.refs.scrollContainer.scrollTop = 0; this.refs.scrollContainer.scrollLeft = 0; }); } else { event.preventDefault(); } // If the input event is fired while the accented character menu is open it // means that the user has chosen one of the accented alternatives. Thus, we // will replace the original non accented character with the selected // alternative. if (this.accentedCharacterMenuIsOpen) { this.props.model.selectLeft(); } this.props.model.insertText(event.data, { groupUndo: true }); } } // We need to get clever to detect when the accented character menu is // opened on macOS. Usually, every keydown event that could cause input is // followed by a corresponding keypress. However, pressing and holding // long enough to open the accented character menu causes additional keydown // events to fire that aren't followed by their own keypress and textInput // events. // // Therefore, we assume the accented character menu has been deployed if, // before observing any keyup event, we observe events in the following // sequence: // // keydown(code: X), keypress, keydown(code: X) // // The code X must be the same in the keydown events that bracket the // keypress, meaning we're *holding* the _same_ key we initially pressed. // Got that? didKeydown(event) { // Stop dragging when user interacts with the keyboard. This prevents // unwanted selections in the case edits are performed while selecting text // at the same time. Modifier keys are exempt to preserve the ability to // add selections, shift-scroll horizontally while selecting. if ( this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta' && event.key !== 'Shift' ) { this.stopDragging(); } if (this.lastKeydownBeforeKeypress != null) { if (this.lastKeydownBeforeKeypress.code === event.code) { this.accentedCharacterMenuIsOpen = true; } this.lastKeydownBeforeKeypress = null; } this.lastKeydown = event; } didKeypress(event) { this.lastKeydownBeforeKeypress = this.lastKeydown; // This cancels the accented character behavior if we type a key normally // with the menu open. this.accentedCharacterMenuIsOpen = false; } didKeyup(event) { if ( this.lastKeydownBeforeKeypress && this.lastKeydownBeforeKeypress.code === event.code ) { this.lastKeydownBeforeKeypress = null; } } // The IME composition events work like this: // // User types 's', chromium pops up the completion helper // 1. compositionstart fired // 2. compositionupdate fired; event.data == 's' // User hits arrow keys to move around in completion helper // 3. compositionupdate fired; event.data == 's' for each arry key press // User escape to cancel OR User chooses a completion // 4. compositionend fired // 5. textInput fired; event.data == the completion string didCompositionStart() { // Workaround for Chromium not preventing composition events when // preventDefault is called on the keydown event that precipitated them. if (this.lastKeydown && this.lastKeydown.defaultPrevented) { this.getHiddenInput().disabled = true; process.nextTick(() => { // Disabling the hidden input makes it lose focus as well, so we have to // re-enable and re-focus it. this.getHiddenInput().disabled = false; this.getHiddenInput().focus({ preventScroll: true }); }); return; } this.compositionCheckpoint = this.props.model.createCheckpoint(); if (this.accentedCharacterMenuIsOpen) { this.props.model.selectLeft(); } } didCompositionUpdate(event) { this.props.model.insertText(event.data, { select: true }); } didCompositionEnd(event) { event.target.value = ''; } didMouseDownOnContent(event) { const { model } = this.props; const { target, button, detail, ctrlKey, shiftKey, metaKey } = event; const platform = this.getPlatform(); // Ignore clicks on block decorations. if (target) { let element = target; while (element && element !== this.element) { if (this.blockDecorationsByElement.has(element)) { return; } element = element.parentElement; } } const screenPosition = this.screenPositionForMouseEvent(event); if (button === 1) { model.setCursorScreenPosition(screenPosition, { autoscroll: false }); // On Linux, pasting happens on middle click. A textInput event with the // contents of the selection clipboard will be dispatched by the browser // automatically on mouseup if editor.selectionClipboard is set to true. if ( platform === 'linux' && this.isInputEnabled() && atom.config.get('editor.selectionClipboard') ) model.insertText(clipboard.readText('selection')); return; } if (button !== 0) return; // Ctrl-click brings up the context menu on macOS if (platform === 'darwin' && ctrlKey) return; if (target && target.matches('.fold-marker')) { const bufferPosition = model.bufferPositionForScreenPosition( screenPosition ); model.destroyFoldsContainingBufferPositions([bufferPosition], false); return; } const allowMultiCursor = atom.config.get('editor.multiCursorOnClick'); const addOrRemoveSelection = allowMultiCursor && (metaKey || (ctrlKey && platform !== 'darwin')); switch (detail) { case 1: if (addOrRemoveSelection) { const existingSelection = model.getSelectionAtScreenPosition( screenPosition ); if (existingSelection) { if (model.hasMultipleCursors()) existingSelection.destroy(); } else { model.addCursorAtScreenPosition(screenPosition, { autoscroll: false }); } } else { if (shiftKey) { model.selectToScreenPosition(screenPosition, { autoscroll: false }); } else { model.setCursorScreenPosition(screenPosition, { autoscroll: false }); } } break; case 2: if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, { autoscroll: false }); model.getLastSelection().selectWord({ autoscroll: false }); break; case 3: if (addOrRemoveSelection) model.addCursorAtScreenPosition(screenPosition, { autoscroll: false }); model.getLastSelection().selectLine(null, { autoscroll: false }); break; } this.handleMouseDragUntilMouseUp({ didDrag: event => { this.autoscrollOnMouseDrag(event); const screenPosition = this.screenPositionForMouseEvent(event); model.selectToScreenPosition(screenPosition, { suppressSelectionMerge: true, autoscroll: false }); this.updateSync(); }, didStopDragging: () => { model.finalizeSelections(); model.mergeIntersectingSelections(); this.updateSync(); } }); } didMouseDownOnLineNumberGutter(event) { const { model } = this.props; const { target, button, ctrlKey, shiftKey, metaKey } = event; // Only handle mousedown events for left mouse button if (button !== 0) return; const clickedScreenRow = this.screenPositionForMouseEvent(event).row; const startBufferRow = model.bufferPositionForScreenPosition([ clickedScreenRow, 0 ]).row; if ( target && (target.matches('.foldable .icon-right') || target.matches('.folded .icon-right')) ) { model.toggleFoldAtBufferRow(startBufferRow); return; } const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin'); const endBufferRow = model.bufferPositionForScreenPosition([ clickedScreenRow, Infinity ]).row; const clickedLineBufferRange = Range( Point(startBufferRow, 0), Point(endBufferRow + 1, 0) ); let initialBufferRange; if (shiftKey) { const lastSelection = model.getLastSelection(); initialBufferRange = lastSelection.getBufferRange(); lastSelection.setBufferRange( initialBufferRange.union(clickedLineBufferRange), { reversed: clickedScreenRow < lastSelection.getScreenRange().start.row, autoscroll: false, preserveFolds: true, suppressSelectionMerge: true } ); } else { initialBufferRange = clickedLineBufferRange; if (addOrRemoveSelection) { model.addSelectionForBufferRange(clickedLineBufferRange, { autoscroll: false, preserveFolds: true }); } else { model.setSelectedBufferRange(clickedLineBufferRange, { autoscroll: false, preserveFolds: true }); } } const initialScreenRange = model.screenRangeForBufferRange( initialBufferRange ); this.handleMouseDragUntilMouseUp({ didDrag: event => { this.autoscrollOnMouseDrag(event, true); const dragRow = this.screenPositionForMouseEvent(event).row; const draggedLineScreenRange = Range( Point(dragRow, 0), Point(dragRow + 1, 0) ); model .getLastSelection() .setScreenRange(draggedLineScreenRange.union(initialScreenRange), { reversed: dragRow < initialScreenRange.start.row, autoscroll: false, preserveFolds: true }); this.updateSync(); }, didStopDragging: () => { model.mergeIntersectingSelections(); this.updateSync(); } }); } handleMouseDragUntilMouseUp({ didDrag, didStopDragging }) { let dragging = false; let lastMousemoveEvent; const animationFrameLoop = () => { window.requestAnimationFrame(() => { if (dragging && this.visible) { didDrag(lastMousemoveEvent); animationFrameLoop(); } }); }; function didMouseMove(event) { lastMousemoveEvent = event; if (!dragging) { dragging = true; animationFrameLoop(); } } function didMouseUp() { this.stopDragging = null; window.removeEventListener('mousemove', didMouseMove); window.removeEventListener('mouseup', didMouseUp, { capture: true }); if (dragging) { dragging = false; didStopDragging(); } } window.addEventListener('mousemove', didMouseMove); window.addEventListener('mouseup', didMouseUp, { capture: true }); this.stopDragging = didMouseUp; } autoscrollOnMouseDrag({ clientX, clientY }, verticalOnly = false) { let { top, bottom, left, right } = this.refs.scrollContainer.getBoundingClientRect(); // Using var to avoid deopt on += assignments below top += MOUSE_DRAG_AUTOSCROLL_MARGIN; bottom -= MOUSE_DRAG_AUTOSCROLL_MARGIN; left += MOUSE_DRAG_AUTOSCROLL_MARGIN; right -= MOUSE_DRAG_AUTOSCROLL_MARGIN; let yDelta, yDirection; if (clientY < top) { yDelta = top - clientY; yDirection = -1; } else if (clientY > bottom) { yDelta = clientY - bottom; yDirection = 1; } let xDelta, xDirection; if (clientX < left) { xDelta = left - clientX; xDirection = -1; } else if (clientX > right) { xDelta = clientX - right; xDirection = 1; } let scrolled = false; if (yDelta != null) { const scaledDelta = scaleMouseDragAutoscrollDelta(yDelta) * yDirection; scrolled = this.setScrollTop(this.getScrollTop() + scaledDelta); } if (!verticalOnly && xDelta != null) { const scaledDelta = scaleMouseDragAutoscrollDelta(xDelta) * xDirection; scrolled = this.setScrollLeft(this.getScrollLeft() + scaledDelta); } if (scrolled) this.updateSync(); } screenPositionForMouseEvent(event) { return this.screenPositionForPixelPosition( this.pixelPositionForMouseEvent(event) ); } pixelPositionForMouseEvent({ clientX, clientY }) { const scrollContainerRect = this.refs.scrollContainer.getBoundingClientRect(); clientX = Math.min( scrollContainerRect.right, Math.max(scrollContainerRect.left, clientX) ); clientY = Math.min( scrollContainerRect.bottom, Math.max(scrollContainerRect.top, clientY) ); const linesRect = this.refs.lineTiles.getBoundingClientRect(); return { top: clientY - linesRect.top, left: clientX - linesRect.left }; } didUpdateSelections() { this.pauseCursorBlinking(); this.scheduleUpdate(); } pauseCursorBlinking() { this.stopCursorBlinking(); this.debouncedResumeCursorBlinking(); } resumeCursorBlinking() { this.cursorsBlinkedOff = true; this.startCursorBlinking(); } stopCursorBlinking() { if (this.cursorsBlinking) { this.cursorsBlinkedOff = false; this.cursorsBlinking = false; window.clearInterval(this.cursorBlinkIntervalHandle); this.cursorBlinkIntervalHandle = null; this.scheduleUpdate(); } } startCursorBlinking() { if (!this.cursorsBlinking) { this.cursorBlinkIntervalHandle = window.setInterval(() => { this.cursorsBlinkedOff = !this.cursorsBlinkedOff; this.scheduleUpdate(true); }, (this.props.cursorBlinkPeriod || CURSOR_BLINK_PERIOD) / 2); this.cursorsBlinking = true; this.scheduleUpdate(true); } } didRequestAutoscroll(autoscroll) { this.pendingAutoscroll = autoscroll; this.scheduleUpdate(); } flushPendingLogicalScrollPosition() { let changedScrollTop = false; if (this.pendingScrollTopRow > 0) { changedScrollTop = this.setScrollTopRow(this.pendingScrollTopRow, false); this.pendingScrollTopRow = null; } let changedScrollLeft = false; if (this.pendingScrollLeftColumn > 0) { changedScrollLeft = this.setScrollLeftColumn( this.pendingScrollLeftColumn, false ); this.pendingScrollLeftColumn = null; } if (changedScrollTop || changedScrollLeft) { this.updateSync(); } } autoscrollVertically(screenRange, options) { const screenRangeTop = this.pixelPositionAfterBlocksForRow( screenRange.start.row ); const screenRangeBottom = this.pixelPositionAfterBlocksForRow(screenRange.end.row) + this.getLineHeight(); const verticalScrollMargin = this.getVerticalAutoscrollMargin(); let desiredScrollTop, desiredScrollBottom; if (options && options.center) { const desiredScrollCenter = (screenRangeTop + screenRangeBottom) / 2; desiredScrollTop = desiredScrollCenter - this.getScrollContainerClientHeight() / 2; desiredScrollBottom = desiredScrollCenter + this.getScrollContainerClientHeight() / 2; } else { desiredScrollTop = screenRangeTop - verticalScrollMargin; desiredScrollBottom = screenRangeBottom + verticalScrollMargin; } if (!options || options.reversed !== false) { if (desiredScrollBottom > this.getScrollBottom()) { this.setScrollBottom(desiredScrollBottom); } if (desiredScrollTop < this.getScrollTop()) { this.setScrollTop(desiredScrollTop); } } else { if (desiredScrollTop < this.getScrollTop()) { this.setScrollTop(desiredScrollTop); } if (desiredScrollBottom > this.getScrollBottom()) { this.setScrollBottom(desiredScrollBottom); } } return false; } autoscrollHorizontally(screenRange, options) { const horizontalScrollMargin = this.getHorizontalAutoscrollMargin(); const gutterContainerWidth = this.getGutterContainerWidth(); let left = this.pixelLeftForRowAndColumn( screenRange.start.row, screenRange.start.column ) + gutterContainerWidth; let right = this.pixelLeftForRowAndColumn( screenRange.end.row, screenRange.end.column ) + gutterContainerWidth; const desiredScrollLeft = Math.max( 0, left - horizontalScrollMargin - gutterContainerWidth ); const desiredScrollRight = Math.min( this.getScrollWidth(), right + horizontalScrollMargin ); if (!options || options.reversed !== false) { if (desiredScrollRight > this.getScrollRight()) { this.setScrollRight(desiredScrollRight); } if (desiredScrollLeft < this.getScrollLeft()) { this.setScrollLeft(desiredScrollLeft); } } else { if (desiredScrollLeft < this.getScrollLeft()) { this.setScrollLeft(desiredScrollLeft); } if (desiredScrollRight > this.getScrollRight()) { this.setScrollRight(desiredScrollRight); } } } getVerticalAutoscrollMargin() { const maxMarginInLines = Math.floor( (this.getScrollContainerClientHeight() / this.getLineHeight() - 1) / 2 ); const marginInLines = Math.min( this.props.model.verticalScrollMargin, maxMarginInLines ); return marginInLines * this.getLineHeight(); } getHorizontalAutoscrollMargin() { const maxMarginInBaseCharacters = Math.floor( (this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() - 1) / 2 ); const marginInBaseCharacters = Math.min( this.props.model.horizontalScrollMargin, maxMarginInBaseCharacters ); return marginInBaseCharacters * this.getBaseCharacterWidth(); } // This method is called at the beginning of a frame render to relay any // potential changes in the editor's width into the model before proceeding. updateModelSoftWrapColumn() { const { model } = this.props; const newEditorWidthInChars = this.getScrollContainerClientWidthInBaseCharacters(); if (newEditorWidthInChars !== model.getEditorWidthInChars()) { this.suppressUpdates = true; const renderedStartRow = this.getRenderedStartRow(); this.props.model.setEditorWidthInChars(newEditorWidthInChars); // Relaying a change in to the editor's client width may cause the // vertical scrollbar to appear or disappear, which causes the editor's // client width to change *again*. Make sure the display layer is fully // populated for the visible area before recalculating the editor's // width in characters. Then update the display layer *again* just in // case a change in scrollbar visibility causes lines to wrap // differently. We capture the renderedStartRow before resetting the // display layer because once it has been reset, we can't compute the // rendered start row accurately. 😥 this.populateVisibleRowRange(renderedStartRow); this.props.model.setEditorWidthInChars( this.getScrollContainerClientWidthInBaseCharacters() ); this.derivedDimensionsCache = {}; this.suppressUpdates = false; } } // This method exists because it existed in the previous implementation and some // package tests relied on it measureDimensions() { this.measureCharacterDimensions(); this.measureGutterDimensions(); this.measureClientContainerHeight(); this.measureClientContainerWidth(); this.measureScrollbarDimensions(); this.hasInitialMeasurements = true; } measureCharacterDimensions() { this.measurements.lineHeight = Math.max( 1, this.refs.characterMeasurementLine.getBoundingClientRect().height ); this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width; this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width; this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width; this.measurements.koreanCharacterWidth = this.refs.koreanCharacterSpan.getBoundingClientRect().width; this.props.model.setLineHeightInPixels(this.measurements.lineHeight); this.props.model.setDefaultCharWidth( this.measurements.baseCharacterWidth, this.measurements.doubleWidthCharacterWidth, this.measurements.halfWidthCharacterWidth, this.measurements.koreanCharacterWidth ); this.lineTopIndex.setDefaultLineHeight(this.measurements.lineHeight); } measureGutterDimensions() { let dimensionsChanged = false; if (this.refs.gutterContainer) { const gutterContainerWidth = this.refs.gutterContainer.element .offsetWidth; if (gutterContainerWidth !== this.measurements.gutterContainerWidth) { dimensionsChanged = true; this.measurements.gutterContainerWidth = gutterContainerWidth; } } else { this.measurements.gutterContainerWidth = 0; } if ( this.refs.gutterContainer && this.refs.gutterContainer.refs.lineNumberGutter ) { const lineNumberGutterWidth = this.refs.gutterContainer.refs .lineNumberGutter.element.offsetWidth; if (lineNumberGutterWidth !== this.measurements.lineNumberGutterWidth) { dimensionsChanged = true; this.measurements.lineNumberGutterWidth = lineNumberGutterWidth; } } else { this.measurements.lineNumberGutterWidth = 0; } return dimensionsChanged; } measureClientContainerHeight() { const clientContainerHeight = this.refs.clientContainer.offsetHeight; if (clientContainerHeight !== this.measurements.clientContainerHeight) { this.measurements.clientContainerHeight = clientContainerHeight; return true; } else { return false; } } measureClientContainerWidth() { const clientContainerWidth = this.refs.clientContainer.offsetWidth; if (clientContainerWidth !== this.measurements.clientContainerWidth) { this.measurements.clientContainerWidth = clientContainerWidth; return true; } else { return false; } } measureScrollbarDimensions() { if (this.props.model.isMini()) { this.measurements.verticalScrollbarWidth = 0; this.measurements.horizontalScrollbarHeight = 0; } else { this.measurements.verticalScrollbarWidth = this.refs.verticalScrollbar.getRealScrollbarWidth(); this.measurements.horizontalScrollbarHeight = this.refs.horizontalScrollbar.getRealScrollbarHeight(); } } measureLongestLineWidth() { if (this.longestLineToMeasure) { const lineComponent = this.lineComponentsByScreenLineId.get( this.longestLineToMeasure.id ); this.measurements.longestLineWidth = lineComponent.element.firstChild.offsetWidth; this.longestLineToMeasure = null; } } requestLineToMeasure(row, screenLine) { this.linesToMeasure.set(row, screenLine); } requestHorizontalMeasurement(row, column) { if (column === 0) return; const screenLine = this.props.model.screenLineForScreenRow(row); if (screenLine) { this.requestLineToMeasure(row, screenLine); let columns = this.horizontalPositionsToMeasure.get(row); if (columns == null) { columns = []; this.horizontalPositionsToMeasure.set(row, columns); } columns.push(column); } } measureHorizontalPositions() { this.horizontalPositionsToMeasure.forEach((columnsToMeasure, row) => { columnsToMeasure.sort((a, b) => a - b); const screenLine = this.renderedScreenLineForRow(row); const lineComponent = this.lineComponentsByScreenLineId.get( screenLine.id ); if (!lineComponent) { const error = new Error( 'Requested measurement of a line component that is not currently rendered' ); error.metadata = { row, columnsToMeasure, renderedScreenLineIds: this.renderedScreenLines.map(line => line.id), extraRenderedScreenLineIds: Array.from( this.extraRenderedScreenLines.keys() ), lineComponentScreenLineIds: Array.from( this.lineComponentsByScreenLineId.keys() ), renderedStartRow: this.getRenderedStartRow(), renderedEndRow: this.getRenderedEndRow(), requestedScreenLineId: screenLine.id }; throw error; } const lineNode = lineComponent.element; const textNodes = lineComponent.textNodes; let positionsForLine = this.horizontalPixelPositionsByScreenLineId.get( screenLine.id ); if (positionsForLine == null) { positionsForLine = new Map(); this.horizontalPixelPositionsByScreenLineId.set( screenLine.id, positionsForLine ); } this.measureHorizontalPositionsOnLine( lineNode, textNodes, columnsToMeasure, positionsForLine ); }); this.horizontalPositionsToMeasure.clear(); } measureHorizontalPositionsOnLine( lineNode, textNodes, columnsToMeasure, positions ) { let lineNodeClientLeft = -1; let textNodeStartColumn = 0; let textNodesIndex = 0; let lastTextNodeRight = null; // eslint-disable-next-line no-labels columnLoop: for ( let columnsIndex = 0; columnsIndex < columnsToMeasure.length; columnsIndex++ ) { const nextColumnToMeasure = columnsToMeasure[columnsIndex]; while (textNodesIndex < textNodes.length) { if (nextColumnToMeasure === 0) { positions.set(0, 0); continue columnLoop; // eslint-disable-line no-labels } if (positions.has(nextColumnToMeasure)) continue columnLoop; // eslint-disable-line no-labels const textNode = textNodes[textNodesIndex]; const textNodeEndColumn = textNodeStartColumn + textNode.textContent.length; if (nextColumnToMeasure < textNodeEndColumn) { let clientPixelPosition; if (nextColumnToMeasure === textNodeStartColumn) { clientPixelPosition = clientRectForRange(textNode, 0, 1).left; } else { clientPixelPosition = clientRectForRange( textNode, 0, nextColumnToMeasure - textNodeStartColumn ).right; } if (lineNodeClientLeft === -1) { lineNodeClientLeft = lineNode.getBoundingClientRect().left; } positions.set( nextColumnToMeasure, Math.round(clientPixelPosition - lineNodeClientLeft) ); continue columnLoop; // eslint-disable-line no-labels } else { textNodesIndex++; textNodeStartColumn = textNodeEndColumn; } } if (lastTextNodeRight == null) { const lastTextNode = textNodes[textNodes.length - 1]; lastTextNodeRight = clientRectForRange( lastTextNode, 0, lastTextNode.textContent.length ).right; } if (lineNodeClientLeft === -1) { lineNodeClientLeft = lineNode.getBoundingClientRect().left; } positions.set( nextColumnToMeasure, Math.round(lastTextNodeRight - lineNodeClientLeft) ); } } rowForPixelPosition(pixelPosition) { return Math.max(0, this.lineTopIndex.rowForPixelPosition(pixelPosition)); } heightForBlockDecorationsBeforeRow(row) { return ( this.pixelPositionAfterBlocksForRow(row) - this.pixelPositionBeforeBlocksForRow(row) ); } heightForBlockDecorationsAfterRow(row) { const currentRowBottom = this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight(); const nextRowTop = this.pixelPositionBeforeBlocksForRow(row + 1); return nextRowTop - currentRowBottom; } pixelPositionBeforeBlocksForRow(row) { return this.lineTopIndex.pixelPositionBeforeBlocksForRow(row); } pixelPositionAfterBlocksForRow(row) { return this.lineTopIndex.pixelPositionAfterBlocksForRow(row); } pixelLeftForRowAndColumn(row, column) { if (column === 0) return 0; const screenLine = this.renderedScreenLineForRow(row); if (screenLine) { const horizontalPositionsByColumn = this.horizontalPixelPositionsByScreenLineId.get( screenLine.id ); if (horizontalPositionsByColumn) { return horizontalPositionsByColumn.get(column); } } } screenPositionForPixelPosition({ top, left }) { const { model } = this.props; const row = Math.min( this.rowForPixelPosition(top), model.getApproximateScreenLineCount() - 1 ); let screenLine = this.renderedScreenLineForRow(row); if (!screenLine) { this.requestLineToMeasure(row, model.screenLineForScreenRow(row)); this.updateSyncBeforeMeasuringContent(); this.measureContentDuringUpdateSync(); screenLine = this.renderedScreenLineForRow(row); } const linesClientLeft = this.refs.lineTiles.getBoundingClientRect().left; const targetClientLeft = linesClientLeft + Math.max(0, left); const { textNodes } = this.lineComponentsByScreenLineId.get(screenLine.id); let containingTextNodeIndex; { let low = 0; let high = textNodes.length - 1; while (low <= high) { const mid = low + ((high - low) >> 1); const textNode = textNodes[mid]; const textNodeRect = clientRectForRange(textNode, 0, textNode.length); if (targetClientLeft < textNodeRect.left) { high = mid - 1; containingTextNodeIndex = Math.max(0, mid - 1); } else if (targetClientLeft > textNodeRect.right) { low = mid + 1; containingTextNodeIndex = Math.min(textNodes.length - 1, mid + 1); } else { containingTextNodeIndex = mid; break; } } } const containingTextNode = textNodes[containingTextNodeIndex]; let characterIndex = 0; { let low = 0; let high = containingTextNode.length - 1; while (low <= high) { const charIndex = low + ((high - low) >> 1); const nextCharIndex = isPairedCharacter( containingTextNode.textContent, charIndex ) ? charIndex + 2 : charIndex + 1; const rangeRect = clientRectForRange( containingTextNode, charIndex, nextCharIndex ); if (targetClientLeft < rangeRect.left) { high = charIndex - 1; characterIndex = Math.max(0, charIndex - 1); } else if (targetClientLeft > rangeRect.right) { low = nextCharIndex; characterIndex = Math.min( containingTextNode.textContent.length, nextCharIndex ); } else { if (targetClientLeft <= (rangeRect.left + rangeRect.right) / 2) { characterIndex = charIndex; } else { characterIndex = nextCharIndex; } break; } } } let textNodeStartColumn = 0; for (let i = 0; i < containingTextNodeIndex; i++) { textNodeStartColumn = textNodeStartColumn + textNodes[i].length; } const column = textNodeStartColumn + characterIndex; return Point(row, column); } didResetDisplayLayer() { this.spliceLineTopIndex(0, Infinity, Infinity); this.scheduleUpdate(); } didChangeDisplayLayer(changes) { for (let i = 0; i < changes.length; i++) { const { oldRange, newRange } = changes[i]; this.spliceLineTopIndex( newRange.start.row, oldRange.end.row - oldRange.start.row, newRange.end.row - newRange.start.row ); } this.scheduleUpdate(); } didChangeSelectionRange() { const { model } = this.props; if (this.getPlatform() === 'linux') { if (this.selectionClipboardImmediateId) { clearImmediate(this.selectionClipboardImmediateId); } this.selectionClipboardImmediateId = setImmediate(() => { this.selectionClipboardImmediateId = null; if (model.isDestroyed()) return; const selectedText = model.getSelectedText(); if (selectedText) { // This uses ipcRenderer.send instead of clipboard.writeText because // clipboard.writeText is a sync ipcRenderer call on Linux and that // will slow down selections. electron.ipcRenderer.send( 'write-text-to-selection-clipboard', selectedText ); } }); } } observeBlockDecorations() { const { model } = this.props; const decorations = model.getDecorations({ type: 'block' }); for (let i = 0; i < decorations.length; i++) { this.addBlockDecoration(decorations[i]); } } addBlockDecoration(decoration, subscribeToChanges = true) { const marker = decoration.getMarker(); const { item, position } = decoration.getProperties(); const element = TextEditor.viewForItem(item); if (marker.isValid()) { const row = marker.getHeadScreenPosition().row; this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after'); this.blockDecorationsToMeasure.add(decoration); this.blockDecorationsByElement.set(element, decoration); this.blockDecorationResizeObserver.observe(element); this.scheduleUpdate(); } if (subscribeToChanges) { let wasValid = marker.isValid(); const didUpdateDisposable = marker.bufferMarker.onDidChange( ({ textChanged }) => { const isValid = marker.isValid(); if (wasValid && !isValid) { wasValid = false; this.blockDecorationsToMeasure.delete(decoration); this.heightsByBlockDecoration.delete(decoration); this.blockDecorationsByElement.delete(element); this.blockDecorationResizeObserver.unobserve(element); this.lineTopIndex.removeBlock(decoration); this.scheduleUpdate(); } else if (!wasValid && isValid) { wasValid = true; this.addBlockDecoration(decoration, false); } else if (isValid && !textChanged) { this.lineTopIndex.moveBlock( decoration, marker.getHeadScreenPosition().row ); this.scheduleUpdate(); } } ); const didDestroyDisposable = decoration.onDidDestroy(() => { didUpdateDisposable.dispose(); didDestroyDisposable.dispose(); if (wasValid) { wasValid = false; this.blockDecorationsToMeasure.delete(decoration); this.heightsByBlockDecoration.delete(decoration); this.blockDecorationsByElement.delete(element); this.blockDecorationResizeObserver.unobserve(element); this.lineTopIndex.removeBlock(decoration); this.scheduleUpdate(); } }); } } didResizeBlockDecorations(entries) { if (!this.visible) return; for (let i = 0; i < entries.length; i++) { const { target, contentRect } = entries[i]; const decoration = this.blockDecorationsByElement.get(target); const previousHeight = this.heightsByBlockDecoration.get(decoration); if ( this.element.contains(target) && contentRect.height !== previousHeight ) { this.invalidateBlockDecorationDimensions(decoration); } } } invalidateBlockDecorationDimensions(decoration) { this.blockDecorationsToMeasure.add(decoration); this.scheduleUpdate(); } spliceLineTopIndex(startRow, oldExtent, newExtent) { const invalidatedBlockDecorations = this.lineTopIndex.splice( startRow, oldExtent, newExtent ); invalidatedBlockDecorations.forEach(decoration => { const newPosition = decoration.getMarker().getHeadScreenPosition(); this.lineTopIndex.moveBlock(decoration, newPosition.row); }); } isVisible() { return this.element.offsetWidth > 0 || this.element.offsetHeight > 0; } getWindowInnerHeight() { return window.innerHeight; } getWindowInnerWidth() { return window.innerWidth; } getLineHeight() { return this.measurements.lineHeight; } getBaseCharacterWidth() { return this.measurements.baseCharacterWidth; } getLongestLineWidth() { return this.measurements.longestLineWidth; } getClientContainerHeight() { return this.measurements.clientContainerHeight; } getClientContainerWidth() { return this.measurements.clientContainerWidth; } getScrollContainerWidth() { if (this.props.model.getAutoWidth()) { return this.getScrollWidth(); } else { return this.getClientContainerWidth() - this.getGutterContainerWidth(); } } getScrollContainerHeight() { if (this.props.model.getAutoHeight()) { return this.getScrollHeight() + this.getHorizontalScrollbarHeight(); } else { return this.getClientContainerHeight(); } } getScrollContainerClientWidth() { return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth(); } getScrollContainerClientHeight() { return ( this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() ); } canScrollVertically() { const { model } = this.props; if (model.isMini()) return false; if (model.getAutoHeight()) return false; return this.getContentHeight() > this.getScrollContainerClientHeight(); } canScrollHorizontally() { const { model } = this.props; if (model.isMini()) return false; if (model.getAutoWidth()) return false; if (model.isSoftWrapped()) return false; return this.getContentWidth() > this.getScrollContainerClientWidth(); } getScrollHeight() { if (this.props.model.getScrollPastEnd()) { return ( this.getContentHeight() + Math.max( 3 * this.getLineHeight(), this.getScrollContainerClientHeight() - 3 * this.getLineHeight() ) ); } else if (this.props.model.getAutoHeight()) { return this.getContentHeight(); } else { return Math.max( this.getContentHeight(), this.getScrollContainerClientHeight() ); } } getScrollWidth() { const { model } = this.props; if (model.isSoftWrapped()) { return this.getScrollContainerClientWidth(); } else if (model.getAutoWidth()) { return this.getContentWidth(); } else { return Math.max( this.getContentWidth(), this.getScrollContainerClientWidth() ); } } getContentHeight() { return this.pixelPositionAfterBlocksForRow( this.props.model.getApproximateScreenLineCount() ); } getContentWidth() { return Math.ceil(this.getLongestLineWidth() + this.getBaseCharacterWidth()); } getScrollContainerClientWidthInBaseCharacters() { return Math.floor( this.getScrollContainerClientWidth() / this.getBaseCharacterWidth() ); } getGutterContainerWidth() { return this.measurements.gutterContainerWidth; } getLineNumberGutterWidth() { return this.measurements.lineNumberGutterWidth; } getVerticalScrollbarWidth() { return this.measurements.verticalScrollbarWidth; } getHorizontalScrollbarHeight() { return this.measurements.horizontalScrollbarHeight; } getRowsPerTile() { return this.props.rowsPerTile || DEFAULT_ROWS_PER_TILE; } tileStartRowForRow(row) { return row - (row % this.getRowsPerTile()); } getRenderedStartRow() { if (this.derivedDimensionsCache.renderedStartRow == null) { this.derivedDimensionsCache.renderedStartRow = this.tileStartRowForRow( this.getFirstVisibleRow() ); } return this.derivedDimensionsCache.renderedStartRow; } getRenderedEndRow() { if (this.derivedDimensionsCache.renderedEndRow == null) { this.derivedDimensionsCache.renderedEndRow = Math.min( this.props.model.getApproximateScreenLineCount(), this.getRenderedStartRow() + this.getVisibleTileCount() * this.getRowsPerTile() ); } return this.derivedDimensionsCache.renderedEndRow; } getRenderedRowCount() { if (this.derivedDimensionsCache.renderedRowCount == null) { this.derivedDimensionsCache.renderedRowCount = Math.max( 0, this.getRenderedEndRow() - this.getRenderedStartRow() ); } return this.derivedDimensionsCache.renderedRowCount; } getRenderedTileCount() { if (this.derivedDimensionsCache.renderedTileCount == null) { this.derivedDimensionsCache.renderedTileCount = Math.ceil( this.getRenderedRowCount() / this.getRowsPerTile() ); } return this.derivedDimensionsCache.renderedTileCount; } getFirstVisibleRow() { if (this.derivedDimensionsCache.firstVisibleRow == null) { this.derivedDimensionsCache.firstVisibleRow = this.rowForPixelPosition( this.getScrollTop() ); } return this.derivedDimensionsCache.firstVisibleRow; } getLastVisibleRow() { if (this.derivedDimensionsCache.lastVisibleRow == null) { this.derivedDimensionsCache.lastVisibleRow = Math.min( this.props.model.getApproximateScreenLineCount() - 1, this.rowForPixelPosition(this.getScrollBottom()) ); } return this.derivedDimensionsCache.lastVisibleRow; } // We may render more tiles than needed if some contain block decorations, // but keeping this calculation simple ensures the number of tiles remains // fixed for a given editor height, which eliminates situations where a // tile is repeatedly added and removed during scrolling in certain // combinations of editor height and line height. getVisibleTileCount() { if (this.derivedDimensionsCache.visibleTileCount == null) { const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() / this.getRowsPerTile(); this.derivedDimensionsCache.visibleTileCount = Math.ceil(editorHeightInTiles) + 1; } return this.derivedDimensionsCache.visibleTileCount; } getFirstVisibleColumn() { return Math.floor(this.getScrollLeft() / this.getBaseCharacterWidth()); } getScrollTop() { this.scrollTop = Math.min(this.getMaxScrollTop(), this.scrollTop); return this.scrollTop; } setScrollTop(scrollTop) { if (Number.isNaN(scrollTop) || scrollTop == null) return false; scrollTop = roundToPhysicalPixelBoundary( Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)) ); if (scrollTop !== this.scrollTop) { this.derivedDimensionsCache = {}; this.scrollTopPending = true; this.scrollTop = scrollTop; this.element.emitter.emit('did-change-scroll-top', scrollTop); return true; } else { return false; } } getMaxScrollTop() { return Math.round( Math.max( 0, this.getScrollHeight() - this.getScrollContainerClientHeight() ) ); } getScrollBottom() { return this.getScrollTop() + this.getScrollContainerClientHeight(); } setScrollBottom(scrollBottom) { return this.setScrollTop( scrollBottom - this.getScrollContainerClientHeight() ); } getScrollLeft() { return this.scrollLeft; } setScrollLeft(scrollLeft) { if (Number.isNaN(scrollLeft) || scrollLeft == null) return false; scrollLeft = roundToPhysicalPixelBoundary( Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)) ); if (scrollLeft !== this.scrollLeft) { this.scrollLeftPending = true; this.scrollLeft = scrollLeft; this.element.emitter.emit('did-change-scroll-left', scrollLeft); return true; } else { return false; } } getMaxScrollLeft() { return Math.round( Math.max(0, this.getScrollWidth() - this.getScrollContainerClientWidth()) ); } getScrollRight() { return this.getScrollLeft() + this.getScrollContainerClientWidth(); } setScrollRight(scrollRight) { return this.setScrollLeft( scrollRight - this.getScrollContainerClientWidth() ); } setScrollTopRow(scrollTopRow, scheduleUpdate = true) { if (this.hasInitialMeasurements) { const didScroll = this.setScrollTop( this.pixelPositionBeforeBlocksForRow(scrollTopRow) ); if (didScroll && scheduleUpdate) { this.scheduleUpdate(); } return didScroll; } else { this.pendingScrollTopRow = scrollTopRow; return false; } } getScrollTopRow() { if (this.hasInitialMeasurements) { return this.rowForPixelPosition(this.getScrollTop()); } else { return this.pendingScrollTopRow || 0; } } setScrollLeftColumn(scrollLeftColumn, scheduleUpdate = true) { if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) { const didScroll = this.setScrollLeft( scrollLeftColumn * this.getBaseCharacterWidth() ); if (didScroll && scheduleUpdate) { this.scheduleUpdate(); } return didScroll; } else { this.pendingScrollLeftColumn = scrollLeftColumn; return false; } } getScrollLeftColumn() { if (this.hasInitialMeasurements && this.getLongestLineWidth() != null) { return Math.round(this.getScrollLeft() / this.getBaseCharacterWidth()); } else { return this.pendingScrollLeftColumn || 0; } } // Ensure the spatial index is populated with rows that are currently visible populateVisibleRowRange(renderedStartRow) { const { model } = this.props; const previousScreenLineCount = model.getApproximateScreenLineCount(); const renderedEndRow = renderedStartRow + this.getVisibleTileCount() * this.getRowsPerTile(); this.props.model.displayLayer.populateSpatialIndexIfNeeded( Infinity, renderedEndRow ); // If the approximate screen line count changes, previously-cached derived // dimensions could now be out of date. if (model.getApproximateScreenLineCount() !== previousScreenLineCount) { this.derivedDimensionsCache = {}; } } populateVisibleTiles() { const startRow = this.getRenderedStartRow(); const endRow = this.getRenderedEndRow(); const freeTileIds = []; for (let i = 0; i < this.renderedTileStartRows.length; i++) { const tileStartRow = this.renderedTileStartRows[i]; if (tileStartRow < startRow || tileStartRow >= endRow) { const tileId = this.idsByTileStartRow.get(tileStartRow); freeTileIds.push(tileId); this.idsByTileStartRow.delete(tileStartRow); } } const rowsPerTile = this.getRowsPerTile(); this.renderedTileStartRows.length = this.getRenderedTileCount(); for ( let tileStartRow = startRow, i = 0; tileStartRow < endRow; tileStartRow = tileStartRow + rowsPerTile, i++ ) { this.renderedTileStartRows[i] = tileStartRow; if (!this.idsByTileStartRow.has(tileStartRow)) { if (freeTileIds.length > 0) { this.idsByTileStartRow.set(tileStartRow, freeTileIds.shift()); } else { this.idsByTileStartRow.set(tileStartRow, this.nextTileId++); } } } this.renderedTileStartRows.sort( (a, b) => this.idsByTileStartRow.get(a) - this.idsByTileStartRow.get(b) ); } getNextUpdatePromise() { if (!this.nextUpdatePromise) { this.nextUpdatePromise = new Promise(resolve => { this.resolveNextUpdatePromise = () => { this.nextUpdatePromise = null; this.resolveNextUpdatePromise = null; resolve(); }; }); } return this.nextUpdatePromise; } setInputEnabled(inputEnabled) { this.props.model.update({ keyboardInputEnabled: inputEnabled }); } isInputEnabled() { return ( !this.props.model.isReadOnly() && this.props.model.isKeyboardInputEnabled() ); } getHiddenInput() { return this.refs.cursorsAndInput.refs.hiddenInput; } getPlatform() { return this.props.platform || process.platform; } getChromeVersion() { return this.props.chromeVersion || parseInt(process.versions.chrome); } }; class DummyScrollbarComponent { constructor(props) { this.props = props; etch.initialize(this); } update(newProps) { const oldProps = this.props; this.props = newProps; etch.updateSync(this); const shouldFlushScrollPosition = newProps.scrollTop !== oldProps.scrollTop || newProps.scrollLeft !== oldProps.scrollLeft; if (shouldFlushScrollPosition) this.flushScrollPosition(); } flushScrollPosition() { if (this.props.orientation === 'horizontal') { this.element.scrollLeft = this.props.scrollLeft; } else { this.element.scrollTop = this.props.scrollTop; } } render() { const { orientation, scrollWidth, scrollHeight, verticalScrollbarWidth, horizontalScrollbarHeight, canScroll, forceScrollbarVisible, didScroll } = this.props; const outerStyle = { position: 'absolute', contain: 'content', zIndex: 1, willChange: 'transform' }; if (!canScroll) outerStyle.visibility = 'hidden'; const innerStyle = {}; if (orientation === 'horizontal') { let right = verticalScrollbarWidth || 0; outerStyle.bottom = 0; outerStyle.left = 0; outerStyle.right = right + 'px'; outerStyle.height = '15px'; outerStyle.overflowY = 'hidden'; outerStyle.overflowX = forceScrollbarVisible ? 'scroll' : 'auto'; outerStyle.cursor = 'default'; innerStyle.height = '15px'; innerStyle.width = (scrollWidth || 0) + 'px'; } else { let bottom = horizontalScrollbarHeight || 0; outerStyle.right = 0; outerStyle.top = 0; outerStyle.bottom = bottom + 'px'; outerStyle.width = '15px'; outerStyle.overflowX = 'hidden'; outerStyle.overflowY = forceScrollbarVisible ? 'scroll' : 'auto'; outerStyle.cursor = 'default'; innerStyle.width = '15px'; innerStyle.height = (scrollHeight || 0) + 'px'; } return $.div( { className: `${orientation}-scrollbar`, style: outerStyle, on: { scroll: didScroll, mousedown: this.didMouseDown } }, $.div({ style: innerStyle }) ); } didMouseDown(event) { let { bottom, right } = this.element.getBoundingClientRect(); const clickedOnScrollbar = this.props.orientation === 'horizontal' ? event.clientY >= bottom - this.getRealScrollbarHeight() : event.clientX >= right - this.getRealScrollbarWidth(); if (!clickedOnScrollbar) this.props.didMouseDown(event); } getRealScrollbarWidth() { return this.element.offsetWidth - this.element.clientWidth; } getRealScrollbarHeight() { return this.element.offsetHeight - this.element.clientHeight; } } class GutterContainerComponent { constructor(props) { this.props = props; etch.initialize(this); } update(props) { if (this.shouldUpdate(props)) { this.props = props; etch.updateSync(this); } } shouldUpdate(props) { return ( !props.measuredContent || props.lineNumberGutterWidth !== this.props.lineNumberGutterWidth ); } render() { const { hasInitialMeasurements, scrollTop, scrollHeight, guttersToRender, decorationsToRender } = this.props; const innerStyle = { willChange: 'transform', display: 'flex' }; if (hasInitialMeasurements) { innerStyle.transform = `translateY(${-roundToPhysicalPixelBoundary( scrollTop )}px)`; } return $.div( { ref: 'gutterContainer', key: 'gutterContainer', className: 'gutter-container', style: { position: 'relative', zIndex: 1, backgroundColor: 'inherit' } }, $.div( { style: innerStyle }, guttersToRender.map(gutter => { if (gutter.type === 'line-number') { return this.renderLineNumberGutter(gutter); } else { return $(CustomGutterComponent, { key: gutter, element: gutter.getElement(), name: gutter.name, visible: gutter.isVisible(), height: scrollHeight, decorations: decorationsToRender.customGutter.get(gutter.name) }); } }) ) ); } renderLineNumberGutter(gutter) { const { rootComponent, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, scrollHeight, lineNumberGutterWidth, lineHeight } = this.props; if (!gutter.isVisible()) { return null; } const oneTrueLineNumberGutter = gutter.name === 'line-number'; const ref = oneTrueLineNumberGutter ? 'lineNumberGutter' : undefined; const width = oneTrueLineNumberGutter ? lineNumberGutterWidth : undefined; if (hasInitialMeasurements) { const { maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags } = lineNumbersToRender; return $(LineNumberGutterComponent, { ref, element: gutter.getElement(), name: gutter.name, className: gutter.className, labelFn: gutter.labelFn, onMouseDown: gutter.onMouseDown, onMouseMove: gutter.onMouseMove, rootComponent: rootComponent, startRow: renderedStartRow, endRow: renderedEndRow, rowsPerTile: rowsPerTile, maxDigits: maxDigits, keys: keys, bufferRows: bufferRows, screenRows: screenRows, softWrappedFlags: softWrappedFlags, foldableFlags: foldableFlags, decorations: decorationsToRender.lineNumbers.get(gutter.name) || [], blockDecorations: decorationsToRender.blocks, didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, height: scrollHeight, width, lineHeight: lineHeight, showLineNumbers }); } else { return $(LineNumberGutterComponent, { ref, element: gutter.getElement(), name: gutter.name, className: gutter.className, onMouseDown: gutter.onMouseDown, onMouseMove: gutter.onMouseMove, maxDigits: lineNumbersToRender.maxDigits, showLineNumbers }); } } } class LineNumberGutterComponent { constructor(props) { this.props = props; this.element = this.props.element; this.virtualNode = $.div(null); this.virtualNode.domNode = this.element; this.nodePool = new NodePool(); etch.updateSync(this); } update(newProps) { if (this.shouldUpdate(newProps)) { this.props = newProps; etch.updateSync(this); } } render() { const { rootComponent, showLineNumbers, height, width, startRow, endRow, rowsPerTile, maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations, className } = this.props; let children = null; if (bufferRows) { children = new Array(rootComponent.renderedTileStartRows.length); for (let i = 0; i < rootComponent.renderedTileStartRows.length; i++) { const tileStartRow = rootComponent.renderedTileStartRows[i]; const tileEndRow = Math.min(endRow, tileStartRow + rowsPerTile); const tileChildren = new Array(tileEndRow - tileStartRow); for (let row = tileStartRow; row < tileEndRow; row++) { const indexInTile = row - tileStartRow; const j = row - startRow; const key = keys[j]; const softWrapped = softWrappedFlags[j]; const foldable = foldableFlags[j]; const bufferRow = bufferRows[j]; const screenRow = screenRows[j]; let className = 'line-number'; if (foldable) className = className + ' foldable'; const decorationsForRow = decorations[row - startRow]; if (decorationsForRow) className = className + ' ' + decorationsForRow; let number = null; if (showLineNumbers) { if (this.props.labelFn == null) { number = softWrapped ? '•' : bufferRow + 1; number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number; } else { number = this.props.labelFn({ bufferRow, screenRow, foldable, softWrapped, maxDigits }); } } // We need to adjust the line number position to account for block // decorations preceding the current row and following the preceding // row. Note that we ignore the latter when the line number starts at // the beginning of the tile, because the tile will already be // positioned to take into account block decorations added after the // last row of the previous tile. let marginTop = rootComponent.heightForBlockDecorationsBeforeRow(row); if (indexInTile > 0) marginTop += rootComponent.heightForBlockDecorationsAfterRow( row - 1 ); tileChildren[row - tileStartRow] = $(LineNumberComponent, { key, className, width, bufferRow, screenRow, number, marginTop, nodePool: this.nodePool }); } const tileTop = rootComponent.pixelPositionBeforeBlocksForRow( tileStartRow ); const tileBottom = rootComponent.pixelPositionBeforeBlocksForRow( tileEndRow ); const tileHeight = tileBottom - tileTop; const tileWidth = width != null && width > 0 ? width + 'px' : ''; children[i] = $.div( { key: rootComponent.idsByTileStartRow.get(tileStartRow), style: { contain: 'layout style', position: 'absolute', top: 0, height: tileHeight + 'px', width: tileWidth, transform: `translateY(${tileTop}px)` } }, ...tileChildren ); } } let rootClassName = 'gutter line-numbers'; if (className) { rootClassName += ' ' + className; } return $.div( { className: rootClassName, attributes: { 'gutter-name': this.props.name }, style: { position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px' }, on: { mousedown: this.didMouseDown, mousemove: this.didMouseMove } }, $.div( { key: 'placeholder', className: 'line-number dummy', style: { visibility: 'hidden' } }, showLineNumbers ? '0'.repeat(maxDigits) : null, $.div({ className: 'icon-right' }) ), children ); } shouldUpdate(newProps) { const oldProps = this.props; if (oldProps.showLineNumbers !== newProps.showLineNumbers) return true; if (oldProps.height !== newProps.height) return true; if (oldProps.width !== newProps.width) return true; if (oldProps.lineHeight !== newProps.lineHeight) return true; if (oldProps.startRow !== newProps.startRow) return true; if (oldProps.endRow !== newProps.endRow) return true; if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true; if (oldProps.maxDigits !== newProps.maxDigits) return true; if (oldProps.labelFn !== newProps.labelFn) return true; if (oldProps.className !== newProps.className) return true; if (newProps.didMeasureVisibleBlockDecoration) return true; if (!arraysEqual(oldProps.keys, newProps.keys)) return true; if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true; if (!arraysEqual(oldProps.foldableFlags, newProps.foldableFlags)) return true; if (!arraysEqual(oldProps.decorations, newProps.decorations)) return true; let oldTileStartRow = oldProps.startRow; let newTileStartRow = newProps.startRow; while ( oldTileStartRow < oldProps.endRow || newTileStartRow < newProps.endRow ) { let oldTileBlockDecorations = oldProps.blockDecorations.get( oldTileStartRow ); let newTileBlockDecorations = newProps.blockDecorations.get( newTileStartRow ); if (oldTileBlockDecorations && newTileBlockDecorations) { if (oldTileBlockDecorations.size !== newTileBlockDecorations.size) return true; let blockDecorationsChanged = false; oldTileBlockDecorations.forEach((oldDecorations, screenLineId) => { if (!blockDecorationsChanged) { const newDecorations = newTileBlockDecorations.get(screenLineId); blockDecorationsChanged = newDecorations == null || !arraysEqual(oldDecorations, newDecorations); } }); if (blockDecorationsChanged) return true; newTileBlockDecorations.forEach((newDecorations, screenLineId) => { if (!blockDecorationsChanged) { const oldDecorations = oldTileBlockDecorations.get(screenLineId); blockDecorationsChanged = oldDecorations == null; } }); if (blockDecorationsChanged) return true; } else if (oldTileBlockDecorations) { return true; } else if (newTileBlockDecorations) { return true; } oldTileStartRow += oldProps.rowsPerTile; newTileStartRow += newProps.rowsPerTile; } return false; } didMouseDown(event) { if (this.props.onMouseDown == null) { this.props.rootComponent.didMouseDownOnLineNumberGutter(event); } else { const { bufferRow, screenRow } = event.target.dataset; this.props.onMouseDown({ bufferRow: parseInt(bufferRow, 10), screenRow: parseInt(screenRow, 10), domEvent: event }); } } didMouseMove(event) { if (this.props.onMouseMove != null) { const { bufferRow, screenRow } = event.target.dataset; this.props.onMouseMove({ bufferRow: parseInt(bufferRow, 10), screenRow: parseInt(screenRow, 10), domEvent: event }); } } } class LineNumberComponent { constructor(props) { const { className, width, marginTop, bufferRow, screenRow, number, nodePool } = props; this.props = props; const style = {}; if (width != null && width > 0) style.width = width + 'px'; if (marginTop != null && marginTop > 0) style.marginTop = marginTop + 'px'; this.element = nodePool.getElement('DIV', className, style); this.element.dataset.bufferRow = bufferRow; this.element.dataset.screenRow = screenRow; if (number) this.element.appendChild(nodePool.getTextNode(number)); this.element.appendChild(nodePool.getElement('DIV', 'icon-right', null)); } destroy() { this.element.remove(); this.props.nodePool.release(this.element); } update(props) { const { nodePool, className, width, marginTop, bufferRow, screenRow, number } = props; if (this.props.bufferRow !== bufferRow) this.element.dataset.bufferRow = bufferRow; if (this.props.screenRow !== screenRow) this.element.dataset.screenRow = screenRow; if (this.props.className !== className) this.element.className = className; if (this.props.width !== width) { if (width != null && width > 0) { this.element.style.width = width + 'px'; } else { this.element.style.width = ''; } } if (this.props.marginTop !== marginTop) { if (marginTop != null && marginTop > 0) { this.element.style.marginTop = marginTop + 'px'; } else { this.element.style.marginTop = ''; } } if (this.props.number !== number) { if (this.props.number != null) { const numberNode = this.element.firstChild; numberNode.remove(); nodePool.release(numberNode); } if (number != null) { this.element.insertBefore( nodePool.getTextNode(number), this.element.firstChild ); } } this.props = props; } } class CustomGutterComponent { constructor(props) { this.props = props; this.element = this.props.element; this.virtualNode = $.div(null); this.virtualNode.domNode = this.element; etch.updateSync(this); } update(props) { this.props = props; etch.updateSync(this); } destroy() { etch.destroy(this); } render() { let className = 'gutter'; if (this.props.className) { className += ' ' + this.props.className; } return $.div( { className, attributes: { 'gutter-name': this.props.name }, style: { display: this.props.visible ? '' : 'none' } }, $.div( { className: 'custom-decorations', style: { height: this.props.height + 'px' } }, this.renderDecorations() ) ); } renderDecorations() { if (!this.props.decorations) return null; return this.props.decorations.map(({ className, element, top, height }) => { return $(CustomGutterDecorationComponent, { className, element, top, height }); }); } } class CustomGutterDecorationComponent { constructor(props) { this.props = props; this.element = document.createElement('div'); const { top, height, className, element } = this.props; this.element.style.position = 'absolute'; this.element.style.top = top + 'px'; this.element.style.height = height + 'px'; if (className != null) this.element.className = className; if (element != null) { this.element.appendChild(element); element.style.height = height + 'px'; } } update(newProps) { const oldProps = this.props; this.props = newProps; if (newProps.top !== oldProps.top) this.element.style.top = newProps.top + 'px'; if (newProps.height !== oldProps.height) { this.element.style.height = newProps.height + 'px'; if (newProps.element) newProps.element.style.height = newProps.height + 'px'; } if (newProps.className !== oldProps.className) this.element.className = newProps.className || ''; if (newProps.element !== oldProps.element) { if (this.element.firstChild) this.element.firstChild.remove(); if (newProps.element != null) { this.element.appendChild(newProps.element); newProps.element.style.height = newProps.height + 'px'; } } } } class CursorsAndInputComponent { constructor(props) { this.props = props; etch.initialize(this); } update(props) { if (props.measuredContent) { this.props = props; etch.updateSync(this); } } updateCursorBlinkSync(cursorsBlinkedOff) { this.props.cursorsBlinkedOff = cursorsBlinkedOff; const className = this.getCursorsClassName(); this.refs.cursors.className = className; this.virtualNode.props.className = className; } render() { const { lineHeight, decorationsToRender, scrollHeight, scrollWidth } = this.props; const className = this.getCursorsClassName(); const cursorHeight = lineHeight + 'px'; const children = [this.renderHiddenInput()]; for (let i = 0; i < decorationsToRender.cursors.length; i++) { const { pixelLeft, pixelTop, pixelWidth, className: extraCursorClassName, style: extraCursorStyle } = decorationsToRender.cursors[i]; let cursorClassName = 'cursor'; if (extraCursorClassName) cursorClassName += ' ' + extraCursorClassName; const cursorStyle = { height: cursorHeight, width: Math.min(pixelWidth, scrollWidth - pixelLeft) + 'px', transform: `translate(${pixelLeft}px, ${pixelTop}px)` }; if (extraCursorStyle) Object.assign(cursorStyle, extraCursorStyle); children.push( $.div({ className: cursorClassName, style: cursorStyle }) ); } return $.div( { key: 'cursors', ref: 'cursors', className, style: { position: 'absolute', contain: 'strict', zIndex: 1, width: scrollWidth + 'px', height: scrollHeight + 'px', pointerEvents: 'none', userSelect: 'none' } }, children ); } getCursorsClassName() { return this.props.cursorsBlinkedOff ? 'cursors blink-off' : 'cursors'; } renderHiddenInput() { const { lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput, didPaste, didTextInput, didKeydown, didKeyup, didKeypress, didCompositionStart, didCompositionUpdate, didCompositionEnd, tabIndex } = this.props; let top, left; if (hiddenInputPosition) { top = hiddenInputPosition.pixelTop; left = hiddenInputPosition.pixelLeft; } else { top = 0; left = 0; } return $.input({ ref: 'hiddenInput', key: 'hiddenInput', className: 'hidden-input', on: { blur: didBlurHiddenInput, focus: didFocusHiddenInput, paste: didPaste, textInput: didTextInput, keydown: didKeydown, keyup: didKeyup, keypress: didKeypress, compositionstart: didCompositionStart, compositionupdate: didCompositionUpdate, compositionend: didCompositionEnd }, tabIndex: tabIndex, style: { position: 'absolute', width: '1px', height: lineHeight + 'px', top: top + 'px', left: left + 'px', opacity: 0, padding: 0, border: 0 } }); } } class LinesTileComponent { constructor(props) { this.props = props; etch.initialize(this); this.createLines(); this.updateBlockDecorations({}, props); } update(newProps) { if (this.shouldUpdate(newProps)) { const oldProps = this.props; this.props = newProps; etch.updateSync(this); if (!newProps.measuredContent) { this.updateLines(oldProps, newProps); this.updateBlockDecorations(oldProps, newProps); } } } destroy() { for (let i = 0; i < this.lineComponents.length; i++) { this.lineComponents[i].destroy(); } this.lineComponents.length = 0; return etch.destroy(this); } render() { const { height, width, top } = this.props; return $.div( { style: { contain: 'layout style', position: 'absolute', height: height + 'px', width: width + 'px', transform: `translateY(${top}px)` } } // Lines and block decorations will be manually inserted here for efficiency ); } createLines() { const { tileStartRow, screenLines, lineDecorations, textDecorations, nodePool, displayLayer, lineComponentsByScreenLineId } = this.props; this.lineComponents = []; for (let i = 0, length = screenLines.length; i < length; i++) { const component = new LineComponent({ screenLine: screenLines[i], screenRow: tileStartRow + i, lineDecoration: lineDecorations[i], textDecorations: textDecorations[i], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.appendChild(component.element); this.lineComponents.push(component); } } updateLines(oldProps, newProps) { const { screenLines, tileStartRow, lineDecorations, textDecorations, nodePool, displayLayer, lineComponentsByScreenLineId } = newProps; const oldScreenLines = oldProps.screenLines; const newScreenLines = screenLines; const oldScreenLinesEndIndex = oldScreenLines.length; const newScreenLinesEndIndex = newScreenLines.length; let oldScreenLineIndex = 0; let newScreenLineIndex = 0; let lineComponentIndex = 0; while ( oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex ) { const oldScreenLine = oldScreenLines[oldScreenLineIndex]; const newScreenLine = newScreenLines[newScreenLineIndex]; if (oldScreenLineIndex >= oldScreenLinesEndIndex) { var newScreenLineComponent = new LineComponent({ screenLine: newScreenLine, screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.appendChild(newScreenLineComponent.element); this.lineComponents.push(newScreenLineComponent); newScreenLineIndex++; lineComponentIndex++; } else if (newScreenLineIndex >= newScreenLinesEndIndex) { this.lineComponents[lineComponentIndex].destroy(); this.lineComponents.splice(lineComponentIndex, 1); oldScreenLineIndex++; } else if (oldScreenLine === newScreenLine) { const lineComponent = this.lineComponents[lineComponentIndex]; lineComponent.update({ screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex] }); oldScreenLineIndex++; newScreenLineIndex++; lineComponentIndex++; } else { const oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf( oldScreenLine ); const newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf( newScreenLine ); if ( newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex ) { const newScreenLineComponents = []; while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { // eslint-disable-next-line no-redeclare var newScreenLineComponent = new LineComponent({ screenLine: newScreenLines[newScreenLineIndex], screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.insertBefore( newScreenLineComponent.element, this.getFirstElementForScreenLine(oldProps, oldScreenLine) ); newScreenLineComponents.push(newScreenLineComponent); newScreenLineIndex++; } this.lineComponents.splice( lineComponentIndex, 0, ...newScreenLineComponents ); lineComponentIndex = lineComponentIndex + newScreenLineComponents.length; } else if ( oldScreenLineIndex < newScreenLineIndexInOldScreenLines && newScreenLineIndexInOldScreenLines < oldScreenLinesEndIndex ) { while (oldScreenLineIndex < newScreenLineIndexInOldScreenLines) { this.lineComponents[lineComponentIndex].destroy(); this.lineComponents.splice(lineComponentIndex, 1); oldScreenLineIndex++; } } else { const oldScreenLineComponent = this.lineComponents[ lineComponentIndex ]; // eslint-disable-next-line no-redeclare var newScreenLineComponent = new LineComponent({ screenLine: newScreenLines[newScreenLineIndex], screenRow: tileStartRow + newScreenLineIndex, lineDecoration: lineDecorations[newScreenLineIndex], textDecorations: textDecorations[newScreenLineIndex], displayLayer, nodePool, lineComponentsByScreenLineId }); this.element.insertBefore( newScreenLineComponent.element, oldScreenLineComponent.element ); oldScreenLineComponent.destroy(); this.lineComponents[lineComponentIndex] = newScreenLineComponent; oldScreenLineIndex++; newScreenLineIndex++; lineComponentIndex++; } } } } getFirstElementForScreenLine(oldProps, screenLine) { const blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null; if (blockDecorations) { const blockDecorationElementsBeforeOldScreenLine = []; for (let i = 0; i < blockDecorations.length; i++) { const decoration = blockDecorations[i]; if (decoration.position !== 'after') { blockDecorationElementsBeforeOldScreenLine.push( TextEditor.viewForItem(decoration.item) ); } } for ( let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++ ) { const blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i]; if ( !blockDecorationElementsBeforeOldScreenLine.includes( blockDecorationElement.previousSibling ) ) { return blockDecorationElement; } } } return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element; } updateBlockDecorations(oldProps, newProps) { const { blockDecorations, lineComponentsByScreenLineId } = newProps; if (oldProps.blockDecorations) { oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { const newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null; for (let i = 0; i < oldDecorations.length; i++) { const oldDecoration = oldDecorations[i]; if (newDecorations && newDecorations.includes(oldDecoration)) continue; const element = TextEditor.viewForItem(oldDecoration.item); if (element.parentElement !== this.element) continue; element.remove(); } }); } if (blockDecorations) { blockDecorations.forEach((newDecorations, screenLineId) => { const oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null; const lineNode = lineComponentsByScreenLineId.get(screenLineId).element; let lastAfter = lineNode; for (let i = 0; i < newDecorations.length; i++) { const newDecoration = newDecorations[i]; const element = TextEditor.viewForItem(newDecoration.item); if (oldDecorations && oldDecorations.includes(newDecoration)) { if (newDecoration.position === 'after') { lastAfter = element; } continue; } if (newDecoration.position === 'after') { this.element.insertBefore(element, lastAfter.nextSibling); lastAfter = element; } else { this.element.insertBefore(element, lineNode); } } }); } } shouldUpdate(newProps) { const oldProps = this.props; if (oldProps.top !== newProps.top) return true; if (oldProps.height !== newProps.height) return true; if (oldProps.width !== newProps.width) return true; if (oldProps.lineHeight !== newProps.lineHeight) return true; if (oldProps.tileStartRow !== newProps.tileStartRow) return true; if (oldProps.tileEndRow !== newProps.tileEndRow) return true; if (!arraysEqual(oldProps.screenLines, newProps.screenLines)) return true; if (!arraysEqual(oldProps.lineDecorations, newProps.lineDecorations)) return true; if (oldProps.blockDecorations && newProps.blockDecorations) { if (oldProps.blockDecorations.size !== newProps.blockDecorations.size) return true; let blockDecorationsChanged = false; oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { if (!blockDecorationsChanged) { const newDecorations = newProps.blockDecorations.get(screenLineId); blockDecorationsChanged = newDecorations == null || !arraysEqual(oldDecorations, newDecorations); } }); if (blockDecorationsChanged) return true; newProps.blockDecorations.forEach((newDecorations, screenLineId) => { if (!blockDecorationsChanged) { const oldDecorations = oldProps.blockDecorations.get(screenLineId); blockDecorationsChanged = oldDecorations == null; } }); if (blockDecorationsChanged) return true; } else if (oldProps.blockDecorations) { return true; } else if (newProps.blockDecorations) { return true; } if (oldProps.textDecorations.length !== newProps.textDecorations.length) return true; for (let i = 0; i < oldProps.textDecorations.length; i++) { if ( !textDecorationsEqual( oldProps.textDecorations[i], newProps.textDecorations[i] ) ) return true; } return false; } } class LineComponent { constructor(props) { const { nodePool, screenRow, screenLine, lineComponentsByScreenLineId, offScreen } = props; this.props = props; this.element = nodePool.getElement('DIV', this.buildClassName(), null); this.element.dataset.screenRow = screenRow; this.textNodes = []; if (offScreen) { this.element.style.position = 'absolute'; this.element.style.visibility = 'hidden'; this.element.dataset.offScreen = true; } this.appendContents(); lineComponentsByScreenLineId.set(screenLine.id, this); } update(newProps) { if (this.props.lineDecoration !== newProps.lineDecoration) { this.props.lineDecoration = newProps.lineDecoration; this.element.className = this.buildClassName(); } if (this.props.screenRow !== newProps.screenRow) { this.props.screenRow = newProps.screenRow; this.element.dataset.screenRow = newProps.screenRow; } if ( !textDecorationsEqual( this.props.textDecorations, newProps.textDecorations ) ) { this.props.textDecorations = newProps.textDecorations; this.element.firstChild.remove(); this.appendContents(); } } destroy() { const { nodePool, lineComponentsByScreenLineId, screenLine } = this.props; if (lineComponentsByScreenLineId.get(screenLine.id) === this) { lineComponentsByScreenLineId.delete(screenLine.id); } this.element.remove(); nodePool.release(this.element); } appendContents() { const { displayLayer, nodePool, screenLine, textDecorations } = this.props; this.textNodes.length = 0; const { lineText, tags } = screenLine; let openScopeNode = nodePool.getElement('SPAN', null, null); this.element.appendChild(openScopeNode); let decorationIndex = 0; let column = 0; let activeClassName = null; let activeStyle = null; let nextDecoration = textDecorations ? textDecorations[decorationIndex] : null; if (nextDecoration && nextDecoration.column === 0) { column = nextDecoration.column; activeClassName = nextDecoration.className; activeStyle = nextDecoration.style; nextDecoration = textDecorations[++decorationIndex]; } for (let i = 0; i < tags.length; i++) { const tag = tags[i]; if (tag !== 0) { if (displayLayer.isCloseTag(tag)) { openScopeNode = openScopeNode.parentElement; } else if (displayLayer.isOpenTag(tag)) { const newScopeNode = nodePool.getElement( 'SPAN', displayLayer.classNameForTag(tag), null ); openScopeNode.appendChild(newScopeNode); openScopeNode = newScopeNode; } else { const nextTokenColumn = column + tag; while (nextDecoration && nextDecoration.column <= nextTokenColumn) { const text = lineText.substring(column, nextDecoration.column); this.appendTextNode( openScopeNode, text, activeClassName, activeStyle ); column = nextDecoration.column; activeClassName = nextDecoration.className; activeStyle = nextDecoration.style; nextDecoration = textDecorations[++decorationIndex]; } if (column < nextTokenColumn) { const text = lineText.substring(column, nextTokenColumn); this.appendTextNode( openScopeNode, text, activeClassName, activeStyle ); column = nextTokenColumn; } } } } if (column === 0) { const textNode = nodePool.getTextNode(' '); this.element.appendChild(textNode); this.textNodes.push(textNode); } if (lineText.endsWith(displayLayer.foldCharacter)) { // Insert a zero-width non-breaking whitespace, so that LinesYardstick can // take the fold-marker::after pseudo-element into account during // measurements when such marker is the last character on the line. const textNode = nodePool.getTextNode(ZERO_WIDTH_NBSP_CHARACTER); this.element.appendChild(textNode); this.textNodes.push(textNode); } } appendTextNode(openScopeNode, text, activeClassName, activeStyle) { const { nodePool } = this.props; if (activeClassName || activeStyle) { const decorationNode = nodePool.getElement( 'SPAN', activeClassName, activeStyle ); openScopeNode.appendChild(decorationNode); openScopeNode = decorationNode; } const textNode = nodePool.getTextNode(text); openScopeNode.appendChild(textNode); this.textNodes.push(textNode); } buildClassName() { const { lineDecoration } = this.props; let className = 'line'; if (lineDecoration != null) className = className + ' ' + lineDecoration; return className; } } class HighlightsComponent { constructor(props) { this.props = {}; this.element = document.createElement('div'); this.element.className = 'highlights'; this.element.style.contain = 'strict'; this.element.style.position = 'absolute'; this.element.style.overflow = 'hidden'; this.element.style.userSelect = 'none'; this.highlightComponentsByKey = new Map(); this.update(props); } destroy() { this.highlightComponentsByKey.forEach(highlightComponent => { highlightComponent.destroy(); }); this.highlightComponentsByKey.clear(); } update(newProps) { if (this.shouldUpdate(newProps)) { this.props = newProps; const { height, width, lineHeight, highlightDecorations } = this.props; this.element.style.height = height + 'px'; this.element.style.width = width + 'px'; const visibleHighlightDecorations = new Set(); if (highlightDecorations) { for (let i = 0; i < highlightDecorations.length; i++) { const highlightDecoration = highlightDecorations[i]; const highlightProps = Object.assign( { lineHeight }, highlightDecorations[i] ); let highlightComponent = this.highlightComponentsByKey.get( highlightDecoration.key ); if (highlightComponent) { highlightComponent.update(highlightProps); } else { highlightComponent = new HighlightComponent(highlightProps); this.element.appendChild(highlightComponent.element); this.highlightComponentsByKey.set( highlightDecoration.key, highlightComponent ); } highlightDecorations[i].flashRequested = false; visibleHighlightDecorations.add(highlightDecoration.key); } } this.highlightComponentsByKey.forEach((highlightComponent, key) => { if (!visibleHighlightDecorations.has(key)) { highlightComponent.destroy(); this.highlightComponentsByKey.delete(key); } }); } } shouldUpdate(newProps) { const oldProps = this.props; if (!newProps.hasInitialMeasurements) return false; if (oldProps.width !== newProps.width) return true; if (oldProps.height !== newProps.height) return true; if (oldProps.lineHeight !== newProps.lineHeight) return true; if (!oldProps.highlightDecorations && newProps.highlightDecorations) return true; if (oldProps.highlightDecorations && !newProps.highlightDecorations) return true; if (oldProps.highlightDecorations && newProps.highlightDecorations) { if ( oldProps.highlightDecorations.length !== newProps.highlightDecorations.length ) return true; for ( let i = 0, length = oldProps.highlightDecorations.length; i < length; i++ ) { const oldHighlight = oldProps.highlightDecorations[i]; const newHighlight = newProps.highlightDecorations[i]; if (oldHighlight.className !== newHighlight.className) return true; if (newHighlight.flashRequested) return true; if (oldHighlight.startPixelTop !== newHighlight.startPixelTop) return true; if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true; if (oldHighlight.endPixelTop !== newHighlight.endPixelTop) return true; if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true; if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true; } } } } class HighlightComponent { constructor(props) { this.props = props; etch.initialize(this); if (this.props.flashRequested) this.performFlash(); } destroy() { if (this.timeoutsByClassName) { this.timeoutsByClassName.forEach(timeout => { window.clearTimeout(timeout); }); this.timeoutsByClassName.clear(); } return etch.destroy(this); } update(newProps) { this.props = newProps; etch.updateSync(this); if (newProps.flashRequested) this.performFlash(); } performFlash() { const { flashClass, flashDuration } = this.props; if (!this.timeoutsByClassName) this.timeoutsByClassName = new Map(); // If a flash of this class is already in progress, clear it early and // flash again on the next frame to ensure CSS transitions apply to the // second flash. if (this.timeoutsByClassName.has(flashClass)) { window.clearTimeout(this.timeoutsByClassName.get(flashClass)); this.timeoutsByClassName.delete(flashClass); this.element.classList.remove(flashClass); requestAnimationFrame(() => this.performFlash()); } else { this.element.classList.add(flashClass); this.timeoutsByClassName.set( flashClass, window.setTimeout(() => { this.element.classList.remove(flashClass); }, flashDuration) ); } } render() { const { className, screenRange, lineHeight, startPixelTop, startPixelLeft, endPixelTop, endPixelLeft } = this.props; const regionClassName = 'region ' + className; let children; if (screenRange.start.row === screenRange.end.row) { children = $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: startPixelTop + 'px', left: startPixelLeft + 'px', width: endPixelLeft - startPixelLeft + 'px', height: lineHeight + 'px' } }); } else { children = []; children.push( $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: startPixelTop + 'px', left: startPixelLeft + 'px', right: 0, height: lineHeight + 'px' } }) ); if (screenRange.end.row - screenRange.start.row > 1) { children.push( $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: startPixelTop + lineHeight + 'px', left: 0, right: 0, height: endPixelTop - startPixelTop - lineHeight * 2 + 'px' } }) ); } if (endPixelLeft > 0) { children.push( $.div({ className: regionClassName, style: { position: 'absolute', boxSizing: 'border-box', top: endPixelTop - lineHeight + 'px', left: 0, width: endPixelLeft + 'px', height: lineHeight + 'px' } }) ); } } return $.div({ className: 'highlight ' + className }, children); } } class OverlayComponent { constructor(props) { this.props = props; this.element = document.createElement('atom-overlay'); if (this.props.className != null) this.element.classList.add(this.props.className); this.element.appendChild(this.props.element); this.element.style.position = 'fixed'; this.element.style.zIndex = 4; this.element.style.top = (this.props.pixelTop || 0) + 'px'; this.element.style.left = (this.props.pixelLeft || 0) + 'px'; this.currentContentRect = null; // Synchronous DOM updates in response to resize events might trigger a // "loop limit exceeded" error. We disconnect the observer before // potentially mutating the DOM, and then reconnect it on the next tick. // Note: ResizeObserver calls its callback when .observe is called this.resizeObserver = new ResizeObserver(entries => { const { contentRect } = entries[0]; if ( this.currentContentRect && (this.currentContentRect.width !== contentRect.width || this.currentContentRect.height !== contentRect.height) ) { this.resizeObserver.disconnect(); this.props.didResize(this); process.nextTick(() => { this.resizeObserver.observe(this.props.element); }); } this.currentContentRect = contentRect; }); this.didAttach(); this.props.overlayComponents.add(this); } destroy() { this.props.overlayComponents.delete(this); this.didDetach(); } getNextUpdatePromise() { if (!this.nextUpdatePromise) { this.nextUpdatePromise = new Promise(resolve => { this.resolveNextUpdatePromise = () => { this.nextUpdatePromise = null; this.resolveNextUpdatePromise = null; resolve(); }; }); } return this.nextUpdatePromise; } update(newProps) { const oldProps = this.props; this.props = Object.assign({}, oldProps, newProps); if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px'; if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px'; if (newProps.className !== oldProps.className) { if (oldProps.className != null) this.element.classList.remove(oldProps.className); if (newProps.className != null) this.element.classList.add(newProps.className); } if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise(); } didAttach() { this.resizeObserver.observe(this.props.element); } didDetach() { this.resizeObserver.disconnect(); } } let rangeForMeasurement; function clientRectForRange(textNode, startIndex, endIndex) { if (!rangeForMeasurement) rangeForMeasurement = document.createRange(); rangeForMeasurement.setStart(textNode, startIndex); rangeForMeasurement.setEnd(textNode, endIndex); return rangeForMeasurement.getBoundingClientRect(); } function textDecorationsEqual(oldDecorations, newDecorations) { if (!oldDecorations && newDecorations) return false; if (oldDecorations && !newDecorations) return false; if (oldDecorations && newDecorations) { if (oldDecorations.length !== newDecorations.length) return false; for (let j = 0; j < oldDecorations.length; j++) { if (oldDecorations[j].column !== newDecorations[j].column) return false; if (oldDecorations[j].className !== newDecorations[j].className) return false; if (!objectsEqual(oldDecorations[j].style, newDecorations[j].style)) return false; } } return true; } function arraysEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0, length = a.length; i < length; i++) { if (a[i] !== b[i]) return false; } return true; } function objectsEqual(a, b) { if (!a && b) return false; if (a && !b) return false; if (a && b) { for (const key in a) { if (a[key] !== b[key]) return false; } for (const key in b) { if (a[key] !== b[key]) return false; } } return true; } function constrainRangeToRows(range, startRow, endRow) { if (range.start.row < startRow || range.end.row >= endRow) { range = range.copy(); if (range.start.row < startRow) { range.start.row = startRow; range.start.column = 0; } if (range.end.row >= endRow) { range.end.row = endRow; range.end.column = 0; } } return range; } function debounce(fn, wait) { let timestamp, timeout; function later() { const last = Date.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; fn(); } } return function() { timestamp = Date.now(); if (!timeout) timeout = setTimeout(later, wait); }; } class NodePool { constructor() { this.elementsByType = {}; this.textNodes = []; } getElement(type, className, style) { let element; const elementsByDepth = this.elementsByType[type]; if (elementsByDepth) { while (elementsByDepth.length > 0) { const elements = elementsByDepth[elementsByDepth.length - 1]; if (elements && elements.length > 0) { element = elements.pop(); if (elements.length === 0) elementsByDepth.pop(); break; } else { elementsByDepth.pop(); } } } if (element) { element.className = className || ''; element.attributeStyleMap.forEach((value, key) => { if (!style || style[key] == null) element.style[key] = ''; }); if (style) Object.assign(element.style, style); for (const key in element.dataset) delete element.dataset[key]; while (element.firstChild) element.firstChild.remove(); return element; } else { const newElement = document.createElement(type); if (className) newElement.className = className; if (style) Object.assign(newElement.style, style); return newElement; } } getTextNode(text) { if (this.textNodes.length > 0) { const node = this.textNodes.pop(); node.textContent = text; return node; } else { return document.createTextNode(text); } } release(node, depth = 0) { const { nodeName } = node; if (nodeName === '#text') { this.textNodes.push(node); } else { let elementsByDepth = this.elementsByType[nodeName]; if (!elementsByDepth) { elementsByDepth = []; this.elementsByType[nodeName] = elementsByDepth; } let elements = elementsByDepth[depth]; if (!elements) { elements = []; elementsByDepth[depth] = elements; } elements.push(node); for (let i = 0; i < node.childNodes.length; i++) { this.release(node.childNodes[i], depth + 1); } } } } function roundToPhysicalPixelBoundary(virtualPixelPosition) { const virtualPixelsPerPhysicalPixel = 1 / window.devicePixelRatio; return ( Math.round(virtualPixelPosition / virtualPixelsPerPhysicalPixel) * virtualPixelsPerPhysicalPixel ); } function ceilToPhysicalPixelBoundary(virtualPixelPosition) { const virtualPixelsPerPhysicalPixel = 1 / window.devicePixelRatio; return ( Math.ceil(virtualPixelPosition / virtualPixelsPerPhysicalPixel) * virtualPixelsPerPhysicalPixel ); }