Skip to content
This repository has been archived by the owner on Apr 1, 2020. It is now read-only.

Commit

Permalink
Feature/experimental indent guide lines (#2344)
Browse files Browse the repository at this point in the history
* 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
akinsho authored Jun 26, 2018
1 parent 74a4dc7 commit fd7ac52
Show file tree
Hide file tree
Showing 10 changed files with 490 additions and 3 deletions.
70 changes: 69 additions & 1 deletion browser/src/Editor/BufferManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ import { IBufferLayer } from "./NeovimEditor/BufferLayerManager"
* Candidate API methods
*/
export interface IBuffer extends Oni.Buffer {
setLanguage(lang: string): Promise<void>
tabstop: number
shiftwidth: number
comment: ICommentFormats

setLanguage(lang: string): Promise<void>
getLayerById<T>(id: string): T

getCursorPosition(): Promise<types.Position>
Expand All @@ -59,6 +62,13 @@ export interface IBuffer extends Oni.Buffer {

type NvimError = [1, string]

interface ICommentFormats {
start: string
middle: string
end: string
defaults: string[]
}

const isStringArray = (value: NvimError | string[]): value is string[] => {
if (value && Array.isArray(value)) {
return typeof value[0] === "string"
Expand Down Expand Up @@ -100,10 +110,26 @@ export class Buffer implements IBuffer {
private _version: number
private _modified: boolean
private _lineCount: number
private _tabstop: number
private _shiftwidth: number
private _comment: ICommentFormats

private _bufferHighlightId: BufferHighlightId = null

private _promiseQueue = new PromiseQueue()

public get shiftwidth(): number {
return this._shiftwidth
}

public get tabstop(): number {
return this._tabstop
}

public get comment(): ICommentFormats {
return this._comment
}

public get filePath(): string {
return this._filePath
}
Expand Down Expand Up @@ -435,12 +461,54 @@ export class Buffer implements IBuffer {
this._modified = evt.modified
this._lineCount = evt.bufferTotalLines
this._cursorOffset = evt.byte
this._tabstop = evt.tabstop
this._shiftwidth = evt.shiftwidth
this._comment = this.formatCommentOption(evt.comments)

this._cursor = {
line: evt.line - 1,
column: evt.column - 1,
}
}

public formatCommentOption(comments: string): ICommentFormats {
if (!comments) {
return null
}
try {
const commentsArray = comments.split(",")
const commentFormats = commentsArray.reduce<ICommentFormats>(
(acc, str) => {
const [flag, character] = str.split(":")
switch (true) {
case flag.includes("s"):
acc.start = character
return acc
case flag.includes("m"):
acc.middle = character
return acc
case flag.includes("e"):
acc.end = character
return acc
default:
acc.defaults.push(character)
return acc
}
},
{
start: null,
middle: null,
end: null,
defaults: [],
},
)

return commentFormats
} catch (e) {
Log.warn(`Error formatting neovim comment options due to ${e.message}`)
return null
}
}
}

// Helper for managing buffer state
Expand Down
234 changes: 234 additions & 0 deletions browser/src/Editor/OniEditor/IndentGuideBufferLayer.tsx
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
19 changes: 17 additions & 2 deletions browser/src/Editor/OniEditor/OniEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ import { NeovimEditor } from "./../NeovimEditor"

import { SplitDirection, windowManager } from "./../../Services/WindowManager"

import { IBuffer } from "../BufferManager"
import { ImageBufferLayer } from "./ImageBufferLayer"
import IndentLineBufferLayer from "./IndentGuideBufferLayer"

// Helper method to wrap a react component into a layer
const wrapReactComponentWithLayer = (id: string, component: JSX.Element): Oni.BufferLayer => {
Expand Down Expand Up @@ -170,11 +172,24 @@ export class OniEditor extends Utility.Disposable implements IEditor {
wrapReactComponentWithLayer("oni.layer.errors", <ErrorsContainer />),
)

const extensions = this._configuration.getValue("editor.imageLayerExtensions")
const imageExtensions = this._configuration.getValue("editor.imageLayerExtensions")
const indentExtensions = this._configuration.getValue("experimental.indentLines.filetypes")
this._neovimEditor.bufferLayers.addBufferLayer(
buf => extensions.includes(path.extname(buf.filePath)),
buf => imageExtensions.includes(path.extname(buf.filePath)),
buf => new ImageBufferLayer(buf),
)

if (this._configuration.getValue("experimental.indentLines.enabled")) {
this._neovimEditor.bufferLayers.addBufferLayer(
buf => indentExtensions.includes(path.extname(buf.filePath)),
buffer =>
new IndentLineBufferLayer({
buffer: buffer as IBuffer,
configuration: this._configuration,
neovimEditor: this._neovimEditor,
}),
)
}
}

public dispose(): void {
Expand Down
Loading

0 comments on commit fd7ac52

Please sign in to comment.