mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-19 23:17:16 +03:00
Merge pull request #17736 from atom/aw/custom-line-number-gutter
Multiple, custom line number gutters
This commit is contained in:
commit
4e150179a9
@ -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', () => {
|
||||
|
@ -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())
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user