diff --git a/browser/src/Editor/OniEditor/IndentGuideBufferLayer.tsx b/browser/src/Editor/OniEditor/IndentGuideBufferLayer.tsx index 06f60e2957..3834269e4d 100644 --- a/browser/src/Editor/OniEditor/IndentGuideBufferLayer.tsx +++ b/browser/src/Editor/OniEditor/IndentGuideBufferLayer.tsx @@ -3,12 +3,14 @@ import * as React from "react" import * as detectIndent from "detect-indent" import * as flatten from "lodash/flatten" import * as last from "lodash/last" -import * as memoize from "lodash/memoize" +import moize from "moize" import * as Oni from "oni-api" import { IBuffer } from "../BufferManager" import styled, { pixel, withProps } from "./../../UI/components/common" +type IContext = Oni.BufferLayerRenderContext + interface IWrappedLine { start: number end: number @@ -54,200 +56,206 @@ const IndentLine = withProps(styled.span).attrs({ position: absolute; ` -interface IndentLayerArgs { - buffer: IBuffer - configuration: Oni.Configuration +const determineIfShouldSkip = (props: LinePropsWithLevels, options: ConfigOptions) => { + const skipFirstIndentLine = options.skipFirst && props.levelOfIndentation === props.indentBy - 1 + return skipFirstIndentLine } -class IndentGuideBufferLayer implements Oni.BufferLayer { - public render = memoize((bufferLayerContext: Oni.BufferLayerRenderContext) => { - return {this._renderIndentLines(bufferLayerContext)} - }) - - private _buffer: IBuffer - private _userSpacing: number - private _configuration: Oni.Configuration - - constructor({ buffer, configuration }: IndentLayerArgs) { - this._buffer = buffer - this._configuration = configuration - this._userSpacing = this._buffer.shiftwidth || this._buffer.tabstop - } - get id() { - return "indent-guides" - } - - get friendlyName() { - return "Indent Guide Lines" - } +/** + * Remove one indent from left positioning and move lines slightly inwards - + * by a third of a character for a better visual appearance + */ +const calculateLeftPosition = (props: LinePropsWithLevels) => { + const adjustedLeft = + props.left - + props.indentSize - + props.levelOfIndentation * props.indentSize + + props.characterWidth / 3 + + return adjustedLeft +} - private _getIndentLines = (guidePositions: IndentLinesProps[], options: ConfigOptions) => { - return flatten( - guidePositions.map((props, idx) => { - const indents: JSX.Element[] = [] - // Create a line per indentation - for ( - let levelOfIndentation = 0; - levelOfIndentation < props.indentBy; - levelOfIndentation++ - ) { - const lineProps = { ...props, levelOfIndentation } - const adjustedLeft = this._calculateLeftPosition(lineProps) - const shouldSkip = this._determineIfShouldSkip(lineProps, options) - const key = `${props.line.trim()}-${idx}-${levelOfIndentation}` - indents.push( - !shouldSkip && ( - - ), - ) - } - return indents - }), - ) - } +interface IIndentsPerLine { + guidePositions: IndentLinesProps[] + options: ConfigOptions +} - private _determineIfShouldSkip(props: LinePropsWithLevels, options: ConfigOptions) { - const skipFirstIndentLine = - options.skipFirst && props.levelOfIndentation === props.indentBy - 1 +const IndentsPerLine: React.SFC = ({ guidePositions, options }) => { + const indents = guidePositions.map((props, idx) => + // Create a line per indentation + Array.from({ length: props.indentBy }, (_, levelOfIndentation) => { + const lineProps = { ...props, levelOfIndentation } + const adjustedLeft = calculateLeftPosition(lineProps) + const shouldSkip = determineIfShouldSkip(lineProps, options) + const key = `${props.line.trim()}-${idx}-${levelOfIndentation}` + return ( + !shouldSkip && ( + + ) + ) + }), + ) + return <>{flatten(indents)} +} - return skipFirstIndentLine - } +const MemoizedIndentsPerLine = moize.reactSimple(IndentsPerLine, { + isDeepEqual: true, +}) - /** - * Remove one indent from left positioning and move lines slightly inwards - - * by a third of a character for a better visual appearance - */ - private _calculateLeftPosition(props: LinePropsWithLevels) { - const adjustedLeft = - props.left - - props.indentSize - - props.levelOfIndentation * props.indentSize + - props.characterWidth / 3 - - return adjustedLeft - } +interface IGuideLines { + context: IContext + userSpacing: number + configuration: ConfigOptions +} - private _getWrappedLines(context: Oni.BufferLayerRenderContext): IWrappedLine[] { - const { lines } = context.visibleLines.reduce( - (acc, line, index) => { - const currentLine = context.topBufferLine + index - const bufferInfo = context.bufferToScreen({ line: currentLine, character: 0 }) - - if (bufferInfo && bufferInfo.screenY) { - const { screenY: screenLine } = bufferInfo - if (acc.expectedLine !== screenLine) { - acc.lines.push({ - start: acc.expectedLine, - end: screenLine, - line, - }) - acc.expectedLine = screenLine + 1 - } else { - acc.expectedLine += 1 - } - } +/** + * Calculates the position of each indent guide element using shiftwidth or tabstop if no + * shift width available + */ +const IndentGuideLines: React.SFC = ({ context, ...props }) => { + const wrappedLines = getWrappedLines(context) + const { visibleLines, fontPixelHeight, fontPixelWidth, topBufferLine } = context + const indentSize = props.userSpacing * fontPixelWidth + + // TODO: If the beginning of the visible lines is wrapping no lines are drawn + const { allIndentations } = visibleLines.reduce( + (acc, line, currentLineNumber) => { + const rawIndentation = detectIndent(line) + const regularisedIndent = regulariseIndentation(rawIndentation, props.userSpacing) + const previous = last(acc.allIndentations) + const height = Math.ceil(fontPixelHeight) + + // start position helps determine the initial indent offset + const startPosition = context.bufferToScreen({ + line: topBufferLine, + character: regularisedIndent, + }) + + const wrappedLine = wrappedLines.find(wrapped => wrapped.line === line) + const levelsOfWrapping = wrappedLine ? wrappedLine.end - wrappedLine.start : 1 + const adjustedHeight = height * levelsOfWrapping + + if (!startPosition) { return acc - }, - { lines: [], expectedLine: 1 }, - ) - return lines - } + } - private _regulariseIndentation(indentation: detectIndent.IndentInfo) { - const isOddBy = indentation.amount % this._userSpacing - const amountToIndent = isOddBy ? indentation.amount - isOddBy : indentation.amount - return amountToIndent - } + const { pixelX: left, pixelY: top } = context.screenToPixel({ + screenX: startPosition.screenX, + screenY: currentLineNumber, + }) - /** - * Calculates the position of each indent guide element using shiftwidth or tabstop if no - * shift width available - * @name _renderIndentLines - * @function - * @param {Oni.BufferLayerRenderContext} bufferLayerContext The buffer layer context - * @returns {JSX.Element[]} An array of react elements - */ - private _renderIndentLines = (bufferLayerContext: Oni.BufferLayerRenderContext) => { - // TODO: If the beginning of the visible lines is wrapping no lines are drawn - const wrappedScreenLines = this._getWrappedLines(bufferLayerContext) - - const options = { - color: this._configuration.getValue("experimental.indentLines.color"), - skipFirst: this._configuration.getValue("experimental.indentLines.skipFirst"), - } + const adjustedTop = top + acc.wrappedHeightAdjustment - const { visibleLines, fontPixelHeight, fontPixelWidth, topBufferLine } = bufferLayerContext - const indentSize = this._userSpacing * fontPixelWidth + // Only adjust height for Subsequent lines! + if (wrappedLine) { + acc.wrappedHeightAdjustment += adjustedHeight + } - const { allIndentations } = visibleLines.reduce( - (acc, line, currenLineNumber) => { - const rawIndentation = detectIndent(line) - - const regularisedIndent = this._regulariseIndentation(rawIndentation) - - const previous = last(acc.allIndentations) - const height = Math.ceil(fontPixelHeight) - - // start position helps determine the initial indent offset - const startPosition = bufferLayerContext.bufferToScreen({ - line: topBufferLine, - character: regularisedIndent, + if (!line && previous) { + acc.allIndentations.push({ + ...previous, + line, + top: adjustedTop, }) + return acc + } + + const indent = { + left, + line, + indentSize, + top: adjustedTop, + height: adjustedHeight, + characterWidth: fontPixelWidth, + indentBy: regularisedIndent / props.userSpacing, + } + + acc.allIndentations.push(indent) + + return acc + }, + { allIndentations: [], wrappedHeightAdjustment: 0 }, + ) + + return +} - const wrappedLine = wrappedScreenLines.find(wrapped => wrapped.line === line) - const levelsOfWrapping = wrappedLine ? wrappedLine.end - wrappedLine.start : 1 - const adjustedHeight = height * levelsOfWrapping - - if (!startPosition) { +const getWrappedLines = (ctx: Partial): IWrappedLine[] => { + const { lines } = ctx.visibleLines.reduce( + (acc, line, index) => { + const currentLine = ctx.topBufferLine + index + const bufferInfo = ctx.bufferToScreen({ line: currentLine, character: 0 }) + + if (bufferInfo && bufferInfo.screenY) { + const { screenY: screenLine } = bufferInfo + if (acc.expectedLine !== screenLine) { + acc.lines.push({ + start: acc.expectedLine, + end: screenLine, + line, + }) + acc.expectedLine = screenLine + 1 return acc } + acc.expectedLine += 1 + } + return acc + }, + { lines: [], expectedLine: 1 }, + ) + return lines +} - const { pixelX: left, pixelY: top } = bufferLayerContext.screenToPixel({ - screenX: startPosition.screenX, - screenY: currenLineNumber, - }) +const regulariseIndentation = (indentation: detectIndent.IndentInfo, userSpacing: number) => { + const isOddBy = indentation.amount % userSpacing + const amountToIndent = isOddBy ? indentation.amount - isOddBy : indentation.amount + return amountToIndent +} - const adjustedTop = top + acc.wrappedHeightAdjustment +interface IndentLayerArgs { + buffer: IBuffer + configuration: Oni.Configuration +} - // Only adjust height for Subsequent lines! - if (wrappedLine) { - acc.wrappedHeightAdjustment += adjustedHeight - } +class IndentGuideBufferLayer implements Oni.BufferLayer { + private _buffer: IBuffer + private _userSpacing: number + private _configuration: ConfigOptions - if (!line && previous) { - acc.allIndentations.push({ - ...previous, - line, - top: adjustedTop, - }) - return acc - } + get id() { + return "indent-guides" + } - const indent = { - left, - line, - indentSize, - top: adjustedTop, - height: adjustedHeight, - characterWidth: fontPixelWidth, - indentBy: regularisedIndent / this._userSpacing, - } + get friendlyName() { + return "Indent Guide Lines" + } - acc.allIndentations.push(indent) + constructor({ buffer, configuration }: IndentLayerArgs) { + this._buffer = buffer + this._configuration = { + color: configuration.getValue("experimental.indentLines.color"), + skipFirst: configuration.getValue("experimental.indentLines.skipFirst"), + } + this._userSpacing = this._buffer.shiftwidth || this._buffer.tabstop + } - return acc - }, - { allIndentations: [], wrappedHeightAdjustment: 0 }, + public render = (bufferLayerContext: IContext) => { + return ( + + + ) - - return this._getIndentLines(allIndentations, options) } } diff --git a/package.json b/package.json index 5526ff7554..c55e1aa2b2 100644 --- a/package.json +++ b/package.json @@ -870,6 +870,7 @@ "keyboard-layout": "^2.0.13", "marked": "^0.4.0", "minimist": "1.2.0", + "moize": "^5.4.1", "msgpack-lite": "0.1.26", "ocaml-language-server": "^1.0.27", "oni-api": "0.0.49", diff --git a/yarn.lock b/yarn.lock index d799ed8f1f..342c272151 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4142,6 +4142,10 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" +fast-equals@^1.6.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-1.6.1.tgz#66cc5a0922ea747599f41aedf44a76ec2908adc0" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -4154,6 +4158,10 @@ fast-plist@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" +fast-stringify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-stringify/-/fast-stringify-1.1.0.tgz#6caf0af2456296adac9d0c9c6f0522d18297f57a" + fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" @@ -7007,6 +7015,10 @@ methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" +micro-memoize@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-2.1.1.tgz#5092f1ef475546b28f8da0e11f061850d5549ae4" + micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" @@ -7199,6 +7211,14 @@ mocha@^4.0.1: mkdirp "0.5.1" supports-color "4.4.0" +moize@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/moize/-/moize-5.4.1.tgz#664a1a6856bd8dfe0ceda72f17e6765ff5910243" + dependencies: + fast-equals "^1.6.0" + fast-stringify "^1.1.0" + micro-memoize "^2.1.1" + moment@^2.11.2: version "2.19.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"