Skip to content

Commit a85f18a

Browse files
author
Antonio Scandurra
authored
Merge pull request atom-archive#95 from atom/gutter
Show line numbers
2 parents 85b152b + 31ef102 commit a85f18a

File tree

4 files changed

+186
-34
lines changed

4 files changed

+186
-34
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ Once we get the basic collaboration experience down, we'll be looking to expand
182182
* [x] Key bindings system
183183
* [x] Horizontal scrolling
184184
* [ ] Word- and line-based cursor movements
185-
* [ ] Gutter with line numbers
185+
* [x] Gutter with line numbers
186186
* [ ] Mouse interaction
187187
* [ ] Workspace tabs
188188
* [ ] Split panes

xray_core/src/buffer_view.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,7 @@ impl View for BufferView {
766766

767767
json!({
768768
"first_visible_row": start.row,
769+
"total_row_count": buffer.max_point().row + 1,
769770
"lines": lines,
770771
"longest_line": longest_line,
771772
"scroll_top": self.scroll_top(),

xray_ui/lib/text_editor/text_editor.js

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class TextEditor extends React.Component {
114114
width: this.getScrollWidth(),
115115
selections: this.props.selections,
116116
firstVisibleRow: this.props.first_visible_row,
117+
totalRowCount: this.props.total_row_count,
117118
lines: this.props.lines,
118119
ref: textPlane => {
119120
this.textPlane = textPlane;
@@ -208,18 +209,25 @@ class TextEditor extends React.Component {
208209

209210
flushHorizontalAutoscroll() {
210211
const { horizontal_autoscroll, horizontal_margin, width } = this.props;
211-
if (horizontal_autoscroll && width && this.canUseTextPlane()) {
212+
const gutterWidth = this.getGutterWidth();
213+
if (
214+
horizontal_autoscroll &&
215+
width &&
216+
gutterWidth &&
217+
this.canUseTextPlane()
218+
) {
212219
const desiredScrollLeft = this.textPlane.measureLine(
213220
horizontal_autoscroll.start_line,
214221
Math.max(0, horizontal_autoscroll.start.column - horizontal_margin)
215222
);
216-
const desiredScrollRight = this.textPlane.measureLine(
217-
horizontal_autoscroll.end_line,
218-
Math.min(
219-
horizontal_autoscroll.end_line.length,
220-
horizontal_autoscroll.end.column + horizontal_margin
221-
)
222-
);
223+
const desiredScrollRight =
224+
this.textPlane.measureLine(
225+
horizontal_autoscroll.end_line,
226+
Math.min(
227+
horizontal_autoscroll.end_line.length,
228+
horizontal_autoscroll.end.column + horizontal_margin
229+
)
230+
) + gutterWidth;
223231

224232
// This function will be called during render, so we avoid calling
225233
// setState and we manually manipulate this.state instead.
@@ -279,8 +287,13 @@ class TextEditor extends React.Component {
279287
getContentWidth() {
280288
const longestLineWidth = this.getLongestLineWidth();
281289
const cursorWidth = this.getCursorWidth();
282-
if (longestLineWidth != null && cursorWidth != null) {
283-
return Math.ceil(longestLineWidth + cursorWidth);
290+
const gutterWidth = this.getGutterWidth();
291+
if (
292+
longestLineWidth != null &&
293+
cursorWidth != null &&
294+
gutterWidth != null
295+
) {
296+
return Math.ceil(gutterWidth + longestLineWidth + cursorWidth);
284297
} else {
285298
return null;
286299
}
@@ -302,6 +315,14 @@ class TextEditor extends React.Component {
302315
return this.longestLineWidth;
303316
}
304317

318+
getGutterWidth() {
319+
if (this.canUseTextPlane()) {
320+
return this.textPlane.getGutterWidth(this.props.total_row_count);
321+
} else {
322+
return null;
323+
}
324+
}
325+
305326
canUseTextPlane() {
306327
return this.textPlane && this.textPlane.isReady();
307328
}

xray_ui/lib/text_editor/text_plane.js

Lines changed: 153 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ class TextPlane extends React.Component {
6363
canvasHeight: this.props.height * window.devicePixelRatio,
6464
scrollTop: this.props.scrollTop,
6565
scrollLeft: this.props.scrollLeft,
66-
paddingLeft: this.props.paddingLeft || 0,
66+
paddingLeft: this.props.paddingLeft * window.devicePixelRatio || 0,
6767
firstVisibleRow: this.props.firstVisibleRow,
68+
totalRowCount: this.props.totalRowCount,
6869
lines: this.props.lines,
6970
selections: this.props.selections,
7071
showLocalCursors: this.props.showLocalCursors,
@@ -78,6 +79,10 @@ class TextPlane extends React.Component {
7879
return this.renderer.measureLine(line, column);
7980
}
8081

82+
getGutterWidth(totalRowCount) {
83+
return this.renderer.getGutterWidth(totalRowCount);
84+
}
85+
8186
isReady() {
8287
return this.renderer != null;
8388
}
@@ -192,7 +197,12 @@ class Renderer {
192197
this.gl.STATIC_DRAW
193198
);
194199

195-
this.glyphInstances = new Float32Array(MAX_INSTANCES * GLYPH_INSTANCE_SIZE);
200+
this.lineGlyphInstances = new Float32Array(
201+
MAX_INSTANCES * GLYPH_INSTANCE_SIZE
202+
);
203+
this.gutterGlyphInstances = new Float32Array(
204+
MAX_INSTANCES * GLYPH_INSTANCE_SIZE
205+
);
196206
this.glyphInstancesBuffer = this.gl.createBuffer();
197207

198208
this.selectionSolidInstances = new Float32Array(
@@ -201,6 +211,7 @@ class Renderer {
201211
this.cursorSolidInstances = new Float32Array(
202212
MAX_INSTANCES * SOLID_INSTANCE_SIZE
203213
);
214+
this.opaqueRectangleInstances = new Float32Array(1 * SOLID_INSTANCE_SIZE);
204215
this.solidInstancesBuffer = this.gl.createBuffer();
205216
}
206217

@@ -359,6 +370,7 @@ class Renderer {
359370
scrollLeft,
360371
paddingLeft,
361372
firstVisibleRow,
373+
totalRowCount,
362374
lines,
363375
selections,
364376
showLocalCursors,
@@ -372,6 +384,7 @@ class Renderer {
372384

373385
const textColor = { r: 0, g: 0, b: 0, a: 255 };
374386
const cursorWidth = 2;
387+
const gutterWidth = this.getGutterWidth(totalRowCount);
375388

376389
const xPositions = new Map();
377390
for (let i = 0; i < selections.length; i++) {
@@ -380,23 +393,29 @@ class Renderer {
380393
xPositions.set(keyForPoint(end), 0);
381394
}
382395

383-
const glyphCount = this.populateGlyphInstances(
396+
const gutterGlyphCount = this.populateGutterGlyphInstances(
397+
scrollTop,
398+
firstVisibleRow,
399+
firstVisibleRow + lines.length,
400+
totalRowCount,
401+
textColor
402+
);
403+
const lineGlyphCount = this.populateLineGlyphInstances(
384404
scrollTop,
385405
firstVisibleRow,
386-
paddingLeft,
406+
gutterWidth + paddingLeft,
387407
lines,
388408
selections,
389409
textColor,
390410
xPositions
391411
);
392-
393412
const {
394413
selectionSolidCount,
395414
cursorSolidCount
396415
} = this.populateSelectionSolidInstances(
397416
scrollTop,
398417
canvasWidth,
399-
paddingLeft,
418+
gutterWidth + paddingLeft,
400419
selections,
401420
xPositions,
402421
selectionColors,
@@ -416,13 +435,69 @@ class Renderer {
416435
viewportScaleX,
417436
viewportScaleY
418437
);
419-
this.drawText(glyphCount, scrollLeft, viewportScaleX, viewportScaleY);
438+
this.drawText(
439+
this.lineGlyphInstances,
440+
lineGlyphCount,
441+
scrollLeft,
442+
viewportScaleX,
443+
viewportScaleY
444+
);
420445
this.drawCursors(
421446
cursorSolidCount,
422447
scrollLeft,
423448
viewportScaleX,
424449
viewportScaleY
425450
);
451+
452+
this.clearRectangle(
453+
0,
454+
0,
455+
gutterWidth,
456+
canvasHeight,
457+
viewportScaleX,
458+
viewportScaleY
459+
);
460+
this.drawText(
461+
this.gutterGlyphInstances,
462+
gutterGlyphCount,
463+
0,
464+
viewportScaleX,
465+
viewportScaleY
466+
);
467+
}
468+
469+
clearRectangle(x, y, width, height, viewportScaleX, viewportScaleY) {
470+
this.gl.bindVertexArray(this.solidVAO);
471+
this.gl.disable(this.gl.BLEND);
472+
this.gl.useProgram(this.solidProgram);
473+
this.gl.uniform2f(
474+
this.solidViewportScaleLocation,
475+
viewportScaleX,
476+
viewportScaleY
477+
);
478+
this.gl.uniform1f(this.solidScrollLeftLocation, 0);
479+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.solidInstancesBuffer);
480+
this.updateSolidInstance(
481+
this.opaqueRectangleInstances,
482+
0,
483+
x,
484+
y,
485+
width,
486+
height,
487+
{ r: 255, g: 255, b: 255, a: 1 }
488+
);
489+
this.gl.bufferData(
490+
this.gl.ARRAY_BUFFER,
491+
this.opaqueRectangleInstances,
492+
this.gl.STREAM_DRAW
493+
);
494+
this.gl.drawElementsInstanced(
495+
this.gl.TRIANGLES,
496+
6,
497+
this.gl.UNSIGNED_BYTE,
498+
0,
499+
1
500+
);
426501
}
427502

428503
drawSelections(
@@ -461,7 +536,13 @@ class Renderer {
461536
);
462537
}
463538

464-
drawText(glyphCount, scrollLeft, viewportScaleX, viewportScaleY) {
539+
drawText(
540+
glyphInstances,
541+
glyphCount,
542+
scrollLeft,
543+
viewportScaleX,
544+
viewportScaleY
545+
) {
465546
this.gl.bindVertexArray(this.textBlendVAO);
466547
this.gl.enable(this.gl.BLEND);
467548
this.gl.useProgram(this.textBlendPass1Program);
@@ -474,7 +555,7 @@ class Renderer {
474555
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphInstancesBuffer);
475556
this.gl.bufferData(
476557
this.gl.ARRAY_BUFFER,
477-
this.glyphInstances,
558+
glyphInstances,
478559
this.gl.STREAM_DRAW
479560
);
480561
this.gl.blendFuncSeparate(
@@ -538,7 +619,50 @@ class Renderer {
538619
);
539620
}
540621

541-
populateGlyphInstances(
622+
populateGutterGlyphInstances(
623+
scrollTop,
624+
firstVisibleRow,
625+
lastVisibleRow,
626+
totalRowCount,
627+
textColor
628+
) {
629+
const firstVisibleRowY = firstVisibleRow * this.style.computedLineHeight;
630+
let glyphCount = 0;
631+
let y = Math.round((firstVisibleRowY - scrollTop) * this.style.dpiScale);
632+
633+
for (let row = firstVisibleRow; row < lastVisibleRow; row++) {
634+
const text = (row + 1).toString();
635+
let x = 0;
636+
for (let i = 0; i < text.length; i++) {
637+
const char = text[i];
638+
const variantIndex =
639+
Math.round(x * SUBPIXEL_DIVISOR) % SUBPIXEL_DIVISOR;
640+
const glyph = this.atlas.getGlyph(char, variantIndex);
641+
this.updateGlyphInstance(
642+
this.gutterGlyphInstances,
643+
glyphCount++,
644+
Math.round(x - glyph.variantOffset),
645+
y,
646+
glyph,
647+
textColor
648+
);
649+
650+
x += glyph.subpixelWidth;
651+
}
652+
653+
y += Math.round(this.style.computedLineHeight * this.style.dpiScale);
654+
}
655+
656+
if (glyphCount > MAX_INSTANCES) {
657+
console.error(
658+
`glyphCount of ${glyphCount} exceeds MAX_INSTANCES of ${MAX_INSTANCES}`
659+
);
660+
}
661+
662+
return glyphCount;
663+
}
664+
665+
populateLineGlyphInstances(
542666
scrollTop,
543667
firstVisibleRow,
544668
paddingLeft,
@@ -576,6 +700,7 @@ class Renderer {
576700
const glyph = this.atlas.getGlyph(char, variantIndex);
577701

578702
this.updateGlyphInstance(
703+
this.lineGlyphInstances,
579704
glyphCount++,
580705
Math.round(x - glyph.variantOffset),
581706
y,
@@ -599,25 +724,25 @@ class Renderer {
599724
return glyphCount;
600725
}
601726

602-
updateGlyphInstance(i, x, y, glyph, color) {
727+
updateGlyphInstance(glyphInstances, i, x, y, glyph, color) {
603728
const startOffset = 12 * i;
604729
// targetOrigin
605-
this.glyphInstances[0 + startOffset] = x;
606-
this.glyphInstances[1 + startOffset] = y;
730+
glyphInstances[0 + startOffset] = x;
731+
glyphInstances[1 + startOffset] = y;
607732
// targetSize
608-
this.glyphInstances[2 + startOffset] = glyph.width;
609-
this.glyphInstances[3 + startOffset] = glyph.height;
733+
glyphInstances[2 + startOffset] = glyph.width;
734+
glyphInstances[3 + startOffset] = glyph.height;
610735
// textColorRGBA
611-
this.glyphInstances[4 + startOffset] = color.r;
612-
this.glyphInstances[5 + startOffset] = color.g;
613-
this.glyphInstances[6 + startOffset] = color.b;
614-
this.glyphInstances[7 + startOffset] = color.a;
736+
glyphInstances[4 + startOffset] = color.r;
737+
glyphInstances[5 + startOffset] = color.g;
738+
glyphInstances[6 + startOffset] = color.b;
739+
glyphInstances[7 + startOffset] = color.a;
615740
// atlasOrigin
616-
this.glyphInstances[8 + startOffset] = glyph.textureU;
617-
this.glyphInstances[9 + startOffset] = glyph.textureV;
741+
glyphInstances[8 + startOffset] = glyph.textureU;
742+
glyphInstances[9 + startOffset] = glyph.textureV;
618743
// atlasSize
619-
this.glyphInstances[10 + startOffset] = glyph.textureWidth;
620-
this.glyphInstances[11 + startOffset] = glyph.textureHeight;
744+
glyphInstances[10 + startOffset] = glyph.textureWidth;
745+
glyphInstances[11 + startOffset] = glyph.textureHeight;
621746
}
622747

623748
populateSelectionSolidInstances(
@@ -746,6 +871,11 @@ class Renderer {
746871
arrayBuffer[7 + startOffset] = color.a;
747872
}
748873

874+
getGutterWidth(totalRowCount) {
875+
const digitsCount = Math.floor(Math.log10(totalRowCount)) + 1;
876+
return Math.ceil(digitsCount * this.atlas.getGlyph("9", 0).subpixelWidth);
877+
}
878+
749879
createProgram(vertexShader, fragmentShader) {
750880
const program = this.gl.createProgram();
751881
this.gl.attachShader(program, vertexShader);

0 commit comments

Comments
 (0)