This repository has been archived by the owner on Apr 1, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/experimental indent guide lines (#2344)
* pull upstream * add early stages indentlines plugin * create separate interface fix wrong indentlines start position * fix rendering errors with scroll * remove unused interface vals and destructure line props * add default experimental config for indentlines off by default separate init into a function * add comment detection and indentation handling use atomic calls and swap tabstop in if no shiftwidth * add ci test for indent guides * undo accidental ciTest commit * fix lint error * use comment option not comment string as its more extensize add functioning ci test * add initial processing to check of line is wrapped * rename functions and variables * return metadata re wrapped lines and use this to calculate indent positions * use adjusted top for comments and 0 indent lines * convert to getwrapped lines to a reduce * remove unindented line "optimisation" make sure height adjustment applies to comments as well * add test for buffer manager class, make format comment public for testing * pass neovim editor to indent layer to update layer with changes to comments for active file * add more refined comment parsing * tweak comment handling in indent plugin * add newline between fixme comment and code * indent end of multiline comment * update tests to pass in valid comment string
- Loading branch information
Showing
10 changed files
with
490 additions
and
3 deletions.
There are no files selected for viewing
This file contains 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
234 changes: 234 additions & 0 deletions
234
browser/src/Editor/OniEditor/IndentGuideBufferLayer.tsx
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,234 @@ | ||
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 * as Oni from "oni-api" | ||
|
||
import { IBuffer } from "../BufferManager" | ||
import { NeovimEditor } from "../NeovimEditor/NeovimEditor" | ||
import styled, { pixel, withProps } from "./../../UI/components/common" | ||
|
||
interface IWrappedLine { | ||
start: number | ||
end: number | ||
line: string | ||
} | ||
|
||
interface IProps { | ||
height: number | ||
left: number | ||
top: number | ||
color?: string | ||
} | ||
|
||
interface IndentLinesProps { | ||
top: number | ||
left: number | ||
height: number | ||
line: string | ||
indentBy: number | ||
characterWidth: number | ||
} | ||
|
||
const Container = styled.div`` | ||
|
||
const IndentLine = withProps<IProps>(styled.span).attrs({ | ||
style: ({ height, left, top }: IProps) => ({ | ||
height: pixel(height), | ||
left: pixel(left), | ||
top: pixel(top), | ||
}), | ||
})` | ||
border-left: 1px solid ${p => p.color || "rgba(100, 100, 100, 0.4)"}; | ||
position: absolute; | ||
` | ||
|
||
interface IndentLayerArgs { | ||
buffer: IBuffer | ||
neovimEditor: NeovimEditor | ||
configuration: Oni.Configuration | ||
} | ||
|
||
class IndentGuideBufferLayer implements Oni.BufferLayer { | ||
public render = memoize((bufferLayerContext: Oni.BufferLayerRenderContext) => { | ||
return <Container id={this.id}>{this._renderIndentLines(bufferLayerContext)}</Container> | ||
}) | ||
|
||
private _checkForComment = memoize((line: string) => { | ||
const trimmedLine = line.trim() | ||
const isMultiLine = Object.entries(this._comments).reduce( | ||
(acc, [key, comment]) => { | ||
const match = Array.isArray(comment) | ||
? comment.some(char => trimmedLine.startsWith(char)) | ||
: trimmedLine.startsWith(comment) | ||
|
||
if (match) { | ||
return { | ||
isMultiline: key !== "default", | ||
position: key, | ||
isComment: true, | ||
} | ||
} | ||
return acc | ||
}, | ||
{ isComment: false, isMultiline: null, position: null }, | ||
) | ||
return isMultiLine | ||
}) | ||
|
||
private _buffer: IBuffer | ||
private _comments: IBuffer["comment"] | ||
private _userSpacing: number | ||
private _configuration: Oni.Configuration | ||
|
||
constructor({ buffer, neovimEditor, configuration }: IndentLayerArgs) { | ||
this._buffer = buffer | ||
this._configuration = configuration | ||
this._comments = this._buffer.comment | ||
this._userSpacing = this._buffer.shiftwidth || this._buffer.tabstop | ||
|
||
neovimEditor.onBufferEnter.subscribe(buf => { | ||
this._comments = (buf as IBuffer).comment | ||
}) | ||
} | ||
get id() { | ||
return "indent-guides" | ||
} | ||
|
||
get friendlyName() { | ||
return "Indent Guide Lines" | ||
} | ||
|
||
private _getIndentLines = (guidePositions: IndentLinesProps[], color?: string) => { | ||
return flatten( | ||
guidePositions.map(({ line, height, characterWidth, indentBy, left, top }, lineNo) => { | ||
const indentation = characterWidth * this._userSpacing | ||
return Array.from({ length: indentBy }, (_, level) => { | ||
const adjustedLeft = left - level * indentation - characterWidth | ||
return ( | ||
<IndentLine | ||
top={top} | ||
color={color} | ||
height={height} | ||
left={adjustedLeft} | ||
key={`${line.trim()}-${lineNo}-${indentation}-${level}`} | ||
data-id="indent-line" | ||
/> | ||
) | ||
}) | ||
}), | ||
) | ||
} | ||
|
||
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 | ||
} | ||
} | ||
return acc | ||
}, | ||
{ lines: [], expectedLine: 1 }, | ||
) | ||
return lines | ||
} | ||
|
||
/** | ||
* 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) => { | ||
// FIXME: Outstanding issues - | ||
// 1. If the beginning of the visible lines is wrapping no lines are drawn | ||
// 2. If a line wraps but the wrapped line has no content line positions are off by one | ||
|
||
const wrappedScreenLines = this._getWrappedLines(bufferLayerContext) | ||
const color = this._configuration.getValue<string>("experimental.indentLines.color") | ||
const { visibleLines, fontPixelHeight, fontPixelWidth, topBufferLine } = bufferLayerContext | ||
|
||
const { allIndentations } = visibleLines.reduce( | ||
(acc, line, currenLineNumber) => { | ||
const indentation = detectIndent(line) | ||
|
||
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: indentation.amount, | ||
}) | ||
|
||
const wrappedLine = wrappedScreenLines.find(wrapped => wrapped.line === line) | ||
const levelsOfWrapping = wrappedLine ? wrappedLine.end - wrappedLine.start : 1 | ||
const adjustedHeight = height * levelsOfWrapping | ||
|
||
if (!startPosition) { | ||
return acc | ||
} | ||
|
||
const { pixelX: left, pixelY: top } = bufferLayerContext.screenToPixel({ | ||
screenX: startPosition.screenX, | ||
screenY: currenLineNumber, | ||
}) | ||
|
||
const adjustedTop = top + acc.wrappedHeightAdjustment | ||
|
||
// Only adjust height for Subsequent lines! | ||
if (wrappedLine) { | ||
acc.wrappedHeightAdjustment += adjustedHeight | ||
} | ||
|
||
const { isComment, position } = this._checkForComment(line) | ||
const inMultiLineComment = | ||
isComment && (position === "middle" || position === "end") | ||
|
||
if ((!line && previous) || inMultiLineComment) { | ||
acc.allIndentations.push({ | ||
...previous, | ||
line, | ||
top: adjustedTop, | ||
}) | ||
return acc | ||
} | ||
|
||
const indent = { | ||
left, | ||
line, | ||
top: adjustedTop, | ||
height: adjustedHeight, | ||
indentBy: indentation.amount / this._userSpacing, | ||
characterWidth: fontPixelWidth, | ||
} | ||
|
||
acc.allIndentations.push(indent) | ||
|
||
return acc | ||
}, | ||
{ allIndentations: [], wrappedHeightAdjustment: 0 }, | ||
) | ||
|
||
return this._getIndentLines(allIndentations, color) | ||
} | ||
} | ||
|
||
export default IndentGuideBufferLayer |
This file contains 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
Oops, something went wrong.