terminal_search.dart 6.0 KB

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