Quellcode durchsuchen

First Search implementation

Currently the search pattern is hard coded ("test") and the visual representation is not done yet (currently just red foreground color)
devmil vor 4 Jahren
Ursprung
Commit
38be8eaa17

+ 8 - 0
lib/buffer/line/line.dart

@@ -17,6 +17,13 @@ class BufferLine {
   }
 
   late BufferLineData _data;
+  var _isSearchDirty = true;
+
+  bool get isSearchDirty => _isSearchDirty;
+
+  void markSearchDone() {
+    _isSearchDirty = false;
+  }
 
   BufferLineData get data => _data;
 
@@ -134,6 +141,7 @@ class BufferLine {
 
   void _invalidateCaches() {
     _searchStringCache = null;
+    _isSearchDirty = true;
   }
 
   String? _searchStringCache;

+ 11 - 2
lib/frontend/terminal_painters.dart

@@ -124,6 +124,7 @@ class TerminalPainter extends CustomPainter {
 
   void _paintText(Canvas canvas) {
     final lines = terminal.getVisibleLines();
+    final searchResult = terminal.searchHits;
 
     for (var row = 0; row < lines.length; row++) {
       final line = lines[row];
@@ -139,7 +140,10 @@ class TerminalPainter extends CustomPainter {
         }
 
         final offsetX = col * charSize.cellWidth;
-        _paintCell(canvas, line, col, offsetX, offsetY);
+        final absoluteY = terminal.convertViewLineToRawLine(row) -
+            terminal.scrollOffsetFromBottom;
+        _paintCell(canvas, line, col, offsetX, offsetY,
+            searchResult.contains(absoluteY, col));
       }
     }
   }
@@ -150,9 +154,10 @@ class TerminalPainter extends CustomPainter {
     int cell,
     double offsetX,
     double offsetY,
+    bool isInSearchResult,
   ) {
     final codePoint = line.cellGetContent(cell);
-    final fgColor = line.cellGetFgColor(cell);
+    var fgColor = line.cellGetFgColor(cell);
     final bgColor = line.cellGetBgColor(cell);
     final flags = line.cellGetFlags(cell);
 
@@ -160,6 +165,10 @@ class TerminalPainter extends CustomPainter {
       return;
     }
 
+    if (isInSearchResult) {
+      fgColor = Color.fromARGB(255, 255, 0, 0).value;
+    }
+
     // final cellHash = line.cellGetHash(cell);
     final cellHash = hashValues(codePoint, fgColor, bgColor, flags);
 

+ 21 - 5
lib/terminal/terminal.dart

@@ -17,6 +17,7 @@ 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_ui_interaction.dart';
 import 'package:xterm/theme/terminal_color.dart';
 import 'package:xterm/theme/terminal_theme.dart';
@@ -43,6 +44,11 @@ class Terminal with Observable implements TerminalUiInteraction {
     this.theme = TerminalThemes.defaultTheme,
     required int maxLines,
   }) : _maxLines = maxLines {
+    _search = TerminalSearch(
+      this,
+      (line) => line.markSearchDone(),
+      (line) => line.isSearchDirty,
+    );
     backend?.init();
     backend?.exitCode.then((value) {
       _isTerminated = true;
@@ -62,6 +68,8 @@ class Terminal with Observable implements TerminalUiInteraction {
     tabs.reset();
   }
 
+  late TerminalSearch _search;
+
   bool _dirty = false;
   @override
   bool get dirty {
@@ -97,10 +105,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;
 
@@ -597,8 +606,6 @@ class Terminal with Observable implements TerminalUiInteraction {
     backend?.write(data);
   }
 
-  void selectWord(int x, int y) {}
-
   int get _tabIndexFromCursor {
     var index = buffer.cursorX;
 
@@ -733,4 +740,13 @@ class Terminal with Observable implements TerminalUiInteraction {
     _composingString = value;
     refresh();
   }
+
+  String? _searchPattern = "test";
+
+  TerminalSearchResult get searchHits {
+    if (_searchPattern == null) {
+      return TerminalSearchResult.empty();
+    }
+    return _search.doSearch(_searchPattern!);
+  }
 }

+ 27 - 19
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';
@@ -128,25 +129,25 @@ void terminalMain(SendPort port) async {
         }
         if (_terminal.dirty) {
           final newState = TerminalState(
-            _terminal.scrollOffsetFromBottom,
-            _terminal.scrollOffsetFromTop,
-            _terminal.buffer.height,
-            _terminal.invisibleHeight,
-            _terminal.viewHeight,
-            _terminal.viewWidth,
-            _terminal.selection!,
-            _terminal.getSelectedText(),
-            _terminal.theme.background,
-            _terminal.cursorX,
-            _terminal.cursorY,
-            _terminal.showCursor,
-            _terminal.theme.cursor,
-            _terminal
-                .getVisibleLines()
-                .map((bl) => BufferLine.withDataFrom(bl))
-                .toList(growable: false),
-            _terminal.composingString,
-          );
+              _terminal.scrollOffsetFromBottom,
+              _terminal.scrollOffsetFromTop,
+              _terminal.buffer.height,
+              _terminal.invisibleHeight,
+              _terminal.viewHeight,
+              _terminal.viewWidth,
+              _terminal.selection!,
+              _terminal.getSelectedText(),
+              _terminal.theme.background,
+              _terminal.cursorX,
+              _terminal.cursorY,
+              _terminal.showCursor,
+              _terminal.theme.cursor,
+              _terminal
+                  .getVisibleLines()
+                  .map((bl) => BufferLine.withDataFrom(bl))
+                  .toList(growable: false),
+              _terminal.composingString,
+              _terminal.searchHits);
           port.send([_IsolateEvent.newState, newState]);
           _needNotify = true;
         }
@@ -205,6 +206,8 @@ class TerminalState {
 
   String composingString;
 
+  TerminalSearchResult searchResult;
+
   TerminalState(
     this.scrollOffsetFromBottom,
     this.scrollOffsetFromTop,
@@ -221,6 +224,7 @@ class TerminalState {
     this.cursorColor,
     this.visibleLines,
     this.composingString,
+    this.searchResult,
   );
 }
 
@@ -529,4 +533,8 @@ class TerminalIsolate with Observable implements TerminalUiInteraction {
   void updateComposingString(String value) {
     _sendPort?.send([_IsolateCommand.updateComposingString, value]);
   }
+
+  @override
+  TerminalSearchResult get searchHits =>
+      _lastState?.searchResult ?? TerminalSearchResult.empty();
 }

+ 147 - 0
lib/terminal/terminal_search.dart

@@ -0,0 +1,147 @@
+import 'package:xterm/buffer/line/line.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+class TerminalSearchResult {
+  final _hitsByLine = Map<int, List<TerminalSearchHit>>();
+
+  TerminalSearchResult.fromHits(List<TerminalSearchHit> hits) {
+    for (final hit in hits) {
+      if (!_hitsByLine.containsKey(hit.startLineIndex)) {
+        _hitsByLine[hit.startLineIndex] =
+            List<TerminalSearchHit>.empty(growable: true);
+      }
+      if (!_hitsByLine.containsKey(hit.endLineIndex)) {
+        _hitsByLine[hit.endLineIndex] =
+            List<TerminalSearchHit>.empty(growable: true);
+      }
+      _hitsByLine[hit.startLineIndex]!.add(hit);
+      if (hit.startLineIndex != hit.endLineIndex) {
+        _hitsByLine[hit.endLineIndex]!.add(hit);
+      }
+    }
+  }
+
+  TerminalSearchResult.empty();
+
+  bool hasEntriesForLine(int line) {
+    return _hitsByLine.containsKey(line);
+  }
+
+  List<TerminalSearchHit> getEntriesForLine(int line) {
+    return _hitsByLine[line] ?? List<TerminalSearchHit>.empty(growable: false);
+  }
+
+  bool contains(int line, int col) {
+    return _hitsByLine[line]?.any((hit) => hit.contains(line, col)) ?? false;
+  }
+}
+
+class TerminalSearchHit {
+  TerminalSearchHit(
+      this.startLineIndex, this.startIndex, this.endLineIndex, this.endIndex);
+
+  final int startLineIndex;
+  final int startIndex;
+  final int endLineIndex;
+  final int endIndex;
+
+  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;
+  }
+}
+
+typedef MarkSearchDoneFunc = void Function(BufferLine line);
+typedef IsSearchDirtyFunc = bool Function(BufferLine line);
+
+class TerminalSearch {
+  TerminalSearch(
+      this._terminal, this._markSearchDoneFunc, this._isSearchDirtyFunc);
+
+  final Terminal _terminal;
+  final MarkSearchDoneFunc _markSearchDoneFunc;
+  final IsSearchDirtyFunc _isSearchDirtyFunc;
+
+  String? _lastSearchPattern = null;
+  TerminalSearchResult? _lastSearchResult = null;
+  bool? _hasBeenUsingAltBuffer;
+
+  TerminalSearchResult doSearch(String searchPattern) {
+    final bufferLength = _terminal.buffer.lines.length;
+    final terminalWidth = _terminal.terminalWidth;
+
+    var isSearchDirty = false;
+    //check if the search is dirty and return if not
+    if (_lastSearchPattern != null &&
+        _lastSearchPattern == searchPattern &&
+        _lastSearchResult != null &&
+        _hasBeenUsingAltBuffer != null &&
+        _hasBeenUsingAltBuffer! == _terminal.isUsingAltBuffer()) {
+      for (var i = 0; i < bufferLength; i++) {
+        if (_isSearchDirtyFunc(_terminal.buffer.lines[i])) {
+          isSearchDirty = true;
+          break;
+        }
+      }
+    } else {
+      isSearchDirty = true;
+    }
+
+    if (!isSearchDirty) {
+      return _lastSearchResult!;
+    }
+
+    //TODO: make caseSensitive an option
+    final searchRegex =
+        RegExp(searchPattern, caseSensitive: false, multiLine: false);
+
+    final result = List<TerminalSearchHit>.empty(growable: true);
+
+    final bufferContent = StringBuffer();
+    for (var i = 0; i < bufferLength; i++) {
+      final BufferLine line = _terminal.buffer.lines[i];
+      final searchString = line.toSearchString(terminalWidth);
+      _markSearchDoneFunc(line);
+      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, ' '));
+      }
+    }
+
+    for (final match in searchRegex.allMatches(bufferContent.toString())) {
+      final startLineIndex = (match.start / terminalWidth).floor();
+      final endLineIndex = (match.end / terminalWidth).floor();
+
+      // subtract the lines that got added in order to get the index inside the line
+      final startIndex = match.start - startLineIndex * terminalWidth;
+      final endIndex = match.end - endLineIndex * terminalWidth;
+
+      result.add(
+        TerminalSearchHit(
+          startLineIndex,
+          startIndex,
+          endLineIndex,
+          endIndex,
+        ),
+      );
+    }
+    _lastSearchPattern = searchPattern;
+    _lastSearchResult = TerminalSearchResult.fromHits(result);
+    _hasBeenUsingAltBuffer = _terminal.isUsingAltBuffer();
+    return _lastSearchResult!;
+  }
+}

+ 4 - 0
lib/terminal/terminal_ui_interaction.dart

@@ -3,6 +3,7 @@ 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/util/observable.dart';
 
 /// this interface describes what a Terminal UI needs from a Terminal
@@ -129,4 +130,7 @@ 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 searchHits;
 }