瀏覽代碼

feat: add BufferRangeBlock to denote a block within a buffer

Georg Wechslberger 3 年之前
父節點
當前提交
eedb9e8ba1

+ 2 - 0
lib/core.dart

@@ -3,6 +3,8 @@ export 'src/core/buffer/cell_flags.dart';
 export 'src/core/buffer/cell_offset.dart';
 export 'src/core/buffer/line.dart';
 export 'src/core/buffer/range.dart';
+export 'src/core/buffer/range_block.dart';
+export 'src/core/buffer/range_line.dart';
 export 'src/core/buffer/segment.dart';
 export 'src/core/cell.dart';
 export 'src/core/color.dart';

+ 17 - 21
lib/src/core/buffer/buffer.dart

@@ -3,6 +3,7 @@ import 'dart:math' show max, min;
 import 'package:flutter/material.dart';
 import 'package:xterm/src/core/buffer/cell_offset.dart';
 import 'package:xterm/src/core/buffer/line.dart';
+import 'package:xterm/src/core/buffer/range_line.dart';
 import 'package:xterm/src/core/buffer/range.dart';
 import 'package:xterm/src/core/charset.dart';
 import 'package:xterm/src/core/cursor.dart';
@@ -500,7 +501,7 @@ class Buffer {
       return null;
     }
 
-    return BufferRange(
+    return BufferRangeLine(
       CellOffset(start, position.y),
       CellOffset(end, position.y),
     );
@@ -509,7 +510,7 @@ class Buffer {
   /// Get the plain text content of the buffer including the scrollback.
   /// Accepts an optional [range] to get a specific part of the buffer.
   String getText([BufferRange? range]) {
-    range ??= BufferRange(
+    range ??= BufferRangeLine(
       CellOffset(0, 0),
       CellOffset(viewWidth - 1, height - 1),
     );
@@ -518,25 +519,20 @@ class Buffer {
 
     final builder = StringBuffer();
 
-    final firstLine = range.begin.y.clamp(0, height - 1);
-    final lastLine = range.end.y.clamp(firstLine, height - 1);
-
-    for (var i = firstLine; i <= lastLine; i++) {
-      if (i < 0 || i >= lines.length) {
-        continue;
-      }
-
-      final line = lines[i];
-      final start = i == firstLine ? range.begin.x : 0;
-      final end = i == lastLine ? range.end.x : line.length;
-
-      if (!(i == firstLine || line.isWrapped)) {
-        builder.write('\n');
-      }
-
-      builder.write(line.getText(start, end));
-    }
-
+    range.toSegments().forEach(
+      (segment) {
+        if (segment.line < 0 || segment.line >= height) {
+          return;
+        }
+        final line = lines[segment.line];
+        if (!(segment.line == range!.begin.y ||
+            segment.line == 0 ||
+            line.isWrapped)) {
+          builder.write("\n");
+        }
+        builder.write(line.getText(segment.start, segment.end));
+      },
+    );
     return builder.toString();
   }
 

+ 1 - 1
lib/src/core/buffer/cell_offset.dart

@@ -36,7 +36,7 @@ class CellOffset {
   }
 
   bool isWithin(BufferRange range) {
-    return range.begin.isBeforeOrSame(this) && range.end.isAfterOrSame(this);
+    return range.contains(this);
   }
 
   @override

+ 7 - 39
lib/src/core/buffer/range.dart

@@ -1,12 +1,12 @@
 import 'package:xterm/src/core/buffer/cell_offset.dart';
 import 'package:xterm/src/core/buffer/segment.dart';
 
-class BufferRange {
+abstract class BufferRange {
   final CellOffset begin;
 
   final CellOffset end;
 
-  BufferRange(this.begin, this.end);
+  const BufferRange(this.begin, this.end);
 
   BufferRange.collapsed(this.begin) : end = begin;
 
@@ -18,45 +18,13 @@ class BufferRange {
     return begin.isEqual(end);
   }
 
-  BufferRange get normalized {
-    if (isNormalized) {
-      return this;
-    } else {
-      return BufferRange(end, begin);
-    }
-  }
-
-  Iterable<BufferSegment> toSegments() sync* {
-    var begin = this.begin;
-    var end = this.end;
-
-    if (!isNormalized) {
-      end = this.begin;
-      begin = this.end;
-    }
-
-    for (var i = begin.y; i <= end.y; i++) {
-      var startX = i == begin.y ? begin.x : null;
-      var endX = i == end.y ? end.x : null;
-      yield BufferSegment(this, i, startX, endX);
-    }
-  }
-
-  bool contains(CellOffset position) {
-    return begin.isBeforeOrSame(position) && end.isAfterOrSame(position);
-  }
+  BufferRange get normalized;
 
-  BufferRange merge(BufferRange range) {
-    final begin = this.begin.isBefore(range.begin) ? this.begin : range.begin;
-    final end = this.end.isAfter(range.end) ? this.end : range.end;
-    return BufferRange(begin, end);
-  }
+  Iterable<BufferSegment> toSegments();
 
-  BufferRange extend(CellOffset position) {
-    final begin = this.begin.isBefore(position) ? position : this.begin;
-    final end = this.end.isAfter(position) ? position : this.end;
-    return BufferRange(begin, end);
-  }
+  bool contains(CellOffset position);
+  BufferRange merge(BufferRange range);
+  BufferRange extend(CellOffset position);
 
   @override
   operator ==(Object other) {

+ 111 - 0
lib/src/core/buffer/range_block.dart

@@ -0,0 +1,111 @@
+import 'dart:math';
+
+import 'package:xterm/src/core/buffer/cell_offset.dart';
+import 'package:xterm/src/core/buffer/range.dart';
+import 'package:xterm/src/core/buffer/segment.dart';
+
+class BufferRangeBlock extends BufferRange {
+  BufferRangeBlock(super.begin, super.end);
+
+  BufferRangeBlock.collapsed(CellOffset begin) : super.collapsed(begin);
+
+  @override
+  bool get isNormalized {
+    // A block range is normalized if begin is the top left corner of the range
+    // and end the bottom right corner.
+    return (begin.isBefore(end) && begin.x <= end.x) || begin.isEqual(end);
+  }
+
+  @override
+  BufferRangeBlock get normalized {
+    if (isNormalized) {
+      return this;
+    }
+    // Determine new normalized begin and end offset, such that begin is the
+    // top left corner and end is the bottom right corner of the block.
+    final normalBegin = CellOffset(min(begin.x, end.x), min(begin.y, end.y));
+    final normalEnd = CellOffset(max(begin.x, end.x), max(begin.y, end.y));
+    return BufferRangeBlock(normalBegin, normalEnd);
+  }
+
+  @override
+  Iterable<BufferSegment> toSegments() sync* {
+    var begin = this.begin;
+    var end = this.end;
+
+    if (!isNormalized) {
+      end = this.begin;
+      begin = this.end;
+    }
+
+    final startX = min(begin.x, end.x);
+    final endX = max(begin.x, end.x);
+    for (var i = begin.y; i <= end.y; i++) {
+      yield BufferSegment(this, i, startX, endX);
+    }
+  }
+
+  @override
+  bool contains(CellOffset position) {
+    var begin = this.begin;
+    var end = this.end;
+
+    if (!isNormalized) {
+      end = this.begin;
+      begin = this.end;
+    }
+    if (!(begin.y <= position.y && position.y <= end.y)) {
+      return false;
+    }
+
+    final startX = min(begin.x, end.x);
+    final endX = max(begin.x, end.x);
+    return startX <= position.x && position.x <= endX;
+  }
+
+  @override
+  BufferRangeBlock merge(BufferRange range) {
+    // Enlarge the block such that both borders of the range
+    // are within the selected block.
+    return extend(range.begin).extend(range.end);
+  }
+
+  @override
+  BufferRangeBlock extend(CellOffset position) {
+    // If the position is within the block, there is nothing to do.
+    if (contains(position)) {
+      return this;
+    }
+    // Otherwise normalize the block and push the borders outside up to
+    // the position to which the block has to extended.
+    final normal = normalized;
+    final extendBegin = CellOffset(
+      min(normal.begin.x, position.x),
+      min(normal.begin.y, position.y),
+    );
+    final extendEnd = CellOffset(
+      max(normal.end.x, position.x),
+      max(normal.end.y, position.y),
+    );
+    return BufferRangeBlock(extendBegin, extendEnd);
+  }
+
+  @override
+  operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+
+    if (other is! BufferRangeBlock) {
+      return false;
+    }
+
+    return begin == other.begin && end == other.end;
+  }
+
+  @override
+  int get hashCode => begin.hashCode ^ end.hashCode;
+
+  @override
+  String toString() => 'Block Range($begin, $end)';
+}

+ 69 - 0
lib/src/core/buffer/range_line.dart

@@ -0,0 +1,69 @@
+import 'package:xterm/src/core/buffer/cell_offset.dart';
+import 'package:xterm/src/core/buffer/range.dart';
+import 'package:xterm/src/core/buffer/segment.dart';
+
+class BufferRangeLine extends BufferRange {
+  BufferRangeLine(super.begin, super.end);
+
+  BufferRangeLine.collapsed(CellOffset begin) : super.collapsed(begin);
+
+  @override
+  BufferRangeLine get normalized {
+    return isNormalized ? this : BufferRangeLine(end, begin);
+  }
+
+  @override
+  Iterable<BufferSegment> toSegments() sync* {
+    var begin = this.begin;
+    var end = this.end;
+
+    if (!isNormalized) {
+      end = this.begin;
+      begin = this.end;
+    }
+
+    for (var i = begin.y; i <= end.y; i++) {
+      var startX = i == begin.y ? begin.x : null;
+      var endX = i == end.y ? end.x : null;
+      yield BufferSegment(this, i, startX, endX);
+    }
+  }
+
+  @override
+  bool contains(CellOffset position) {
+    return begin.isBeforeOrSame(position) && end.isAfterOrSame(position);
+  }
+
+  @override
+  BufferRangeLine merge(BufferRange range) {
+    final begin = this.begin.isBefore(range.begin) ? this.begin : range.begin;
+    final end = this.end.isAfter(range.end) ? this.end : range.end;
+    return BufferRangeLine(begin, end);
+  }
+
+  @override
+  BufferRangeLine extend(CellOffset position) {
+    final begin = this.begin.isBefore(position) ? position : this.begin;
+    final end = this.end.isAfter(position) ? position : this.end;
+    return BufferRangeLine(begin, end);
+  }
+
+  @override
+  operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+
+    if (other is! BufferRangeLine) {
+      return false;
+    }
+
+    return begin == other.begin && end == other.end;
+  }
+
+  @override
+  int get hashCode => begin.hashCode ^ end.hashCode;
+
+  @override
+  String toString() => 'Line Range($begin, $end)';
+}

+ 2 - 2
lib/src/ui/shortcut/actions.dart

@@ -1,7 +1,7 @@
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 import 'package:xterm/src/core/buffer/cell_offset.dart';
-import 'package:xterm/src/core/buffer/range.dart';
+import 'package:xterm/src/core/buffer/range_line.dart';
 import 'package:xterm/src/terminal.dart';
 import 'package:xterm/src/ui/controller.dart';
 
@@ -54,7 +54,7 @@ class TerminalActions extends StatelessWidget {
         SelectAllTextIntent: CallbackAction<SelectAllTextIntent>(
           onInvoke: (intent) {
             controller.setSelection(
-              BufferRange(
+              BufferRangeLine(
                 CellOffset(0, terminal.buffer.height - terminal.viewHeight),
                 CellOffset(terminal.viewWidth, terminal.buffer.height - 1),
               ),

+ 17 - 3
test/src/core/buffer/buffer_test.dart

@@ -37,7 +37,7 @@ void main() {
 
       expect(
         terminal.buffer.getText(
-          BufferRange(CellOffset(-100, -100), CellOffset(100, 100)),
+          BufferRangeLine(CellOffset(-100, -100), CellOffset(100, 100)),
         ),
         startsWith('Hello World'),
       );
@@ -50,7 +50,7 @@ void main() {
 
       expect(
         terminal.buffer.getText(
-          BufferRange(CellOffset(0, 0), CellOffset(100, 100)),
+          BufferRangeLine(CellOffset(0, 0), CellOffset(100, 100)),
         ),
         startsWith('Hello World'),
       );
@@ -63,10 +63,24 @@ void main() {
 
       expect(
         terminal.buffer.getText(
-          BufferRange(CellOffset(5, 5), CellOffset(0, 0)),
+          BufferRangeLine(CellOffset(5, 5), CellOffset(0, 0)),
         ),
         startsWith('Hello World'),
       );
     });
+
+    test('can handle block range', () {
+      final terminal = Terminal();
+
+      terminal.write('Hello World\r\n');
+      terminal.write('Nice to meet you\r\n');
+
+      expect(
+        terminal.buffer.getText(
+          BufferRangeBlock(CellOffset(2, 0), CellOffset(5, 1)),
+        ),
+        startsWith('llo\nce '),
+      );
+    });
   });
 }