123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { findFirstInSorted } from '../../../base/common/arrays.js';
- import { RunOnceScheduler, TimeoutTimer } from '../../../base/common/async.js';
- import { DisposableStore, dispose } from '../../../base/common/lifecycle.js';
- import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from '../../common/commands/replaceCommand.js';
- import { Position } from '../../common/core/position.js';
- import { Range } from '../../common/core/range.js';
- import { Selection } from '../../common/core/selection.js';
- import { SearchParams } from '../../common/model/textModelSearch.js';
- import { FindDecorations } from './findDecorations.js';
- import { ReplaceAllCommand } from './replaceAllCommand.js';
- import { parseReplaceString, ReplacePattern } from './replacePattern.js';
- import { RawContextKey } from '../../../platform/contextkey/common/contextkey.js';
- export const CONTEXT_FIND_WIDGET_VISIBLE = new RawContextKey('findWidgetVisible', false);
- export const CONTEXT_FIND_WIDGET_NOT_VISIBLE = CONTEXT_FIND_WIDGET_VISIBLE.toNegated();
- // Keep ContextKey use of 'Focussed' to not break when clauses
- export const CONTEXT_FIND_INPUT_FOCUSED = new RawContextKey('findInputFocussed', false);
- export const CONTEXT_REPLACE_INPUT_FOCUSED = new RawContextKey('replaceInputFocussed', false);
- export const ToggleCaseSensitiveKeybinding = {
- primary: 512 /* Alt */ | 33 /* KeyC */,
- mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 33 /* KeyC */ }
- };
- export const ToggleWholeWordKeybinding = {
- primary: 512 /* Alt */ | 53 /* KeyW */,
- mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 53 /* KeyW */ }
- };
- export const ToggleRegexKeybinding = {
- primary: 512 /* Alt */ | 48 /* KeyR */,
- mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 48 /* KeyR */ }
- };
- export const ToggleSearchScopeKeybinding = {
- primary: 512 /* Alt */ | 42 /* KeyL */,
- mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 42 /* KeyL */ }
- };
- export const TogglePreserveCaseKeybinding = {
- primary: 512 /* Alt */ | 46 /* KeyP */,
- mac: { primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 46 /* KeyP */ }
- };
- export const FIND_IDS = {
- StartFindAction: 'actions.find',
- StartFindWithSelection: 'actions.findWithSelection',
- StartFindWithArgs: 'editor.actions.findWithArgs',
- NextMatchFindAction: 'editor.action.nextMatchFindAction',
- PreviousMatchFindAction: 'editor.action.previousMatchFindAction',
- NextSelectionMatchFindAction: 'editor.action.nextSelectionMatchFindAction',
- PreviousSelectionMatchFindAction: 'editor.action.previousSelectionMatchFindAction',
- StartFindReplaceAction: 'editor.action.startFindReplaceAction',
- CloseFindWidgetCommand: 'closeFindWidget',
- ToggleCaseSensitiveCommand: 'toggleFindCaseSensitive',
- ToggleWholeWordCommand: 'toggleFindWholeWord',
- ToggleRegexCommand: 'toggleFindRegex',
- ToggleSearchScopeCommand: 'toggleFindInSelection',
- TogglePreserveCaseCommand: 'togglePreserveCase',
- ReplaceOneAction: 'editor.action.replaceOne',
- ReplaceAllAction: 'editor.action.replaceAll',
- SelectAllMatchesAction: 'editor.action.selectAllMatches'
- };
- export const MATCHES_LIMIT = 19999;
- const RESEARCH_DELAY = 240;
- export class FindModelBoundToEditorModel {
- constructor(editor, state) {
- this._toDispose = new DisposableStore();
- this._editor = editor;
- this._state = state;
- this._isDisposed = false;
- this._startSearchingTimer = new TimeoutTimer();
- this._decorations = new FindDecorations(editor);
- this._toDispose.add(this._decorations);
- this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100);
- this._toDispose.add(this._updateDecorationsScheduler);
- this._toDispose.add(this._editor.onDidChangeCursorPosition((e) => {
- if (e.reason === 3 /* Explicit */
- || e.reason === 5 /* Undo */
- || e.reason === 6 /* Redo */) {
- this._decorations.setStartPosition(this._editor.getPosition());
- }
- }));
- this._ignoreModelContentChanged = false;
- this._toDispose.add(this._editor.onDidChangeModelContent((e) => {
- if (this._ignoreModelContentChanged) {
- return;
- }
- if (e.isFlush) {
- // a model.setValue() was called
- this._decorations.reset();
- }
- this._decorations.setStartPosition(this._editor.getPosition());
- this._updateDecorationsScheduler.schedule();
- }));
- this._toDispose.add(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
- this.research(false, this._state.searchScope);
- }
- dispose() {
- this._isDisposed = true;
- dispose(this._startSearchingTimer);
- this._toDispose.dispose();
- }
- _onStateChanged(e) {
- if (this._isDisposed) {
- // The find model is disposed during a find state changed event
- return;
- }
- if (!this._editor.hasModel()) {
- // The find model will be disposed momentarily
- return;
- }
- if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) {
- let model = this._editor.getModel();
- if (model.isTooLargeForSyncing()) {
- this._startSearchingTimer.cancel();
- this._startSearchingTimer.setIfNotSet(() => {
- if (e.searchScope) {
- this.research(e.moveCursor, this._state.searchScope);
- }
- else {
- this.research(e.moveCursor);
- }
- }, RESEARCH_DELAY);
- }
- else {
- if (e.searchScope) {
- this.research(e.moveCursor, this._state.searchScope);
- }
- else {
- this.research(e.moveCursor);
- }
- }
- }
- }
- static _getSearchRange(model, findScope) {
- // If we have set now or before a find scope, use it for computing the search range
- if (findScope) {
- return findScope;
- }
- return model.getFullModelRange();
- }
- research(moveCursor, newFindScope) {
- let findScopes = null;
- if (typeof newFindScope !== 'undefined') {
- if (newFindScope !== null) {
- if (!Array.isArray(newFindScope)) {
- findScopes = [newFindScope];
- }
- else {
- findScopes = newFindScope;
- }
- }
- }
- else {
- findScopes = this._decorations.getFindScopes();
- }
- if (findScopes !== null) {
- findScopes = findScopes.map(findScope => {
- if (findScope.startLineNumber !== findScope.endLineNumber) {
- let endLineNumber = findScope.endLineNumber;
- if (findScope.endColumn === 1) {
- endLineNumber = endLineNumber - 1;
- }
- return new Range(findScope.startLineNumber, 1, endLineNumber, this._editor.getModel().getLineMaxColumn(endLineNumber));
- }
- return findScope;
- });
- }
- let findMatches = this._findMatches(findScopes, false, MATCHES_LIMIT);
- this._decorations.set(findMatches, findScopes);
- const editorSelection = this._editor.getSelection();
- let currentMatchesPosition = this._decorations.getCurrentMatchesPosition(editorSelection);
- if (currentMatchesPosition === 0 && findMatches.length > 0) {
- // current selection is not on top of a match
- // try to find its nearest result from the top of the document
- const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.range), range => Range.compareRangesUsingStarts(range, editorSelection) >= 0);
- currentMatchesPosition = matchAfterSelection > 0 ? matchAfterSelection - 1 + 1 /** match position is one based */ : currentMatchesPosition;
- }
- this._state.changeMatchInfo(currentMatchesPosition, this._decorations.getCount(), undefined);
- if (moveCursor && this._editor.getOption(35 /* find */).cursorMoveOnType) {
- this._moveToNextMatch(this._decorations.getStartPosition());
- }
- }
- _hasMatches() {
- return (this._state.matchesCount > 0);
- }
- _cannotFind() {
- if (!this._hasMatches()) {
- let findScope = this._decorations.getFindScope();
- if (findScope) {
- // Reveal the selection so user is reminded that 'selection find' is on.
- this._editor.revealRangeInCenterIfOutsideViewport(findScope, 0 /* Smooth */);
- }
- return true;
- }
- return false;
- }
- _setCurrentFindMatch(match) {
- let matchesPosition = this._decorations.setCurrentFindMatch(match);
- this._state.changeMatchInfo(matchesPosition, this._decorations.getCount(), match);
- this._editor.setSelection(match);
- this._editor.revealRangeInCenterIfOutsideViewport(match, 0 /* Smooth */);
- }
- _prevSearchPosition(before) {
- let isUsingLineStops = this._state.isRegex && (this._state.searchString.indexOf('^') >= 0
- || this._state.searchString.indexOf('$') >= 0);
- let { lineNumber, column } = before;
- let model = this._editor.getModel();
- if (isUsingLineStops || column === 1) {
- if (lineNumber === 1) {
- lineNumber = model.getLineCount();
- }
- else {
- lineNumber--;
- }
- column = model.getLineMaxColumn(lineNumber);
- }
- else {
- column--;
- }
- return new Position(lineNumber, column);
- }
- _moveToPrevMatch(before, isRecursed = false) {
- if (!this._state.canNavigateBack()) {
- // we are beyond the first matched find result
- // instead of doing nothing, we should refocus the first item
- const nextMatchRange = this._decorations.matchAfterPosition(before);
- if (nextMatchRange) {
- this._setCurrentFindMatch(nextMatchRange);
- }
- return;
- }
- if (this._decorations.getCount() < MATCHES_LIMIT) {
- let prevMatchRange = this._decorations.matchBeforePosition(before);
- if (prevMatchRange && prevMatchRange.isEmpty() && prevMatchRange.getStartPosition().equals(before)) {
- before = this._prevSearchPosition(before);
- prevMatchRange = this._decorations.matchBeforePosition(before);
- }
- if (prevMatchRange) {
- this._setCurrentFindMatch(prevMatchRange);
- }
- return;
- }
- if (this._cannotFind()) {
- return;
- }
- let findScope = this._decorations.getFindScope();
- let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
- // ...(----)...|...
- if (searchRange.getEndPosition().isBefore(before)) {
- before = searchRange.getEndPosition();
- }
- // ...|...(----)...
- if (before.isBefore(searchRange.getStartPosition())) {
- before = searchRange.getEndPosition();
- }
- let { lineNumber, column } = before;
- let model = this._editor.getModel();
- let position = new Position(lineNumber, column);
- let prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, false);
- if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) {
- // Looks like we're stuck at this position, unacceptable!
- position = this._prevSearchPosition(position);
- prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, false);
- }
- if (!prevMatch) {
- // there is precisely one match and selection is on top of it
- return;
- }
- if (!isRecursed && !searchRange.containsRange(prevMatch.range)) {
- return this._moveToPrevMatch(prevMatch.range.getStartPosition(), true);
- }
- this._setCurrentFindMatch(prevMatch.range);
- }
- moveToPrevMatch() {
- this._moveToPrevMatch(this._editor.getSelection().getStartPosition());
- }
- _nextSearchPosition(after) {
- let isUsingLineStops = this._state.isRegex && (this._state.searchString.indexOf('^') >= 0
- || this._state.searchString.indexOf('$') >= 0);
- let { lineNumber, column } = after;
- let model = this._editor.getModel();
- if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) {
- if (lineNumber === model.getLineCount()) {
- lineNumber = 1;
- }
- else {
- lineNumber++;
- }
- column = 1;
- }
- else {
- column++;
- }
- return new Position(lineNumber, column);
- }
- _moveToNextMatch(after) {
- if (!this._state.canNavigateForward()) {
- // we are beyond the last matched find result
- // instead of doing nothing, we should refocus the last item
- const prevMatchRange = this._decorations.matchBeforePosition(after);
- if (prevMatchRange) {
- this._setCurrentFindMatch(prevMatchRange);
- }
- return;
- }
- if (this._decorations.getCount() < MATCHES_LIMIT) {
- let nextMatchRange = this._decorations.matchAfterPosition(after);
- if (nextMatchRange && nextMatchRange.isEmpty() && nextMatchRange.getStartPosition().equals(after)) {
- // Looks like we're stuck at this position, unacceptable!
- after = this._nextSearchPosition(after);
- nextMatchRange = this._decorations.matchAfterPosition(after);
- }
- if (nextMatchRange) {
- this._setCurrentFindMatch(nextMatchRange);
- }
- return;
- }
- let nextMatch = this._getNextMatch(after, false, true);
- if (nextMatch) {
- this._setCurrentFindMatch(nextMatch.range);
- }
- }
- _getNextMatch(after, captureMatches, forceMove, isRecursed = false) {
- if (this._cannotFind()) {
- return null;
- }
- let findScope = this._decorations.getFindScope();
- let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), findScope);
- // ...(----)...|...
- if (searchRange.getEndPosition().isBefore(after)) {
- after = searchRange.getStartPosition();
- }
- // ...|...(----)...
- if (after.isBefore(searchRange.getStartPosition())) {
- after = searchRange.getStartPosition();
- }
- let { lineNumber, column } = after;
- let model = this._editor.getModel();
- let position = new Position(lineNumber, column);
- let nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, captureMatches);
- if (forceMove && nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) {
- // Looks like we're stuck at this position, unacceptable!
- position = this._nextSearchPosition(position);
- nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null, captureMatches);
- }
- if (!nextMatch) {
- // there is precisely one match and selection is on top of it
- return null;
- }
- if (!isRecursed && !searchRange.containsRange(nextMatch.range)) {
- return this._getNextMatch(nextMatch.range.getEndPosition(), captureMatches, forceMove, true);
- }
- return nextMatch;
- }
- moveToNextMatch() {
- this._moveToNextMatch(this._editor.getSelection().getEndPosition());
- }
- _getReplacePattern() {
- if (this._state.isRegex) {
- return parseReplaceString(this._state.replaceString);
- }
- return ReplacePattern.fromStaticValue(this._state.replaceString);
- }
- replace() {
- if (!this._hasMatches()) {
- return;
- }
- let replacePattern = this._getReplacePattern();
- let selection = this._editor.getSelection();
- let nextMatch = this._getNextMatch(selection.getStartPosition(), true, false);
- if (nextMatch) {
- if (selection.equalsRange(nextMatch.range)) {
- // selection sits on a find match => replace it!
- let replaceString = replacePattern.buildReplaceString(nextMatch.matches, this._state.preserveCase);
- let command = new ReplaceCommand(selection, replaceString);
- this._executeEditorCommand('replace', command);
- this._decorations.setStartPosition(new Position(selection.startLineNumber, selection.startColumn + replaceString.length));
- this.research(true);
- }
- else {
- this._decorations.setStartPosition(this._editor.getPosition());
- this._setCurrentFindMatch(nextMatch.range);
- }
- }
- }
- _findMatches(findScopes, captureMatches, limitResultCount) {
- const searchRanges = (findScopes || [null]).map((scope) => FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), scope));
- 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);
- }
- replaceAll() {
- if (!this._hasMatches()) {
- return;
- }
- const findScopes = this._decorations.getFindScopes();
- if (findScopes === null && this._state.matchesCount >= MATCHES_LIMIT) {
- // Doing a replace on the entire file that is over ${MATCHES_LIMIT} matches
- this._largeReplaceAll();
- }
- else {
- this._regularReplaceAll(findScopes);
- }
- this.research(false);
- }
- _largeReplaceAll() {
- const searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(116 /* wordSeparators */) : null);
- const searchData = searchParams.parseSearchRequest();
- if (!searchData) {
- return;
- }
- let searchRegex = searchData.regex;
- if (!searchRegex.multiline) {
- let mod = 'mu';
- if (searchRegex.ignoreCase) {
- mod += 'i';
- }
- if (searchRegex.global) {
- mod += 'g';
- }
- searchRegex = new RegExp(searchRegex.source, mod);
- }
- const model = this._editor.getModel();
- const modelText = model.getValue(1 /* LF */);
- const fullModelRange = model.getFullModelRange();
- const replacePattern = this._getReplacePattern();
- let resultText;
- const preserveCase = this._state.preserveCase;
- if (replacePattern.hasReplacementPatterns || preserveCase) {
- resultText = modelText.replace(searchRegex, function () {
- return replacePattern.buildReplaceString(arguments, preserveCase);
- });
- }
- else {
- resultText = modelText.replace(searchRegex, replacePattern.buildReplaceString(null, preserveCase));
- }
- let command = new ReplaceCommandThatPreservesSelection(fullModelRange, resultText, this._editor.getSelection());
- this._executeEditorCommand('replaceAll', command);
- }
- _regularReplaceAll(findScopes) {
- const replacePattern = this._getReplacePattern();
- // Get all the ranges (even more than the highlighted ones)
- let matches = this._findMatches(findScopes, replacePattern.hasReplacementPatterns || this._state.preserveCase, 1073741824 /* MAX_SAFE_SMALL_INTEGER */);
- let replaceStrings = [];
- for (let i = 0, len = matches.length; i < len; i++) {
- replaceStrings[i] = replacePattern.buildReplaceString(matches[i].matches, this._state.preserveCase);
- }
- let command = new ReplaceAllCommand(this._editor.getSelection(), matches.map(m => m.range), replaceStrings);
- this._executeEditorCommand('replaceAll', command);
- }
- selectAllMatches() {
- if (!this._hasMatches()) {
- return;
- }
- let findScopes = this._decorations.getFindScopes();
- // Get all the ranges (even more than the highlighted ones)
- let matches = this._findMatches(findScopes, false, 1073741824 /* MAX_SAFE_SMALL_INTEGER */);
- let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn));
- // If one of the ranges is the editor selection, then maintain it as primary
- let editorSelection = this._editor.getSelection();
- for (let i = 0, len = selections.length; i < len; i++) {
- let sel = selections[i];
- if (sel.equalsRange(editorSelection)) {
- selections = [editorSelection].concat(selections.slice(0, i)).concat(selections.slice(i + 1));
- break;
- }
- }
- this._editor.setSelections(selections);
- }
- _executeEditorCommand(source, command) {
- try {
- this._ignoreModelContentChanged = true;
- this._editor.pushUndoStop();
- this._editor.executeCommand(source, command);
- this._editor.pushUndoStop();
- }
- finally {
- this._ignoreModelContentChanged = false;
- }
- }
- }
|