findModel.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { findFirstInSorted } from '../../../base/common/arrays.js';
  6. import { RunOnceScheduler, TimeoutTimer } from '../../../base/common/async.js';
  7. import { DisposableStore, dispose } from '../../../base/common/lifecycle.js';
  8. import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from '../../common/commands/replaceCommand.js';
  9. import { Position } from '../../common/core/position.js';
  10. import { Range } from '../../common/core/range.js';
  11. import { Selection } from '../../common/core/selection.js';
  12. import { SearchParams } from '../../common/model/textModelSearch.js';
  13. import { FindDecorations } from './findDecorations.js';
  14. import { ReplaceAllCommand } from './replaceAllCommand.js';
  15. import { parseReplaceString, ReplacePattern } from './replacePattern.js';
  16. import { RawContextKey } from '../../../platform/contextkey/common/contextkey.js';
  17. export const CONTEXT_FIND_WIDGET_VISIBLE = new RawContextKey('findWidgetVisible', false);
  18. export const CONTEXT_FIND_WIDGET_NOT_VISIBLE = CONTEXT_FIND_WIDGET_VISIBLE.toNegated();
  19. // Keep ContextKey use of 'Focussed' to not break when clauses
  20. export const CONTEXT_FIND_INPUT_FOCUSED = new RawContextKey('findInputFocussed', false);
  21. export const CONTEXT_REPLACE_INPUT_FOCUSED = new RawContextKey('replaceInputFocussed', false);
  22. export const ToggleCaseSensitiveKeybinding = {
  23. primary: 512 /* Alt */ | 33 /* KeyC */,
  24. mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 33 /* KeyC */ }
  25. };
  26. export const ToggleWholeWordKeybinding = {
  27. primary: 512 /* Alt */ | 53 /* KeyW */,
  28. mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 53 /* KeyW */ }
  29. };
  30. export const ToggleRegexKeybinding = {
  31. primary: 512 /* Alt */ | 48 /* KeyR */,
  32. mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 48 /* KeyR */ }
  33. };
  34. export const ToggleSearchScopeKeybinding = {
  35. primary: 512 /* Alt */ | 42 /* KeyL */,
  36. mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 42 /* KeyL */ }
  37. };
  38. export const TogglePreserveCaseKeybinding = {
  39. primary: 512 /* Alt */ | 46 /* KeyP */,
  40. mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 46 /* KeyP */ }
  41. };
  42. export const FIND_IDS = {
  43. StartFindAction: 'actions.find',
  44. StartFindWithSelection: 'actions.findWithSelection',
  45. StartFindWithArgs: 'editor.actions.findWithArgs',
  46. NextMatchFindAction: 'editor.action.nextMatchFindAction',
  47. PreviousMatchFindAction: 'editor.action.previousMatchFindAction',
  48. NextSelectionMatchFindAction: 'editor.action.nextSelectionMatchFindAction',
  49. PreviousSelectionMatchFindAction: 'editor.action.previousSelectionMatchFindAction',
  50. StartFindReplaceAction: 'editor.action.startFindReplaceAction',
  51. CloseFindWidgetCommand: 'closeFindWidget',
  52. ToggleCaseSensitiveCommand: 'toggleFindCaseSensitive',
  53. ToggleWholeWordCommand: 'toggleFindWholeWord',
  54. ToggleRegexCommand: 'toggleFindRegex',
  55. ToggleSearchScopeCommand: 'toggleFindInSelection',
  56. TogglePreserveCaseCommand: 'togglePreserveCase',
  57. ReplaceOneAction: 'editor.action.replaceOne',
  58. ReplaceAllAction: 'editor.action.replaceAll',
  59. SelectAllMatchesAction: 'editor.action.selectAllMatches'
  60. };
  61. export const MATCHES_LIMIT = 19999;
  62. const RESEARCH_DELAY = 240;
  63. export class FindModelBoundToEditorModel {
  64. constructor(editor, state) {
  65. this._toDispose = new DisposableStore();
  66. this._editor = editor;
  67. this._state = state;
  68. this._isDisposed = false;
  69. this._startSearchingTimer = new TimeoutTimer();
  70. this._decorations = new FindDecorations(editor);
  71. this._toDispose.add(this._decorations);
  72. this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100);
  73. this._toDispose.add(this._updateDecorationsScheduler);
  74. this._toDispose.add(this._editor.onDidChangeCursorPosition((e) => {
  75. if (e.reason === 3 /* Explicit */
  76. || e.reason === 5 /* Undo */
  77. || e.reason === 6 /* Redo */) {
  78. this._decorations.setStartPosition(this._editor.getPosition());
  79. }
  80. }));
  81. this._ignoreModelContentChanged = false;
  82. this._toDispose.add(this._editor.onDidChangeModelContent((e) => {
  83. if (this._ignoreModelContentChanged) {
  84. return;
  85. }
  86. if (e.isFlush) {
  87. // a model.setValue() was called
  88. this._decorations.reset();
  89. }
  90. this._decorations.setStartPosition(this._editor.getPosition());
  91. this._updateDecorationsScheduler.schedule();
  92. }));
  93. this._toDispose.add(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
  94. this.research(false, this._state.searchScope);
  95. }
  96. dispose() {
  97. this._isDisposed = true;
  98. dispose(this._startSearchingTimer);
  99. this._toDispose.dispose();
  100. }
  101. _onStateChanged(e) {
  102. if (this._isDisposed) {
  103. // The find model is disposed during a find state changed event
  104. return;
  105. }
  106. if (!this._editor.hasModel()) {
  107. // The find model will be disposed momentarily
  108. return;
  109. }
  110. if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) {
  111. let model = this._editor.getModel();
  112. if (model.isTooLargeForSyncing()) {
  113. this._startSearchingTimer.cancel();
  114. this._startSearchingTimer.setIfNotSet(() => {
  115. if (e.searchScope) {
  116. this.research(e.moveCursor, this._state.searchScope);
  117. }
  118. else {
  119. this.research(e.moveCursor);
  120. }
  121. }, RESEARCH_DELAY);
  122. }
  123. else {
  124. if (e.searchScope) {
  125. this.research(e.moveCursor, this._state.searchScope);
  126. }
  127. else {
  128. this.research(e.moveCursor);
  129. }
  130. }
  131. }
  132. }
  133. static _getSearchRange(model, findScope) {
  134. // If we have set now or before a find scope, use it for computing the search range
  135. if (findScope) {
  136. return findScope;
  137. }
  138. return model.getFullModelRange();
  139. }
  140. research(moveCursor, newFindScope) {
  141. let findScopes = null;
  142. if (typeof newFindScope !== 'undefined') {
  143. if (newFindScope !== null) {
  144. if (!Array.isArray(newFindScope)) {
  145. findScopes = [newFindScope];
  146. }
  147. else {
  148. findScopes = newFindScope;
  149. }
  150. }
  151. }
  152. else {
  153. findScopes = this._decorations.getFindScopes();
  154. }
  155. if (findScopes !== null) {
  156. findScopes = findScopes.map(findScope => {
  157. if (findScope.startLineNumber !== findScope.endLineNumber) {
  158. let endLineNumber = findScope.endLineNumber;
  159. if (findScope.endColumn === 1) {
  160. endLineNumber = endLineNumber - 1;
  161. }
  162. return new Range(findScope.startLineNumber, 1, endLineNumber, this._editor.getModel().getLineMaxColumn(endLineNumber));
  163. }
  164. return findScope;
  165. });
  166. }
  167. let findMatches = this._findMatches(findScopes, false, MATCHES_LIMIT);
  168. this._decorations.set(findMatches, findScopes);
  169. const editorSelection = this._editor.getSelection();
  170. let currentMatchesPosition = this._decorations.getCurrentMatchesPosition(editorSelection);
  171. if (currentMatchesPosition === 0 && findMatches.length > 0) {
  172. // current selection is not on top of a match
  173. // try to find its nearest result from the top of the document
  174. const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.range), range => Range.compareRangesUsingStarts(range, editorSelection) >= 0);
  175. currentMatchesPosition = matchAfterSelection > 0 ? matchAfterSelection - 1 + 1 /** match position is one based */ : currentMatchesPosition;
  176. }
  177. this._state.changeMatchInfo(currentMatchesPosition, this._decorations.getCount(), undefined);
  178. if (moveCursor && this._editor.getOption(35 /* find */).cursorMoveOnType) {
  179. this._moveToNextMatch(this._decorations.getStartPosition());
  180. }
  181. }
  182. _hasMatches() {
  183. return (this._state.matchesCount > 0);
  184. }
  185. _cannotFind() {
  186. if (!this._hasMatches()) {
  187. let findScope = this._decorations.getFindScope();
  188. if (findScope) {
  189. // Reveal the selection so user is reminded that 'selection find' is on.
  190. this._editor.revealRangeInCenterIfOutsideViewport(findScope, 0 /* Smooth */);
  191. }
  192. return true;
  193. }
  194. return false;
  195. }
  196. _setCurrentFindMatch(match) {
  197. let matchesPosition = this._decorations.setCurrentFindMatch(match);
  198. this._state.changeMatchInfo(matchesPosition, this._decorations.getCount(), match);
  199. this._editor.setSelection(match);
  200. this._editor.revealRangeInCenterIfOutsideViewport(match, 0 /* Smooth */);
  201. }
  202. _prevSearchPosition(before) {
  203. let isUsingLineStops = this._state.isRegex && (this._state.searchString.indexOf('^') >= 0
  204. || this._state.searchString.indexOf('$') >= 0);
  205. let { lineNumber, column } = before;
  206. let model = this._editor.getModel();
  207. if (isUsingLineStops || column === 1) {
  208. if (lineNumber === 1) {
  209. lineNumber = model.getLineCount();
  210. }
  211. else {
  212. lineNumber--;
  213. }
  214. column = model.getLineMaxColumn(lineNumber);
  215. }
  216. else {
  217. column--;
  218. }
  219. return new Position(lineNumber, column);
  220. }
  221. _moveToPrevMatch(before, isRecursed = false) {
  222. if (!this._state.canNavigateBack()) {
  223. // we are beyond the first matched find result
  224. // instead of doing nothing, we should refocus the first item
  225. const nextMatchRange = this._decorations.matchAfterPosition(before);
  226. if (nextMatchRange) {
  227. this._setCurrentFindMatch(nextMatchRange);
  228. }
  229. return;
  230. }
  231. if (this._decorations.getCount() < MATCHES_LIMIT) {
  232. let prevMatchRange = this._decorations.matchBeforePosition(before);
  233. if (prevMatchRange && prevMatchRange.isEmpty() && prevMatchRange.getStartPosition().equals(before)) {
  234. before = this._prevSearchPosition(before);
  235. prevMatchRange = this._decorations.matchBeforePosition(before);
  236. }
  237. if (prevMatchRange) {
  238. this._setCurrentFindMatch(prevMatchRange);
  239. }
  240. return;
  241. }
  242. if (this._cannotFind()) {
  243. return;
  244. }
  245. let findScope = this._decorations.getFindScope();
  246. let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
  247. // ...(----)...|...
  248. if (searchRange.getEndPosition().isBefore(before)) {
  249. before = searchRange.getEndPosition();
  250. }
  251. // ...|...(----)...
  252. if (before.isBefore(searchRange.getStartPosition())) {
  253. before = searchRange.getEndPosition();
  254. }
  255. let { lineNumber, column } = before;
  256. let model = this._editor.getModel();
  257. let position = new Position(lineNumber, column);
  258. let prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, false);
  259. if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) {
  260. // Looks like we're stuck at this position, unacceptable!
  261. position = this._prevSearchPosition(position);
  262. prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, false);
  263. }
  264. if (!prevMatch) {
  265. // there is precisely one match and selection is on top of it
  266. return;
  267. }
  268. if (!isRecursed && !searchRange.containsRange(prevMatch.range)) {
  269. return this._moveToPrevMatch(prevMatch.range.getStartPosition(), true);
  270. }
  271. this._setCurrentFindMatch(prevMatch.range);
  272. }
  273. moveToPrevMatch() {
  274. this._moveToPrevMatch(this._editor.getSelection().getStartPosition());
  275. }
  276. _nextSearchPosition(after) {
  277. let isUsingLineStops = this._state.isRegex && (this._state.searchString.indexOf('^') >= 0
  278. || this._state.searchString.indexOf('$') >= 0);
  279. let { lineNumber, column } = after;
  280. let model = this._editor.getModel();
  281. if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) {
  282. if (lineNumber === model.getLineCount()) {
  283. lineNumber = 1;
  284. }
  285. else {
  286. lineNumber++;
  287. }
  288. column = 1;
  289. }
  290. else {
  291. column++;
  292. }
  293. return new Position(lineNumber, column);
  294. }
  295. _moveToNextMatch(after) {
  296. if (!this._state.canNavigateForward()) {
  297. // we are beyond the last matched find result
  298. // instead of doing nothing, we should refocus the last item
  299. const prevMatchRange = this._decorations.matchBeforePosition(after);
  300. if (prevMatchRange) {
  301. this._setCurrentFindMatch(prevMatchRange);
  302. }
  303. return;
  304. }
  305. if (this._decorations.getCount() < MATCHES_LIMIT) {
  306. let nextMatchRange = this._decorations.matchAfterPosition(after);
  307. if (nextMatchRange && nextMatchRange.isEmpty() && nextMatchRange.getStartPosition().equals(after)) {
  308. // Looks like we're stuck at this position, unacceptable!
  309. after = this._nextSearchPosition(after);
  310. nextMatchRange = this._decorations.matchAfterPosition(after);
  311. }
  312. if (nextMatchRange) {
  313. this._setCurrentFindMatch(nextMatchRange);
  314. }
  315. return;
  316. }
  317. let nextMatch = this._getNextMatch(after, false, true);
  318. if (nextMatch) {
  319. this._setCurrentFindMatch(nextMatch.range);
  320. }
  321. }
  322. _getNextMatch(after, captureMatches, forceMove, isRecursed = false) {
  323. if (this._cannotFind()) {
  324. return null;
  325. }
  326. let findScope = this._decorations.getFindScope();
  327. let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
  328. // ...(----)...|...
  329. if (searchRange.getEndPosition().isBefore(after)) {
  330. after = searchRange.getStartPosition();
  331. }
  332. // ...|...(----)...
  333. if (after.isBefore(searchRange.getStartPosition())) {
  334. after = searchRange.getStartPosition();
  335. }
  336. let { lineNumber, column } = after;
  337. let model = this._editor.getModel();
  338. let position = new Position(lineNumber, column);
  339. let nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, captureMatches);
  340. if (forceMove && nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) {
  341. // Looks like we're stuck at this position, unacceptable!
  342. position = this._nextSearchPosition(position);
  343. nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, captureMatches);
  344. }
  345. if (!nextMatch) {
  346. // there is precisely one match and selection is on top of it
  347. return null;
  348. }
  349. if (!isRecursed && !searchRange.containsRange(nextMatch.range)) {
  350. return this._getNextMatch(nextMatch.range.getEndPosition(), captureMatches, forceMove, true);
  351. }
  352. return nextMatch;
  353. }
  354. moveToNextMatch() {
  355. this._moveToNextMatch(this._editor.getSelection().getEndPosition());
  356. }
  357. _getReplacePattern() {
  358. if (this._state.isRegex) {
  359. return parseReplaceString(this._state.replaceString);
  360. }
  361. return ReplacePattern.fromStaticValue(this._state.replaceString);
  362. }
  363. replace() {
  364. if (!this._hasMatches()) {
  365. return;
  366. }
  367. let replacePattern = this._getReplacePattern();
  368. let selection = this._editor.getSelection();
  369. let nextMatch = this._getNextMatch(selection.getStartPosition(), true, false);
  370. if (nextMatch) {
  371. if (selection.equalsRange(nextMatch.range)) {
  372. // selection sits on a find match => replace it!
  373. let replaceString = replacePattern.buildReplaceString(nextMatch.matches, this._state.preserveCase);
  374. let command = new ReplaceCommand(selection, replaceString);
  375. this._executeEditorCommand('replace', command);
  376. this._decorations.setStartPosition(new Position(selection.startLineNumber, selection.startColumn + replaceString.length));
  377. this.research(true);
  378. }
  379. else {
  380. this._decorations.setStartPosition(this._editor.getPosition());
  381. this._setCurrentFindMatch(nextMatch.range);
  382. }
  383. }
  384. }
  385. _findMatches(findScopes, captureMatches, limitResultCount) {
  386. const searchRanges = (findScopes || [null]).map((scope) => FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), scope));
  387. return this._editor.getModel().findMatches(this._state.searchString, searchRanges, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, captureMatches, limitResultCount);
  388. }
  389. replaceAll() {
  390. if (!this._hasMatches()) {
  391. return;
  392. }
  393. const findScopes = this._decorations.getFindScopes();
  394. if (findScopes === null && this._state.matchesCount >= MATCHES_LIMIT) {
  395. // Doing a replace on the entire file that is over ${MATCHES_LIMIT} matches
  396. this._largeReplaceAll();
  397. }
  398. else {
  399. this._regularReplaceAll(findScopes);
  400. }
  401. this.research(false);
  402. }
  403. _largeReplaceAll() {
  404. const searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null);
  405. const searchData = searchParams.parseSearchRequest();
  406. if (!searchData) {
  407. return;
  408. }
  409. let searchRegex = searchData.regex;
  410. if (!searchRegex.multiline) {
  411. let mod = 'mu';
  412. if (searchRegex.ignoreCase) {
  413. mod += 'i';
  414. }
  415. if (searchRegex.global) {
  416. mod += 'g';
  417. }
  418. searchRegex = new RegExp(searchRegex.source, mod);
  419. }
  420. const model = this._editor.getModel();
  421. const modelText = model.getValue(1 /* LF */);
  422. const fullModelRange = model.getFullModelRange();
  423. const replacePattern = this._getReplacePattern();
  424. let resultText;
  425. const preserveCase = this._state.preserveCase;
  426. if (replacePattern.hasReplacementPatterns || preserveCase) {
  427. resultText = modelText.replace(searchRegex, function () {
  428. return replacePattern.buildReplaceString(arguments, preserveCase);
  429. });
  430. }
  431. else {
  432. resultText = modelText.replace(searchRegex, replacePattern.buildReplaceString(null, preserveCase));
  433. }
  434. let command = new ReplaceCommandThatPreservesSelection(fullModelRange, resultText, this._editor.getSelection());
  435. this._executeEditorCommand('replaceAll', command);
  436. }
  437. _regularReplaceAll(findScopes) {
  438. const replacePattern = this._getReplacePattern();
  439. // Get all the ranges (even more than the highlighted ones)
  440. let matches = this._findMatches(findScopes, replacePattern.hasReplacementPatterns || this._state.preserveCase, 1073741824 /* MAX_SAFE_SMALL_INTEGER */);
  441. let replaceStrings = [];
  442. for (let i = 0, len = matches.length; i < len; i++) {
  443. replaceStrings[i] = replacePattern.buildReplaceString(matches[i].matches, this._state.preserveCase);
  444. }
  445. let command = new ReplaceAllCommand(this._editor.getSelection(), matches.map(m => m.range), replaceStrings);
  446. this._executeEditorCommand('replaceAll', command);
  447. }
  448. selectAllMatches() {
  449. if (!this._hasMatches()) {
  450. return;
  451. }
  452. let findScopes = this._decorations.getFindScopes();
  453. // Get all the ranges (even more than the highlighted ones)
  454. let matches = this._findMatches(findScopes, false, 1073741824 /* MAX_SAFE_SMALL_INTEGER */);
  455. let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn));
  456. // If one of the ranges is the editor selection, then maintain it as primary
  457. let editorSelection = this._editor.getSelection();
  458. for (let i = 0, len = selections.length; i < len; i++) {
  459. let sel = selections[i];
  460. if (sel.equalsRange(editorSelection)) {
  461. selections = [editorSelection].concat(selections.slice(0, i)).concat(selections.slice(i + 1));
  462. break;
  463. }
  464. }
  465. this._editor.setSelections(selections);
  466. }
  467. _executeEditorCommand(source, command) {
  468. try {
  469. this._ignoreModelContentChanged = true;
  470. this._editor.pushUndoStop();
  471. this._editor.executeCommand(source, command);
  472. this._editor.pushUndoStop();
  473. }
  474. finally {
  475. this._ignoreModelContentChanged = false;
  476. }
  477. }
  478. }