Merge pull request #18773 from atom/aw/block-decoration-order

Explicit block decoration ordering
This commit is contained in:
Ash Wilson
2019-02-01 20:01:54 -05:00
committed by GitHub
6 changed files with 127 additions and 57 deletions

View File

@@ -65,7 +65,9 @@ describe "DecorationManager", ->
{oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
expect(oldProperties).toEqual decorationProperties
expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
expect(newProperties.type).toBe 'line-number'
expect(newProperties.gutterName).toBe 'line-number'
expect(newProperties.class).toBe 'two'
describe "::getDecorations(properties)", ->
it "returns decorations matching the given optional properties", ->

View File

@@ -2638,7 +2638,71 @@ describe('TextEditorComponent', () => {
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
})
function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, marginTop, marginBottom, position, invalidate}) {
it('uses the order property to control the order of block decorations at the same screen row', async () => {
const editor = buildEditor({autoHeight: false})
const {component, element} = buildComponent({editor})
element.style.height = 10 * component.getLineHeight() + horizontalScrollbarHeight + 'px'
await component.getNextUpdatePromise()
// Order parameters that differ from creation order; that collide; and that are not provided.
const [beforeItems, beforeDecorations] = [30, 20, undefined, 20, 10, undefined].map(order => {
return createBlockDecorationAtScreenRow(editor, 2, {height: 10, position: 'before', order})
}).reduce((lists, result) => {
lists[0].push(result.item)
lists[1].push(result.decoration)
return lists
}, [[], []])
const [afterItems, afterDecorations] = [undefined, 1, 6, undefined, 6, 2].map(order => {
return createBlockDecorationAtScreenRow(editor, 2, {height: 10, position: 'after', order})
}).reduce((lists, result) => {
lists[0].push(result.item)
lists[1].push(result.decoration)
return lists
}, [[], []])
await component.getNextUpdatePromise()
expect(beforeItems[4].previousSibling).toBe(lineNodeForScreenRow(component, 1))
expect(beforeItems[4].nextSibling).toBe(beforeItems[1])
expect(beforeItems[1].nextSibling).toBe(beforeItems[3])
expect(beforeItems[3].nextSibling).toBe(beforeItems[0])
expect(beforeItems[0].nextSibling).toBe(beforeItems[2])
expect(beforeItems[2].nextSibling).toBe(beforeItems[5])
expect(beforeItems[5].nextSibling).toBe(lineNodeForScreenRow(component, 2))
expect(afterItems[1].previousSibling).toBe(lineNodeForScreenRow(component, 2))
expect(afterItems[1].nextSibling).toBe(afterItems[5])
expect(afterItems[5].nextSibling).toBe(afterItems[2])
expect(afterItems[2].nextSibling).toBe(afterItems[4])
expect(afterItems[4].nextSibling).toBe(afterItems[0])
expect(afterItems[0].nextSibling).toBe(afterItems[3])
// Create a decoration somewhere else and move it to the same screen row as the existing decorations
const {item: later, decoration} = createBlockDecorationAtScreenRow(editor, 4, {height: 20, position: 'after', order: 3})
await component.getNextUpdatePromise()
expect(later.previousSibling).toBe(lineNodeForScreenRow(component, 4))
expect(later.nextSibling).toBe(lineNodeForScreenRow(component, 5))
decoration.getMarker().setHeadScreenPosition([2, 0])
await component.getNextUpdatePromise()
expect(later.previousSibling).toBe(afterItems[5])
expect(later.nextSibling).toBe(afterItems[2])
// Move a decoration away from its screen row and ensure the rest maintain their order
beforeDecorations[3].getMarker().setHeadScreenPosition([5, 0])
await component.getNextUpdatePromise()
expect(beforeItems[3].previousSibling).toBe(lineNodeForScreenRow(component, 4))
expect(beforeItems[3].nextSibling).toBe(lineNodeForScreenRow(component, 5))
expect(beforeItems[4].previousSibling).toBe(lineNodeForScreenRow(component, 1))
expect(beforeItems[4].nextSibling).toBe(beforeItems[1])
expect(beforeItems[1].nextSibling).toBe(beforeItems[0])
expect(beforeItems[0].nextSibling).toBe(beforeItems[2])
expect(beforeItems[2].nextSibling).toBe(beforeItems[5])
expect(beforeItems[5].nextSibling).toBe(lineNodeForScreenRow(component, 2))
});
function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, marginTop, marginBottom, position, order, invalidate}) {
const marker = editor.markScreenPosition([screenRow, 0], {invalidate: invalidate || 'never'})
const item = document.createElement('div')
item.style.height = height + 'px'
@@ -2646,7 +2710,7 @@ describe('TextEditorComponent', () => {
if (marginTop != null) item.style.marginTop = marginTop + 'px'
if (marginBottom != null) item.style.marginBottom = marginBottom + 'px'
item.style.width = 30 + 'px'
const decoration = editor.decorateMarker(marker, {type: 'block', item, position})
const decoration = editor.decorateMarker(marker, {type: 'block', item, position, order})
return {item, decoration, marker}
}

View File

@@ -6861,7 +6861,7 @@ describe('TextEditor', () => {
const marker = editor.markBufferRange([[2, 4], [6, 8]])
const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'foo'})
expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual({
properties: {type: 'highlight', class: 'foo'},
properties: {id: decoration.id, order: Infinity, type: 'highlight', class: 'foo'},
screenRange: marker.getScreenRange(),
bufferRange: marker.getBufferRange(),
rangeIsReversed: false

View File

@@ -3,12 +3,17 @@ const {Emitter} = require('event-kit')
let idCounter = 0
const nextId = () => idCounter++
// Applies changes to a decorationsParam {Object} to make it possible to
// differentiate decorations on custom gutters versus the line-number gutter.
const translateDecorationParamsOldToNew = function (decorationParams) {
if (decorationParams.type === 'line-number') {
const normalizeDecorationProperties = function (decoration, decorationParams) {
decorationParams.id = decoration.id
if (decorationParams.type === 'line-number' && decorationParams.gutterName == null) {
decorationParams.gutterName = 'line-number'
}
if (decorationParams.order == null) {
decorationParams.order = Infinity
}
return decorationParams
}
@@ -164,7 +169,7 @@ class Decoration {
setProperties (newProperties) {
if (this.destroyed) { return }
const oldProperties = this.properties
this.properties = translateDecorationParamsOldToNew(newProperties)
this.properties = normalizeDecorationProperties(this, newProperties)
if (newProperties.type != null) {
this.decorationManager.decorationDidChangeType(this)
}

View File

@@ -1191,6 +1191,10 @@ class TextEditorComponent {
decorationsByScreenLine.set(screenLine.id, decorations)
}
decorations.push(decoration)
// Order block decorations by increasing values of their "order" property. Break ties with "id", which mirrors
// their creation sequence.
decorations.sort((a, b) => a.order !== b.order ? a.order - b.order : a.id - b.id)
}
addTextDecorationToRender (decoration, screenRange, marker) {
@@ -3862,15 +3866,24 @@ class LinesTileComponent {
if (blockDecorations) {
blockDecorations.forEach((newDecorations, screenLineId) => {
var oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null
for (var i = 0; i < newDecorations.length; i++) {
var newDecoration = newDecorations[i]
if (oldDecorations && oldDecorations.includes(newDecoration)) continue
const oldDecorations = oldProps.blockDecorations ? oldProps.blockDecorations.get(screenLineId) : null
const lineNode = lineComponentsByScreenLineId.get(screenLineId).element
let lastAfter = lineNode
for (let i = 0; i < newDecorations.length; i++) {
const newDecoration = newDecorations[i]
const element = TextEditor.viewForItem(newDecoration.item)
if (oldDecorations && oldDecorations.includes(newDecoration)) {
if (newDecoration.position === 'after') {
lastAfter = element
}
continue
}
var element = TextEditor.viewForItem(newDecoration.item)
var lineNode = lineComponentsByScreenLineId.get(screenLineId).element
if (newDecoration.position === 'after') {
this.element.insertBefore(element, lineNode.nextSibling)
this.element.insertBefore(element, lastAfter.nextSibling)
lastAfter = element
} else {
this.element.insertBefore(element, lineNode)
}

View File

@@ -2221,14 +2221,17 @@ class TextEditor {
//
// The following are the supported decorations types:
//
// * __line__: Adds your CSS `class` to the line nodes within the range
// marked by the marker
// * __line-number__: Adds your CSS `class` to the line number nodes within the
// range marked by the marker
// * __highlight__: Adds a new highlight div to the editor surrounding the
// range marked by the marker. When the user selects text, the selection is
// visualized with a highlight decoration internally. The structure of this
// highlight will be
// * __line__: Adds the given CSS `class` to the lines overlapping the rows
// spanned by the marker.
// * __line-number__: Adds the given CSS `class` to the line numbers overlapping
// the rows spanned by the marker
// * __text__: Injects spans into all text overlapping the marked range, then adds
// the given `class` or `style` to these spans. Use this to manipulate the foreground
// color or styling of text in a range.
// * __highlight__: Creates an absolutely-positioned `.highlight` div to the editor
// containing nested divs that cover the marked region. For example, when the user
// selects text, the selection is implemented with a highlight decoration. The structure
// of this highlight will be:
// ```html
// <div class="highlight <your-class>">
// <!-- Will be one region for each row in the range. Spans 2 lines? There will be 2 regions. -->
@@ -2236,45 +2239,25 @@ class TextEditor {
// </div>
// ```
// * __overlay__: Positions the view associated with the given item at the head
// or tail of the given `DisplayMarker`.
// * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter
// decorations are created by calling {Gutter::decorateMarker} on the
// desired `Gutter` instance.
// or tail of the given `DisplayMarker`, depending on the `position` property.
// * __gutter__: Tracks a {DisplayMarker} in a {Gutter}. Gutter decorations are created
// by calling {Gutter::decorateMarker} on the desired `Gutter` instance.
// * __block__: Positions the view associated with the given item before or
// after the row of the given `TextEditorMarker`.
// after the row of the given {DisplayMarker}, depending on the `position` property.
// Block decorations at the same screen row are ordered by their `order` property.
// * __cursor__: Render a cursor at the head of the {DisplayMarker}. If multiple cursor decorations
// are created for the same marker, their class strings and style objects are combined
// into a single cursor. This decoration type may be used to style existing cursors
// by passing in their markers or to render artificial cursors that don't actaully
// exist in the model by passing a marker that isn't associated with a real cursor.
//
// ## Arguments
//
// * `marker` A {DisplayMarker} you want this decoration to follow.
// * `decorationParams` An {Object} representing the decoration e.g.
// `{type: 'line-number', class: 'linter-error'}`
// * `type` There are several supported decoration types. The behavior of the
// types are as follows:
// * `line` Adds the given `class` to the lines overlapping the rows
// spanned by the `DisplayMarker`.
// * `line-number` Adds the given `class` to the line numbers overlapping
// the rows spanned by the `DisplayMarker`.
// * `text` Injects spans into all text overlapping the marked range,
// then adds the given `class` or `style` properties to these spans.
// Use this to manipulate the foreground color or styling of text in
// a given range.
// * `highlight` Creates an absolutely-positioned `.highlight` div
// containing nested divs to cover the marked region. For example, this
// is used to implement selections.
// * `overlay` Positions the view associated with the given item at the
// head or tail of the given `DisplayMarker`, depending on the `position`
// property.
// * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling
// {Gutter::decorateMarker} on the desired `Gutter` instance.
// * `block` Positions the view associated with the given item before or
// after the row of the given `TextEditorMarker`, depending on the `position`
// property.
// * `cursor` Renders a cursor at the head of the given marker. If multiple
// decorations are created for the same marker, their class strings and
// style objects are combined into a single cursor. You can use this
// decoration type to style existing cursors by passing in their markers
// or render artificial cursors that don't actually exist in the model
// by passing a marker that isn't actually associated with a cursor.
// * `type` Determines the behavior and appearance of this {Decoration}. Supported decoration types
// and their uses are listed above.
// * `class` This CSS class will be applied to the decorated line number,
// line, text spans, highlight regions, cursors, or overlay.
// * `style` An {Object} containing CSS style properties to apply to the
@@ -2300,12 +2283,15 @@ class TextEditor {
// Controls where the view is positioned relative to the `TextEditorMarker`.
// Values can be `'head'` (the default) or `'tail'` for overlay decorations, and
// `'before'` (the default) or `'after'` for block decorations.
// * `order` (optional) Only applicable to decorations of type `block`. Controls
// where the view is positioned relative to other block decorations at the
// same screen row. If unspecified, block decorations render oldest to newest.
// * `avoidOverflow` (optional) Only applicable to decorations of type
// `overlay`. Determines whether the decoration adjusts its horizontal or
// vertical position to remain fully visible when it would otherwise
// overflow the editor. Defaults to `true`.
//
// Returns a {Decoration} object
// Returns the created {Decoration} object.
decorateMarker (marker, decorationParams) {
return this.decorationManager.decorateMarker(marker, decorationParams)
}