terminal_search.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import 'package:xterm/buffer/line/line.dart';
  2. import 'package:xterm/terminal/terminal.dart';
  3. class TerminalSearchResult {
  4. final _hitsByLine = Map<int, List<TerminalSearchHit>>();
  5. late final _allHits;
  6. TerminalSearchResult.fromHits(List<TerminalSearchHit> hits) {
  7. _allHits = hits;
  8. for (final hit in hits) {
  9. if (!_hitsByLine.containsKey(hit.startLineIndex)) {
  10. _hitsByLine[hit.startLineIndex] =
  11. List<TerminalSearchHit>.empty(growable: true);
  12. }
  13. if (!_hitsByLine.containsKey(hit.endLineIndex)) {
  14. _hitsByLine[hit.endLineIndex] =
  15. List<TerminalSearchHit>.empty(growable: true);
  16. }
  17. _hitsByLine[hit.startLineIndex]!.add(hit);
  18. if (hit.startLineIndex != hit.endLineIndex) {
  19. _hitsByLine[hit.endLineIndex]!.add(hit);
  20. }
  21. }
  22. }
  23. TerminalSearchResult.empty();
  24. List<TerminalSearchHit> get allHits => _allHits;
  25. bool hasEntriesForLine(int line) {
  26. return _hitsByLine.containsKey(line);
  27. }
  28. List<TerminalSearchHit> getEntriesForLine(int line) {
  29. return _hitsByLine[line] ?? List<TerminalSearchHit>.empty(growable: false);
  30. }
  31. bool contains(int line, int col) {
  32. return _hitsByLine[line]?.any((hit) => hit.contains(line, col)) ?? false;
  33. }
  34. }
  35. class TerminalSearchHit {
  36. TerminalSearchHit(
  37. this.startLineIndex, this.startIndex, this.endLineIndex, this.endIndex);
  38. final int startLineIndex;
  39. final int startIndex;
  40. final int endLineIndex;
  41. final int endIndex;
  42. bool contains(int line, int col) {
  43. if (line < startLineIndex || line > endLineIndex) {
  44. return false;
  45. }
  46. if (line == startLineIndex && startLineIndex == endLineIndex) {
  47. return col >= startIndex && col < endIndex;
  48. }
  49. if (line == startLineIndex) {
  50. return col >= startIndex;
  51. }
  52. if (line == endLineIndex) {
  53. return col < endIndex;
  54. }
  55. // here we are sure that the given point is inside a full line match
  56. return true;
  57. }
  58. }
  59. class TerminalSearchTask {
  60. TerminalSearchTask(this._search, this._terminal, this._dirtyTagName);
  61. final TerminalSearch _search;
  62. final Terminal _terminal;
  63. String? _pattern = null;
  64. bool _isPatternDirty = true;
  65. RegExp? _searchRegexp = null;
  66. final String _dirtyTagName;
  67. bool? _hasBeenUsingAltBuffer;
  68. TerminalSearchResult? _lastSearchResult = null;
  69. bool _isAnyLineDirty() {
  70. final bufferLength = _terminal.buffer.lines.length;
  71. for (var i = 0; i < bufferLength; i++) {
  72. if (_terminal.buffer.lines[i].isTagDirty(_dirtyTagName)) {
  73. return true;
  74. }
  75. }
  76. return false;
  77. }
  78. void _markLinesForSearchDone() {
  79. final bufferLength = _terminal.buffer.lines.length;
  80. for (var i = 0; i < bufferLength; i++) {
  81. _terminal.buffer.lines[i].markTagAsNonDirty(_dirtyTagName);
  82. }
  83. }
  84. bool _isTerminalStateDirty() {
  85. if (_isAnyLineDirty()) {
  86. return true;
  87. }
  88. if (_hasBeenUsingAltBuffer != null &&
  89. _hasBeenUsingAltBuffer! != _terminal.isUsingAltBuffer()) {
  90. return true;
  91. }
  92. return false;
  93. }
  94. bool get _isDirty {
  95. if (_isPatternDirty) {
  96. return true;
  97. }
  98. return _isTerminalStateDirty();
  99. }
  100. String? get pattern => _pattern;
  101. void set pattern(String? newPattern) {
  102. if (newPattern != _pattern) {
  103. _pattern = newPattern;
  104. _isPatternDirty = true;
  105. _searchRegexp = null;
  106. }
  107. }
  108. TerminalSearchResult get searchResult {
  109. if (_pattern == null) {
  110. return TerminalSearchResult.empty();
  111. }
  112. if (_lastSearchResult != null && !_isDirty) {
  113. return _lastSearchResult!;
  114. }
  115. final terminalWidth = _terminal.terminalWidth;
  116. //TODO: make caseSensitive an option
  117. if (_searchRegexp == null) {
  118. _searchRegexp = RegExp(_pattern!, caseSensitive: false, multiLine: false);
  119. }
  120. final hits = List<TerminalSearchHit>.empty(growable: true);
  121. for (final match
  122. in _searchRegexp!.allMatches(_search.terminalSearchString)) {
  123. final startLineIndex = (match.start / terminalWidth).floor();
  124. final endLineIndex = (match.end / terminalWidth).floor();
  125. // subtract the lines that got added in order to get the index inside the line
  126. final startIndex = match.start - startLineIndex * terminalWidth;
  127. final endIndex = match.end - endLineIndex * terminalWidth;
  128. hits.add(
  129. TerminalSearchHit(
  130. startLineIndex,
  131. startIndex,
  132. endLineIndex,
  133. endIndex,
  134. ),
  135. );
  136. }
  137. _markLinesForSearchDone();
  138. _isPatternDirty = false;
  139. _lastSearchResult = TerminalSearchResult.fromHits(hits);
  140. _hasBeenUsingAltBuffer = _terminal.isUsingAltBuffer();
  141. return _lastSearchResult!;
  142. }
  143. }
  144. class TerminalSearch {
  145. TerminalSearch(this._terminal);
  146. final Terminal _terminal;
  147. String? _cachedSearchString;
  148. int? _lastTerminalWidth;
  149. TerminalSearchTask createSearchTask(String dirtyTagName) {
  150. return TerminalSearchTask(this, _terminal, dirtyTagName);
  151. }
  152. String get terminalSearchString {
  153. final bufferLength = _terminal.buffer.lines.length;
  154. final terminalWidth = _terminal.terminalWidth;
  155. var isAnySearchStringInvalid = false;
  156. for (var i = 0; i < bufferLength; i++) {
  157. if (!_terminal.buffer.lines[i].hasCachedSearchString) {
  158. isAnySearchStringInvalid = true;
  159. }
  160. }
  161. late String completeSearchString;
  162. if (_cachedSearchString != null &&
  163. _lastTerminalWidth != null &&
  164. _lastTerminalWidth! == terminalWidth &&
  165. !isAnySearchStringInvalid) {
  166. completeSearchString = _cachedSearchString!;
  167. } else {
  168. final bufferContent = StringBuffer();
  169. for (var i = 0; i < bufferLength; i++) {
  170. final BufferLine line = _terminal.buffer.lines[i];
  171. final searchString = line.toSearchString(terminalWidth);
  172. bufferContent.write(searchString);
  173. if (searchString.length < terminalWidth) {
  174. // fill up so that the row / col can be mapped back later on
  175. bufferContent.writeAll(
  176. List<String>.filled(terminalWidth - searchString.length, ' '));
  177. }
  178. }
  179. completeSearchString = bufferContent.toString();
  180. _cachedSearchString = completeSearchString;
  181. _lastTerminalWidth = terminalWidth;
  182. }
  183. return completeSearchString;
  184. }
  185. }