Last active
January 18, 2018 15:42
-
-
Save Ben3eeE/22ad32cbd9106bad16648d1c23f85709 to your computer and use it in GitHub Desktop.
tree-sitter crash file
This file has been truncated, but you can view the full file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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 clipboard = require('./safe-clipboard') | |
| const electron = require('electron') | |
| 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 = new TextEditorElement() | |
| } | |
| 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: [], | |
| keys: [], | |
| softWrappedFlags: [], | |
| foldableFlags: [] | |
| } | |
| this.decorationsToRender = { | |
| lineNumbers: null, | |
| 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(() => { | |
| this.measureContentDuringUpdateSync() | |
| scheduler.updateDocument(() => { | |
| this.updateSyncAfterMeasuringContent() | |
| }) | |
| }) | |
| } else { | |
| this.measureContentDuringUpdateSync() | |
| this.updateSyncAfterMeasuringContent() | |
| } | |
| this.updateScheduled = false | |
| } | |
| measureBlockDecorations () { | |
| if (this.remeasureAllBlockDecorations) { | |
| this.remeasureAllBlockDecorations = false | |
| const decorations = this.props.model.getDecorations() | |
| for (var 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 () { | |
| if (this.remeasureGutterDimensions) { | |
| this.measureGutterDimensions() | |
| this.remeasureGutterDimensions = false | |
| } | |
| const wasHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| this.measureLongestLineWidth() | |
| this.measureHorizontalPositions() | |
| this.updateAbsolutePositionedDecorations() | |
| if (this.pendingAutoscroll) { | |
| this.derivedDimensionsCache = {} | |
| const {screenRange, options} = this.pendingAutoscroll | |
| this.autoscrollHorizontally(screenRange, options) | |
| const isHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { | |
| this.autoscrollVertically(screenRange, options) | |
| } | |
| this.pendingAutoscroll = null | |
| } | |
| this.linesToMeasure.clear() | |
| this.measuredContent = true | |
| } | |
| 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() | |
| if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight() | |
| clientContainerHeight += 'px' | |
| } | |
| if (model.getAutoWidth()) { | |
| style.width = 'min-content' | |
| clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() | |
| if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth() | |
| clientContainerWidth += 'px' | |
| } else { | |
| style.width = this.element.style.width | |
| } | |
| } | |
| let attributes = {} | |
| if (model.isMini()) { | |
| attributes.mini = '' | |
| } | |
| if (!this.isInputEnabled()) { | |
| 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 = | |
| canScrollHorizontally | |
| ? this.getHorizontalScrollbarHeight() | |
| : 0 | |
| verticalScrollbarWidth = | |
| canScrollVertically | |
| ? this.getVerticalScrollbarWidth() | |
| : 0 | |
| forceScrollbarVisible = this.remeasureScrollbars | |
| } else { | |
| forceScrollbarVisible = true | |
| } | |
| const dummyScrollbarVnodes = [ | |
| $(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 | |
| }) | |
| ] | |
| // If both scrollbars are visible, push a dummy element to force a "corner" | |
| // to render where the two scrollbars meet at the lower right | |
| if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) { | |
| dummyScrollbarVnodes.push($.div( | |
| { | |
| ref: 'scrollbarCorner', | |
| className: 'scrollbar-corner', | |
| style: { | |
| position: 'absolute', | |
| height: '20px', | |
| width: '20px', | |
| bottom: 0, | |
| right: 0, | |
| overflow: 'scroll' | |
| } | |
| } | |
| )) | |
| } | |
| return dummyScrollbarVnodes | |
| } 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++) { | |
| const className = newClassList[i] | |
| if (!oldClassList || !oldClassList.includes(className)) { | |
| this.element.classList.add(className) | |
| } | |
| } | |
| 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.isLineNumberGutterVisible()) 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.isLineNumberGutterVisible()) { | |
| 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 = [] | |
| 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) { | |
| const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers | |
| 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) | |
| } | |
| 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 () { | |
| // This element can be focused from a parent custom element's | |
| // attachedCallback before *its* attachedCallback is fired. This protects | |
| // against that case. | |
| if (!this.attached) this.didAttach() | |
| // The element can be focused before the intersection observer detects that | |
| // it has been shown for the first time. If this element is being focused, | |
| // it is necessarily visible, so we call `didShow` to ensure the hidden | |
| // input is rendered before we try to shift focus to it. | |
| if (!this.visible) this.didShow() | |
| if (!this.focused) { | |
| this.focused = true | |
| this.startCursorBlinking() | |
| this.scheduleUpdate() | |
| } | |
| this.getHiddenInput().focus() | |
| } | |
| // 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 () { | |
| // Focusing the hidden input when it is off-screen causes the browser to | |
| // scroll it into view. Since we use synthetic scrolling this behavior | |
| // causes all the lines to disappear so we counteract it by always setting | |
| // the scroll position to 0. | |
| this.refs.scrollContainer.scrollTop = 0 | |
| this.refs.scrollContainer.scrollLeft = 0 | |
| 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 = (Math.sign(wheelDeltaX) === 1) | |
| ? Math.max(1, wheelDeltaX * scrollSensitivity) | |
| : Math.min(-1, wheelDeltaX * scrollSensitivity) | |
| wheelDeltaY = 0 | |
| } else { | |
| wheelDeltaX = 0 | |
| wheelDeltaY = (Math.sign(wheelDeltaY) === 1) | |
| ? Math.max(1, wheelDeltaY * scrollSensitivity) | |
| : Math.min(-1, 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) 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 intially 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() | |
| }) | |
| 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 !== 0 || (platform === 'darwin' && ctrlKey)) { | |
| // Always set cursor position on middle-click | |
| // Only set cursor position on right-click if there is one cursor with no selection | |
| const ranges = model.getSelectedBufferRanges() | |
| if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) { | |
| 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 (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection')) | |
| return | |
| } | |
| if (target && target.matches('.fold-marker')) { | |
| const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) | |
| model.destroyFoldsContainingBufferPositions([bufferPosition], false) | |
| return | |
| } | |
| const addOrRemoveSelection = metaKey || ctrlKey | |
| 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) { | |
| var {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 | |
| if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) { | |
| 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 | |
| columnLoop: // eslint-disable-line no-labels | |
| 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() | |
| } else { | |
| return this.getClientContainerHeight() | |
| } | |
| } | |
| getScrollContainerClientWidth () { | |
| if (this.canScrollVertically()) { | |
| return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth() | |
| } else { | |
| return this.getScrollContainerWidth() | |
| } | |
| } | |
| getScrollContainerClientHeight () { | |
| if (this.canScrollHorizontally()) { | |
| return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() | |
| } else { | |
| return this.getScrollContainerHeight() | |
| } | |
| } | |
| canScrollVertically () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoHeight()) return false | |
| if (this.getContentHeight() > this.getScrollContainerHeight()) return true | |
| return ( | |
| this.getContentWidth() > this.getScrollContainerWidth() && | |
| this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()) | |
| ) | |
| } | |
| canScrollHorizontally () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoWidth()) return false | |
| if (model.isSoftWrapped()) return false | |
| if (this.getContentWidth() > this.getScrollContainerWidth()) return true | |
| return ( | |
| this.getContentHeight() > this.getScrollContainerHeight() && | |
| this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()) | |
| ) | |
| } | |
| 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.round(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 = Math.round(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 = Math.round(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({readOnly: !inputEnabled}) | |
| } | |
| isInputEnabled (inputEnabled) { | |
| return !this.props.model.isReadOnly() | |
| } | |
| 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.name === '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, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, | |
| renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, | |
| scrollHeight, lineNumberGutterWidth, lineHeight | |
| } = this.props | |
| if (!isLineNumberGutterVisible) return null | |
| if (hasInitialMeasurements) { | |
| const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| rootComponent: rootComponent, | |
| startRow: renderedStartRow, | |
| endRow: renderedEndRow, | |
| rowsPerTile: rowsPerTile, | |
| maxDigits: maxDigits, | |
| keys: keys, | |
| bufferRows: bufferRows, | |
| screenRows: screenRows, | |
| softWrappedFlags: softWrappedFlags, | |
| foldableFlags: foldableFlags, | |
| decorations: decorationsToRender.lineNumbers, | |
| blockDecorations: decorationsToRender.blocks, | |
| didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, | |
| height: scrollHeight, | |
| width: lineNumberGutterWidth, | |
| lineHeight: lineHeight, | |
| showLineNumbers | |
| }) | |
| } else { | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| 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 | |
| } = 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) { | |
| number = softWrapped ? '•' : bufferRow + 1 | |
| number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number | |
| } | |
| // 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 | |
| children[i] = $.div({ | |
| key: rootComponent.idsByTileStartRow.get(tileStartRow), | |
| style: { | |
| contain: 'layout style', | |
| position: 'absolute', | |
| top: 0, | |
| height: tileHeight + 'px', | |
| width: width + 'px', | |
| transform: `translateY(${tileTop}px)` | |
| } | |
| }, ...tileChildren) | |
| } | |
| } | |
| return $.div( | |
| { | |
| className: 'gutter line-numbers', | |
| attributes: {'gutter-name': 'line-number'}, | |
| style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'}, | |
| on: { | |
| mousedown: this.didMouseDown | |
| } | |
| }, | |
| $.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 (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) { | |
| this.props.rootComponent.didMouseDownOnLineNumberGutter(event) | |
| } | |
| } | |
| class LineNumberComponent { | |
| constructor (props) { | |
| const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props | |
| this.props = props | |
| const 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) this.element.style.width = width + 'px' | |
| if (this.props.marginTop !== marginTop) { | |
| if (marginTop != null) { | |
| this.element.style.marginTop = marginTop + 'px' | |
| } else { | |
| this.element.style.marginTop = '' | |
| } | |
| } | |
| if (this.props.number !== number) { | |
| if (number) { | |
| this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild) | |
| } else { | |
| const numberNode = this.element.firstChild | |
| numberNode.remove() | |
| nodePool.release(numberNode) | |
| } | |
| } | |
| 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 () { | |
| return $.div( | |
| { | |
| className: 'gutter', | |
| 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: pixelWidth + '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) { | |
| var { | |
| screenLines, tileStartRow, lineDecorations, textDecorations, | |
| nodePool, displayLayer, lineComponentsByScreenLineId | |
| } = newProps | |
| var oldScreenLines = oldProps.screenLines | |
| var newScreenLines = screenLines | |
| var oldScreenLinesEndIndex = oldScreenLines.length | |
| var newScreenLinesEndIndex = newScreenLines.length | |
| var oldScreenLineIndex = 0 | |
| var newScreenLineIndex = 0 | |
| var lineComponentIndex = 0 | |
| while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) { | |
| var oldScreenLine = oldScreenLines[oldScreenLineIndex] | |
| var 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) { | |
| var lineComponent = this.lineComponents[lineComponentIndex] | |
| lineComponent.update({ | |
| screenRow: tileStartRow + newScreenLineIndex, | |
| lineDecoration: lineDecorations[newScreenLineIndex], | |
| textDecorations: textDecorations[newScreenLineIndex] | |
| }) | |
| oldScreenLineIndex++ | |
| newScreenLineIndex++ | |
| lineComponentIndex++ | |
| } else { | |
| var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine) | |
| var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine) | |
| if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) { | |
| var newScreenLineComponents = [] | |
| while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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 { | |
| var oldScreenLineComponent = this.lineComponents[lineComponentIndex] | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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) { | |
| var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null | |
| if (blockDecorations) { | |
| var blockDecorationElementsBeforeOldScreenLine = [] | |
| for (let i = 0; i < blockDecorations.length; i++) { | |
| var decoration = blockDecorations[i] | |
| if (decoration.position !== 'after') { | |
| blockDecorationElementsBeforeOldScreenLine.push( | |
| TextEditor.viewForItem(decoration.item) | |
| ) | |
| } | |
| } | |
| for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) { | |
| var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i] | |
| if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) { | |
| return blockDecorationElement | |
| } | |
| } | |
| } | |
| return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element | |
| } | |
| updateBlockDecorations (oldProps, newProps) { | |
| var {blockDecorations, lineComponentsByScreenLineId} = newProps | |
| if (oldProps.blockDecorations) { | |
| oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { | |
| var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < oldDecorations.length; i++) { | |
| var oldDecoration = oldDecorations[i] | |
| if (newDecorations && newDecorations.includes(oldDecoration)) continue | |
| var element = TextEditor.viewForItem(oldDecoration.item) | |
| if (element.parentElement !== this.element) continue | |
| element.remove() | |
| } | |
| }) | |
| } | |
| if (blockDecorations) { | |
| blockDecorations.forEach((newDecorations, screenLineId) => { | |
| var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < newDecorations.length; i++) { | |
| var newDecoration = newDecorations[i] | |
| if (oldDecorations && oldDecorations.includes(newDecoration)) continue | |
| var element = TextEditor.viewForItem(newDecoration.item) | |
| var lineNode = lineComponentsByScreenLineId.get(screenLineId).element | |
| if (newDecoration.position === 'after') { | |
| this.element.insertBefore(element, lineNode.nextSibling) | |
| } 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) { | |
| var element | |
| var elementsByDepth = this.elementsByType[type] | |
| if (elementsByDepth) { | |
| while (elementsByDepth.length > 0) { | |
| var 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.styleMap.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 { | |
| var 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) { | |
| var node = this.textNodes.pop() | |
| node.textContent = text | |
| return node | |
| } else { | |
| return document.createTextNode(text) | |
| } | |
| } | |
| release (node, depth = 0) { | |
| var {nodeName} = node | |
| if (nodeName === '#text') { | |
| this.textNodes.push(node) | |
| } else { | |
| var elementsByDepth = this.elementsByType[nodeName] | |
| if (!elementsByDepth) { | |
| elementsByDepth = [] | |
| this.elementsByType[nodeName] = elementsByDepth | |
| } | |
| var elements = elementsByDepth[depth] | |
| if (!elements) { | |
| elements = [] | |
| elementsByDepth[depth] = elements | |
| } | |
| elements.push(node) | |
| for (var 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 | |
| } | |
| /* 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 clipboard = require('./safe-clipboard') | |
| const electron = require('electron') | |
| 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 = new TextEditorElement() | |
| } | |
| 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: [], | |
| keys: [], | |
| softWrappedFlags: [], | |
| foldableFlags: [] | |
| } | |
| this.decorationsToRender = { | |
| lineNumbers: null, | |
| 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(() => { | |
| this.measureContentDuringUpdateSync() | |
| scheduler.updateDocument(() => { | |
| this.updateSyncAfterMeasuringContent() | |
| }) | |
| }) | |
| } else { | |
| this.measureContentDuringUpdateSync() | |
| this.updateSyncAfterMeasuringContent() | |
| } | |
| this.updateScheduled = false | |
| } | |
| measureBlockDecorations () { | |
| if (this.remeasureAllBlockDecorations) { | |
| this.remeasureAllBlockDecorations = false | |
| const decorations = this.props.model.getDecorations() | |
| for (var 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 () { | |
| if (this.remeasureGutterDimensions) { | |
| this.measureGutterDimensions() | |
| this.remeasureGutterDimensions = false | |
| } | |
| const wasHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| this.measureLongestLineWidth() | |
| this.measureHorizontalPositions() | |
| this.updateAbsolutePositionedDecorations() | |
| if (this.pendingAutoscroll) { | |
| this.derivedDimensionsCache = {} | |
| const {screenRange, options} = this.pendingAutoscroll | |
| this.autoscrollHorizontally(screenRange, options) | |
| const isHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { | |
| this.autoscrollVertically(screenRange, options) | |
| } | |
| this.pendingAutoscroll = null | |
| } | |
| this.linesToMeasure.clear() | |
| this.measuredContent = true | |
| } | |
| 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() | |
| if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight() | |
| clientContainerHeight += 'px' | |
| } | |
| if (model.getAutoWidth()) { | |
| style.width = 'min-content' | |
| clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() | |
| if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth() | |
| clientContainerWidth += 'px' | |
| } else { | |
| style.width = this.element.style.width | |
| } | |
| } | |
| let attributes = {} | |
| if (model.isMini()) { | |
| attributes.mini = '' | |
| } | |
| if (!this.isInputEnabled()) { | |
| 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 = | |
| canScrollHorizontally | |
| ? this.getHorizontalScrollbarHeight() | |
| : 0 | |
| verticalScrollbarWidth = | |
| canScrollVertically | |
| ? this.getVerticalScrollbarWidth() | |
| : 0 | |
| forceScrollbarVisible = this.remeasureScrollbars | |
| } else { | |
| forceScrollbarVisible = true | |
| } | |
| const dummyScrollbarVnodes = [ | |
| $(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 | |
| }) | |
| ] | |
| // If both scrollbars are visible, push a dummy element to force a "corner" | |
| // to render where the two scrollbars meet at the lower right | |
| if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) { | |
| dummyScrollbarVnodes.push($.div( | |
| { | |
| ref: 'scrollbarCorner', | |
| className: 'scrollbar-corner', | |
| style: { | |
| position: 'absolute', | |
| height: '20px', | |
| width: '20px', | |
| bottom: 0, | |
| right: 0, | |
| overflow: 'scroll' | |
| } | |
| } | |
| )) | |
| } | |
| return dummyScrollbarVnodes | |
| } 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++) { | |
| const className = newClassList[i] | |
| if (!oldClassList || !oldClassList.includes(className)) { | |
| this.element.classList.add(className) | |
| } | |
| } | |
| 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.isLineNumberGutterVisible()) 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.isLineNumberGutterVisible()) { | |
| 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 = [] | |
| 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) { | |
| const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers | |
| 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) | |
| } | |
| 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 () { | |
| // This element can be focused from a parent custom element's | |
| // attachedCallback before *its* attachedCallback is fired. This protects | |
| // against that case. | |
| if (!this.attached) this.didAttach() | |
| // The element can be focused before the intersection observer detects that | |
| // it has been shown for the first time. If this element is being focused, | |
| // it is necessarily visible, so we call `didShow` to ensure the hidden | |
| // input is rendered before we try to shift focus to it. | |
| if (!this.visible) this.didShow() | |
| if (!this.focused) { | |
| this.focused = true | |
| this.startCursorBlinking() | |
| this.scheduleUpdate() | |
| } | |
| this.getHiddenInput().focus() | |
| } | |
| // 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 () { | |
| // Focusing the hidden input when it is off-screen causes the browser to | |
| // scroll it into view. Since we use synthetic scrolling this behavior | |
| // causes all the lines to disappear so we counteract it by always setting | |
| // the scroll position to 0. | |
| this.refs.scrollContainer.scrollTop = 0 | |
| this.refs.scrollContainer.scrollLeft = 0 | |
| 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 = (Math.sign(wheelDeltaX) === 1) | |
| ? Math.max(1, wheelDeltaX * scrollSensitivity) | |
| : Math.min(-1, wheelDeltaX * scrollSensitivity) | |
| wheelDeltaY = 0 | |
| } else { | |
| wheelDeltaX = 0 | |
| wheelDeltaY = (Math.sign(wheelDeltaY) === 1) | |
| ? Math.max(1, wheelDeltaY * scrollSensitivity) | |
| : Math.min(-1, 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) 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 intially 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() | |
| }) | |
| 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 !== 0 || (platform === 'darwin' && ctrlKey)) { | |
| // Always set cursor position on middle-click | |
| // Only set cursor position on right-click if there is one cursor with no selection | |
| const ranges = model.getSelectedBufferRanges() | |
| if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) { | |
| 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 (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection')) | |
| return | |
| } | |
| if (target && target.matches('.fold-marker')) { | |
| const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) | |
| model.destroyFoldsContainingBufferPositions([bufferPosition], false) | |
| return | |
| } | |
| const addOrRemoveSelection = metaKey || ctrlKey | |
| 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) { | |
| var {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 | |
| if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) { | |
| 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 | |
| columnLoop: // eslint-disable-line no-labels | |
| 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() | |
| } else { | |
| return this.getClientContainerHeight() | |
| } | |
| } | |
| getScrollContainerClientWidth () { | |
| if (this.canScrollVertically()) { | |
| return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth() | |
| } else { | |
| return this.getScrollContainerWidth() | |
| } | |
| } | |
| getScrollContainerClientHeight () { | |
| if (this.canScrollHorizontally()) { | |
| return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() | |
| } else { | |
| return this.getScrollContainerHeight() | |
| } | |
| } | |
| canScrollVertically () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoHeight()) return false | |
| if (this.getContentHeight() > this.getScrollContainerHeight()) return true | |
| return ( | |
| this.getContentWidth() > this.getScrollContainerWidth() && | |
| this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()) | |
| ) | |
| } | |
| canScrollHorizontally () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoWidth()) return false | |
| if (model.isSoftWrapped()) return false | |
| if (this.getContentWidth() > this.getScrollContainerWidth()) return true | |
| return ( | |
| this.getContentHeight() > this.getScrollContainerHeight() && | |
| this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()) | |
| ) | |
| } | |
| 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.round(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 = Math.round(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 = Math.round(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({readOnly: !inputEnabled}) | |
| } | |
| isInputEnabled (inputEnabled) { | |
| return !this.props.model.isReadOnly() | |
| } | |
| 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.name === '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, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, | |
| renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, | |
| scrollHeight, lineNumberGutterWidth, lineHeight | |
| } = this.props | |
| if (!isLineNumberGutterVisible) return null | |
| if (hasInitialMeasurements) { | |
| const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| rootComponent: rootComponent, | |
| startRow: renderedStartRow, | |
| endRow: renderedEndRow, | |
| rowsPerTile: rowsPerTile, | |
| maxDigits: maxDigits, | |
| keys: keys, | |
| bufferRows: bufferRows, | |
| screenRows: screenRows, | |
| softWrappedFlags: softWrappedFlags, | |
| foldableFlags: foldableFlags, | |
| decorations: decorationsToRender.lineNumbers, | |
| blockDecorations: decorationsToRender.blocks, | |
| didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, | |
| height: scrollHeight, | |
| width: lineNumberGutterWidth, | |
| lineHeight: lineHeight, | |
| showLineNumbers | |
| }) | |
| } else { | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| 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 | |
| } = 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) { | |
| number = softWrapped ? '•' : bufferRow + 1 | |
| number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number | |
| } | |
| // 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 | |
| children[i] = $.div({ | |
| key: rootComponent.idsByTileStartRow.get(tileStartRow), | |
| style: { | |
| contain: 'layout style', | |
| position: 'absolute', | |
| top: 0, | |
| height: tileHeight + 'px', | |
| width: width + 'px', | |
| transform: `translateY(${tileTop}px)` | |
| } | |
| }, ...tileChildren) | |
| } | |
| } | |
| return $.div( | |
| { | |
| className: 'gutter line-numbers', | |
| attributes: {'gutter-name': 'line-number'}, | |
| style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'}, | |
| on: { | |
| mousedown: this.didMouseDown | |
| } | |
| }, | |
| $.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 (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) { | |
| this.props.rootComponent.didMouseDownOnLineNumberGutter(event) | |
| } | |
| } | |
| class LineNumberComponent { | |
| constructor (props) { | |
| const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props | |
| this.props = props | |
| const 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) this.element.style.width = width + 'px' | |
| if (this.props.marginTop !== marginTop) { | |
| if (marginTop != null) { | |
| this.element.style.marginTop = marginTop + 'px' | |
| } else { | |
| this.element.style.marginTop = '' | |
| } | |
| } | |
| if (this.props.number !== number) { | |
| if (number) { | |
| this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild) | |
| } else { | |
| const numberNode = this.element.firstChild | |
| numberNode.remove() | |
| nodePool.release(numberNode) | |
| } | |
| } | |
| 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 () { | |
| return $.div( | |
| { | |
| className: 'gutter', | |
| 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: pixelWidth + '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) { | |
| var { | |
| screenLines, tileStartRow, lineDecorations, textDecorations, | |
| nodePool, displayLayer, lineComponentsByScreenLineId | |
| } = newProps | |
| var oldScreenLines = oldProps.screenLines | |
| var newScreenLines = screenLines | |
| var oldScreenLinesEndIndex = oldScreenLines.length | |
| var newScreenLinesEndIndex = newScreenLines.length | |
| var oldScreenLineIndex = 0 | |
| var newScreenLineIndex = 0 | |
| var lineComponentIndex = 0 | |
| while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) { | |
| var oldScreenLine = oldScreenLines[oldScreenLineIndex] | |
| var 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) { | |
| var lineComponent = this.lineComponents[lineComponentIndex] | |
| lineComponent.update({ | |
| screenRow: tileStartRow + newScreenLineIndex, | |
| lineDecoration: lineDecorations[newScreenLineIndex], | |
| textDecorations: textDecorations[newScreenLineIndex] | |
| }) | |
| oldScreenLineIndex++ | |
| newScreenLineIndex++ | |
| lineComponentIndex++ | |
| } else { | |
| var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine) | |
| var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine) | |
| if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) { | |
| var newScreenLineComponents = [] | |
| while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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 { | |
| var oldScreenLineComponent = this.lineComponents[lineComponentIndex] | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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) { | |
| var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null | |
| if (blockDecorations) { | |
| var blockDecorationElementsBeforeOldScreenLine = [] | |
| for (let i = 0; i < blockDecorations.length; i++) { | |
| var decoration = blockDecorations[i] | |
| if (decoration.position !== 'after') { | |
| blockDecorationElementsBeforeOldScreenLine.push( | |
| TextEditor.viewForItem(decoration.item) | |
| ) | |
| } | |
| } | |
| for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) { | |
| var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i] | |
| if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) { | |
| return blockDecorationElement | |
| } | |
| } | |
| } | |
| return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element | |
| } | |
| updateBlockDecorations (oldProps, newProps) { | |
| var {blockDecorations, lineComponentsByScreenLineId} = newProps | |
| if (oldProps.blockDecorations) { | |
| oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { | |
| var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < oldDecorations.length; i++) { | |
| var oldDecoration = oldDecorations[i] | |
| if (newDecorations && newDecorations.includes(oldDecoration)) continue | |
| var element = TextEditor.viewForItem(oldDecoration.item) | |
| if (element.parentElement !== this.element) continue | |
| element.remove() | |
| } | |
| }) | |
| } | |
| if (blockDecorations) { | |
| blockDecorations.forEach((newDecorations, screenLineId) => { | |
| var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < newDecorations.length; i++) { | |
| var newDecoration = newDecorations[i] | |
| if (oldDecorations && oldDecorations.includes(newDecoration)) continue | |
| var element = TextEditor.viewForItem(newDecoration.item) | |
| var lineNode = lineComponentsByScreenLineId.get(screenLineId).element | |
| if (newDecoration.position === 'after') { | |
| this.element.insertBefore(element, lineNode.nextSibling) | |
| } 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) { | |
| var element | |
| var elementsByDepth = this.elementsByType[type] | |
| if (elementsByDepth) { | |
| while (elementsByDepth.length > 0) { | |
| var 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.styleMap.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 { | |
| var 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) { | |
| var node = this.textNodes.pop() | |
| node.textContent = text | |
| return node | |
| } else { | |
| return document.createTextNode(text) | |
| } | |
| } | |
| release (node, depth = 0) { | |
| var {nodeName} = node | |
| if (nodeName === '#text') { | |
| this.textNodes.push(node) | |
| } else { | |
| var elementsByDepth = this.elementsByType[nodeName] | |
| if (!elementsByDepth) { | |
| elementsByDepth = [] | |
| this.elementsByType[nodeName] = elementsByDepth | |
| } | |
| var elements = elementsByDepth[depth] | |
| if (!elements) { | |
| elements = [] | |
| elementsByDepth[depth] = elements | |
| } | |
| elements.push(node) | |
| for (var 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 | |
| } | |
| /* 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 clipboard = require('./safe-clipboard') | |
| const electron = require('electron') | |
| 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 = new TextEditorElement() | |
| } | |
| 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: [], | |
| keys: [], | |
| softWrappedFlags: [], | |
| foldableFlags: [] | |
| } | |
| this.decorationsToRender = { | |
| lineNumbers: null, | |
| 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(() => { | |
| this.measureContentDuringUpdateSync() | |
| scheduler.updateDocument(() => { | |
| this.updateSyncAfterMeasuringContent() | |
| }) | |
| }) | |
| } else { | |
| this.measureContentDuringUpdateSync() | |
| this.updateSyncAfterMeasuringContent() | |
| } | |
| this.updateScheduled = false | |
| } | |
| measureBlockDecorations () { | |
| if (this.remeasureAllBlockDecorations) { | |
| this.remeasureAllBlockDecorations = false | |
| const decorations = this.props.model.getDecorations() | |
| for (var 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 () { | |
| if (this.remeasureGutterDimensions) { | |
| this.measureGutterDimensions() | |
| this.remeasureGutterDimensions = false | |
| } | |
| const wasHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| this.measureLongestLineWidth() | |
| this.measureHorizontalPositions() | |
| this.updateAbsolutePositionedDecorations() | |
| if (this.pendingAutoscroll) { | |
| this.derivedDimensionsCache = {} | |
| const {screenRange, options} = this.pendingAutoscroll | |
| this.autoscrollHorizontally(screenRange, options) | |
| const isHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { | |
| this.autoscrollVertically(screenRange, options) | |
| } | |
| this.pendingAutoscroll = null | |
| } | |
| this.linesToMeasure.clear() | |
| this.measuredContent = true | |
| } | |
| 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() | |
| if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight() | |
| clientContainerHeight += 'px' | |
| } | |
| if (model.getAutoWidth()) { | |
| style.width = 'min-content' | |
| clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() | |
| if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth() | |
| clientContainerWidth += 'px' | |
| } else { | |
| style.width = this.element.style.width | |
| } | |
| } | |
| let attributes = {} | |
| if (model.isMini()) { | |
| attributes.mini = '' | |
| } | |
| if (!this.isInputEnabled()) { | |
| 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 = | |
| canScrollHorizontally | |
| ? this.getHorizontalScrollbarHeight() | |
| : 0 | |
| verticalScrollbarWidth = | |
| canScrollVertically | |
| ? this.getVerticalScrollbarWidth() | |
| : 0 | |
| forceScrollbarVisible = this.remeasureScrollbars | |
| } else { | |
| forceScrollbarVisible = true | |
| } | |
| const dummyScrollbarVnodes = [ | |
| $(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 | |
| }) | |
| ] | |
| // If both scrollbars are visible, push a dummy element to force a "corner" | |
| // to render where the two scrollbars meet at the lower right | |
| if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) { | |
| dummyScrollbarVnodes.push($.div( | |
| { | |
| ref: 'scrollbarCorner', | |
| className: 'scrollbar-corner', | |
| style: { | |
| position: 'absolute', | |
| height: '20px', | |
| width: '20px', | |
| bottom: 0, | |
| right: 0, | |
| overflow: 'scroll' | |
| } | |
| } | |
| )) | |
| } | |
| return dummyScrollbarVnodes | |
| } 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++) { | |
| const className = newClassList[i] | |
| if (!oldClassList || !oldClassList.includes(className)) { | |
| this.element.classList.add(className) | |
| } | |
| } | |
| 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.isLineNumberGutterVisible()) 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.isLineNumberGutterVisible()) { | |
| 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 = [] | |
| 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) { | |
| const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers | |
| 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) | |
| } | |
| 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 () { | |
| // This element can be focused from a parent custom element's | |
| // attachedCallback before *its* attachedCallback is fired. This protects | |
| // against that case. | |
| if (!this.attached) this.didAttach() | |
| // The element can be focused before the intersection observer detects that | |
| // it has been shown for the first time. If this element is being focused, | |
| // it is necessarily visible, so we call `didShow` to ensure the hidden | |
| // input is rendered before we try to shift focus to it. | |
| if (!this.visible) this.didShow() | |
| if (!this.focused) { | |
| this.focused = true | |
| this.startCursorBlinking() | |
| this.scheduleUpdate() | |
| } | |
| this.getHiddenInput().focus() | |
| } | |
| // 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 () { | |
| // Focusing the hidden input when it is off-screen causes the browser to | |
| // scroll it into view. Since we use synthetic scrolling this behavior | |
| // causes all the lines to disappear so we counteract it by always setting | |
| // the scroll position to 0. | |
| this.refs.scrollContainer.scrollTop = 0 | |
| this.refs.scrollContainer.scrollLeft = 0 | |
| 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 = (Math.sign(wheelDeltaX) === 1) | |
| ? Math.max(1, wheelDeltaX * scrollSensitivity) | |
| : Math.min(-1, wheelDeltaX * scrollSensitivity) | |
| wheelDeltaY = 0 | |
| } else { | |
| wheelDeltaX = 0 | |
| wheelDeltaY = (Math.sign(wheelDeltaY) === 1) | |
| ? Math.max(1, wheelDeltaY * scrollSensitivity) | |
| : Math.min(-1, 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) 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 intially 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() | |
| }) | |
| 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 !== 0 || (platform === 'darwin' && ctrlKey)) { | |
| // Always set cursor position on middle-click | |
| // Only set cursor position on right-click if there is one cursor with no selection | |
| const ranges = model.getSelectedBufferRanges() | |
| if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) { | |
| 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 (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection')) | |
| return | |
| } | |
| if (target && target.matches('.fold-marker')) { | |
| const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) | |
| model.destroyFoldsContainingBufferPositions([bufferPosition], false) | |
| return | |
| } | |
| const addOrRemoveSelection = metaKey || ctrlKey | |
| 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) { | |
| var {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 | |
| if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) { | |
| 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 | |
| columnLoop: // eslint-disable-line no-labels | |
| 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() | |
| } else { | |
| return this.getClientContainerHeight() | |
| } | |
| } | |
| getScrollContainerClientWidth () { | |
| if (this.canScrollVertically()) { | |
| return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth() | |
| } else { | |
| return this.getScrollContainerWidth() | |
| } | |
| } | |
| getScrollContainerClientHeight () { | |
| if (this.canScrollHorizontally()) { | |
| return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() | |
| } else { | |
| return this.getScrollContainerHeight() | |
| } | |
| } | |
| canScrollVertically () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoHeight()) return false | |
| if (this.getContentHeight() > this.getScrollContainerHeight()) return true | |
| return ( | |
| this.getContentWidth() > this.getScrollContainerWidth() && | |
| this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()) | |
| ) | |
| } | |
| canScrollHorizontally () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoWidth()) return false | |
| if (model.isSoftWrapped()) return false | |
| if (this.getContentWidth() > this.getScrollContainerWidth()) return true | |
| return ( | |
| this.getContentHeight() > this.getScrollContainerHeight() && | |
| this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()) | |
| ) | |
| } | |
| 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.round(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 = Math.round(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 = Math.round(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({readOnly: !inputEnabled}) | |
| } | |
| isInputEnabled (inputEnabled) { | |
| return !this.props.model.isReadOnly() | |
| } | |
| 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.name === '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, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, | |
| renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, | |
| scrollHeight, lineNumberGutterWidth, lineHeight | |
| } = this.props | |
| if (!isLineNumberGutterVisible) return null | |
| if (hasInitialMeasurements) { | |
| const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| rootComponent: rootComponent, | |
| startRow: renderedStartRow, | |
| endRow: renderedEndRow, | |
| rowsPerTile: rowsPerTile, | |
| maxDigits: maxDigits, | |
| keys: keys, | |
| bufferRows: bufferRows, | |
| screenRows: screenRows, | |
| softWrappedFlags: softWrappedFlags, | |
| foldableFlags: foldableFlags, | |
| decorations: decorationsToRender.lineNumbers, | |
| blockDecorations: decorationsToRender.blocks, | |
| didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, | |
| height: scrollHeight, | |
| width: lineNumberGutterWidth, | |
| lineHeight: lineHeight, | |
| showLineNumbers | |
| }) | |
| } else { | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| 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 | |
| } = 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) { | |
| number = softWrapped ? '•' : bufferRow + 1 | |
| number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number | |
| } | |
| // 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 | |
| children[i] = $.div({ | |
| key: rootComponent.idsByTileStartRow.get(tileStartRow), | |
| style: { | |
| contain: 'layout style', | |
| position: 'absolute', | |
| top: 0, | |
| height: tileHeight + 'px', | |
| width: width + 'px', | |
| transform: `translateY(${tileTop}px)` | |
| } | |
| }, ...tileChildren) | |
| } | |
| } | |
| return $.div( | |
| { | |
| className: 'gutter line-numbers', | |
| attributes: {'gutter-name': 'line-number'}, | |
| style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'}, | |
| on: { | |
| mousedown: this.didMouseDown | |
| } | |
| }, | |
| $.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 (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) { | |
| this.props.rootComponent.didMouseDownOnLineNumberGutter(event) | |
| } | |
| } | |
| class LineNumberComponent { | |
| constructor (props) { | |
| const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props | |
| this.props = props | |
| const 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) this.element.style.width = width + 'px' | |
| if (this.props.marginTop !== marginTop) { | |
| if (marginTop != null) { | |
| this.element.style.marginTop = marginTop + 'px' | |
| } else { | |
| this.element.style.marginTop = '' | |
| } | |
| } | |
| if (this.props.number !== number) { | |
| if (number) { | |
| this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild) | |
| } else { | |
| const numberNode = this.element.firstChild | |
| numberNode.remove() | |
| nodePool.release(numberNode) | |
| } | |
| } | |
| 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 () { | |
| return $.div( | |
| { | |
| className: 'gutter', | |
| 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: pixelWidth + '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) { | |
| var { | |
| screenLines, tileStartRow, lineDecorations, textDecorations, | |
| nodePool, displayLayer, lineComponentsByScreenLineId | |
| } = newProps | |
| var oldScreenLines = oldProps.screenLines | |
| var newScreenLines = screenLines | |
| var oldScreenLinesEndIndex = oldScreenLines.length | |
| var newScreenLinesEndIndex = newScreenLines.length | |
| var oldScreenLineIndex = 0 | |
| var newScreenLineIndex = 0 | |
| var lineComponentIndex = 0 | |
| while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) { | |
| var oldScreenLine = oldScreenLines[oldScreenLineIndex] | |
| var 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) { | |
| var lineComponent = this.lineComponents[lineComponentIndex] | |
| lineComponent.update({ | |
| screenRow: tileStartRow + newScreenLineIndex, | |
| lineDecoration: lineDecorations[newScreenLineIndex], | |
| textDecorations: textDecorations[newScreenLineIndex] | |
| }) | |
| oldScreenLineIndex++ | |
| newScreenLineIndex++ | |
| lineComponentIndex++ | |
| } else { | |
| var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine) | |
| var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine) | |
| if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) { | |
| var newScreenLineComponents = [] | |
| while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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 { | |
| var oldScreenLineComponent = this.lineComponents[lineComponentIndex] | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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) { | |
| var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null | |
| if (blockDecorations) { | |
| var blockDecorationElementsBeforeOldScreenLine = [] | |
| for (let i = 0; i < blockDecorations.length; i++) { | |
| var decoration = blockDecorations[i] | |
| if (decoration.position !== 'after') { | |
| blockDecorationElementsBeforeOldScreenLine.push( | |
| TextEditor.viewForItem(decoration.item) | |
| ) | |
| } | |
| } | |
| for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) { | |
| var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i] | |
| if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) { | |
| return blockDecorationElement | |
| } | |
| } | |
| } | |
| return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element | |
| } | |
| updateBlockDecorations (oldProps, newProps) { | |
| var {blockDecorations, lineComponentsByScreenLineId} = newProps | |
| if (oldProps.blockDecorations) { | |
| oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { | |
| var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < oldDecorations.length; i++) { | |
| var oldDecoration = oldDecorations[i] | |
| if (newDecorations && newDecorations.includes(oldDecoration)) continue | |
| var element = TextEditor.viewForItem(oldDecoration.item) | |
| if (element.parentElement !== this.element) continue | |
| element.remove() | |
| } | |
| }) | |
| } | |
| if (blockDecorations) { | |
| blockDecorations.forEach((newDecorations, screenLineId) => { | |
| var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < newDecorations.length; i++) { | |
| var newDecoration = newDecorations[i] | |
| if (oldDecorations && oldDecorations.includes(newDecoration)) continue | |
| var element = TextEditor.viewForItem(newDecoration.item) | |
| var lineNode = lineComponentsByScreenLineId.get(screenLineId).element | |
| if (newDecoration.position === 'after') { | |
| this.element.insertBefore(element, lineNode.nextSibling) | |
| } 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) { | |
| var element | |
| var elementsByDepth = this.elementsByType[type] | |
| if (elementsByDepth) { | |
| while (elementsByDepth.length > 0) { | |
| var 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.styleMap.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 { | |
| var 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) { | |
| var node = this.textNodes.pop() | |
| node.textContent = text | |
| return node | |
| } else { | |
| return document.createTextNode(text) | |
| } | |
| } | |
| release (node, depth = 0) { | |
| var {nodeName} = node | |
| if (nodeName === '#text') { | |
| this.textNodes.push(node) | |
| } else { | |
| var elementsByDepth = this.elementsByType[nodeName] | |
| if (!elementsByDepth) { | |
| elementsByDepth = [] | |
| this.elementsByType[nodeName] = elementsByDepth | |
| } | |
| var elements = elementsByDepth[depth] | |
| if (!elements) { | |
| elements = [] | |
| elementsByDepth[depth] = elements | |
| } | |
| elements.push(node) | |
| for (var 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 | |
| } | |
| /* 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 clipboard = require('./safe-clipboard') | |
| const electron = require('electron') | |
| 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 = new TextEditorElement() | |
| } | |
| 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: [], | |
| keys: [], | |
| softWrappedFlags: [], | |
| foldableFlags: [] | |
| } | |
| this.decorationsToRender = { | |
| lineNumbers: null, | |
| 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(() => { | |
| this.measureContentDuringUpdateSync() | |
| scheduler.updateDocument(() => { | |
| this.updateSyncAfterMeasuringContent() | |
| }) | |
| }) | |
| } else { | |
| this.measureContentDuringUpdateSync() | |
| this.updateSyncAfterMeasuringContent() | |
| } | |
| this.updateScheduled = false | |
| } | |
| measureBlockDecorations () { | |
| if (this.remeasureAllBlockDecorations) { | |
| this.remeasureAllBlockDecorations = false | |
| const decorations = this.props.model.getDecorations() | |
| for (var 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 () { | |
| if (this.remeasureGutterDimensions) { | |
| this.measureGutterDimensions() | |
| this.remeasureGutterDimensions = false | |
| } | |
| const wasHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| this.measureLongestLineWidth() | |
| this.measureHorizontalPositions() | |
| this.updateAbsolutePositionedDecorations() | |
| if (this.pendingAutoscroll) { | |
| this.derivedDimensionsCache = {} | |
| const {screenRange, options} = this.pendingAutoscroll | |
| this.autoscrollHorizontally(screenRange, options) | |
| const isHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { | |
| this.autoscrollVertically(screenRange, options) | |
| } | |
| this.pendingAutoscroll = null | |
| } | |
| this.linesToMeasure.clear() | |
| this.measuredContent = true | |
| } | |
| 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() | |
| if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight() | |
| clientContainerHeight += 'px' | |
| } | |
| if (model.getAutoWidth()) { | |
| style.width = 'min-content' | |
| clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() | |
| if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth() | |
| clientContainerWidth += 'px' | |
| } else { | |
| style.width = this.element.style.width | |
| } | |
| } | |
| let attributes = {} | |
| if (model.isMini()) { | |
| attributes.mini = '' | |
| } | |
| if (!this.isInputEnabled()) { | |
| 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 = | |
| canScrollHorizontally | |
| ? this.getHorizontalScrollbarHeight() | |
| : 0 | |
| verticalScrollbarWidth = | |
| canScrollVertically | |
| ? this.getVerticalScrollbarWidth() | |
| : 0 | |
| forceScrollbarVisible = this.remeasureScrollbars | |
| } else { | |
| forceScrollbarVisible = true | |
| } | |
| const dummyScrollbarVnodes = [ | |
| $(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 | |
| }) | |
| ] | |
| // If both scrollbars are visible, push a dummy element to force a "corner" | |
| // to render where the two scrollbars meet at the lower right | |
| if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) { | |
| dummyScrollbarVnodes.push($.div( | |
| { | |
| ref: 'scrollbarCorner', | |
| className: 'scrollbar-corner', | |
| style: { | |
| position: 'absolute', | |
| height: '20px', | |
| width: '20px', | |
| bottom: 0, | |
| right: 0, | |
| overflow: 'scroll' | |
| } | |
| } | |
| )) | |
| } | |
| return dummyScrollbarVnodes | |
| } 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++) { | |
| const className = newClassList[i] | |
| if (!oldClassList || !oldClassList.includes(className)) { | |
| this.element.classList.add(className) | |
| } | |
| } | |
| 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.isLineNumberGutterVisible()) 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.isLineNumberGutterVisible()) { | |
| 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 = [] | |
| 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) { | |
| const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers | |
| 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) | |
| } | |
| 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 () { | |
| // This element can be focused from a parent custom element's | |
| // attachedCallback before *its* attachedCallback is fired. This protects | |
| // against that case. | |
| if (!this.attached) this.didAttach() | |
| // The element can be focused before the intersection observer detects that | |
| // it has been shown for the first time. If this element is being focused, | |
| // it is necessarily visible, so we call `didShow` to ensure the hidden | |
| // input is rendered before we try to shift focus to it. | |
| if (!this.visible) this.didShow() | |
| if (!this.focused) { | |
| this.focused = true | |
| this.startCursorBlinking() | |
| this.scheduleUpdate() | |
| } | |
| this.getHiddenInput().focus() | |
| } | |
| // 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 () { | |
| // Focusing the hidden input when it is off-screen causes the browser to | |
| // scroll it into view. Since we use synthetic scrolling this behavior | |
| // causes all the lines to disappear so we counteract it by always setting | |
| // the scroll position to 0. | |
| this.refs.scrollContainer.scrollTop = 0 | |
| this.refs.scrollContainer.scrollLeft = 0 | |
| 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 = (Math.sign(wheelDeltaX) === 1) | |
| ? Math.max(1, wheelDeltaX * scrollSensitivity) | |
| : Math.min(-1, wheelDeltaX * scrollSensitivity) | |
| wheelDeltaY = 0 | |
| } else { | |
| wheelDeltaX = 0 | |
| wheelDeltaY = (Math.sign(wheelDeltaY) === 1) | |
| ? Math.max(1, wheelDeltaY * scrollSensitivity) | |
| : Math.min(-1, 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) 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 intially 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() | |
| }) | |
| 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 !== 0 || (platform === 'darwin' && ctrlKey)) { | |
| // Always set cursor position on middle-click | |
| // Only set cursor position on right-click if there is one cursor with no selection | |
| const ranges = model.getSelectedBufferRanges() | |
| if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) { | |
| 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 (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection')) | |
| return | |
| } | |
| if (target && target.matches('.fold-marker')) { | |
| const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) | |
| model.destroyFoldsContainingBufferPositions([bufferPosition], false) | |
| return | |
| } | |
| const addOrRemoveSelection = metaKey || ctrlKey | |
| 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) { | |
| var {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 | |
| if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) { | |
| 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 | |
| columnLoop: // eslint-disable-line no-labels | |
| 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() | |
| } else { | |
| return this.getClientContainerHeight() | |
| } | |
| } | |
| getScrollContainerClientWidth () { | |
| if (this.canScrollVertically()) { | |
| return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth() | |
| } else { | |
| return this.getScrollContainerWidth() | |
| } | |
| } | |
| getScrollContainerClientHeight () { | |
| if (this.canScrollHorizontally()) { | |
| return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() | |
| } else { | |
| return this.getScrollContainerHeight() | |
| } | |
| } | |
| canScrollVertically () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoHeight()) return false | |
| if (this.getContentHeight() > this.getScrollContainerHeight()) return true | |
| return ( | |
| this.getContentWidth() > this.getScrollContainerWidth() && | |
| this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()) | |
| ) | |
| } | |
| canScrollHorizontally () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoWidth()) return false | |
| if (model.isSoftWrapped()) return false | |
| if (this.getContentWidth() > this.getScrollContainerWidth()) return true | |
| return ( | |
| this.getContentHeight() > this.getScrollContainerHeight() && | |
| this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()) | |
| ) | |
| } | |
| 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.round(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 = Math.round(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 = Math.round(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({readOnly: !inputEnabled}) | |
| } | |
| isInputEnabled (inputEnabled) { | |
| return !this.props.model.isReadOnly() | |
| } | |
| 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.name === '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, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, | |
| renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, | |
| scrollHeight, lineNumberGutterWidth, lineHeight | |
| } = this.props | |
| if (!isLineNumberGutterVisible) return null | |
| if (hasInitialMeasurements) { | |
| const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| rootComponent: rootComponent, | |
| startRow: renderedStartRow, | |
| endRow: renderedEndRow, | |
| rowsPerTile: rowsPerTile, | |
| maxDigits: maxDigits, | |
| keys: keys, | |
| bufferRows: bufferRows, | |
| screenRows: screenRows, | |
| softWrappedFlags: softWrappedFlags, | |
| foldableFlags: foldableFlags, | |
| decorations: decorationsToRender.lineNumbers, | |
| blockDecorations: decorationsToRender.blocks, | |
| didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, | |
| height: scrollHeight, | |
| width: lineNumberGutterWidth, | |
| lineHeight: lineHeight, | |
| showLineNumbers | |
| }) | |
| } else { | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| 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 | |
| } = 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) { | |
| number = softWrapped ? '•' : bufferRow + 1 | |
| number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number | |
| } | |
| // 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 | |
| children[i] = $.div({ | |
| key: rootComponent.idsByTileStartRow.get(tileStartRow), | |
| style: { | |
| contain: 'layout style', | |
| position: 'absolute', | |
| top: 0, | |
| height: tileHeight + 'px', | |
| width: width + 'px', | |
| transform: `translateY(${tileTop}px)` | |
| } | |
| }, ...tileChildren) | |
| } | |
| } | |
| return $.div( | |
| { | |
| className: 'gutter line-numbers', | |
| attributes: {'gutter-name': 'line-number'}, | |
| style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'}, | |
| on: { | |
| mousedown: this.didMouseDown | |
| } | |
| }, | |
| $.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 (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) { | |
| this.props.rootComponent.didMouseDownOnLineNumberGutter(event) | |
| } | |
| } | |
| class LineNumberComponent { | |
| constructor (props) { | |
| const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props | |
| this.props = props | |
| const 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) this.element.style.width = width + 'px' | |
| if (this.props.marginTop !== marginTop) { | |
| if (marginTop != null) { | |
| this.element.style.marginTop = marginTop + 'px' | |
| } else { | |
| this.element.style.marginTop = '' | |
| } | |
| } | |
| if (this.props.number !== number) { | |
| if (number) { | |
| this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild) | |
| } else { | |
| const numberNode = this.element.firstChild | |
| numberNode.remove() | |
| nodePool.release(numberNode) | |
| } | |
| } | |
| 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 () { | |
| return $.div( | |
| { | |
| className: 'gutter', | |
| 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: pixelWidth + '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) { | |
| var { | |
| screenLines, tileStartRow, lineDecorations, textDecorations, | |
| nodePool, displayLayer, lineComponentsByScreenLineId | |
| } = newProps | |
| var oldScreenLines = oldProps.screenLines | |
| var newScreenLines = screenLines | |
| var oldScreenLinesEndIndex = oldScreenLines.length | |
| var newScreenLinesEndIndex = newScreenLines.length | |
| var oldScreenLineIndex = 0 | |
| var newScreenLineIndex = 0 | |
| var lineComponentIndex = 0 | |
| while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) { | |
| var oldScreenLine = oldScreenLines[oldScreenLineIndex] | |
| var 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) { | |
| var lineComponent = this.lineComponents[lineComponentIndex] | |
| lineComponent.update({ | |
| screenRow: tileStartRow + newScreenLineIndex, | |
| lineDecoration: lineDecorations[newScreenLineIndex], | |
| textDecorations: textDecorations[newScreenLineIndex] | |
| }) | |
| oldScreenLineIndex++ | |
| newScreenLineIndex++ | |
| lineComponentIndex++ | |
| } else { | |
| var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine) | |
| var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine) | |
| if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) { | |
| var newScreenLineComponents = [] | |
| while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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 { | |
| var oldScreenLineComponent = this.lineComponents[lineComponentIndex] | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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) { | |
| var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null | |
| if (blockDecorations) { | |
| var blockDecorationElementsBeforeOldScreenLine = [] | |
| for (let i = 0; i < blockDecorations.length; i++) { | |
| var decoration = blockDecorations[i] | |
| if (decoration.position !== 'after') { | |
| blockDecorationElementsBeforeOldScreenLine.push( | |
| TextEditor.viewForItem(decoration.item) | |
| ) | |
| } | |
| } | |
| for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) { | |
| var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i] | |
| if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) { | |
| return blockDecorationElement | |
| } | |
| } | |
| } | |
| return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element | |
| } | |
| updateBlockDecorations (oldProps, newProps) { | |
| var {blockDecorations, lineComponentsByScreenLineId} = newProps | |
| if (oldProps.blockDecorations) { | |
| oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { | |
| var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < oldDecorations.length; i++) { | |
| var oldDecoration = oldDecorations[i] | |
| if (newDecorations && newDecorations.includes(oldDecoration)) continue | |
| var element = TextEditor.viewForItem(oldDecoration.item) | |
| if (element.parentElement !== this.element) continue | |
| element.remove() | |
| } | |
| }) | |
| } | |
| if (blockDecorations) { | |
| blockDecorations.forEach((newDecorations, screenLineId) => { | |
| var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < newDecorations.length; i++) { | |
| var newDecoration = newDecorations[i] | |
| if (oldDecorations && oldDecorations.includes(newDecoration)) continue | |
| var element = TextEditor.viewForItem(newDecoration.item) | |
| var lineNode = lineComponentsByScreenLineId.get(screenLineId).element | |
| if (newDecoration.position === 'after') { | |
| this.element.insertBefore(element, lineNode.nextSibling) | |
| } 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) { | |
| var element | |
| var elementsByDepth = this.elementsByType[type] | |
| if (elementsByDepth) { | |
| while (elementsByDepth.length > 0) { | |
| var 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.styleMap.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 { | |
| var 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) { | |
| var node = this.textNodes.pop() | |
| node.textContent = text | |
| return node | |
| } else { | |
| return document.createTextNode(text) | |
| } | |
| } | |
| release (node, depth = 0) { | |
| var {nodeName} = node | |
| if (nodeName === '#text') { | |
| this.textNodes.push(node) | |
| } else { | |
| var elementsByDepth = this.elementsByType[nodeName] | |
| if (!elementsByDepth) { | |
| elementsByDepth = [] | |
| this.elementsByType[nodeName] = elementsByDepth | |
| } | |
| var elements = elementsByDepth[depth] | |
| if (!elements) { | |
| elements = [] | |
| elementsByDepth[depth] = elements | |
| } | |
| elements.push(node) | |
| for (var 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 | |
| } | |
| /* 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 clipboard = require('./safe-clipboard') | |
| const electron = require('electron') | |
| 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 = new TextEditorElement() | |
| } | |
| 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: [], | |
| keys: [], | |
| softWrappedFlags: [], | |
| foldableFlags: [] | |
| } | |
| this.decorationsToRender = { | |
| lineNumbers: null, | |
| 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(() => { | |
| this.measureContentDuringUpdateSync() | |
| scheduler.updateDocument(() => { | |
| this.updateSyncAfterMeasuringContent() | |
| }) | |
| }) | |
| } else { | |
| this.measureContentDuringUpdateSync() | |
| this.updateSyncAfterMeasuringContent() | |
| } | |
| this.updateScheduled = false | |
| } | |
| measureBlockDecorations () { | |
| if (this.remeasureAllBlockDecorations) { | |
| this.remeasureAllBlockDecorations = false | |
| const decorations = this.props.model.getDecorations() | |
| for (var 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 () { | |
| if (this.remeasureGutterDimensions) { | |
| this.measureGutterDimensions() | |
| this.remeasureGutterDimensions = false | |
| } | |
| const wasHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| this.measureLongestLineWidth() | |
| this.measureHorizontalPositions() | |
| this.updateAbsolutePositionedDecorations() | |
| if (this.pendingAutoscroll) { | |
| this.derivedDimensionsCache = {} | |
| const {screenRange, options} = this.pendingAutoscroll | |
| this.autoscrollHorizontally(screenRange, options) | |
| const isHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { | |
| this.autoscrollVertically(screenRange, options) | |
| } | |
| this.pendingAutoscroll = null | |
| } | |
| this.linesToMeasure.clear() | |
| this.measuredContent = true | |
| } | |
| 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() | |
| if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight() | |
| clientContainerHeight += 'px' | |
| } | |
| if (model.getAutoWidth()) { | |
| style.width = 'min-content' | |
| clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() | |
| if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth() | |
| clientContainerWidth += 'px' | |
| } else { | |
| style.width = this.element.style.width | |
| } | |
| } | |
| let attributes = {} | |
| if (model.isMini()) { | |
| attributes.mini = '' | |
| } | |
| if (!this.isInputEnabled()) { | |
| 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 = | |
| canScrollHorizontally | |
| ? this.getHorizontalScrollbarHeight() | |
| : 0 | |
| verticalScrollbarWidth = | |
| canScrollVertically | |
| ? this.getVerticalScrollbarWidth() | |
| : 0 | |
| forceScrollbarVisible = this.remeasureScrollbars | |
| } else { | |
| forceScrollbarVisible = true | |
| } | |
| const dummyScrollbarVnodes = [ | |
| $(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 | |
| }) | |
| ] | |
| // If both scrollbars are visible, push a dummy element to force a "corner" | |
| // to render where the two scrollbars meet at the lower right | |
| if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) { | |
| dummyScrollbarVnodes.push($.div( | |
| { | |
| ref: 'scrollbarCorner', | |
| className: 'scrollbar-corner', | |
| style: { | |
| position: 'absolute', | |
| height: '20px', | |
| width: '20px', | |
| bottom: 0, | |
| right: 0, | |
| overflow: 'scroll' | |
| } | |
| } | |
| )) | |
| } | |
| return dummyScrollbarVnodes | |
| } 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++) { | |
| const className = newClassList[i] | |
| if (!oldClassList || !oldClassList.includes(className)) { | |
| this.element.classList.add(className) | |
| } | |
| } | |
| 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.isLineNumberGutterVisible()) 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.isLineNumberGutterVisible()) { | |
| 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 = [] | |
| 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) { | |
| const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers | |
| 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) | |
| } | |
| 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 () { | |
| // This element can be focused from a parent custom element's | |
| // attachedCallback before *its* attachedCallback is fired. This protects | |
| // against that case. | |
| if (!this.attached) this.didAttach() | |
| // The element can be focused before the intersection observer detects that | |
| // it has been shown for the first time. If this element is being focused, | |
| // it is necessarily visible, so we call `didShow` to ensure the hidden | |
| // input is rendered before we try to shift focus to it. | |
| if (!this.visible) this.didShow() | |
| if (!this.focused) { | |
| this.focused = true | |
| this.startCursorBlinking() | |
| this.scheduleUpdate() | |
| } | |
| this.getHiddenInput().focus() | |
| } | |
| // 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 () { | |
| // Focusing the hidden input when it is off-screen causes the browser to | |
| // scroll it into view. Since we use synthetic scrolling this behavior | |
| // causes all the lines to disappear so we counteract it by always setting | |
| // the scroll position to 0. | |
| this.refs.scrollContainer.scrollTop = 0 | |
| this.refs.scrollContainer.scrollLeft = 0 | |
| 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 = (Math.sign(wheelDeltaX) === 1) | |
| ? Math.max(1, wheelDeltaX * scrollSensitivity) | |
| : Math.min(-1, wheelDeltaX * scrollSensitivity) | |
| wheelDeltaY = 0 | |
| } else { | |
| wheelDeltaX = 0 | |
| wheelDeltaY = (Math.sign(wheelDeltaY) === 1) | |
| ? Math.max(1, wheelDeltaY * scrollSensitivity) | |
| : Math.min(-1, 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) 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 intially 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() | |
| }) | |
| 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 !== 0 || (platform === 'darwin' && ctrlKey)) { | |
| // Always set cursor position on middle-click | |
| // Only set cursor position on right-click if there is one cursor with no selection | |
| const ranges = model.getSelectedBufferRanges() | |
| if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) { | |
| 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 (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection')) | |
| return | |
| } | |
| if (target && target.matches('.fold-marker')) { | |
| const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) | |
| model.destroyFoldsContainingBufferPositions([bufferPosition], false) | |
| return | |
| } | |
| const addOrRemoveSelection = metaKey || ctrlKey | |
| 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) { | |
| var {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 | |
| if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) { | |
| 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 | |
| columnLoop: // eslint-disable-line no-labels | |
| 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() | |
| } else { | |
| return this.getClientContainerHeight() | |
| } | |
| } | |
| getScrollContainerClientWidth () { | |
| if (this.canScrollVertically()) { | |
| return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth() | |
| } else { | |
| return this.getScrollContainerWidth() | |
| } | |
| } | |
| getScrollContainerClientHeight () { | |
| if (this.canScrollHorizontally()) { | |
| return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() | |
| } else { | |
| return this.getScrollContainerHeight() | |
| } | |
| } | |
| canScrollVertically () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoHeight()) return false | |
| if (this.getContentHeight() > this.getScrollContainerHeight()) return true | |
| return ( | |
| this.getContentWidth() > this.getScrollContainerWidth() && | |
| this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()) | |
| ) | |
| } | |
| canScrollHorizontally () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoWidth()) return false | |
| if (model.isSoftWrapped()) return false | |
| if (this.getContentWidth() > this.getScrollContainerWidth()) return true | |
| return ( | |
| this.getContentHeight() > this.getScrollContainerHeight() && | |
| this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()) | |
| ) | |
| } | |
| 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.round(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 = Math.round(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 = Math.round(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({readOnly: !inputEnabled}) | |
| } | |
| isInputEnabled (inputEnabled) { | |
| return !this.props.model.isReadOnly() | |
| } | |
| 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.name === '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, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, | |
| renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, | |
| scrollHeight, lineNumberGutterWidth, lineHeight | |
| } = this.props | |
| if (!isLineNumberGutterVisible) return null | |
| if (hasInitialMeasurements) { | |
| const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| rootComponent: rootComponent, | |
| startRow: renderedStartRow, | |
| endRow: renderedEndRow, | |
| rowsPerTile: rowsPerTile, | |
| maxDigits: maxDigits, | |
| keys: keys, | |
| bufferRows: bufferRows, | |
| screenRows: screenRows, | |
| softWrappedFlags: softWrappedFlags, | |
| foldableFlags: foldableFlags, | |
| decorations: decorationsToRender.lineNumbers, | |
| blockDecorations: decorationsToRender.blocks, | |
| didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, | |
| height: scrollHeight, | |
| width: lineNumberGutterWidth, | |
| lineHeight: lineHeight, | |
| showLineNumbers | |
| }) | |
| } else { | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| 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 | |
| } = 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) { | |
| number = softWrapped ? '•' : bufferRow + 1 | |
| number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number | |
| } | |
| // 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 | |
| children[i] = $.div({ | |
| key: rootComponent.idsByTileStartRow.get(tileStartRow), | |
| style: { | |
| contain: 'layout style', | |
| position: 'absolute', | |
| top: 0, | |
| height: tileHeight + 'px', | |
| width: width + 'px', | |
| transform: `translateY(${tileTop}px)` | |
| } | |
| }, ...tileChildren) | |
| } | |
| } | |
| return $.div( | |
| { | |
| className: 'gutter line-numbers', | |
| attributes: {'gutter-name': 'line-number'}, | |
| style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'}, | |
| on: { | |
| mousedown: this.didMouseDown | |
| } | |
| }, | |
| $.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 (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) { | |
| this.props.rootComponent.didMouseDownOnLineNumberGutter(event) | |
| } | |
| } | |
| class LineNumberComponent { | |
| constructor (props) { | |
| const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props | |
| this.props = props | |
| const 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) this.element.style.width = width + 'px' | |
| if (this.props.marginTop !== marginTop) { | |
| if (marginTop != null) { | |
| this.element.style.marginTop = marginTop + 'px' | |
| } else { | |
| this.element.style.marginTop = '' | |
| } | |
| } | |
| if (this.props.number !== number) { | |
| if (number) { | |
| this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild) | |
| } else { | |
| const numberNode = this.element.firstChild | |
| numberNode.remove() | |
| nodePool.release(numberNode) | |
| } | |
| } | |
| 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 () { | |
| return $.div( | |
| { | |
| className: 'gutter', | |
| 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: pixelWidth + '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) { | |
| var { | |
| screenLines, tileStartRow, lineDecorations, textDecorations, | |
| nodePool, displayLayer, lineComponentsByScreenLineId | |
| } = newProps | |
| var oldScreenLines = oldProps.screenLines | |
| var newScreenLines = screenLines | |
| var oldScreenLinesEndIndex = oldScreenLines.length | |
| var newScreenLinesEndIndex = newScreenLines.length | |
| var oldScreenLineIndex = 0 | |
| var newScreenLineIndex = 0 | |
| var lineComponentIndex = 0 | |
| while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) { | |
| var oldScreenLine = oldScreenLines[oldScreenLineIndex] | |
| var 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) { | |
| var lineComponent = this.lineComponents[lineComponentIndex] | |
| lineComponent.update({ | |
| screenRow: tileStartRow + newScreenLineIndex, | |
| lineDecoration: lineDecorations[newScreenLineIndex], | |
| textDecorations: textDecorations[newScreenLineIndex] | |
| }) | |
| oldScreenLineIndex++ | |
| newScreenLineIndex++ | |
| lineComponentIndex++ | |
| } else { | |
| var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine) | |
| var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine) | |
| if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) { | |
| var newScreenLineComponents = [] | |
| while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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 { | |
| var oldScreenLineComponent = this.lineComponents[lineComponentIndex] | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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) { | |
| var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null | |
| if (blockDecorations) { | |
| var blockDecorationElementsBeforeOldScreenLine = [] | |
| for (let i = 0; i < blockDecorations.length; i++) { | |
| var decoration = blockDecorations[i] | |
| if (decoration.position !== 'after') { | |
| blockDecorationElementsBeforeOldScreenLine.push( | |
| TextEditor.viewForItem(decoration.item) | |
| ) | |
| } | |
| } | |
| for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) { | |
| var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i] | |
| if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) { | |
| return blockDecorationElement | |
| } | |
| } | |
| } | |
| return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element | |
| } | |
| updateBlockDecorations (oldProps, newProps) { | |
| var {blockDecorations, lineComponentsByScreenLineId} = newProps | |
| if (oldProps.blockDecorations) { | |
| oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { | |
| var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < oldDecorations.length; i++) { | |
| var oldDecoration = oldDecorations[i] | |
| if (newDecorations && newDecorations.includes(oldDecoration)) continue | |
| var element = TextEditor.viewForItem(oldDecoration.item) | |
| if (element.parentElement !== this.element) continue | |
| element.remove() | |
| } | |
| }) | |
| } | |
| if (blockDecorations) { | |
| blockDecorations.forEach((newDecorations, screenLineId) => { | |
| var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < newDecorations.length; i++) { | |
| var newDecoration = newDecorations[i] | |
| if (oldDecorations && oldDecorations.includes(newDecoration)) continue | |
| var element = TextEditor.viewForItem(newDecoration.item) | |
| var lineNode = lineComponentsByScreenLineId.get(screenLineId).element | |
| if (newDecoration.position === 'after') { | |
| this.element.insertBefore(element, lineNode.nextSibling) | |
| } 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) { | |
| var element | |
| var elementsByDepth = this.elementsByType[type] | |
| if (elementsByDepth) { | |
| while (elementsByDepth.length > 0) { | |
| var 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.styleMap.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 { | |
| var 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) { | |
| var node = this.textNodes.pop() | |
| node.textContent = text | |
| return node | |
| } else { | |
| return document.createTextNode(text) | |
| } | |
| } | |
| release (node, depth = 0) { | |
| var {nodeName} = node | |
| if (nodeName === '#text') { | |
| this.textNodes.push(node) | |
| } else { | |
| var elementsByDepth = this.elementsByType[nodeName] | |
| if (!elementsByDepth) { | |
| elementsByDepth = [] | |
| this.elementsByType[nodeName] = elementsByDepth | |
| } | |
| var elements = elementsByDepth[depth] | |
| if (!elements) { | |
| elements = [] | |
| elementsByDepth[depth] = elements | |
| } | |
| elements.push(node) | |
| for (var 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 | |
| } | |
| /* 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 clipboard = require('./safe-clipboard') | |
| const electron = require('electron') | |
| 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 = new TextEditorElement() | |
| } | |
| 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: [], | |
| keys: [], | |
| softWrappedFlags: [], | |
| foldableFlags: [] | |
| } | |
| this.decorationsToRender = { | |
| lineNumbers: null, | |
| 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(() => { | |
| this.measureContentDuringUpdateSync() | |
| scheduler.updateDocument(() => { | |
| this.updateSyncAfterMeasuringContent() | |
| }) | |
| }) | |
| } else { | |
| this.measureContentDuringUpdateSync() | |
| this.updateSyncAfterMeasuringContent() | |
| } | |
| this.updateScheduled = false | |
| } | |
| measureBlockDecorations () { | |
| if (this.remeasureAllBlockDecorations) { | |
| this.remeasureAllBlockDecorations = false | |
| const decorations = this.props.model.getDecorations() | |
| for (var 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 () { | |
| if (this.remeasureGutterDimensions) { | |
| this.measureGutterDimensions() | |
| this.remeasureGutterDimensions = false | |
| } | |
| const wasHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| this.measureLongestLineWidth() | |
| this.measureHorizontalPositions() | |
| this.updateAbsolutePositionedDecorations() | |
| if (this.pendingAutoscroll) { | |
| this.derivedDimensionsCache = {} | |
| const {screenRange, options} = this.pendingAutoscroll | |
| this.autoscrollHorizontally(screenRange, options) | |
| const isHorizontalScrollbarVisible = ( | |
| this.canScrollHorizontally() && | |
| this.getHorizontalScrollbarHeight() > 0 | |
| ) | |
| if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) { | |
| this.autoscrollVertically(screenRange, options) | |
| } | |
| this.pendingAutoscroll = null | |
| } | |
| this.linesToMeasure.clear() | |
| this.measuredContent = true | |
| } | |
| 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() | |
| if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight() | |
| clientContainerHeight += 'px' | |
| } | |
| if (model.getAutoWidth()) { | |
| style.width = 'min-content' | |
| clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth() | |
| if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth() | |
| clientContainerWidth += 'px' | |
| } else { | |
| style.width = this.element.style.width | |
| } | |
| } | |
| let attributes = {} | |
| if (model.isMini()) { | |
| attributes.mini = '' | |
| } | |
| if (!this.isInputEnabled()) { | |
| 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 = | |
| canScrollHorizontally | |
| ? this.getHorizontalScrollbarHeight() | |
| : 0 | |
| verticalScrollbarWidth = | |
| canScrollVertically | |
| ? this.getVerticalScrollbarWidth() | |
| : 0 | |
| forceScrollbarVisible = this.remeasureScrollbars | |
| } else { | |
| forceScrollbarVisible = true | |
| } | |
| const dummyScrollbarVnodes = [ | |
| $(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 | |
| }) | |
| ] | |
| // If both scrollbars are visible, push a dummy element to force a "corner" | |
| // to render where the two scrollbars meet at the lower right | |
| if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) { | |
| dummyScrollbarVnodes.push($.div( | |
| { | |
| ref: 'scrollbarCorner', | |
| className: 'scrollbar-corner', | |
| style: { | |
| position: 'absolute', | |
| height: '20px', | |
| width: '20px', | |
| bottom: 0, | |
| right: 0, | |
| overflow: 'scroll' | |
| } | |
| } | |
| )) | |
| } | |
| return dummyScrollbarVnodes | |
| } 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++) { | |
| const className = newClassList[i] | |
| if (!oldClassList || !oldClassList.includes(className)) { | |
| this.element.classList.add(className) | |
| } | |
| } | |
| 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.isLineNumberGutterVisible()) 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.isLineNumberGutterVisible()) { | |
| 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 = [] | |
| 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) { | |
| const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers | |
| 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) | |
| } | |
| 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 () { | |
| // This element can be focused from a parent custom element's | |
| // attachedCallback before *its* attachedCallback is fired. This protects | |
| // against that case. | |
| if (!this.attached) this.didAttach() | |
| // The element can be focused before the intersection observer detects that | |
| // it has been shown for the first time. If this element is being focused, | |
| // it is necessarily visible, so we call `didShow` to ensure the hidden | |
| // input is rendered before we try to shift focus to it. | |
| if (!this.visible) this.didShow() | |
| if (!this.focused) { | |
| this.focused = true | |
| this.startCursorBlinking() | |
| this.scheduleUpdate() | |
| } | |
| this.getHiddenInput().focus() | |
| } | |
| // 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 () { | |
| // Focusing the hidden input when it is off-screen causes the browser to | |
| // scroll it into view. Since we use synthetic scrolling this behavior | |
| // causes all the lines to disappear so we counteract it by always setting | |
| // the scroll position to 0. | |
| this.refs.scrollContainer.scrollTop = 0 | |
| this.refs.scrollContainer.scrollLeft = 0 | |
| 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 = (Math.sign(wheelDeltaX) === 1) | |
| ? Math.max(1, wheelDeltaX * scrollSensitivity) | |
| : Math.min(-1, wheelDeltaX * scrollSensitivity) | |
| wheelDeltaY = 0 | |
| } else { | |
| wheelDeltaX = 0 | |
| wheelDeltaY = (Math.sign(wheelDeltaY) === 1) | |
| ? Math.max(1, wheelDeltaY * scrollSensitivity) | |
| : Math.min(-1, 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) 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 intially 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() | |
| }) | |
| 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 !== 0 || (platform === 'darwin' && ctrlKey)) { | |
| // Always set cursor position on middle-click | |
| // Only set cursor position on right-click if there is one cursor with no selection | |
| const ranges = model.getSelectedBufferRanges() | |
| if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) { | |
| 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 (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection')) | |
| return | |
| } | |
| if (target && target.matches('.fold-marker')) { | |
| const bufferPosition = model.bufferPositionForScreenPosition(screenPosition) | |
| model.destroyFoldsContainingBufferPositions([bufferPosition], false) | |
| return | |
| } | |
| const addOrRemoveSelection = metaKey || ctrlKey | |
| 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) { | |
| var {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 | |
| if (desiredScrollCenter < this.getScrollTop() || desiredScrollCenter > this.getScrollBottom()) { | |
| 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 | |
| columnLoop: // eslint-disable-line no-labels | |
| 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() | |
| } else { | |
| return this.getClientContainerHeight() | |
| } | |
| } | |
| getScrollContainerClientWidth () { | |
| if (this.canScrollVertically()) { | |
| return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth() | |
| } else { | |
| return this.getScrollContainerWidth() | |
| } | |
| } | |
| getScrollContainerClientHeight () { | |
| if (this.canScrollHorizontally()) { | |
| return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight() | |
| } else { | |
| return this.getScrollContainerHeight() | |
| } | |
| } | |
| canScrollVertically () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoHeight()) return false | |
| if (this.getContentHeight() > this.getScrollContainerHeight()) return true | |
| return ( | |
| this.getContentWidth() > this.getScrollContainerWidth() && | |
| this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()) | |
| ) | |
| } | |
| canScrollHorizontally () { | |
| const {model} = this.props | |
| if (model.isMini()) return false | |
| if (model.getAutoWidth()) return false | |
| if (model.isSoftWrapped()) return false | |
| if (this.getContentWidth() > this.getScrollContainerWidth()) return true | |
| return ( | |
| this.getContentHeight() > this.getScrollContainerHeight() && | |
| this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()) | |
| ) | |
| } | |
| 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.round(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 = Math.round(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 = Math.round(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({readOnly: !inputEnabled}) | |
| } | |
| isInputEnabled (inputEnabled) { | |
| return !this.props.model.isReadOnly() | |
| } | |
| 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.name === '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, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender, | |
| renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration, | |
| scrollHeight, lineNumberGutterWidth, lineHeight | |
| } = this.props | |
| if (!isLineNumberGutterVisible) return null | |
| if (hasInitialMeasurements) { | |
| const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| rootComponent: rootComponent, | |
| startRow: renderedStartRow, | |
| endRow: renderedEndRow, | |
| rowsPerTile: rowsPerTile, | |
| maxDigits: maxDigits, | |
| keys: keys, | |
| bufferRows: bufferRows, | |
| screenRows: screenRows, | |
| softWrappedFlags: softWrappedFlags, | |
| foldableFlags: foldableFlags, | |
| decorations: decorationsToRender.lineNumbers, | |
| blockDecorations: decorationsToRender.blocks, | |
| didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration, | |
| height: scrollHeight, | |
| width: lineNumberGutterWidth, | |
| lineHeight: lineHeight, | |
| showLineNumbers | |
| }) | |
| } else { | |
| return $(LineNumberGutterComponent, { | |
| ref: 'lineNumberGutter', | |
| element: gutter.getElement(), | |
| 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 | |
| } = 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) { | |
| number = softWrapped ? '•' : bufferRow + 1 | |
| number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number | |
| } | |
| // 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 | |
| children[i] = $.div({ | |
| key: rootComponent.idsByTileStartRow.get(tileStartRow), | |
| style: { | |
| contain: 'layout style', | |
| position: 'absolute', | |
| top: 0, | |
| height: tileHeight + 'px', | |
| width: width + 'px', | |
| transform: `translateY(${tileTop}px)` | |
| } | |
| }, ...tileChildren) | |
| } | |
| } | |
| return $.div( | |
| { | |
| className: 'gutter line-numbers', | |
| attributes: {'gutter-name': 'line-number'}, | |
| style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'}, | |
| on: { | |
| mousedown: this.didMouseDown | |
| } | |
| }, | |
| $.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 (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) { | |
| this.props.rootComponent.didMouseDownOnLineNumberGutter(event) | |
| } | |
| } | |
| class LineNumberComponent { | |
| constructor (props) { | |
| const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props | |
| this.props = props | |
| const 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) this.element.style.width = width + 'px' | |
| if (this.props.marginTop !== marginTop) { | |
| if (marginTop != null) { | |
| this.element.style.marginTop = marginTop + 'px' | |
| } else { | |
| this.element.style.marginTop = '' | |
| } | |
| } | |
| if (this.props.number !== number) { | |
| if (number) { | |
| this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild) | |
| } else { | |
| const numberNode = this.element.firstChild | |
| numberNode.remove() | |
| nodePool.release(numberNode) | |
| } | |
| } | |
| 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 () { | |
| return $.div( | |
| { | |
| className: 'gutter', | |
| 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: pixelWidth + '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) { | |
| var { | |
| screenLines, tileStartRow, lineDecorations, textDecorations, | |
| nodePool, displayLayer, lineComponentsByScreenLineId | |
| } = newProps | |
| var oldScreenLines = oldProps.screenLines | |
| var newScreenLines = screenLines | |
| var oldScreenLinesEndIndex = oldScreenLines.length | |
| var newScreenLinesEndIndex = newScreenLines.length | |
| var oldScreenLineIndex = 0 | |
| var newScreenLineIndex = 0 | |
| var lineComponentIndex = 0 | |
| while (oldScreenLineIndex < oldScreenLinesEndIndex || newScreenLineIndex < newScreenLinesEndIndex) { | |
| var oldScreenLine = oldScreenLines[oldScreenLineIndex] | |
| var 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) { | |
| var lineComponent = this.lineComponents[lineComponentIndex] | |
| lineComponent.update({ | |
| screenRow: tileStartRow + newScreenLineIndex, | |
| lineDecoration: lineDecorations[newScreenLineIndex], | |
| textDecorations: textDecorations[newScreenLineIndex] | |
| }) | |
| oldScreenLineIndex++ | |
| newScreenLineIndex++ | |
| lineComponentIndex++ | |
| } else { | |
| var oldScreenLineIndexInNewScreenLines = newScreenLines.indexOf(oldScreenLine) | |
| var newScreenLineIndexInOldScreenLines = oldScreenLines.indexOf(newScreenLine) | |
| if (newScreenLineIndex < oldScreenLineIndexInNewScreenLines && oldScreenLineIndexInNewScreenLines < newScreenLinesEndIndex) { | |
| var newScreenLineComponents = [] | |
| while (newScreenLineIndex < oldScreenLineIndexInNewScreenLines) { | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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 { | |
| var oldScreenLineComponent = this.lineComponents[lineComponentIndex] | |
| var newScreenLineComponent = new LineComponent({ // eslint-disable-line no-redeclare | |
| 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) { | |
| var blockDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLine.id) : null | |
| if (blockDecorations) { | |
| var blockDecorationElementsBeforeOldScreenLine = [] | |
| for (let i = 0; i < blockDecorations.length; i++) { | |
| var decoration = blockDecorations[i] | |
| if (decoration.position !== 'after') { | |
| blockDecorationElementsBeforeOldScreenLine.push( | |
| TextEditor.viewForItem(decoration.item) | |
| ) | |
| } | |
| } | |
| for (let i = 0; i < blockDecorationElementsBeforeOldScreenLine.length; i++) { | |
| var blockDecorationElement = blockDecorationElementsBeforeOldScreenLine[i] | |
| if (!blockDecorationElementsBeforeOldScreenLine.includes(blockDecorationElement.previousSibling)) { | |
| return blockDecorationElement | |
| } | |
| } | |
| } | |
| return oldProps.lineComponentsByScreenLineId.get(screenLine.id).element | |
| } | |
| updateBlockDecorations (oldProps, newProps) { | |
| var {blockDecorations, lineComponentsByScreenLineId} = newProps | |
| if (oldProps.blockDecorations) { | |
| oldProps.blockDecorations.forEach((oldDecorations, screenLineId) => { | |
| var newDecorations = newProps.blockDecorations ? newProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < oldDecorations.length; i++) { | |
| var oldDecoration = oldDecorations[i] | |
| if (newDecorations && newDecorations.includes(oldDecoration)) continue | |
| var element = TextEditor.viewForItem(oldDecoration.item) | |
| if (element.parentElement !== this.element) continue | |
| element.remove() | |
| } | |
| }) | |
| } | |
| if (blockDecorations) { | |
| blockDecorations.forEach((newDecorations, screenLineId) => { | |
| var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null | |
| for (var i = 0; i < newDecorations.length; i++) { | |
| var newDecoration = newDecorations[i] | |
| if (oldDecorations && oldDecorations.includes(newDecoration)) continue | |
| var element = TextEditor.viewForItem(newDecoration.item) | |
| var lineNode = lineComponentsByScreenLineId.get(screenLineId).element | |
| if (newDecoration.position === 'after') { | |
| this.element.insertBefore(element, lineNode.nextSibling) | |
| } 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) { | |
| var element | |
| var elementsByDepth = this.elementsByType[type] | |
| if (elementsByDepth) { | |
| while (elementsByDepth.length > 0) { | |
| var 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.styleMap.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 { | |
| var 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) { | |
| var node = this.textNodes.pop() | |
| node.textContent = text | |
| return node | |
| } else { | |
| return document.createTextNode(text) | |
| } | |
| } | |
| release (node, depth = 0) { | |
| var {nodeName} = node | |
| if (nodeName === '#text') { | |
| this.textNodes.push(node) | |
| } else { | |
| var elementsByDepth = this.elementsByType[nodeName] | |
| if (!elementsByDepth) { | |
| elementsByDepth = [] | |
| this.elementsByType[nodeName] = elementsByDepth | |
| } | |
| var elements = elementsByDepth[depth] | |
| if (!elements) { | |
| elements = [] | |
| elementsByDepth[depth] = elements | |
| } | |
| elements.push(node) | |
| for (var 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 | |
| } | |
| /* 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 clipboard = require('./safe-clipboard') | |
| const electron = require('electron') | |
| 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 = new TextEditorElement() | |
| } | |
| 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: [], | |
| keys: [], | |
| softWrappedFlags: [], | |
| foldableFlags: [] | |
| } | |
| this.decorationsToRender = { | |
| lineNumbers: null, | |
| 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(() => { | |
| this.measureContentDuringUpdateSync() | |
| scheduler.updateDocument(() => { | |
| this.updateSyncAfterMeasuringContent() | |
| }) | |
| }) | |
| } else { | |
| this.measureContentDuringUpdateSync() | |
| this.updateSyncAfterMeasuringContent() | |
| } | |
| this.updateScheduled = false | |
| } | |
| measureBlockDecorations () { | |
| if (this.remeasureAllBlockDecorations) { | |
| this.remeasureAllBlockDecorations = false | |
| const decorations = this.props.model.getDecorations() | |
| for (var 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() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment