Merge pull request #17736 from atom/aw/custom-line-number-gutter

Multiple, custom line number gutters
This commit is contained in:
Ash Wilson 2018-08-21 18:17:31 -04:00 committed by GitHub
commit 4e150179a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 181 additions and 28 deletions

View File

@ -2054,6 +2054,37 @@ describe('TextEditorComponent', () => {
expect(decorationNode2.firstChild).toBeNull()
expect(gutterB.getElement().firstChild.children.length).toBe(0)
})
it('renders custom line number gutters', async () => {
const {component, editor} = buildComponent()
const gutterA = editor.addGutter({
name: 'a',
priority: 1,
type: 'line-number',
class: 'a-number',
labelFn: ({bufferRow}) => `a - ${bufferRow}`
})
const gutterB = editor.addGutter({
name: 'b',
priority: 1,
type: 'line-number',
class: 'b-number',
labelFn: ({bufferRow}) => `b - ${bufferRow}`
})
editor.setText('0000\n0001\n0002\n0003\n0004\n')
await component.getNextUpdatePromise()
const gutterAElement = gutterA.getElement()
const aNumbers = gutterAElement.querySelectorAll('div.line-number[data-buffer-row]')
const aLabels = Array.from(aNumbers, e => e.textContent)
expect(aLabels).toEqual(['a - 0', 'a - 1', 'a - 2', 'a - 3', 'a - 4', 'a - 5'])
const gutterBElement = gutterB.getElement()
const bNumbers = gutterBElement.querySelectorAll('div.line-number[data-buffer-row]')
const bLabels = Array.from(bNumbers, e => e.textContent)
expect(bLabels).toEqual(['b - 0', 'b - 1', 'b - 2', 'b - 3', 'b - 4', 'b - 5'])
})
})
describe('block decorations', () => {

View File

@ -6716,6 +6716,20 @@ describe('TextEditor', () => {
const gutter = editor.addGutter(options)
expect(editor.getGutters().length).toBe(2)
expect(editor.getGutters()[1]).toBe(gutter)
expect(gutter.type).toBe('decorated')
})
it('can add a custom line-number gutter', () => {
expect(editor.getGutters().length).toBe(1)
const options = {
name: 'another-gutter',
priority: 2,
type: 'line-number'
}
const gutter = editor.addGutter(options)
expect(editor.getGutters().length).toBe(2)
expect(editor.getGutters()[1]).toBe(gutter)
expect(gutter.type).toBe('line-number')
})
it("does not allow a custom gutter with the 'line-number' name.", () => expect(editor.addGutter.bind(editor, {name: 'line-number'})).toThrow())

View File

@ -97,7 +97,7 @@ module.exports = class GutterContainer {
// The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
addGutterDecoration (gutter, marker, options) {
if (gutter.name === 'line-number') {
if (gutter.type === 'line-number') {
options.type = 'line-number'
} else {
options.type = 'gutter'

View File

@ -11,6 +11,12 @@ module.exports = class Gutter {
this.name = options && options.name
this.priority = (options && options.priority != null) ? options.priority : DefaultPriority
this.visible = (options && options.visible != null) ? options.visible : true
this.type = (options && options.type != null) ? options.type : 'decorated'
this.labelFn = options && options.labelFn
this.className = options && options.class
this.onMouseDown = options && options.onMouseDown
this.onMouseMove = options && options.onMouseMove
this.emitter = new Emitter()
}

View File

@ -148,12 +148,13 @@ class TextEditorComponent {
this.lineNumbersToRender = {
maxDigits: 2,
bufferRows: [],
screenRows: [],
keys: [],
softWrappedFlags: [],
foldableFlags: []
}
this.decorationsToRender = {
lineNumbers: null,
lineNumbers: new Map(),
lines: null,
highlights: [],
cursors: [],
@ -886,7 +887,7 @@ class TextEditorComponent {
queryLineNumbersToRender () {
const {model} = this.props
if (!model.isLineNumberGutterVisible()) return
if (!model.anyLineNumberGutterVisible()) return
if (this.showLineNumbers !== model.doesShowLineNumbers()) {
this.remeasureGutterDimensions = true
this.showLineNumbers = model.doesShowLineNumbers()
@ -942,7 +943,7 @@ class TextEditorComponent {
queryMaxLineNumberDigits () {
const {model} = this.props
if (model.isLineNumberGutterVisible()) {
if (model.anyLineNumberGutterVisible()) {
const maxDigits = Math.max(2, model.getLineCount().toString().length)
if (maxDigits !== this.lineNumbersToRender.maxDigits) {
this.remeasureGutterDimensions = true
@ -977,7 +978,7 @@ class TextEditorComponent {
}
queryDecorationsToRender () {
this.decorationsToRender.lineNumbers = []
this.decorationsToRender.lineNumbers.clear()
this.decorationsToRender.lines = []
this.decorationsToRender.overlays.length = 0
this.decorationsToRender.customGutter.clear()
@ -1040,7 +1041,17 @@ class TextEditorComponent {
}
addLineDecorationToRender (type, decoration, screenRange, reversed) {
const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers
let decorationsToRender
if (type === 'line') {
decorationsToRender = this.decorationsToRender.lines
} else {
const gutterName = decoration.gutterName || 'line-number'
decorationsToRender = this.decorationsToRender.lineNumbers.get(gutterName)
if (!decorationsToRender) {
decorationsToRender = []
this.decorationsToRender.lineNumbers.set(gutterName, decorationsToRender)
}
}
let omitLastRow = false
if (screenRange.isEmpty()) {
@ -3099,7 +3110,7 @@ class GutterContainerComponent {
},
$.div({style: innerStyle},
guttersToRender.map((gutter) => {
if (gutter.name === 'line-number') {
if (gutter.type === 'line-number') {
return this.renderLineNumberGutter(gutter)
} else {
return $(CustomGutterComponent, {
@ -3118,18 +3129,29 @@ class GutterContainerComponent {
renderLineNumberGutter (gutter) {
const {
rootComponent, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
rootComponent, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration,
scrollHeight, lineNumberGutterWidth, lineHeight
} = this.props
if (!isLineNumberGutterVisible) return null
if (!gutter.isVisible()) {
return null
}
const oneTrueLineNumberGutter = gutter.name === 'line-number'
const ref = oneTrueLineNumberGutter ? 'lineNumberGutter' : undefined
const width = oneTrueLineNumberGutter ? lineNumberGutterWidth : undefined
if (hasInitialMeasurements) {
const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
ref,
element: gutter.getElement(),
name: gutter.name,
className: gutter.className,
labelFn: gutter.labelFn,
onMouseDown: gutter.onMouseDown,
onMouseMove: gutter.onMouseMove,
rootComponent: rootComponent,
startRow: renderedStartRow,
endRow: renderedEndRow,
@ -3140,18 +3162,22 @@ class GutterContainerComponent {
screenRows: screenRows,
softWrappedFlags: softWrappedFlags,
foldableFlags: foldableFlags,
decorations: decorationsToRender.lineNumbers,
decorations: decorationsToRender.lineNumbers.get(gutter.name) || [],
blockDecorations: decorationsToRender.blocks,
didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration,
height: scrollHeight,
width: lineNumberGutterWidth,
width,
lineHeight: lineHeight,
showLineNumbers
})
} else {
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
ref,
element: gutter.getElement(),
name: gutter.name,
className: gutter.className,
onMouseDown: gutter.onMouseDown,
onMouseMove: gutter.onMouseMove,
maxDigits: lineNumbersToRender.maxDigits,
showLineNumbers
})
@ -3179,7 +3205,8 @@ class LineNumberGutterComponent {
render () {
const {
rootComponent, showLineNumbers, height, width, startRow, endRow, rowsPerTile,
maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations
maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations,
className
} = this.props
let children = null
@ -3207,8 +3234,12 @@ class LineNumberGutterComponent {
let number = null
if (showLineNumbers) {
number = softWrapped ? '•' : bufferRow + 1
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
if (this.props.labelFn == null) {
number = softWrapped ? '•' : bufferRow + 1
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
} else {
number = this.props.labelFn({bufferRow, screenRow, foldable, softWrapped, maxDigits})
}
}
// We need to adjust the line number position to account for block
@ -3235,6 +3266,7 @@ class LineNumberGutterComponent {
const tileTop = rootComponent.pixelPositionBeforeBlocksForRow(tileStartRow)
const tileBottom = rootComponent.pixelPositionBeforeBlocksForRow(tileEndRow)
const tileHeight = tileBottom - tileTop
const tileWidth = width != null && width > 0 ? width + 'px' : ''
children[i] = $.div({
key: rootComponent.idsByTileStartRow.get(tileStartRow),
@ -3243,20 +3275,26 @@ class LineNumberGutterComponent {
position: 'absolute',
top: 0,
height: tileHeight + 'px',
width: width + 'px',
width: tileWidth,
transform: `translateY(${tileTop}px)`
}
}, ...tileChildren)
}
}
let rootClassName = 'gutter line-numbers'
if (className) {
rootClassName += ' ' + className
}
return $.div(
{
className: 'gutter line-numbers',
attributes: {'gutter-name': 'line-number'},
className: rootClassName,
attributes: {'gutter-name': this.props.name},
style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'},
on: {
mousedown: this.didMouseDown
mousedown: this.didMouseDown,
mousemove: this.didMouseMove
}
},
$.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}},
@ -3278,6 +3316,8 @@ class LineNumberGutterComponent {
if (oldProps.endRow !== newProps.endRow) return true
if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true
if (oldProps.maxDigits !== newProps.maxDigits) return true
if (oldProps.labelFn !== newProps.labelFn) return true
if (oldProps.className !== newProps.className) return true
if (newProps.didMeasureVisibleBlockDecoration) return true
if (!arraysEqual(oldProps.keys, newProps.keys)) return true
if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true
@ -3324,7 +3364,27 @@ class LineNumberGutterComponent {
}
didMouseDown (event) {
this.props.rootComponent.didMouseDownOnLineNumberGutter(event)
if (this.props.onMouseDown == null) {
this.props.rootComponent.didMouseDownOnLineNumberGutter(event)
} else {
const {bufferRow, screenRow} = event.target.dataset
this.props.onMouseDown({
bufferRow: parseInt(bufferRow, 10),
screenRow: parseInt(screenRow, 10),
domEvent: event
})
}
}
didMouseMove (event) {
if (this.props.onMouseMove != null) {
const {bufferRow, screenRow} = event.target.dataset
this.props.onMouseMove({
bufferRow: parseInt(bufferRow, 10),
screenRow: parseInt(screenRow, 10),
domEvent: event
})
}
}
}
@ -3332,7 +3392,8 @@ class LineNumberComponent {
constructor (props) {
const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props
this.props = props
const style = {width: width + 'px'}
const style = {}
if (width != null && width > 0) style.width = width + 'px'
if (marginTop != null && marginTop > 0) style.marginTop = marginTop + 'px'
this.element = nodePool.getElement('DIV', className, style)
this.element.dataset.bufferRow = bufferRow
@ -3352,22 +3413,31 @@ class LineNumberComponent {
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.width !== width) {
if (width != null && width > 0) {
this.element.style.width = width + 'px'
} else {
this.element.style.width = ''
}
}
if (this.props.marginTop !== marginTop) {
if (marginTop != null) {
if (marginTop != null && marginTop > 0) {
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 {
if (this.props.number != null) {
const numberNode = this.element.firstChild
numberNode.remove()
nodePool.release(numberNode)
}
if (number != null) {
this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild)
}
}
this.props = props
@ -3393,9 +3463,13 @@ class CustomGutterComponent {
}
render () {
let className = 'gutter'
if (this.props.className) {
className += ' ' + this.props.className
}
return $.div(
{
className: 'gutter',
className,
attributes: {'gutter-name': this.props.name},
style: {
display: this.props.visible ? '' : 'none'

View File

@ -258,6 +258,7 @@ class TextEditor {
this.gutterContainer = new GutterContainer(this)
this.lineNumberGutter = this.gutterContainer.addGutter({
name: 'line-number',
type: 'line-number',
priority: 0,
visible: params.lineNumberGutterVisible
})
@ -1020,6 +1021,10 @@ class TextEditor {
isLineNumberGutterVisible () { return this.lineNumberGutter.isVisible() }
anyLineNumberGutterVisible () {
return this.getGutters().some(gutter => gutter.type === 'line-number' && gutter.visible)
}
onDidChangeLineNumberGutterVisible (callback) {
return this.emitter.on('did-change-line-number-gutter-visible', callback)
}
@ -4211,6 +4216,29 @@ class TextEditor {
// window. (default: -100)
// * `visible` (optional) {Boolean} specifying whether the gutter is visible
// initially after being created. (default: true)
// * `type` (optional) {String} specifying the type of gutter to create. `'decorated'`
// gutters are useful as a destination for decorations created with {Gutter::decorateMarker}.
// `'line-number'` gutters.
// * `class` (optional) {String} added to the CSS classnames of the gutter's root DOM element.
// * `labelFn` (optional) {Function} called by a `'line-number'` gutter to generate the label for each line number
// element. Should return a {String} that will be used to label the corresponding line.
// * `lineData` an {Object} containing information about each line to label.
// * `bufferRow` {Number} indicating the zero-indexed buffer index of this line.
// * `screenRow` {Number} indicating the zero-indexed screen index.
// * `foldable` {Boolean} that is `true` if a fold may be created here.
// * `softWrapped` {Boolean} if this screen row is the soft-wrapped continuation of the same buffer row.
// * `maxDigits` {Number} the maximum number of digits necessary to represent any known screen row.
// * `onMouseDown` (optional) {Function} to be called when a mousedown event is received by a line-number
// element within this `type: 'line-number'` {Gutter}. If unspecified, the default behavior is to select the
// clicked buffer row.
// * `lineData` an {Object} containing information about the line that's being clicked.
// * `bufferRow` {Number} of the originating line element
// * `screenRow` {Number}
// * `onMouseMove` (optional) {Function} to be called when a mousemove event occurs on a line-number element within
// within this `type: 'line-number'` {Gutter}.
// * `lineData` an {Object} containing information about the line that's being clicked.
// * `bufferRow` {Number} of the originating line element
// * `screenRow` {Number}
//
// Returns the newly-created {Gutter}.
addGutter (options) {