浏览代码

Merge pull request #60 from devmil/feature/search

Feature: Search
xuty 4 年之前
父节点
当前提交
042fe1dfb8

+ 13 - 6
example/pubspec.lock

@@ -14,7 +14,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.6.1"
+    version: "2.8.2"
   boolean_selector:
     dependency: transitive
     description:
@@ -35,7 +35,7 @@ packages:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.3.1"
   clock:
     dependency: transitive
     description:
@@ -73,6 +73,13 @@ packages:
       url: "https://github.com/TerminalStudio/dartssh"
     source: git
     version: "1.0.4+4"
+  equatable:
+    dependency: transitive
+    description:
+      name: equatable
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
   fake_async:
     dependency: transitive
     description:
@@ -117,14 +124,14 @@ packages:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.10"
+    version: "0.12.11"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.7.0"
   path:
     dependency: transitive
     description:
@@ -206,7 +213,7 @@ packages:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.3.0"
+    version: "0.4.3"
   tweetnacl:
     dependency: transitive
     description:
@@ -243,7 +250,7 @@ packages:
       path: ".."
       relative: true
     source: path
-    version: "2.3.0-pre"
+    version: "2.5.0-pre"
 sdks:
   dart: ">=2.12.0 <3.0.0"
   flutter: ">=2.0.0"

+ 61 - 58
lib/buffer/buffer.dart

@@ -11,23 +11,26 @@ import 'package:xterm/util/unicode_v11.dart';
 
 class Buffer {
   Buffer({
-    required this.terminal,
+    required Terminal terminal,
     required this.isAltBuffer,
-  }) {
+  }) : _terminal = terminal {
     resetVerticalMargins();
 
     lines = CircularList(
-      terminal.maxLines,
+      _terminal.maxLines,
     );
-    for (int i = 0; i < terminal.viewHeight; i++) {
+    for (int i = 0; i < _terminal.viewHeight; i++) {
       lines.push(_newEmptyLine());
     }
   }
 
-  final Terminal terminal;
+  final Terminal _terminal;
   final bool isAltBuffer;
   final charset = Charset();
 
+  int get viewHeight => _terminal.viewHeight;
+  int get viewWidth => _terminal.viewWidth;
+
   /// lines of the buffer. the length of [lines] should always be equal or
   /// greater than [Terminal.viewHeight].
   late final CircularList<BufferLine> lines;
@@ -46,7 +49,7 @@ class Buffer {
   // Indicates how far the top of the viewport is from the top of the entire
   // buffer. 0 if the viewport is scrolled to the top.
   int get scrollOffsetFromTop {
-    return terminal.invisibleHeight - scrollOffsetFromBottom;
+    return _terminal.invisibleHeight - scrollOffsetFromBottom;
   }
 
   /// Indicated whether the terminal should automatically scroll to bottom when
@@ -58,7 +61,7 @@ class Buffer {
 
   /// Horizontal position of the cursor relative to the top-left cornor of the
   /// screen, starting from 0.
-  int get cursorX => _cursorX.clamp(0, terminal.viewWidth - 1);
+  int get cursorX => _cursorX.clamp(0, _terminal.viewWidth - 1);
   int _cursorX = 0;
 
   /// Vertical position of the cursor relative to the top-left cornor of the
@@ -72,7 +75,7 @@ class Buffer {
   int get marginBottom => _marginBottom;
   late int _marginBottom;
 
-  /// Writes data to the terminal. Terminal sequences or special characters are
+  /// Writes data to the _terminal. Terminal sequences or special characters are
   /// not interpreted and directly added to the buffer.
   ///
   /// See also: [Terminal.write]
@@ -82,7 +85,7 @@ class Buffer {
     }
   }
 
-  /// Writes a single character to the terminal. Special chatacters are not
+  /// Writes a single character to the _terminal. Special chatacters are not
   /// interpreted and directly added to the buffer.
   ///
   /// See also: [Terminal.writeChar]
@@ -90,10 +93,10 @@ class Buffer {
     codePoint = charset.translate(codePoint);
 
     final cellWidth = unicodeV11.wcwidth(codePoint);
-    if (_cursorX >= terminal.viewWidth) {
+    if (_cursorX >= _terminal.viewWidth) {
       newLine();
       setCursorX(0);
-      if (terminal.autoWrapMode) {
+      if (_terminal.autoWrapMode) {
         currentLine.isWrapped = true;
       }
     }
@@ -105,10 +108,10 @@ class Buffer {
       _cursorX,
       content: codePoint,
       width: cellWidth,
-      cursor: terminal.cursor,
+      cursor: _terminal.cursor,
     );
 
-    if (_cursorX < terminal.viewWidth) {
+    if (_cursorX < _terminal.viewWidth) {
       _cursorX++;
     }
 
@@ -120,7 +123,7 @@ class Buffer {
   /// get line in the viewport. [index] starts from 0, must be smaller than
   /// [Terminal.viewHeight].
   BufferLine getViewLine(int index) {
-    index = index.clamp(0, terminal.viewHeight - 1);
+    index = index.clamp(0, _terminal.viewHeight - 1);
     return lines[convertViewLineToRawLine(index)];
   }
 
@@ -133,23 +136,23 @@ class Buffer {
   }
 
   int convertViewLineToRawLine(int viewLine) {
-    if (terminal.viewHeight > height) {
+    if (_terminal.viewHeight > height) {
       return viewLine;
     }
 
-    return viewLine + (height - terminal.viewHeight);
+    return viewLine + (height - _terminal.viewHeight);
   }
 
   int convertRawLineToViewLine(int rawLine) {
-    if (terminal.viewHeight > height) {
+    if (_terminal.viewHeight > height) {
       return rawLine;
     }
 
-    return rawLine - (height - terminal.viewHeight);
+    return rawLine - (height - _terminal.viewHeight);
   }
 
   void newLine() {
-    if (terminal.newLineMode) {
+    if (_terminal.newLineMode) {
       setCursorX(0);
     }
 
@@ -163,8 +166,8 @@ class Buffer {
   void backspace() {
     if (_cursorX == 0 && currentLine.isWrapped) {
       currentLine.isWrapped = false;
-      movePosition(terminal.viewWidth - 1, -1);
-    } else if (_cursorX == terminal.viewWidth) {
+      movePosition(_terminal.viewWidth - 1, -1);
+    } else if (_cursorX == _terminal.viewWidth) {
       movePosition(-2, 0);
     } else {
       movePosition(-1, 0);
@@ -172,13 +175,13 @@ class Buffer {
   }
 
   List<BufferLine> getVisibleLines() {
-    if (height < terminal.viewHeight) {
+    if (height < _terminal.viewHeight) {
       return lines.toList();
     }
 
     final result = <BufferLine>[];
 
-    for (var i = height - terminal.viewHeight; i < height; i++) {
+    for (var i = height - _terminal.viewHeight; i < height; i++) {
       final y = i - scrollOffsetFromBottom;
       if (y >= 0 && y < height) {
         result.add(lines[y]);
@@ -191,10 +194,10 @@ class Buffer {
   void eraseDisplayFromCursor() {
     eraseLineFromCursor();
 
-    for (var i = _cursorY + 1; i < terminal.viewHeight; i++) {
+    for (var i = _cursorY + 1; i < _terminal.viewHeight; i++) {
       final line = getViewLine(i);
       line.isWrapped = false;
-      line.erase(terminal.cursor, 0, terminal.viewWidth);
+      line.erase(_terminal.cursor, 0, _terminal.viewWidth);
     }
   }
 
@@ -204,36 +207,36 @@ class Buffer {
     for (var i = 0; i < _cursorY; i++) {
       final line = getViewLine(i);
       line.isWrapped = false;
-      line.erase(terminal.cursor, 0, terminal.viewWidth);
+      line.erase(_terminal.cursor, 0, _terminal.viewWidth);
     }
   }
 
   void eraseDisplay() {
-    for (var i = 0; i < terminal.viewHeight; i++) {
+    for (var i = 0; i < _terminal.viewHeight; i++) {
       final line = getViewLine(i);
       line.isWrapped = false;
-      line.erase(terminal.cursor, 0, terminal.viewWidth);
+      line.erase(_terminal.cursor, 0, _terminal.viewWidth);
     }
   }
 
   void eraseLineFromCursor() {
     currentLine.isWrapped = false;
-    currentLine.erase(terminal.cursor, _cursorX, terminal.viewWidth);
+    currentLine.erase(_terminal.cursor, _cursorX, _terminal.viewWidth);
   }
 
   void eraseLineToCursor() {
     currentLine.isWrapped = false;
-    currentLine.erase(terminal.cursor, 0, _cursorX);
+    currentLine.erase(_terminal.cursor, 0, _cursorX);
   }
 
   void eraseLine() {
     currentLine.isWrapped = false;
-    currentLine.erase(terminal.cursor, 0, terminal.viewWidth);
+    currentLine.erase(_terminal.cursor, 0, _terminal.viewWidth);
   }
 
   void eraseCharacters(int count) {
     final start = _cursorX;
-    currentLine.erase(terminal.cursor, start, start + count);
+    currentLine.erase(_terminal.cursor, start, start + count);
   }
 
   ScrollRange getAreaScrollRange() {
@@ -288,7 +291,7 @@ class Buffer {
     }
 
     // the cursor is not in the scrollable region
-    if (_cursorY >= terminal.viewHeight - 1) {
+    if (_cursorY >= _terminal.viewHeight - 1) {
       // we are at the bottom so a new line is created.
       lines.push(_newEmptyLine());
 
@@ -316,11 +319,11 @@ class Buffer {
   }
 
   void setCursorX(int cursorX) {
-    _cursorX = cursorX.clamp(0, terminal.viewWidth - 1);
+    _cursorX = cursorX.clamp(0, _terminal.viewWidth - 1);
   }
 
   void setCursorY(int cursorY) {
-    _cursorY = cursorY.clamp(0, terminal.viewHeight - 1);
+    _cursorY = cursorY.clamp(0, _terminal.viewHeight - 1);
   }
 
   void moveCursorX(int offset) {
@@ -332,14 +335,14 @@ class Buffer {
   }
 
   void setPosition(int cursorX, int cursorY) {
-    var maxLine = terminal.viewHeight - 1;
+    var maxLine = _terminal.viewHeight - 1;
 
-    if (terminal.originMode) {
+    if (_terminal.originMode) {
       cursorY += _marginTop;
       maxLine = _marginBottom;
     }
 
-    _cursorX = cursorX.clamp(0, terminal.viewWidth - 1);
+    _cursorX = cursorX.clamp(0, _terminal.viewWidth - 1);
     _cursorY = cursorY.clamp(0, maxLine);
   }
 
@@ -350,13 +353,13 @@ class Buffer {
   }
 
   void setScrollOffsetFromBottom(int offsetFromBottom) {
-    if (height < terminal.viewHeight) return;
-    final maxOffsetFromBottom = height - terminal.viewHeight;
+    if (height < _terminal.viewHeight) return;
+    final maxOffsetFromBottom = height - _terminal.viewHeight;
     _scrollOffsetFromBottom = offsetFromBottom.clamp(0, maxOffsetFromBottom);
   }
 
   void setScrollOffsetFromTop(int offsetFromTop) {
-    final bottomOffset = terminal.invisibleHeight - offsetFromTop;
+    final bottomOffset = _terminal.invisibleHeight - offsetFromTop;
     setScrollOffsetFromBottom(bottomOffset);
   }
 
@@ -369,9 +372,9 @@ class Buffer {
   }
 
   void saveCursor() {
-    _savedCellFlags = terminal.cursor.flags;
-    _savedCellFgColor = terminal.cursor.fg;
-    _savedCellBgColor = terminal.cursor.bg;
+    _savedCellFlags = _terminal.cursor.flags;
+    _savedCellFgColor = _terminal.cursor.fg;
+    _savedCellBgColor = _terminal.cursor.bg;
     _savedCursorX = _cursorX;
     _savedCursorY = _cursorY;
     charset.save();
@@ -379,15 +382,15 @@ class Buffer {
 
   void restoreCursor() {
     if (_savedCellFlags != null) {
-      terminal.cursor.flags = _savedCellFlags!;
+      _terminal.cursor.flags = _savedCellFlags!;
     }
 
     if (_savedCellFgColor != null) {
-      terminal.cursor.fg = _savedCellFgColor!;
+      _terminal.cursor.fg = _savedCellFgColor!;
     }
 
     if (_savedCellBgColor != null) {
-      terminal.cursor.bg = _savedCellBgColor!;
+      _terminal.cursor.bg = _savedCellBgColor!;
     }
 
     if (_savedCursorX != null) {
@@ -402,15 +405,15 @@ class Buffer {
   }
 
   void setVerticalMargins(int top, int bottom) {
-    _marginTop = top.clamp(0, terminal.viewHeight - 1);
-    _marginBottom = bottom.clamp(0, terminal.viewHeight - 1);
+    _marginTop = top.clamp(0, _terminal.viewHeight - 1);
+    _marginBottom = bottom.clamp(0, _terminal.viewHeight - 1);
 
     _marginTop = min(_marginTop, _marginBottom);
     _marginBottom = max(_marginTop, _marginBottom);
   }
 
   bool get hasScrollableRegion {
-    return _marginTop > 0 || _marginBottom < (terminal.viewHeight - 1);
+    return _marginTop > 0 || _marginBottom < (_terminal.viewHeight - 1);
   }
 
   bool get isInScrollableRegion {
@@ -420,26 +423,26 @@ class Buffer {
   }
 
   void resetVerticalMargins() {
-    setVerticalMargins(0, terminal.viewHeight - 1);
+    setVerticalMargins(0, _terminal.viewHeight - 1);
   }
 
   void deleteChars(int count) {
-    final start = _cursorX.clamp(0, terminal.viewWidth);
-    final end = min(_cursorX + count, terminal.viewWidth);
+    final start = _cursorX.clamp(0, _terminal.viewWidth);
+    final end = min(_cursorX + count, _terminal.viewWidth);
     currentLine.removeRange(start, end);
   }
 
   void clearScrollback() {
-    if (lines.length <= terminal.viewHeight) {
+    if (lines.length <= _terminal.viewHeight) {
       return;
     }
 
-    lines.trimStart(lines.length - terminal.viewHeight);
+    lines.trimStart(lines.length - _terminal.viewHeight);
   }
 
   void clear() {
     lines.clear();
-    for (int i = 0; i < terminal.viewHeight; i++) {
+    for (int i = 0; i < _terminal.viewHeight; i++) {
       lines.push(_newEmptyLine());
     }
   }
@@ -447,7 +450,7 @@ class Buffer {
   void insertBlankCharacters(int count) {
     for (var i = 0; i < count; i++) {
       currentLine.insert(_cursorX + i);
-      currentLine.cellSetFlags(_cursorX + i, terminal.cursor.flags);
+      currentLine.cellSetFlags(_cursorX + i, _terminal.cursor.flags);
     }
   }
 
@@ -537,7 +540,7 @@ class Buffer {
   }
 
   BufferLine _newEmptyLine() {
-    final line = BufferLine(length: terminal.viewWidth);
+    final line = BufferLine(length: _terminal.viewWidth);
     return line;
   }
 

+ 175 - 7
lib/buffer/line/line.dart

@@ -1,17 +1,183 @@
 import 'dart:math';
 
-import 'package:xterm/buffer/line/line_bytedata.dart';
-import 'package:xterm/buffer/line/line_list.dart';
+import 'package:meta/meta.dart';
+import 'package:xterm/buffer/line/line_bytedata_data.dart';
+import 'package:xterm/buffer/line/line_list_data.dart';
 import 'package:xterm/terminal/cursor.dart';
 import 'package:xterm/util/constants.dart';
 
-abstract class BufferLine {
-  factory BufferLine({int length = 64, bool isWrapped = false}) {
+@sealed
+class BufferLine {
+  BufferLine({int length = 64, bool isWrapped = false}) {
+    _data = BufferLineData(length: length, isWrapped: isWrapped);
+  }
+
+  BufferLine.withDataFrom(BufferLine other) {
+    _data = other.data;
+  }
+
+  late BufferLineData _data;
+  final _nonDirtyTags = Set<String>();
+
+  void markTagAsNonDirty(String tag) {
+    _nonDirtyTags.add(tag);
+  }
+
+  bool isTagDirty(String tag) {
+    return !_nonDirtyTags.contains(tag);
+  }
+
+  BufferLineData get data => _data;
+
+  bool get isWrapped => _data.isWrapped;
+
+  set isWrapped(bool value) => _data.isWrapped = value;
+
+  void ensure(int length) => _data.ensure(length);
+
+  void insert(int index) {
+    _invalidateCaches();
+    _data.insert(index);
+  }
+
+  void insertN(int index, int count) {
+    _invalidateCaches();
+    _data.insertN(index, count);
+  }
+
+  void removeN(int index, int count) {
+    _invalidateCaches();
+    _data.removeN(index, count);
+  }
+
+  void clear() {
+    _invalidateCaches();
+    _data.clear();
+  }
+
+  void erase(Cursor cursor, int start, int end, [bool resetIsWrapped = false]) {
+    _invalidateCaches();
+    _data.erase(cursor, start, end);
+  }
+
+  void cellClear(int index) {
+    _invalidateCaches();
+    _data.cellClear(index);
+  }
+
+  void cellInitialize(
+    int index, {
+    required int content,
+    required int width,
+    required Cursor cursor,
+  }) {
+    _invalidateCaches();
+    _data.cellInitialize(
+      index,
+      content: content,
+      width: width,
+      cursor: cursor,
+    );
+  }
+
+  bool cellHasContent(int index) => _data.cellHasContent(index);
+
+  int cellGetContent(int index) => _data.cellGetContent(index);
+
+  void cellSetContent(int index, int content) {
+    _invalidateCaches();
+    _data.cellSetContent(index, content);
+  }
+
+  int cellGetFgColor(int index) => _data.cellGetFgColor(index);
+
+  void cellSetFgColor(int index, int color) =>
+      _data.cellSetFgColor(index, color);
+
+  int cellGetBgColor(int index) => _data.cellGetBgColor(index);
+
+  void cellSetBgColor(int index, int color) =>
+      _data.cellSetBgColor(index, color);
+
+  int cellGetFlags(int index) => _data.cellGetFlags(index);
+
+  void cellSetFlags(int index, int flags) => _data.cellSetFlags(index, flags);
+
+  int cellGetWidth(int index) => _data.cellGetWidth(index);
+
+  void cellSetWidth(int index, int width) {
+    _invalidateCaches();
+    _data.cellSetWidth(index, width);
+  }
+
+  void cellClearFlags(int index) => _data.cellClearFlags(index);
+
+  bool cellHasFlag(int index, int flag) => _data.cellHasFlag(index, flag);
+
+  void cellSetFlag(int index, int flag) => _data.cellSetFlag(index, flag);
+
+  void cellErase(int index, Cursor cursor) {
+    _invalidateCaches();
+    _data.cellErase(index, cursor);
+  }
+
+  int getTrimmedLength([int? cols]) => _data.getTrimmedLength(cols);
+
+  void copyCellsFrom(
+      covariant BufferLine src, int srcCol, int dstCol, int len) {
+    _invalidateCaches();
+    _data.copyCellsFrom(src.data, srcCol, dstCol, len);
+  }
+
+  void removeRange(int start, int end) {
+    _invalidateCaches();
+    _data.removeRange(start, end);
+  }
+
+  void clearRange(int start, int end) {
+    _invalidateCaches();
+    _data.clearRange(start, end);
+  }
+
+  String toDebugString(int cols) => _data.toDebugString(cols);
+
+  void _invalidateCaches() {
+    _searchStringCache = null;
+    _nonDirtyTags.clear();
+  }
+
+  String? _searchStringCache;
+  bool get hasCachedSearchString => _searchStringCache != null;
+
+  String toSearchString(int cols) {
+    if (_searchStringCache != null) {
+      return _searchStringCache!;
+    }
+    final searchString = StringBuffer();
+    final length = getTrimmedLength();
+    for (int i = 0; i < max(cols, length); i++) {
+      var code = cellGetContent(i);
+      if (code != 0) {
+        final cellString = String.fromCharCode(code);
+        searchString.write(cellString);
+        final widthDiff = cellGetWidth(i) - cellString.length;
+        if (widthDiff > 0) {
+          searchString.write(''.padRight(widthDiff));
+        }
+      }
+    }
+    _searchStringCache = searchString.toString();
+    return _searchStringCache!;
+  }
+}
+
+abstract class BufferLineData {
+  factory BufferLineData({int length = 64, bool isWrapped = false}) {
     if (kIsWeb) {
-      return ListBufferLine(length, isWrapped);
+      return ListBufferLineData(length, isWrapped);
     }
 
-    return ByteDataBufferLine(length, isWrapped);
+    return ByteDataBufferLineData(length, isWrapped);
   }
 
   bool get isWrapped;
@@ -71,7 +237,8 @@ abstract class BufferLine {
 
   int getTrimmedLength([int? cols]);
 
-  void copyCellsFrom(covariant BufferLine src, int srcCol, int dstCol, int len);
+  void copyCellsFrom(
+      covariant BufferLineData src, int srcCol, int dstCol, int len);
 
   // int cellGetHash(int index);
 
@@ -79,6 +246,7 @@ abstract class BufferLine {
 
   void clearRange(int start, int end);
 
+  @nonVirtual
   String toDebugString(int cols) {
     final result = StringBuffer();
     final length = getTrimmedLength();

+ 5 - 4
lib/buffer/line/line_bytedata.dart → lib/buffer/line/line_bytedata_data.dart

@@ -35,10 +35,10 @@ int _nextLength(int lengthRequirement) {
   return nextLength;
 }
 
-/// [ByteData] based [BufferLine], used in non-web platforms to minimize memory
+/// [ByteData] based [BufferLineData], used in non-web platforms to minimize memory
 /// footprint,
-class ByteDataBufferLine with BufferLine {
-  ByteDataBufferLine(int length, this.isWrapped) {
+class ByteDataBufferLineData with BufferLineData {
+  ByteDataBufferLineData(int length, this.isWrapped) {
     _maxCols = _nextLength(length);
     _cells = ByteData(_maxCols * _cellSize);
   }
@@ -244,7 +244,8 @@ class ByteDataBufferLine with BufferLine {
     return 0;
   }
 
-  void copyCellsFrom(ByteDataBufferLine src, int srcCol, int dstCol, int len) {
+  void copyCellsFrom(
+      ByteDataBufferLineData src, int srcCol, int dstCol, int len) {
     ensure(dstCol + len);
 
     final intsToCopy = len * _cellSize64Bit;

+ 4 - 4
lib/buffer/line/line_list.dart → lib/buffer/line/line_list_data.dart

@@ -33,9 +33,9 @@ int _nextLength(int lengthRequirement) {
   return nextLength;
 }
 
-/// [List] based [BufferLine], used in browser where ByteData is not avaliable.
-class ListBufferLine with BufferLine {
-  ListBufferLine(int length, this.isWrapped) {
+/// [List] based [BufferLineData], used in browser where ByteData is not avaliable.
+class ListBufferLineData with BufferLineData {
+  ListBufferLineData(int length, this.isWrapped) {
     _maxCols = _nextLength(length);
     _cells = List.filled(_maxCols * _cellSize, 0);
   }
@@ -226,7 +226,7 @@ class ListBufferLine with BufferLine {
     return 0;
   }
 
-  void copyCellsFrom(ListBufferLine src, int srcCol, int dstCol, int len) {
+  void copyCellsFrom(ListBufferLineData src, int srcCol, int dstCol, int len) {
     ensure(dstCol + len);
 
     final intsToCopy = len * _cellSize;

+ 2 - 2
lib/buffer/reflow_strategy_wider.dart

@@ -67,8 +67,8 @@ class ReflowStrategyWider extends ReflowStrategy {
       i += linesToSkip;
     }
     //buffer doesn't have enough lines
-    while (linesAfterReflow.length < buffer.terminal.viewHeight) {
-      linesAfterReflow.add(BufferLine(length: buffer.terminal.viewWidth));
+    while (linesAfterReflow.length < buffer.viewHeight) {
+      linesAfterReflow.add(BufferLine(length: buffer.viewWidth));
     }
 
     buffer.lines.replaceWith(linesAfterReflow);

+ 147 - 18
lib/frontend/terminal_painters.dart

@@ -1,14 +1,17 @@
+import 'dart:math';
+
 import 'package:flutter/material.dart';
 import 'package:flutter/widgets.dart';
 import 'package:xterm/buffer/cell_flags.dart';
 import 'package:xterm/buffer/line/line.dart';
 import 'package:xterm/mouse/position.dart';
+import 'package:xterm/terminal/terminal_search.dart';
 import 'package:xterm/terminal/terminal_ui_interaction.dart';
 import 'package:xterm/theme/terminal_style.dart';
 import 'package:xterm/util/bit_flags.dart';
 
-import 'char_size.dart';
 import 'cache.dart';
+import 'char_size.dart';
 
 class TerminalPainter extends CustomPainter {
   TerminalPainter({
@@ -27,11 +30,10 @@ class TerminalPainter extends CustomPainter {
   void paint(Canvas canvas, Size size) {
     _paintBackground(canvas);
 
-    // if (oscillator.value) {
-    // }
-
     _paintText(canvas);
 
+    _paintUserSearchResult(canvas, size);
+
     _paintSelection(canvas);
   }
 
@@ -41,7 +43,6 @@ class TerminalPainter extends CustomPainter {
     for (var row = 0; row < lines.length; row++) {
       final line = lines[row];
       final offsetY = row * charSize.cellHeight;
-      // final cellCount = math.min(terminal.viewWidth, line.length);
       final cellCount = terminal.terminalWidth;
 
       for (var col = 0; col < cellCount; col++) {
@@ -65,10 +66,6 @@ class TerminalPainter extends CustomPainter {
           continue;
         }
 
-        // final cellFlags = line.cellGetFlags(i);
-        // final cell = line.getCell(i);
-        // final attr = cell.attr;
-
         final offsetX = col * charSize.cellWidth;
         final effectWidth = charSize.cellWidth * cellWidth + 1;
         final effectHeight = charSize.cellHeight + 1;
@@ -86,6 +83,126 @@ class TerminalPainter extends CustomPainter {
     }
   }
 
+  void _paintUserSearchResult(Canvas canvas, Size size) {
+    final searchResult = terminal.userSearchResult;
+
+    //when there is no ongoing user search then directly return
+    if (!terminal.isUserSearchActive) {
+      return;
+    }
+
+    //make everything dim so that the search result can be seen better
+    final dimPaint = Paint()
+      ..color = Color(terminal.theme.background).withAlpha(128)
+      ..style = PaintingStyle.fill;
+
+    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), dimPaint);
+
+    for (int i = 1; i <= searchResult.allHits.length; i++) {
+      _paintSearchHit(canvas, searchResult.allHits[i - 1], i);
+    }
+  }
+
+  void _paintSearchHit(Canvas canvas, TerminalSearchHit hit, int hitNum) {
+    //check if the hit is visible
+    if (hit.startLineIndex >=
+            terminal.scrollOffsetFromTop + terminal.terminalHeight ||
+        hit.endLineIndex < terminal.scrollOffsetFromTop) {
+      return;
+    }
+
+    final paint = Paint()
+      ..color = Color(terminal.currentSearchHit == hitNum
+          ? terminal.theme.searchHitBackgroundCurrent
+          : terminal.theme.searchHitBackground)
+      ..style = PaintingStyle.fill;
+
+    if (hit.startLineIndex == hit.endLineIndex) {
+      final double y =
+          (hit.startLineIndex.toDouble() - terminal.scrollOffsetFromTop) *
+              charSize.cellHeight;
+      final startX = charSize.cellWidth * hit.startIndex;
+      final endX = charSize.cellWidth * hit.endIndex;
+
+      canvas.drawRect(
+          Rect.fromLTRB(startX, y, endX, y + charSize.cellHeight), paint);
+    } else {
+      //draw first row: start - line end
+      final double yFirstRow =
+          (hit.startLineIndex.toDouble() - terminal.scrollOffsetFromTop) *
+              charSize.cellHeight;
+      final startXFirstRow = charSize.cellWidth * hit.startIndex;
+      final endXFirstRow = charSize.cellWidth * terminal.terminalWidth;
+      canvas.drawRect(
+          Rect.fromLTRB(startXFirstRow, yFirstRow, endXFirstRow,
+              yFirstRow + charSize.cellHeight),
+          paint);
+      //draw middle rows
+      final middleRowCount = hit.endLineIndex - hit.startLineIndex - 1;
+      if (middleRowCount > 0) {
+        final startYMiddleRows =
+            (hit.startLineIndex + 1 - terminal.scrollOffsetFromTop) *
+                charSize.cellHeight;
+        final startXMiddleRows = 0.toDouble();
+        final endYMiddleRows = min(
+                hit.endLineIndex - terminal.scrollOffsetFromTop,
+                terminal.terminalHeight) *
+            charSize.cellHeight;
+        final endXMiddleRows = terminal.terminalWidth * charSize.cellWidth;
+        canvas.drawRect(
+            Rect.fromLTRB(startXMiddleRows, startYMiddleRows, endXMiddleRows,
+                endYMiddleRows),
+            paint);
+      }
+      //draw end row: line start - end
+      if (hit.endLineIndex - terminal.scrollOffsetFromTop <
+          terminal.terminalHeight) {
+        final startXEndRow = 0.toDouble();
+        final startYEndRow = (hit.endLineIndex - terminal.scrollOffsetFromTop) *
+            charSize.cellHeight;
+        final endXEndRow = hit.endIndex * charSize.cellWidth;
+        final endYEndRow = startYEndRow + charSize.cellHeight;
+        canvas.drawRect(
+            Rect.fromLTRB(startXEndRow, startYEndRow, endXEndRow, endYEndRow),
+            paint);
+      }
+    }
+
+    final visibleLines = terminal.getVisibleLines();
+
+    //paint text
+    for (var rawRow = hit.startLineIndex;
+        rawRow <= hit.endLineIndex;
+        rawRow++) {
+      final start = rawRow == hit.startLineIndex ? hit.startIndex : 0;
+      final end =
+          rawRow == hit.endLineIndex ? hit.endIndex : terminal.terminalWidth;
+
+      final row = rawRow - terminal.scrollOffsetFromTop;
+
+      final offsetY = row * charSize.cellHeight;
+
+      if (row >= visibleLines.length || row < 0) {
+        continue;
+      }
+
+      final line = visibleLines[row];
+
+      for (var col = start; col < end; col++) {
+        final offsetX = col * charSize.cellWidth;
+        _paintCell(
+          canvas,
+          line,
+          col,
+          offsetX,
+          offsetY,
+          fgColorOverride: terminal.theme.searchHitForeground,
+          bgColorOverride: terminal.theme.searchHitForeground,
+        );
+      }
+    }
+  }
+
   void _paintSelection(Canvas canvas) {
     final paint = Paint()..color = Colors.white.withOpacity(0.3);
 
@@ -137,9 +254,14 @@ class TerminalPainter extends CustomPainter {
         if (width == 0) {
           continue;
         }
-
         final offsetX = col * charSize.cellWidth;
-        _paintCell(canvas, line, col, offsetX, offsetY);
+        _paintCell(
+          canvas,
+          line,
+          col,
+          offsetX,
+          offsetY,
+        );
       }
     }
   }
@@ -149,11 +271,13 @@ class TerminalPainter extends CustomPainter {
     BufferLine line,
     int cell,
     double offsetX,
-    double offsetY,
-  ) {
+    double offsetY, {
+    int? fgColorOverride,
+    int? bgColorOverride,
+  }) {
     final codePoint = line.cellGetContent(cell);
-    final fgColor = line.cellGetFgColor(cell);
-    final bgColor = line.cellGetBgColor(cell);
+    final fgColor = fgColorOverride ?? line.cellGetFgColor(cell);
+    final bgColor = bgColorOverride ?? line.cellGetBgColor(cell);
     final flags = line.cellGetFlags(cell);
 
     if (codePoint == 0 || flags.hasFlag(CellFlags.invisible)) {
@@ -223,7 +347,8 @@ class CursorPainter extends CustomPainter {
 
   @override
   void paint(Canvas canvas, Size size) {
-    bool isVisible = visible && (blinkVisible || composingString != '');
+    bool isVisible =
+        visible && (blinkVisible || composingString != '' || !focused);
     if (isVisible) {
       _paintCursor(canvas);
     }
@@ -272,7 +397,9 @@ class PaintHelper {
         ? style.textStyleProvider!(
             color: color,
             fontSize: style.fontSize,
-            fontWeight: bold ? FontWeight.bold : FontWeight.normal,
+            fontWeight: bold && !style.ignoreBoldFlag
+                ? FontWeight.bold
+                : FontWeight.normal,
             fontStyle: italic ? FontStyle.italic : FontStyle.normal,
             decoration:
                 underline ? TextDecoration.underline : TextDecoration.none,
@@ -280,7 +407,9 @@ class PaintHelper {
         : TextStyle(
             color: color,
             fontSize: style.fontSize,
-            fontWeight: bold ? FontWeight.bold : FontWeight.normal,
+            fontWeight: bold && !style.ignoreBoldFlag
+                ? FontWeight.bold
+                : FontWeight.normal,
             fontStyle: italic ? FontStyle.italic : FontStyle.normal,
             decoration:
                 underline ? TextDecoration.underline : TextDecoration.none,

+ 1 - 0
lib/frontend/terminal_view.dart

@@ -306,6 +306,7 @@ class _TerminalViewState extends State<TerminalView> {
                   charSize: _cellSize,
                   textLayoutCache: textLayoutCache,
                 ),
+                child: Container(), //to get the size
               ),
               Positioned(
                 key: _keyCursor,

+ 79 - 16
lib/terminal/terminal.dart

@@ -17,10 +17,13 @@ import 'package:xterm/terminal/platform.dart';
 import 'package:xterm/terminal/sbc.dart';
 import 'package:xterm/terminal/tabs.dart';
 import 'package:xterm/terminal/terminal_backend.dart';
+import 'package:xterm/terminal/terminal_search.dart';
+import 'package:xterm/terminal/terminal_search_interaction.dart';
 import 'package:xterm/terminal/terminal_ui_interaction.dart';
 import 'package:xterm/theme/terminal_color.dart';
 import 'package:xterm/theme/terminal_theme.dart';
 import 'package:xterm/theme/terminal_themes.dart';
+import 'package:xterm/util/constants.dart';
 import 'package:xterm/util/debug_handler.dart';
 import 'package:xterm/util/observable.dart';
 
@@ -33,7 +36,9 @@ void _defaultBellHandler() {}
 void _defaultTitleHandler(String _) {}
 void _defaultIconHandler(String _) {}
 
-class Terminal with Observable implements TerminalUiInteraction {
+class Terminal
+    with Observable
+    implements TerminalUiInteraction, TerminalSearchInteraction {
   Terminal({
     this.backend,
     this.onBell = _defaultBellHandler,
@@ -43,6 +48,8 @@ class Terminal with Observable implements TerminalUiInteraction {
     this.theme = TerminalThemes.defaultTheme,
     required int maxLines,
   }) : _maxLines = maxLines {
+    _search = TerminalSearch(this);
+    _userSearchTask = _search.createSearchTask("UserSearch");
     backend?.init();
     backend?.exitCode.then((value) {
       _isTerminated = true;
@@ -62,6 +69,9 @@ class Terminal with Observable implements TerminalUiInteraction {
     tabs.reset();
   }
 
+  late TerminalSearch _search;
+  late TerminalSearchTask _userSearchTask;
+
   bool _dirty = false;
   @override
   bool get dirty {
@@ -97,10 +107,11 @@ class Terminal with Observable implements TerminalUiInteraction {
   /// replacing the character at the cursor position.
   ///
   /// You can set or reset insert/replace mode as follows.
-  bool _replaceMode = true; // ignore: unused_field
+  // ignore: unused_field
+  bool _replaceMode = true;
 
-  bool _screenMode =
-      false; // ignore: unused_field, // DECSCNM (black on white background)
+  // ignore: unused_field
+  bool _screenMode = false; // DECSCNM (black on white background)
   bool _autoWrapMode = true;
   bool get autoWrapMode => _autoWrapMode;
 
@@ -185,6 +196,7 @@ class Terminal with Observable implements TerminalUiInteraction {
   MouseMode _mouseMode = MouseMode.none;
   MouseMode get mouseMode => _mouseMode;
 
+  @override
   final TerminalTheme theme;
 
   // final cellAttr = CellAttrTemplate();
@@ -479,14 +491,6 @@ class Terminal with Observable implements TerminalUiInteraction {
     }
   }
 
-  final wordSeparatorCodes = <String>[
-    String.fromCharCode(0),
-    ' ',
-    '.',
-    ':',
-    '/'
-  ];
-
   void selectWordOrRow(Position position) {
     if (position.y > buffer.lines.length) {
       return;
@@ -516,7 +520,7 @@ class Terminal with Observable implements TerminalUiInteraction {
           break;
         }
         final content = line.cellGetContent(start - 1);
-        if (wordSeparatorCodes.contains(String.fromCharCode(content))) {
+        if (kWordSeparators.contains(String.fromCharCode(content))) {
           break;
         }
         start--;
@@ -526,7 +530,7 @@ class Terminal with Observable implements TerminalUiInteraction {
           break;
         }
         final content = line.cellGetContent(end + 1);
-        if (wordSeparatorCodes.contains(String.fromCharCode(content))) {
+        if (kWordSeparators.contains(String.fromCharCode(content))) {
           break;
         }
         end++;
@@ -535,6 +539,7 @@ class Terminal with Observable implements TerminalUiInteraction {
       _selection.clear();
       _selection.init(Position(start, row));
       _selection.update(Position(end, row));
+      refresh();
     }
   }
 
@@ -597,8 +602,6 @@ class Terminal with Observable implements TerminalUiInteraction {
     backend?.write(data);
   }
 
-  void selectWord(int x, int y) {}
-
   int get _tabIndexFromCursor {
     var index = buffer.cursorX;
 
@@ -721,6 +724,7 @@ class Terminal with Observable implements TerminalUiInteraction {
   void selectAll() {
     _selection.init(Position(0, 0));
     _selection.update(Position(terminalWidth, bufferHeight));
+    refresh();
   }
 
   String _composingString = '';
@@ -733,4 +737,63 @@ class Terminal with Observable implements TerminalUiInteraction {
     _composingString = value;
     refresh();
   }
+
+  @override
+  TerminalSearchResult get userSearchResult => _userSearchTask.searchResult;
+
+  @override
+  int get numberOfSearchHits => _userSearchTask.numberOfSearchHits;
+
+  @override
+  int? get currentSearchHit => _userSearchTask.currentSearchHit;
+
+  @override
+  void set currentSearchHit(int? currentSearchHit) {
+    _userSearchTask.currentSearchHit = currentSearchHit;
+    _scrollCurrentHitIntoView();
+    refresh();
+  }
+
+  @override
+  TerminalSearchOptions get userSearchOptions => _userSearchTask.options;
+
+  @override
+  void set userSearchOptions(TerminalSearchOptions options) {
+    _userSearchTask.options = options;
+    _scrollCurrentHitIntoView();
+    refresh();
+  }
+
+  @override
+  String? get userSearchPattern => _userSearchTask.pattern;
+
+  @override
+  void set userSearchPattern(String? newValue) {
+    _userSearchTask.pattern = newValue;
+    _scrollCurrentHitIntoView();
+    refresh();
+  }
+
+  @override
+  bool get isUserSearchActive => _userSearchTask.isActive;
+
+  @override
+  void set isUserSearchActive(bool isUserSearchActive) {
+    _userSearchTask.isActive = isUserSearchActive;
+    _scrollCurrentHitIntoView();
+    refresh();
+  }
+
+  void _scrollCurrentHitIntoView() {
+    if (!_userSearchTask.isActive) {
+      return;
+    }
+    final currentHit = _userSearchTask.currentSearchHitObject;
+
+    if (currentHit != null) {
+      final desiredScrollOffsetFromTop =
+          currentHit.startLineIndex + (terminalHeight / 2).floor();
+      setScrollOffsetFromBottom(buffer.height - desiredScrollOffsetFromTop);
+    }
+  }
 }

+ 77 - 2
lib/terminal/terminal_isolate.dart

@@ -8,6 +8,7 @@ import 'package:xterm/mouse/selection.dart';
 import 'package:xterm/terminal/platform.dart';
 import 'package:xterm/terminal/terminal.dart';
 import 'package:xterm/terminal/terminal_backend.dart';
+import 'package:xterm/terminal/terminal_search.dart';
 import 'package:xterm/terminal/terminal_ui_interaction.dart';
 import 'package:xterm/theme/terminal_theme.dart';
 import 'package:xterm/theme/terminal_themes.dart';
@@ -32,7 +33,11 @@ enum _IsolateCommand {
   requestNewStateWhenDirty,
   paste,
   terminateBackend,
-  updateComposingString
+  updateComposingString,
+  updateSearchPattern,
+  updateSearchOptions,
+  updateCurrentSearchHit,
+  updateIsUserSearchActive,
 }
 
 enum _IsolateEvent {
@@ -141,8 +146,15 @@ void terminalMain(SendPort port) async {
             _terminal.cursorY,
             _terminal.showCursor,
             _terminal.theme.cursor,
-            _terminal.getVisibleLines(),
+            _terminal
+                .getVisibleLines()
+                .map((bl) => BufferLine.withDataFrom(bl))
+                .toList(growable: false),
             _terminal.composingString,
+            _terminal.userSearchResult,
+            _terminal.userSearchPattern,
+            _terminal.userSearchOptions,
+            _terminal.isUserSearchActive,
           );
           port.send([_IsolateEvent.newState, newState]);
           _needNotify = true;
@@ -157,6 +169,18 @@ void terminalMain(SendPort port) async {
       case _IsolateCommand.updateComposingString:
         _terminal?.updateComposingString(msg[1]);
         break;
+      case _IsolateCommand.updateSearchPattern:
+        _terminal?.userSearchPattern = msg[1];
+        break;
+      case _IsolateCommand.updateSearchOptions:
+        _terminal?.userSearchOptions = msg[1];
+        break;
+      case _IsolateCommand.updateCurrentSearchHit:
+        _terminal?.currentSearchHit = msg[1];
+        break;
+      case _IsolateCommand.updateIsUserSearchActive:
+        _terminal?.isUserSearchActive = msg[1];
+        break;
     }
   }
 }
@@ -202,6 +226,11 @@ class TerminalState {
 
   String composingString;
 
+  TerminalSearchResult searchResult;
+  String? userSearchPattern;
+  TerminalSearchOptions userSearchOptions;
+  bool isUserSearchActive;
+
   TerminalState(
     this.scrollOffsetFromBottom,
     this.scrollOffsetFromTop,
@@ -218,6 +247,10 @@ class TerminalState {
     this.cursorColor,
     this.visibleLines,
     this.composingString,
+    this.searchResult,
+    this.userSearchPattern,
+    this.userSearchOptions,
+    this.isUserSearchActive,
   );
 }
 
@@ -254,6 +287,7 @@ class TerminalIsolate with Observable implements TerminalUiInteraction {
   final IconChangeHandler onIconChange;
   final PlatformBehavior _platform;
 
+  @override
   final TerminalTheme theme;
   final int maxLines;
 
@@ -526,4 +560,45 @@ class TerminalIsolate with Observable implements TerminalUiInteraction {
   void updateComposingString(String value) {
     _sendPort?.send([_IsolateCommand.updateComposingString, value]);
   }
+
+  @override
+  TerminalSearchResult get userSearchResult =>
+      _lastState?.searchResult ?? TerminalSearchResult.empty();
+
+  @override
+  int get numberOfSearchHits => userSearchResult.allHits.length;
+
+  @override
+  int? get currentSearchHit => userSearchResult.currentSearchHit;
+
+  @override
+  void set currentSearchHit(int? currentSearchHit) {
+    _sendPort?.send([_IsolateCommand.updateCurrentSearchHit, currentSearchHit]);
+  }
+
+  @override
+  TerminalSearchOptions get userSearchOptions =>
+      _lastState?.userSearchOptions ?? TerminalSearchOptions();
+
+  @override
+  void set userSearchOptions(TerminalSearchOptions options) {
+    _sendPort?.send([_IsolateCommand.updateSearchOptions, options]);
+  }
+
+  @override
+  String? get userSearchPattern => _lastState?.userSearchPattern;
+
+  @override
+  void set userSearchPattern(String? newValue) {
+    _sendPort?.send([_IsolateCommand.updateSearchPattern, newValue]);
+  }
+
+  @override
+  bool get isUserSearchActive => _lastState?.isUserSearchActive ?? false;
+
+  @override
+  void set isUserSearchActive(bool isUserSearchActive) {
+    _sendPort
+        ?.send([_IsolateCommand.updateIsUserSearchActive, isUserSearchActive]);
+  }
 }

+ 383 - 0
lib/terminal/terminal_search.dart

@@ -0,0 +1,383 @@
+import 'package:equatable/equatable.dart';
+import 'package:xterm/buffer/line/line.dart';
+import 'package:xterm/terminal/terminal_search_interaction.dart';
+import 'package:xterm/util/constants.dart';
+import 'package:xterm/util/unicode_v11.dart';
+
+/// Represents a search result.
+/// This instance will be replaced as a whole when the search has to be re-triggered
+/// It stores the hits the search produced and the navigation state inside
+/// the search results
+class TerminalSearchResult {
+  late final _allHits;
+  int? _currentSearchHit = null;
+
+  /// creates a new search result instance from the given hits
+  TerminalSearchResult.fromHits(List<TerminalSearchHit> hits) {
+    _allHits = hits;
+
+    if (_allHits.length > 0) {
+      _currentSearchHit = _allHits.length;
+    } else {
+      _currentSearchHit = null;
+    }
+  }
+
+  /// creates an empty search result
+  TerminalSearchResult.empty()
+      : _allHits = List<TerminalSearchHit>.empty(growable: false);
+
+  /// returns all hits of this search result
+  List<TerminalSearchHit> get allHits => _allHits;
+
+  /// returns the number of the current search hit
+  int? get currentSearchHit => _currentSearchHit;
+
+  /// sets the current search hit number
+  void set currentSearchHit(int? currentSearchHit) {
+    if (_allHits.length <= 0) {
+      _currentSearchHit = null;
+    } else {
+      _currentSearchHit = currentSearchHit != null
+          ? currentSearchHit.clamp(1, _allHits.length).toInt()
+          : null;
+    }
+  }
+}
+
+/// Represents one search hit
+class TerminalSearchHit {
+  TerminalSearchHit(
+      this.startLineIndex, this.startIndex, this.endLineIndex, this.endIndex);
+
+  /// index of the line where the hit starts
+  final int startLineIndex;
+
+  /// index of the hit start inside the start line
+  final int startIndex;
+
+  /// index of the line where the hit starts
+  final int endLineIndex;
+
+  /// index of the hit end inside the end line
+  final int endIndex;
+
+  /// checks if the given cell (line / col) is contained in this hit
+  bool contains(int line, int col) {
+    if (line < startLineIndex || line > endLineIndex) {
+      return false;
+    }
+    if (line == startLineIndex && startLineIndex == endLineIndex) {
+      return col >= startIndex && col < endIndex;
+    }
+    if (line == startLineIndex) {
+      return col >= startIndex;
+    }
+    if (line == endLineIndex) {
+      return col < endIndex;
+    }
+    // here we are sure that the given point is inside a full line match
+    return true;
+  }
+}
+
+/// represents options for a terminal search
+class TerminalSearchOptions extends Equatable {
+  TerminalSearchOptions({
+    this.caseSensitive = false,
+    this.matchWholeWord = false,
+    this.useRegex = false,
+  });
+
+  /// defines if the search should be case sensitive. If set to [false] then
+  /// the search will be case insensitive
+  final bool caseSensitive;
+
+  /// defines if the search should match whole words.
+  final bool matchWholeWord;
+
+  /// defines if the search should treat the pattern as a regex, or not
+  final bool useRegex;
+
+  /// creates a new TerminalSearchOptions instance based on this one changing the
+  /// given parameters
+  TerminalSearchOptions copyWith(
+      {bool? caseSensitive, bool? matchWholeWord, bool? useRegex}) {
+    return TerminalSearchOptions(
+      caseSensitive: caseSensitive ?? this.caseSensitive,
+      matchWholeWord: matchWholeWord ?? this.matchWholeWord,
+      useRegex: useRegex ?? this.useRegex,
+    );
+  }
+
+  @override
+  bool get stringify => true;
+
+  @override
+  List<Object?> get props => [
+        caseSensitive,
+        matchWholeWord,
+        useRegex,
+      ];
+}
+
+/// represents a search task.
+/// A search task can deliver search results based on the given parameters.
+/// It takes care to cache the results as long as possible and re-trigger a
+/// search on demand only when necessary
+class TerminalSearchTask {
+  TerminalSearchTask(this._search, this._terminal, this._dirtyTagName,
+      this._terminalSearchOptions);
+
+  final TerminalSearch _search;
+  final TerminalSearchInteraction _terminal;
+  String? _pattern = null;
+  bool _isPatternDirty = true;
+  RegExp? _searchRegexp = null;
+  final String _dirtyTagName;
+  TerminalSearchOptions _terminalSearchOptions;
+
+  bool _isActive = false;
+
+  /// indicates if the current search task is active
+  bool get isActive => _isActive;
+
+  /// sets the active state of this search task
+  void set isActive(bool isActive) {
+    _isActive = isActive;
+    if (isActive) {
+      _invalidate();
+    }
+  }
+
+  bool? _hasBeenUsingAltBuffer;
+  TerminalSearchResult? _lastSearchResult = null;
+
+  bool _isAnyLineDirty() {
+    final bufferLength = _terminal.buffer.lines.length;
+    for (var i = 0; i < bufferLength; i++) {
+      if (_terminal.buffer.lines[i].isTagDirty(_dirtyTagName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  void _markLinesForSearchDone() {
+    final bufferLength = _terminal.buffer.lines.length;
+    for (var i = 0; i < bufferLength; i++) {
+      _terminal.buffer.lines[i].markTagAsNonDirty(_dirtyTagName);
+    }
+  }
+
+  bool _isTerminalStateDirty() {
+    if (_isAnyLineDirty()) {
+      return true;
+    }
+    if (_hasBeenUsingAltBuffer != null &&
+        _hasBeenUsingAltBuffer! != _terminal.isUsingAltBuffer()) {
+      return true;
+    }
+    return false;
+  }
+
+  bool get _isDirty {
+    if (_isPatternDirty) {
+      return true;
+    }
+    return _isTerminalStateDirty();
+  }
+
+  /// the currently used pattern of this search task
+  String? get pattern => _pattern;
+
+  /// sets the pattern to use for this search task
+  void set pattern(String? newPattern) {
+    if (newPattern != _pattern) {
+      _pattern = newPattern;
+      _invalidate();
+    }
+  }
+
+  /// the currently used search options
+  TerminalSearchOptions get options => _terminalSearchOptions;
+
+  /// sets the search options to use
+  void set options(TerminalSearchOptions newOptions) {
+    if (_terminalSearchOptions == newOptions) {
+      return;
+    }
+    _terminalSearchOptions = newOptions;
+    _invalidate();
+  }
+
+  /// returns the hit that is currently the selected one (based on the search
+  /// result navigation)
+  TerminalSearchHit? get currentSearchHitObject {
+    if (searchResult.currentSearchHit == null) {
+      return null;
+    }
+    if (searchResult.allHits.length >= searchResult.currentSearchHit! &&
+        searchResult.currentSearchHit! > 0) {
+      return searchResult.allHits[searchResult.currentSearchHit! - 1];
+    }
+    return null;
+  }
+
+  /// the number of search hits in the current search result
+  int get numberOfSearchHits => searchResult.allHits.length;
+
+  /// number of the hit that is currently selected
+  int? get currentSearchHit => searchResult.currentSearchHit;
+
+  /// sets the hit number that shall be selected
+  void set currentSearchHit(int? currentSearchHit) {
+    searchResult.currentSearchHit = currentSearchHit;
+  }
+
+  void _invalidate() {
+    _isPatternDirty = true;
+    _searchRegexp = null;
+    _lastSearchResult = null;
+  }
+
+  String _createRegexPattern(String inputPattern) {
+    final result = StringBuffer();
+
+    for (final rune in inputPattern.runes) {
+      final runeString = String.fromCharCode(rune);
+      result.write(runeString);
+      final cellWidth = unicodeV11.wcwidth(rune);
+      final widthDiff = cellWidth - runeString.length;
+      if (widthDiff > 0) {
+        result.write(''.padRight(widthDiff));
+      }
+    }
+
+    return result.toString();
+  }
+
+  /// returns the current search result or triggers a new search if it has to
+  /// the result is a up to date search result either way
+  TerminalSearchResult get searchResult {
+    if (_pattern == null || !_isActive) {
+      return TerminalSearchResult.empty();
+    }
+    if (_lastSearchResult != null && !_isDirty) {
+      return _lastSearchResult!;
+    }
+
+    final terminalWidth = _terminal.terminalWidth;
+
+    if (_searchRegexp == null) {
+      var pattern = _pattern!;
+      if (!_terminalSearchOptions.useRegex) {
+        pattern = RegExp.escape(_pattern!);
+      }
+      _searchRegexp = RegExp(_createRegexPattern(pattern),
+          caseSensitive: _terminalSearchOptions.caseSensitive,
+          multiLine: false);
+    }
+
+    final hits = List<TerminalSearchHit>.empty(growable: true);
+
+    for (final match
+        in _searchRegexp!.allMatches(_search.terminalSearchString)) {
+      final start = match.start;
+      final end = match.end;
+      final startLineIndex = (start / terminalWidth).floor();
+      final endLineIndex = (end / terminalWidth).floor();
+
+      // subtract the lines that got added in order to get the index inside the line
+      final startIndex = start - startLineIndex * terminalWidth;
+      final endIndex = end - endLineIndex * terminalWidth;
+
+      if (_terminalSearchOptions.matchWholeWord) {
+        // we match a whole word when the hit fulfills:
+        // 1) starts at a line beginning or has a word-separator before it
+        final startIsOK =
+            startIndex == 0 || kWordSeparators.contains(match.input[start - 1]);
+        // 2) ends with a line or has a word-separator after it
+        final endIsOK = endIndex == terminalWidth ||
+            kWordSeparators.contains(match.input[end]);
+
+        if (!startIsOK || !endIsOK) {
+          continue;
+        }
+      }
+
+      hits.add(
+        TerminalSearchHit(
+          startLineIndex,
+          startIndex,
+          endLineIndex,
+          endIndex,
+        ),
+      );
+    }
+
+    _markLinesForSearchDone();
+
+    _isPatternDirty = false;
+    _lastSearchResult = TerminalSearchResult.fromHits(hits);
+    _hasBeenUsingAltBuffer = _terminal.isUsingAltBuffer();
+    return _lastSearchResult!;
+  }
+}
+
+/// main entry for terminal searches. This class is the factory for search tasks
+/// and will cache the search string that gets generated out of the terminal content
+/// so that all search tasks created by this search can use the same cached search string
+class TerminalSearch {
+  TerminalSearch(this._terminal);
+
+  final TerminalSearchInteraction _terminal;
+  String? _cachedSearchString;
+  int? _lastTerminalWidth;
+
+  /// creates a new search task that will use this search to access a cached variant
+  /// of the terminal search string
+  TerminalSearchTask createSearchTask(String dirtyTagName) {
+    return TerminalSearchTask(
+        this, _terminal, dirtyTagName, TerminalSearchOptions());
+  }
+
+  /// returns the current terminal search string. The search string will be
+  /// refreshed on demand if
+  String get terminalSearchString {
+    final bufferLength = _terminal.buffer.lines.length;
+    final terminalWidth = _terminal.terminalWidth;
+
+    var isAnySearchStringInvalid = false;
+    for (var i = 0; i < bufferLength; i++) {
+      if (!_terminal.buffer.lines[i].hasCachedSearchString) {
+        isAnySearchStringInvalid = true;
+      }
+    }
+
+    late String completeSearchString;
+    if (_cachedSearchString != null &&
+        _lastTerminalWidth != null &&
+        _lastTerminalWidth! == terminalWidth &&
+        !isAnySearchStringInvalid) {
+      completeSearchString = _cachedSearchString!;
+    } else {
+      final bufferContent = StringBuffer();
+      for (var i = 0; i < bufferLength; i++) {
+        final BufferLine line = _terminal.buffer.lines[i];
+        final searchString = line.toSearchString(terminalWidth);
+        bufferContent.write(searchString);
+        if (searchString.length < terminalWidth) {
+          // fill up so that the row / col can be mapped back later on
+          bufferContent.writeAll(
+              List<String>.filled(terminalWidth - searchString.length, ' '));
+        }
+      }
+      completeSearchString = bufferContent.toString();
+      _cachedSearchString = completeSearchString;
+      _lastTerminalWidth = terminalWidth;
+    }
+
+    return completeSearchString;
+  }
+}

+ 14 - 0
lib/terminal/terminal_search_interaction.dart

@@ -0,0 +1,14 @@
+import 'package:xterm/buffer/buffer.dart';
+
+/// This interface defines the functionality of a Terminal that is needed
+/// by the search functionality
+abstract class TerminalSearchInteraction {
+  /// the current buffer
+  Buffer get buffer;
+
+  /// indication if the alternative buffer is currently used
+  bool isUsingAltBuffer();
+
+  /// the terminal width
+  int get terminalWidth;
+}

+ 36 - 0
lib/terminal/terminal_ui_interaction.dart

@@ -3,10 +3,15 @@ import 'package:xterm/input/keys.dart';
 import 'package:xterm/mouse/position.dart';
 import 'package:xterm/mouse/selection.dart';
 import 'package:xterm/terminal/platform.dart';
+import 'package:xterm/terminal/terminal_search.dart';
+import 'package:xterm/theme/terminal_theme.dart';
 import 'package:xterm/util/observable.dart';
 
 /// this interface describes what a Terminal UI needs from a Terminal
 abstract class TerminalUiInteraction with Observable {
+  /// The theme associated with this Terminal
+  TerminalTheme get theme;
+
   /// the ViewPort scroll offset from the bottom
   int get scrollOffsetFromBottom;
 
@@ -129,4 +134,35 @@ abstract class TerminalUiInteraction with Observable {
   /// update the composing string. This gets called by the input handling
   /// part of the terminal
   void updateComposingString(String value);
+
+  /// returns the list of search hits
+  TerminalSearchResult get userSearchResult;
+
+  /// gets the number of search hits
+  int get numberOfSearchHits;
+
+  /// gets the current search hit
+  int? get currentSearchHit;
+
+  /// sets the current search hit (gets clamped to the valid bounds)
+  void set currentSearchHit(int? currentSearchHit);
+
+  /// gets the current user search options
+  TerminalSearchOptions get userSearchOptions;
+
+  /// sets new user search options. This invalidates the cached search hits and
+  /// will re-trigger a new search
+  void set userSearchOptions(TerminalSearchOptions options);
+
+  /// the search pattern of a currently active search or [null]
+  String? get userSearchPattern;
+
+  /// sets the currently active search pattern
+  void set userSearchPattern(String? pattern);
+
+  /// gets if a user search is active
+  bool get isUserSearchActive;
+
+  // sets the user search active state
+  void set isUserSearchActive(bool isUserSearchActive);
 }

+ 4 - 0
lib/theme/terminal_style.dart

@@ -1,4 +1,5 @@
 import 'dart:ui' as ui;
+
 import 'package:equatable/equatable.dart';
 import 'package:flutter/material.dart';
 
@@ -35,6 +36,7 @@ class TerminalStyle with EquatableMixin {
     this.fontWidthScaleFactor = 1.0,
     this.fontHeightScaleFactor = 1.1,
     this.textStyleProvider,
+    this.ignoreBoldFlag = false,
   });
 
   final List<String> fontFamily;
@@ -42,6 +44,7 @@ class TerminalStyle with EquatableMixin {
   final double fontWidthScaleFactor;
   final double fontHeightScaleFactor;
   final TextStyleProvider? textStyleProvider;
+  final bool ignoreBoldFlag;
 
   @override
   List<Object?> get props {
@@ -51,6 +54,7 @@ class TerminalStyle with EquatableMixin {
       fontWidthScaleFactor,
       fontHeightScaleFactor,
       textStyleProvider,
+      ignoreBoldFlag
     ];
   }
 }

+ 28 - 22
lib/theme/terminal_theme.dart

@@ -1,26 +1,28 @@
 class TerminalTheme {
-  const TerminalTheme({
-    required this.cursor,
-    required this.selection,
-    required this.foreground,
-    required this.background,
-    required this.black,
-    required this.white,
-    required this.red,
-    required this.green,
-    required this.yellow,
-    required this.blue,
-    required this.magenta,
-    required this.cyan,
-    required this.brightBlack,
-    required this.brightRed,
-    required this.brightGreen,
-    required this.brightYellow,
-    required this.brightBlue,
-    required this.brightMagenta,
-    required this.brightCyan,
-    required this.brightWhite,
-  });
+  const TerminalTheme(
+      {required this.cursor,
+      required this.selection,
+      required this.foreground,
+      required this.background,
+      required this.black,
+      required this.white,
+      required this.red,
+      required this.green,
+      required this.yellow,
+      required this.blue,
+      required this.magenta,
+      required this.cyan,
+      required this.brightBlack,
+      required this.brightRed,
+      required this.brightGreen,
+      required this.brightYellow,
+      required this.brightBlue,
+      required this.brightMagenta,
+      required this.brightCyan,
+      required this.brightWhite,
+      required this.searchHitBackground,
+      required this.searchHitBackgroundCurrent,
+      required this.searchHitForeground});
 
   final int cursor;
   final int selection;
@@ -44,4 +46,8 @@ class TerminalTheme {
   final int brightMagenta;
   final int brightCyan;
   final int brightWhite;
+
+  final int searchHitBackground;
+  final int searchHitBackgroundCurrent;
+  final int searchHitForeground;
 }

+ 6 - 0
lib/theme/terminal_themes.dart

@@ -22,6 +22,9 @@ class TerminalThemes {
     brightMagenta: 0XFFD670D6,
     brightCyan: 0XFF29B8DB,
     brightWhite: 0XFFFFFFFF,
+    searchHitBackground: 0XFFFFFF2B,
+    searchHitBackgroundCurrent: 0XFF31FF26,
+    searchHitForeground: 0XFF000000,
   );
 
   static const whiteOnBlack = TerminalTheme(
@@ -45,5 +48,8 @@ class TerminalThemes {
     brightMagenta: 0XFFD670D6,
     brightCyan: 0XFF29B8DB,
     brightWhite: 0XFFFFFFFF,
+    searchHitBackground: 0XFFFFFF2B,
+    searchHitBackgroundCurrent: 0XFF31FF26,
+    searchHitForeground: 0XFF000000,
   );
 }

+ 14 - 0
lib/util/constants.dart

@@ -11,3 +11,17 @@ const kProfileMode = bool.fromEnvironment(
 const kDebugMode = !kReleaseMode && !kProfileMode;
 
 const kIsWeb = identical(0, 0.0);
+
+final kWordSeparators = [
+  String.fromCharCode(0),
+  ' ',
+  '.',
+  ':',
+  '-',
+  '\'',
+  '"',
+  '*',
+  '+',
+  '/',
+  '\\'
+];

+ 299 - 5
pubspec.lock

@@ -1,13 +1,34 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "24.0.0"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
   async:
     dependency: transitive
     description:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.7.0"
+    version: "2.8.2"
   boolean_selector:
     dependency: transitive
     description:
@@ -15,6 +36,62 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  build:
+    dependency: transitive
+    description:
+      name: build
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  build_config:
+    dependency: transitive
+    description:
+      name: build_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  build_daemon:
+    dependency: transitive
+    description:
+      name: build_daemon
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.0"
+  build_resolvers:
+    dependency: transitive
+    description:
+      name: build_resolvers
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  build_runner:
+    dependency: "direct dev"
+    description:
+      name: build_runner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  build_runner_core:
+    dependency: transitive
+    description:
+      name: build_runner_core
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "7.1.0"
+  built_collection:
+    dependency: transitive
+    description:
+      name: built_collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.0"
+  built_value:
+    dependency: transitive
+    description:
+      name: built_value
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "8.1.2"
   characters:
     dependency: transitive
     description:
@@ -28,7 +105,21 @@ packages:
       name: charcode
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.3.1"
+  checked_yaml:
+    dependency: transitive
+    description:
+      name: checked_yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  cli_util:
+    dependency: transitive
+    description:
+      name: cli_util
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.3.3"
   clock:
     dependency: transitive
     description:
@@ -36,6 +127,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.1.0"
+  code_builder:
+    dependency: transitive
+    description:
+      name: code_builder
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.1.0"
   collection:
     dependency: transitive
     description:
@@ -50,6 +148,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  dart_style:
+    dependency: transitive
+    description:
+      name: dart_style
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
   equatable:
     dependency: "direct main"
     description:
@@ -64,6 +176,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.2.0"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.2"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -74,20 +200,104 @@ packages:
     description: flutter
     source: sdk
     version: "0.0.0"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  graphs:
+    dependency: transitive
+    description:
+      name: graphs
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.0.0"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.3"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.1.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.10"
+    version: "0.12.11"
   meta:
     dependency: "direct main"
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.4.0"
+    version: "1.7.0"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  mockito:
+    dependency: "direct dev"
+    description:
+      name: mockito
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.0.15"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   path:
     dependency: transitive
     description:
@@ -95,6 +305,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.0"
+  pedantic:
+    dependency: transitive
+    description:
+      name: pedantic
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.11.1"
   platform_info:
     dependency: "direct main"
     description:
@@ -102,6 +319,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.0-nullsafety.1"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.0"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  pubspec_parse:
+    dependency: transitive
+    description:
+      name: pubspec_parse
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   quiver:
     dependency: "direct main"
     description:
@@ -109,11 +347,32 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "3.0.0"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.0"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
   sky_engine:
     dependency: transitive
     description: flutter
     source: sdk
     version: "0.0.99"
+  source_gen:
+    dependency: transitive
+    description:
+      name: source_gen
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
   source_span:
     dependency: transitive
     description:
@@ -135,6 +394,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   string_scanner:
     dependency: transitive
     description:
@@ -155,7 +421,14 @@ packages:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.0"
+    version: "0.4.3"
+  timing:
+    dependency: transitive
+    description:
+      name: timing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   typed_data:
     dependency: transitive
     description:
@@ -170,6 +443,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.0"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
 sdks:
   dart: ">=2.12.0 <3.0.0"
   flutter: ">=2.0.0"

+ 2 - 0
pubspec.yaml

@@ -19,6 +19,8 @@ dependencies:
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  mockito: ^5.0.15
+  build_runner: ^2.1.1
 
 # For information on the generic Dart part of this file, see the
 # following page: https://dart.dev/tools/pub/pubspec

+ 409 - 0
test/terminal/terminal_search_test.dart

@@ -0,0 +1,409 @@
+import 'dart:developer';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/line/line.dart';
+import 'package:xterm/terminal/cursor.dart';
+import 'package:xterm/terminal/terminal_search.dart';
+import 'package:xterm/terminal/terminal_search_interaction.dart';
+import 'package:xterm/util/circular_list.dart';
+import 'package:xterm/util/unicode_v11.dart';
+
+import 'terminal_search_test.mocks.dart';
+
+class TerminalSearchTestCircularList extends CircularList<BufferLine> {
+  TerminalSearchTestCircularList(int maxLines) : super(maxLines);
+}
+
+@GenerateMocks([
+  TerminalSearchInteraction,
+  Buffer,
+  TerminalSearchTestCircularList,
+  BufferLine
+])
+void main() {
+  group('Terminal Search Tests', () {
+    test('Creation works', () {
+      _TestFixture();
+    });
+
+    test('Doesn\'t trigger anything when not activated', () {
+      final fixture = _TestFixture();
+      verifyNoMoreInteractions(fixture.terminalSearchInteractionMock);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.pattern = "some test";
+      task.isActive = false;
+      task.searchResult;
+    });
+
+    test('Basic search works', () {
+      final fixture = _TestFixture();
+      fixture.expectTerminalSearchContent(['Simple Content']);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = 'content';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 0);
+      expect(result.allHits[0].startIndex, 7);
+      expect(result.allHits[0].endLineIndex, 0);
+      expect(result.allHits[0].endIndex, 14);
+    });
+
+    test('Multiline search works', () {
+      final fixture = _TestFixture();
+      fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = 'line';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 1);
+      expect(result.allHits[0].startIndex, 7);
+      expect(result.allHits[0].endLineIndex, 1);
+      expect(result.allHits[0].endIndex, 11);
+    });
+
+    test('Emoji search works', () {
+      final fixture = _TestFixture();
+      fixture.expectBufferContentLine([
+        '🍏',
+        '🍎',
+        '🍐',
+        '🍊',
+        '🍋',
+        '🍌',
+        '🍉',
+        '🍇',
+        '🍓',
+        '🫐',
+        '🍈',
+        '🍒',
+        '🍑'
+      ]);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = '🍋';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 0);
+      expect(result.allHits[0].startIndex, 8);
+      expect(result.allHits[0].endLineIndex, 0);
+      expect(result.allHits[0].endIndex, 10);
+    });
+
+    test('CJK search works', () {
+      final fixture = _TestFixture();
+      fixture.expectBufferContentLine(['こ', 'ん', 'に', 'ち', 'は', '世', '界']);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = 'は';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 0);
+      expect(result.allHits[0].startIndex, 8);
+      expect(result.allHits[0].endLineIndex, 0);
+      expect(result.allHits[0].endIndex, 10);
+    });
+
+    test('Finding strings directly on line break works', () {
+      final fixture = _TestFixture();
+      fixture.expectTerminalSearchContent([
+        'The search hit is '.padRight(fixture.terminalWidth - 3) + 'spl',
+        'it over two lines',
+      ]);
+      final task = fixture.uut.createSearchTask('testsearch');
+      task.isActive = true;
+      task.pattern = 'split';
+      task.options = TerminalSearchOptions(
+          caseSensitive: false, matchWholeWord: false, useRegex: false);
+      final result = task.searchResult;
+      expect(result.allHits.length, 1);
+      expect(result.allHits[0].startLineIndex, 0);
+      expect(result.allHits[0].startIndex, 77);
+      expect(result.allHits[0].endLineIndex, 1);
+      expect(result.allHits[0].endIndex, 2);
+    });
+  });
+
+  test('Option: case sensitivity works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.pattern = 'line';
+    task.options = TerminalSearchOptions(
+        caseSensitive: true, matchWholeWord: false, useRegex: false);
+
+    final result = task.searchResult;
+    expect(result.allHits.length, 0);
+
+    task.pattern = 'Line';
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length, 1);
+    expect(secondResult.allHits[0].startLineIndex, 1);
+    expect(secondResult.allHits[0].startIndex, 7);
+    expect(secondResult.allHits[0].endLineIndex, 1);
+    expect(secondResult.allHits[0].endIndex, 11);
+  });
+
+  test('Option: whole word works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.pattern = 'lin';
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: true, useRegex: false);
+
+    final result = task.searchResult;
+    expect(result.allHits.length, 0);
+
+    task.pattern = 'line';
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length, 1);
+    expect(secondResult.allHits[0].startLineIndex, 1);
+    expect(secondResult.allHits[0].startIndex, 7);
+    expect(secondResult.allHits[0].endLineIndex, 1);
+    expect(secondResult.allHits[0].endIndex, 11);
+  });
+
+  test('Option: regex works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: false, useRegex: true);
+
+    task.pattern =
+        r'(^|\s)\w{4}($|\s)'; // match exactly 4 characters (and the whitespace before and/or after)
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length, 1);
+    expect(secondResult.allHits[0].startLineIndex, 1);
+    expect(secondResult.allHits[0].startIndex, 6);
+    expect(secondResult.allHits[0].endLineIndex, 1);
+    expect(secondResult.allHits[0].endIndex, 12);
+  });
+
+  test('Retrigger search when a BufferLine got dirty works', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(
+        ['Simple Content', 'Second Line', 'Third row']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: false, useRegex: false);
+
+    task.pattern = 'line';
+    final result = task.searchResult;
+    expect(result.allHits.length, 1);
+
+    // overwrite expectations, nothing dirty => no new search
+    fixture.expectTerminalSearchContent(
+        ['Simple Content', 'Second Line', 'Third line'],
+        isSearchStringCached: true);
+    task.isActive = false;
+    task.isActive = true;
+
+    final secondResult = task.searchResult;
+    expect(secondResult.allHits.length,
+        1); // nothing was dirty => we get the cached search result
+
+    // overwrite expectations, one line is dirty => new search
+    fixture.expectTerminalSearchContent(
+        ['Simple Content', 'Second Line', 'Third line'],
+        isSearchStringCached: false,
+        dirtyIndices: [1]);
+
+    final thirdResult = task.searchResult;
+    expect(thirdResult.allHits.length,
+        2); //search has happened again so the new content is found
+
+    // overwrite expectations, content has changed => new search
+    fixture.expectTerminalSearchContent(
+        ['First line', 'Second Line', 'Third line'],
+        isSearchStringCached: false,
+        dirtyIndices: [0]);
+
+    final fourthResult = task.searchResult;
+    expect(fourthResult.allHits.length,
+        3); //search has happened again so the new content is found
+  });
+  test('Handles regex special characters in non regex mode correctly', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line.\\{']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.pattern = 'line.\\{';
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: false, useRegex: false);
+
+    final result = task.searchResult;
+    expect(result.allHits.length, 1);
+    expect(result.allHits[0].startLineIndex, 1);
+    expect(result.allHits[0].startIndex, 7);
+    expect(result.allHits[0].endLineIndex, 1);
+    expect(result.allHits[0].endIndex, 14);
+  });
+  test('TerminalWidth change leads to retriggering search', () {
+    final fixture = _TestFixture();
+    fixture.expectTerminalSearchContent(['Simple Content', 'Second Line']);
+    final task = fixture.uut.createSearchTask('testsearch');
+    task.isActive = true;
+    task.pattern = 'line';
+    task.options = TerminalSearchOptions(
+        caseSensitive: false, matchWholeWord: false, useRegex: false);
+
+    final result = task.searchResult;
+    expect(result.allHits.length, 1);
+
+    // change data to detect a search re-run
+    fixture.expectTerminalSearchContent(
+        ['First line', 'Second Line']); //has 2 hits
+    task.isActive = false;
+    task.isActive = true;
+    final secondResult = task.searchResult;
+    expect(
+        secondResult.allHits.length, 1); //nothing changed so the cache is used
+
+    fixture.terminalWidth = 79;
+    task.isActive = false;
+    task.isActive = true;
+    final thirdResult = task.searchResult;
+    //we changed the terminal width which triggered a re-run of the search
+    expect(thirdResult.allHits.length, 2);
+  });
+}
+
+class _TestFixture {
+  _TestFixture({
+    terminalWidth = 80,
+  }) : _terminalWidth = terminalWidth {
+    uut = TerminalSearch(terminalSearchInteractionMock);
+    when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth);
+  }
+
+  int _terminalWidth;
+  int get terminalWidth => _terminalWidth;
+  void set terminalWidth(int terminalWidth) {
+    _terminalWidth = terminalWidth;
+    when(terminalSearchInteractionMock.terminalWidth).thenReturn(terminalWidth);
+  }
+
+  void expectBufferContentLine(
+    List<String> cellData, {
+    isUsingAltBuffer = false,
+  }) {
+    final buffer = _getBufferFromCellData(cellData);
+    when(terminalSearchInteractionMock.buffer).thenReturn(buffer);
+    when(terminalSearchInteractionMock.isUsingAltBuffer())
+        .thenReturn(isUsingAltBuffer);
+  }
+
+  void expectTerminalSearchContent(
+    List<String> lines, {
+    isUsingAltBuffer = false,
+    isSearchStringCached = true,
+    List<int>? dirtyIndices,
+  }) {
+    final buffer = _getBuffer(lines,
+        isCached: isSearchStringCached, dirtyIndices: dirtyIndices);
+
+    when(terminalSearchInteractionMock.buffer).thenReturn(buffer);
+    when(terminalSearchInteractionMock.isUsingAltBuffer())
+        .thenReturn(isUsingAltBuffer);
+  }
+
+  final terminalSearchInteractionMock = MockTerminalSearchInteraction();
+  late final TerminalSearch uut;
+
+  MockBuffer _getBufferFromCellData(List<String> cellData) {
+    final result = MockBuffer();
+    final circularList = MockTerminalSearchTestCircularList();
+    when(result.lines).thenReturn(circularList);
+    when(circularList[0]).thenReturn(_getBufferLineFromData(cellData));
+    when(circularList.length).thenReturn(1);
+
+    return result;
+  }
+
+  MockBuffer _getBuffer(
+    List<String> lines, {
+    isCached = true,
+    List<int>? dirtyIndices,
+  }) {
+    final result = MockBuffer();
+    final circularList = MockTerminalSearchTestCircularList();
+    when(result.lines).thenReturn(circularList);
+
+    final bufferLines = _getBufferLinesWithSearchContent(
+      lines,
+      isCached: isCached,
+      dirtyIndices: dirtyIndices,
+    );
+
+    when(circularList[any]).thenAnswer(
+        (realInvocation) => bufferLines[realInvocation.positionalArguments[0]]);
+    when(circularList.length).thenReturn(bufferLines.length);
+
+    return result;
+  }
+
+  BufferLine _getBufferLineFromData(List<String> cellData) {
+    final result = BufferLine(length: _terminalWidth);
+    int currentIndex = 0;
+    for (var data in cellData) {
+      final codePoint = data.runes.first;
+      final width = unicodeV11.wcwidth(codePoint);
+      result.cellInitialize(
+        currentIndex,
+        content: codePoint,
+        width: width,
+        cursor: Cursor(bg: 0, fg: 0, flags: 0),
+      );
+      currentIndex++;
+      for (int i = 1; i < width; i++) {
+        result.cellInitialize(
+          currentIndex,
+          content: 0,
+          width: 0,
+          cursor: Cursor(bg: 0, fg: 0, flags: 0),
+        );
+        currentIndex++;
+      }
+    }
+    return result;
+  }
+
+  List<MockBufferLine> _getBufferLinesWithSearchContent(
+    List<String> content, {
+    isCached = true,
+    List<int>? dirtyIndices,
+  }) {
+    final result = List<MockBufferLine>.empty(growable: true);
+    for (int i = 0; i < content.length; i++) {
+      final bl = MockBufferLine();
+      when(bl.hasCachedSearchString).thenReturn(isCached);
+      when(bl.toSearchString(any)).thenReturn(content[i]);
+      if (dirtyIndices?.contains(i) ?? false) {
+        when(bl.isTagDirty(any)).thenReturn(true);
+      } else {
+        when(bl.isTagDirty(any)).thenReturn(false);
+      }
+      result.add(bl);
+    }
+
+    return result;
+  }
+}

+ 554 - 0
test/terminal/terminal_search_test.mocks.dart

@@ -0,0 +1,554 @@
+// Mocks generated by Mockito 5.0.15 from annotations
+// in xterm/test/terminal/terminal_search_test.dart.
+// Do not manually edit this file.
+
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:xterm/buffer/buffer.dart' as _i2;
+import 'package:xterm/buffer/line/line.dart' as _i5;
+import 'package:xterm/terminal/charset.dart' as _i3;
+import 'package:xterm/terminal/cursor.dart' as _i9;
+import 'package:xterm/terminal/terminal_search_interaction.dart' as _i7;
+import 'package:xterm/util/circular_list.dart' as _i4;
+import 'package:xterm/util/scroll_range.dart' as _i6;
+
+import 'terminal_search_test.dart' as _i8;
+
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+
+class _FakeBuffer_0 extends _i1.Fake implements _i2.Buffer {}
+
+class _FakeCharset_1 extends _i1.Fake implements _i3.Charset {}
+
+class _FakeCircularList_2<T> extends _i1.Fake implements _i4.CircularList<T> {}
+
+class _FakeBufferLine_3 extends _i1.Fake implements _i5.BufferLine {}
+
+class _FakeScrollRange_4 extends _i1.Fake implements _i6.ScrollRange {}
+
+class _FakeBufferLineData_5 extends _i1.Fake implements _i5.BufferLineData {}
+
+/// A class which mocks [TerminalSearchInteraction].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTerminalSearchInteraction extends _i1.Mock
+    implements _i7.TerminalSearchInteraction {
+  MockTerminalSearchInteraction() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i2.Buffer get buffer => (super.noSuchMethod(Invocation.getter(#buffer),
+      returnValue: _FakeBuffer_0()) as _i2.Buffer);
+  @override
+  int get terminalWidth =>
+      (super.noSuchMethod(Invocation.getter(#terminalWidth), returnValue: 0)
+          as int);
+  @override
+  bool isUsingAltBuffer() =>
+      (super.noSuchMethod(Invocation.method(#isUsingAltBuffer, []),
+          returnValue: false) as bool);
+  @override
+  String toString() => super.toString();
+}
+
+/// A class which mocks [Buffer].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBuffer extends _i1.Mock implements _i2.Buffer {
+  MockBuffer() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  bool get isAltBuffer =>
+      (super.noSuchMethod(Invocation.getter(#isAltBuffer), returnValue: false)
+          as bool);
+  @override
+  _i3.Charset get charset => (super.noSuchMethod(Invocation.getter(#charset),
+      returnValue: _FakeCharset_1()) as _i3.Charset);
+  @override
+  _i4.CircularList<_i5.BufferLine> get lines =>
+      (super.noSuchMethod(Invocation.getter(#lines),
+              returnValue: _FakeCircularList_2<_i5.BufferLine>())
+          as _i4.CircularList<_i5.BufferLine>);
+  @override
+  set lines(_i4.CircularList<_i5.BufferLine>? _lines) =>
+      super.noSuchMethod(Invocation.setter(#lines, _lines),
+          returnValueForMissingStub: null);
+  @override
+  int get viewHeight =>
+      (super.noSuchMethod(Invocation.getter(#viewHeight), returnValue: 0)
+          as int);
+  @override
+  int get viewWidth =>
+      (super.noSuchMethod(Invocation.getter(#viewWidth), returnValue: 0)
+          as int);
+  @override
+  int get scrollOffsetFromBottom =>
+      (super.noSuchMethod(Invocation.getter(#scrollOffsetFromBottom),
+          returnValue: 0) as int);
+  @override
+  int get scrollOffsetFromTop => (super
+          .noSuchMethod(Invocation.getter(#scrollOffsetFromTop), returnValue: 0)
+      as int);
+  @override
+  bool get isUserScrolling => (super
+          .noSuchMethod(Invocation.getter(#isUserScrolling), returnValue: false)
+      as bool);
+  @override
+  int get cursorX =>
+      (super.noSuchMethod(Invocation.getter(#cursorX), returnValue: 0) as int);
+  @override
+  int get cursorY =>
+      (super.noSuchMethod(Invocation.getter(#cursorY), returnValue: 0) as int);
+  @override
+  int get marginTop =>
+      (super.noSuchMethod(Invocation.getter(#marginTop), returnValue: 0)
+          as int);
+  @override
+  int get marginBottom =>
+      (super.noSuchMethod(Invocation.getter(#marginBottom), returnValue: 0)
+          as int);
+  @override
+  _i5.BufferLine get currentLine =>
+      (super.noSuchMethod(Invocation.getter(#currentLine),
+          returnValue: _FakeBufferLine_3()) as _i5.BufferLine);
+  @override
+  int get height =>
+      (super.noSuchMethod(Invocation.getter(#height), returnValue: 0) as int);
+  @override
+  bool get hasScrollableRegion =>
+      (super.noSuchMethod(Invocation.getter(#hasScrollableRegion),
+          returnValue: false) as bool);
+  @override
+  bool get isInScrollableRegion =>
+      (super.noSuchMethod(Invocation.getter(#isInScrollableRegion),
+          returnValue: false) as bool);
+  @override
+  void write(String? text) =>
+      super.noSuchMethod(Invocation.method(#write, [text]),
+          returnValueForMissingStub: null);
+  @override
+  void writeChar(int? codePoint) =>
+      super.noSuchMethod(Invocation.method(#writeChar, [codePoint]),
+          returnValueForMissingStub: null);
+  @override
+  _i5.BufferLine getViewLine(int? index) =>
+      (super.noSuchMethod(Invocation.method(#getViewLine, [index]),
+          returnValue: _FakeBufferLine_3()) as _i5.BufferLine);
+  @override
+  int convertViewLineToRawLine(int? viewLine) => (super.noSuchMethod(
+      Invocation.method(#convertViewLineToRawLine, [viewLine]),
+      returnValue: 0) as int);
+  @override
+  int convertRawLineToViewLine(int? rawLine) => (super.noSuchMethod(
+      Invocation.method(#convertRawLineToViewLine, [rawLine]),
+      returnValue: 0) as int);
+  @override
+  void newLine() => super.noSuchMethod(Invocation.method(#newLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void carriageReturn() =>
+      super.noSuchMethod(Invocation.method(#carriageReturn, []),
+          returnValueForMissingStub: null);
+  @override
+  void backspace() => super.noSuchMethod(Invocation.method(#backspace, []),
+      returnValueForMissingStub: null);
+  @override
+  List<_i5.BufferLine> getVisibleLines() =>
+      (super.noSuchMethod(Invocation.method(#getVisibleLines, []),
+          returnValue: <_i5.BufferLine>[]) as List<_i5.BufferLine>);
+  @override
+  void eraseDisplayFromCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseDisplayFromCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseDisplayToCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseDisplayToCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseDisplay() =>
+      super.noSuchMethod(Invocation.method(#eraseDisplay, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseLineFromCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseLineFromCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseLineToCursor() =>
+      super.noSuchMethod(Invocation.method(#eraseLineToCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void eraseLine() => super.noSuchMethod(Invocation.method(#eraseLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void eraseCharacters(int? count) =>
+      super.noSuchMethod(Invocation.method(#eraseCharacters, [count]),
+          returnValueForMissingStub: null);
+  @override
+  _i6.ScrollRange getAreaScrollRange() =>
+      (super.noSuchMethod(Invocation.method(#getAreaScrollRange, []),
+          returnValue: _FakeScrollRange_4()) as _i6.ScrollRange);
+  @override
+  void areaScrollDown(int? lines) =>
+      super.noSuchMethod(Invocation.method(#areaScrollDown, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void areaScrollUp(int? lines) =>
+      super.noSuchMethod(Invocation.method(#areaScrollUp, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void index() => super.noSuchMethod(Invocation.method(#index, []),
+      returnValueForMissingStub: null);
+  @override
+  void reverseIndex() =>
+      super.noSuchMethod(Invocation.method(#reverseIndex, []),
+          returnValueForMissingStub: null);
+  @override
+  void cursorGoForward() =>
+      super.noSuchMethod(Invocation.method(#cursorGoForward, []),
+          returnValueForMissingStub: null);
+  @override
+  void setCursorX(int? cursorX) =>
+      super.noSuchMethod(Invocation.method(#setCursorX, [cursorX]),
+          returnValueForMissingStub: null);
+  @override
+  void setCursorY(int? cursorY) =>
+      super.noSuchMethod(Invocation.method(#setCursorY, [cursorY]),
+          returnValueForMissingStub: null);
+  @override
+  void moveCursorX(int? offset) =>
+      super.noSuchMethod(Invocation.method(#moveCursorX, [offset]),
+          returnValueForMissingStub: null);
+  @override
+  void moveCursorY(int? offset) =>
+      super.noSuchMethod(Invocation.method(#moveCursorY, [offset]),
+          returnValueForMissingStub: null);
+  @override
+  void setPosition(int? cursorX, int? cursorY) =>
+      super.noSuchMethod(Invocation.method(#setPosition, [cursorX, cursorY]),
+          returnValueForMissingStub: null);
+  @override
+  void movePosition(int? offsetX, int? offsetY) =>
+      super.noSuchMethod(Invocation.method(#movePosition, [offsetX, offsetY]),
+          returnValueForMissingStub: null);
+  @override
+  void setScrollOffsetFromBottom(int? offsetFromBottom) => super.noSuchMethod(
+      Invocation.method(#setScrollOffsetFromBottom, [offsetFromBottom]),
+      returnValueForMissingStub: null);
+  @override
+  void setScrollOffsetFromTop(int? offsetFromTop) => super.noSuchMethod(
+      Invocation.method(#setScrollOffsetFromTop, [offsetFromTop]),
+      returnValueForMissingStub: null);
+  @override
+  void screenScrollUp(int? lines) =>
+      super.noSuchMethod(Invocation.method(#screenScrollUp, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void screenScrollDown(int? lines) =>
+      super.noSuchMethod(Invocation.method(#screenScrollDown, [lines]),
+          returnValueForMissingStub: null);
+  @override
+  void saveCursor() => super.noSuchMethod(Invocation.method(#saveCursor, []),
+      returnValueForMissingStub: null);
+  @override
+  void restoreCursor() =>
+      super.noSuchMethod(Invocation.method(#restoreCursor, []),
+          returnValueForMissingStub: null);
+  @override
+  void setVerticalMargins(int? top, int? bottom) =>
+      super.noSuchMethod(Invocation.method(#setVerticalMargins, [top, bottom]),
+          returnValueForMissingStub: null);
+  @override
+  void resetVerticalMargins() =>
+      super.noSuchMethod(Invocation.method(#resetVerticalMargins, []),
+          returnValueForMissingStub: null);
+  @override
+  void deleteChars(int? count) =>
+      super.noSuchMethod(Invocation.method(#deleteChars, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void clearScrollback() =>
+      super.noSuchMethod(Invocation.method(#clearScrollback, []),
+          returnValueForMissingStub: null);
+  @override
+  void clear() => super.noSuchMethod(Invocation.method(#clear, []),
+      returnValueForMissingStub: null);
+  @override
+  void insertBlankCharacters(int? count) =>
+      super.noSuchMethod(Invocation.method(#insertBlankCharacters, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void insertLines(int? count) =>
+      super.noSuchMethod(Invocation.method(#insertLines, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void insertLine() => super.noSuchMethod(Invocation.method(#insertLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void deleteLines(int? count) =>
+      super.noSuchMethod(Invocation.method(#deleteLines, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void deleteLine() => super.noSuchMethod(Invocation.method(#deleteLine, []),
+      returnValueForMissingStub: null);
+  @override
+  void resize(int? oldWidth, int? oldHeight, int? newWidth, int? newHeight) =>
+      super.noSuchMethod(
+          Invocation.method(
+              #resize, [oldWidth, oldHeight, newWidth, newHeight]),
+          returnValueForMissingStub: null);
+  @override
+  dynamic adjustSavedCursor(int? dx, int? dy) =>
+      super.noSuchMethod(Invocation.method(#adjustSavedCursor, [dx, dy]));
+  @override
+  String toString() => super.toString();
+}
+
+/// A class which mocks [TerminalSearchTestCircularList].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockTerminalSearchTestCircularList extends _i1.Mock
+    implements _i8.TerminalSearchTestCircularList {
+  MockTerminalSearchTestCircularList() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  int get maxLength =>
+      (super.noSuchMethod(Invocation.getter(#maxLength), returnValue: 0)
+          as int);
+  @override
+  set maxLength(int? value) =>
+      super.noSuchMethod(Invocation.setter(#maxLength, value),
+          returnValueForMissingStub: null);
+  @override
+  int get length =>
+      (super.noSuchMethod(Invocation.getter(#length), returnValue: 0) as int);
+  @override
+  set length(int? value) =>
+      super.noSuchMethod(Invocation.setter(#length, value),
+          returnValueForMissingStub: null);
+  @override
+  bool get isFull =>
+      (super.noSuchMethod(Invocation.getter(#isFull), returnValue: false)
+          as bool);
+  @override
+  void forEach(void Function(_i5.BufferLine)? callback) =>
+      super.noSuchMethod(Invocation.method(#forEach, [callback]),
+          returnValueForMissingStub: null);
+  @override
+  _i5.BufferLine operator [](int? index) =>
+      (super.noSuchMethod(Invocation.method(#[], [index]),
+          returnValue: _FakeBufferLine_3()) as _i5.BufferLine);
+  @override
+  void operator []=(int? index, _i5.BufferLine? value) =>
+      super.noSuchMethod(Invocation.method(#[]=, [index, value]),
+          returnValueForMissingStub: null);
+  @override
+  void clear() => super.noSuchMethod(Invocation.method(#clear, []),
+      returnValueForMissingStub: null);
+  @override
+  void pushAll(Iterable<_i5.BufferLine>? items) =>
+      super.noSuchMethod(Invocation.method(#pushAll, [items]),
+          returnValueForMissingStub: null);
+  @override
+  void push(_i5.BufferLine? value) =>
+      super.noSuchMethod(Invocation.method(#push, [value]),
+          returnValueForMissingStub: null);
+  @override
+  _i5.BufferLine pop() => (super.noSuchMethod(Invocation.method(#pop, []),
+      returnValue: _FakeBufferLine_3()) as _i5.BufferLine);
+  @override
+  void remove(int? index, [int? count = 1]) =>
+      super.noSuchMethod(Invocation.method(#remove, [index, count]),
+          returnValueForMissingStub: null);
+  @override
+  void insert(int? index, _i5.BufferLine? item) =>
+      super.noSuchMethod(Invocation.method(#insert, [index, item]),
+          returnValueForMissingStub: null);
+  @override
+  void insertAll(int? index, List<_i5.BufferLine>? items) =>
+      super.noSuchMethod(Invocation.method(#insertAll, [index, items]),
+          returnValueForMissingStub: null);
+  @override
+  void trimStart(int? count) =>
+      super.noSuchMethod(Invocation.method(#trimStart, [count]),
+          returnValueForMissingStub: null);
+  @override
+  void shiftElements(int? start, int? count, int? offset) => super.noSuchMethod(
+      Invocation.method(#shiftElements, [start, count, offset]),
+      returnValueForMissingStub: null);
+  @override
+  void replaceWith(List<_i5.BufferLine>? replacement) =>
+      super.noSuchMethod(Invocation.method(#replaceWith, [replacement]),
+          returnValueForMissingStub: null);
+  @override
+  List<_i5.BufferLine> toList() =>
+      (super.noSuchMethod(Invocation.method(#toList, []),
+          returnValue: <_i5.BufferLine>[]) as List<_i5.BufferLine>);
+  @override
+  String toString() => super.toString();
+}
+
+/// A class which mocks [BufferLine].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBufferLine extends _i1.Mock implements _i5.BufferLine {
+  MockBufferLine() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  _i5.BufferLineData get data => (super.noSuchMethod(Invocation.getter(#data),
+      returnValue: _FakeBufferLineData_5()) as _i5.BufferLineData);
+  @override
+  bool get isWrapped =>
+      (super.noSuchMethod(Invocation.getter(#isWrapped), returnValue: false)
+          as bool);
+  @override
+  set isWrapped(bool? value) =>
+      super.noSuchMethod(Invocation.setter(#isWrapped, value),
+          returnValueForMissingStub: null);
+  @override
+  bool get hasCachedSearchString =>
+      (super.noSuchMethod(Invocation.getter(#hasCachedSearchString),
+          returnValue: false) as bool);
+  @override
+  void markTagAsNonDirty(String? tag) =>
+      super.noSuchMethod(Invocation.method(#markTagAsNonDirty, [tag]),
+          returnValueForMissingStub: null);
+  @override
+  bool isTagDirty(String? tag) =>
+      (super.noSuchMethod(Invocation.method(#isTagDirty, [tag]),
+          returnValue: false) as bool);
+  @override
+  void ensure(int? length) =>
+      super.noSuchMethod(Invocation.method(#ensure, [length]),
+          returnValueForMissingStub: null);
+  @override
+  void insert(int? index) =>
+      super.noSuchMethod(Invocation.method(#insert, [index]),
+          returnValueForMissingStub: null);
+  @override
+  void insertN(int? index, int? count) =>
+      super.noSuchMethod(Invocation.method(#insertN, [index, count]),
+          returnValueForMissingStub: null);
+  @override
+  void removeN(int? index, int? count) =>
+      super.noSuchMethod(Invocation.method(#removeN, [index, count]),
+          returnValueForMissingStub: null);
+  @override
+  void clear() => super.noSuchMethod(Invocation.method(#clear, []),
+      returnValueForMissingStub: null);
+  @override
+  void erase(_i9.Cursor? cursor, int? start, int? end,
+          [bool? resetIsWrapped = false]) =>
+      super.noSuchMethod(
+          Invocation.method(#erase, [cursor, start, end, resetIsWrapped]),
+          returnValueForMissingStub: null);
+  @override
+  void cellClear(int? index) =>
+      super.noSuchMethod(Invocation.method(#cellClear, [index]),
+          returnValueForMissingStub: null);
+  @override
+  void cellInitialize(int? index,
+          {int? content, int? width, _i9.Cursor? cursor}) =>
+      super.noSuchMethod(
+          Invocation.method(#cellInitialize, [index],
+              {#content: content, #width: width, #cursor: cursor}),
+          returnValueForMissingStub: null);
+  @override
+  bool cellHasContent(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellHasContent, [index]),
+          returnValue: false) as bool);
+  @override
+  int cellGetContent(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetContent, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetContent(int? index, int? content) =>
+      super.noSuchMethod(Invocation.method(#cellSetContent, [index, content]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetFgColor(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetFgColor, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetFgColor(int? index, int? color) =>
+      super.noSuchMethod(Invocation.method(#cellSetFgColor, [index, color]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetBgColor(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetBgColor, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetBgColor(int? index, int? color) =>
+      super.noSuchMethod(Invocation.method(#cellSetBgColor, [index, color]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetFlags(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetFlags, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetFlags(int? index, int? flags) =>
+      super.noSuchMethod(Invocation.method(#cellSetFlags, [index, flags]),
+          returnValueForMissingStub: null);
+  @override
+  int cellGetWidth(int? index) =>
+      (super.noSuchMethod(Invocation.method(#cellGetWidth, [index]),
+          returnValue: 0) as int);
+  @override
+  void cellSetWidth(int? index, int? width) =>
+      super.noSuchMethod(Invocation.method(#cellSetWidth, [index, width]),
+          returnValueForMissingStub: null);
+  @override
+  void cellClearFlags(int? index) =>
+      super.noSuchMethod(Invocation.method(#cellClearFlags, [index]),
+          returnValueForMissingStub: null);
+  @override
+  bool cellHasFlag(int? index, int? flag) =>
+      (super.noSuchMethod(Invocation.method(#cellHasFlag, [index, flag]),
+          returnValue: false) as bool);
+  @override
+  void cellSetFlag(int? index, int? flag) =>
+      super.noSuchMethod(Invocation.method(#cellSetFlag, [index, flag]),
+          returnValueForMissingStub: null);
+  @override
+  void cellErase(int? index, _i9.Cursor? cursor) =>
+      super.noSuchMethod(Invocation.method(#cellErase, [index, cursor]),
+          returnValueForMissingStub: null);
+  @override
+  int getTrimmedLength([int? cols]) =>
+      (super.noSuchMethod(Invocation.method(#getTrimmedLength, [cols]),
+          returnValue: 0) as int);
+  @override
+  void copyCellsFrom(_i5.BufferLine? src, int? srcCol, int? dstCol, int? len) =>
+      super.noSuchMethod(
+          Invocation.method(#copyCellsFrom, [src, srcCol, dstCol, len]),
+          returnValueForMissingStub: null);
+  @override
+  void removeRange(int? start, int? end) =>
+      super.noSuchMethod(Invocation.method(#removeRange, [start, end]),
+          returnValueForMissingStub: null);
+  @override
+  void clearRange(int? start, int? end) =>
+      super.noSuchMethod(Invocation.method(#clearRange, [start, end]),
+          returnValueForMissingStub: null);
+  @override
+  String toDebugString(int? cols) =>
+      (super.noSuchMethod(Invocation.method(#toDebugString, [cols]),
+          returnValue: '') as String);
+  @override
+  String toSearchString(int? cols) =>
+      (super.noSuchMethod(Invocation.method(#toSearchString, [cols]),
+          returnValue: '') as String);
+  @override
+  String toString() => super.toString();
+}