未验证 提交 e57b91ce 编写于 作者: G Gustavo Cassel 提交者: GitHub

Developed setting to loop search from the beginning or end of document in Find Widget (#92243)

* Improved documentation of 'title' property on OpenDialogOptions and SaveDialogOptions

* Developed setting which controls whether the search automatically restarts from the beginning (or the end) when no further matches can be found

* Removed needless condition to update Find Widget buttons

* Delete launch.json

File has been unintentionally committed.
Co-authored-by: NGustavo Cassel <GustavoASC@users.noreply.github.com>
Co-authored-by: NPeng Lyu <penlv@microsoft.com>
上级 32929fbb
......@@ -1253,6 +1253,10 @@ export interface IEditorFindOptions {
* Controls if the Find Widget should read or modify the shared find clipboard on macOS
*/
globalFindClipboard?: boolean;
/**
* Controls whether the search automatically restarts from the beginning (or the end) when no further matches can be found
*/
loop?: boolean;
}
export type EditorFindOptions = Readonly<Required<IEditorFindOptions>>;
......@@ -1264,7 +1268,8 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
seedSearchStringFromSelection: true,
autoFindInSelection: 'never',
globalFindClipboard: false,
addExtraSpaceOnTop: true
addExtraSpaceOnTop: true,
loop: true
};
super(
EditorOption.find, 'find', defaults,
......@@ -1295,7 +1300,13 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
type: 'boolean',
default: defaults.addExtraSpaceOnTop,
description: nls.localize('find.addExtraSpaceOnTop', "Controls whether the Find Widget should add extra lines on top of the editor. When true, you can scroll beyond the first line when the Find Widget is visible.")
}
},
'editor.find.loop': {
type: 'boolean',
default: defaults.loop,
description: nls.localize('find.loop', "Controls whether the search automatically restarts from the beginning (or the end) when no further matches can be found.")
},
}
);
}
......@@ -1311,7 +1322,8 @@ class EditorFind extends BaseEditorOption<EditorOption.find, EditorFindOptions>
? (_input.autoFindInSelection ? 'always' : 'never')
: EditorStringEnumOption.stringSet<'never' | 'always' | 'multiline'>(input.autoFindInSelection, this.defaultValue.autoFindInSelection, ['never', 'always', 'multiline']),
globalFindClipboard: EditorBooleanOption.boolean(input.globalFindClipboard, this.defaultValue.globalFindClipboard),
addExtraSpaceOnTop: EditorBooleanOption.boolean(input.addExtraSpaceOnTop, this.defaultValue.addExtraSpaceOnTop)
addExtraSpaceOnTop: EditorBooleanOption.boolean(input.addExtraSpaceOnTop, this.defaultValue.addExtraSpaceOnTop),
loop: EditorBooleanOption.boolean(input.loop, this.defaultValue.loop),
};
}
}
......
......@@ -67,6 +67,7 @@ export interface IFindStartOptions {
shouldFocus: FindStartFocusAction;
shouldAnimate: boolean;
updateSearchScope: boolean;
loop: boolean;
}
export class CommonFindController extends Disposable implements IEditorContribution {
......@@ -126,7 +127,8 @@ export class CommonFindController extends Disposable implements IEditorContribut
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: false,
updateSearchScope: false
updateSearchScope: false,
loop: this._editor.getOption(EditorOption.find).loop
});
}
}));
......@@ -297,6 +299,8 @@ export class CommonFindController extends Disposable implements IEditorContribut
}
}
stateChanges.loop = opts.loop;
this._state.change(stateChanges, false);
if (!this._model) {
......@@ -475,7 +479,8 @@ export class StartFindAction extends EditorAction {
seedSearchStringFromGlobalClipboard: editor.getOption(EditorOption.find).globalFindClipboard,
shouldFocus: FindStartFocusAction.FocusFindInput,
shouldAnimate: true,
updateSearchScope: false
updateSearchScope: false,
loop: editor.getOption(EditorOption.find).loop
});
}
}
......@@ -509,7 +514,8 @@ export class StartFindWithSelectionAction extends EditorAction {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: true,
updateSearchScope: false
updateSearchScope: false,
loop: editor.getOption(EditorOption.find).loop
});
controller.setGlobalBufferTerm(controller.getState().searchString);
......@@ -526,7 +532,8 @@ export abstract class MatchFindAction extends EditorAction {
seedSearchStringFromGlobalClipboard: true,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: true,
updateSearchScope: false
updateSearchScope: false,
loop: editor.getOption(EditorOption.find).loop
});
this._run(controller);
}
......@@ -638,7 +645,8 @@ export abstract class SelectionMatchFindAction extends EditorAction {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: true,
updateSearchScope: false
updateSearchScope: false,
loop: editor.getOption(EditorOption.find).loop
});
this._run(controller);
}
......@@ -743,7 +751,8 @@ export class StartFindReplaceAction extends EditorAction {
seedSearchStringFromGlobalClipboard: editor.getOption(EditorOption.find).seedSearchStringFromSelection,
shouldFocus: shouldFocus,
shouldAnimate: true,
updateSearchScope: false
updateSearchScope: false,
loop: editor.getOption(EditorOption.find).loop
});
}
}
......
......@@ -86,7 +86,8 @@ export class FindDecorations implements IDisposable {
return this._getDecorationIndex(candidate.id);
}
}
return 1;
// We don't know the current match position, so returns zero to show '?' in find widget
return 0;
}
public setCurrentFindMatch(nextMatch: Range | null): number {
......
......@@ -251,6 +251,9 @@ export class FindModelBoundToEditorModel {
}
private _moveToPrevMatch(before: Position, isRecursed: boolean = false): void {
if (!this._state.canNavigateBack()) {
return;
}
if (this._decorations.getCount() < MATCHES_LIMIT) {
let prevMatchRange = this._decorations.matchBeforePosition(before);
......@@ -336,6 +339,9 @@ export class FindModelBoundToEditorModel {
}
private _moveToNextMatch(after: Position): void {
if (!this._state.canNavigateForward()) {
return;
}
if (this._decorations.getCount() < MATCHES_LIMIT) {
let nextMatchRange = this._decorations.matchAfterPosition(after);
......
......@@ -6,6 +6,7 @@
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { MATCHES_LIMIT } from './findModel';
export interface FindReplaceStateChangedEvent {
moveCursor: boolean;
......@@ -23,6 +24,7 @@ export interface FindReplaceStateChangedEvent {
matchesPosition: boolean;
matchesCount: boolean;
currentMatch: boolean;
loop: boolean;
}
export const enum FindOptionOverride {
......@@ -45,6 +47,7 @@ export interface INewFindReplaceState {
preserveCase?: boolean;
preserveCaseOverride?: FindOptionOverride;
searchScope?: Range | null;
loop?: boolean;
}
function effectiveOptionValue(override: FindOptionOverride, value: boolean): boolean {
......@@ -74,6 +77,7 @@ export class FindReplaceState extends Disposable {
private _matchesPosition: number;
private _matchesCount: number;
private _currentMatch: Range | null;
private _loop: boolean;
private readonly _onFindReplaceStateChange = this._register(new Emitter<FindReplaceStateChangedEvent>());
public get searchString(): string { return this._searchString; }
......@@ -114,6 +118,7 @@ export class FindReplaceState extends Disposable {
this._matchesPosition = 0;
this._matchesCount = 0;
this._currentMatch = null;
this._loop = true;
}
public changeMatchInfo(matchesPosition: number, matchesCount: number, currentMatch: Range | undefined): void {
......@@ -131,7 +136,8 @@ export class FindReplaceState extends Disposable {
searchScope: false,
matchesPosition: false,
matchesCount: false,
currentMatch: false
currentMatch: false,
loop: false
};
let somethingChanged = false;
......@@ -181,7 +187,8 @@ export class FindReplaceState extends Disposable {
searchScope: false,
matchesPosition: false,
matchesCount: false,
currentMatch: false
currentMatch: false,
loop: false
};
let somethingChanged = false;
......@@ -237,7 +244,13 @@ export class FindReplaceState extends Disposable {
somethingChanged = true;
}
}
if (typeof newState.loop !== 'undefined') {
if (this._loop !== newState.loop) {
this._loop = newState.loop;
changeEvent.loop = true;
somethingChanged = true;
}
}
// Overrides get set when they explicitly come in and get reset anytime something else changes
this._isRegexOverride = (typeof newState.isRegexOverride !== 'undefined' ? newState.isRegexOverride : FindOptionOverride.NotSet);
this._wholeWordOverride = (typeof newState.wholeWordOverride !== 'undefined' ? newState.wholeWordOverride : FindOptionOverride.NotSet);
......@@ -266,4 +279,17 @@ export class FindReplaceState extends Disposable {
this._onFindReplaceStateChange.fire(changeEvent);
}
}
public canNavigateBack(): boolean {
return this.canNavigateInLoop() || (this.matchesPosition !== 1);
}
public canNavigateForward(): boolean {
return this.canNavigateInLoop() || (this.matchesPosition < this.matchesCount);
}
private canNavigateInLoop(): boolean {
return this._loop || (this.matchesCount >= MATCHES_LIMIT);
}
}
......@@ -363,6 +363,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
if (e.updateHistory) {
this._delayedUpdateHistory();
}
if (e.loop) {
this._updateButtons();
}
}
private _delayedUpdateHistory() {
......@@ -458,8 +461,8 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas
let findInputIsNonEmpty = (this._state.searchString.length > 0);
let matchesCount = this._state.matchesCount ? true : false;
this._prevBtn.setEnabled(this._isVisible && findInputIsNonEmpty && matchesCount);
this._nextBtn.setEnabled(this._isVisible && findInputIsNonEmpty && matchesCount);
this._prevBtn.setEnabled(this._isVisible && findInputIsNonEmpty && matchesCount && this._state.canNavigateBack());
this._nextBtn.setEnabled(this._isVisible && findInputIsNonEmpty && matchesCount && this._state.canNavigateForward());
this._replaceBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);
this._replaceAllBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty);
......
......@@ -276,7 +276,8 @@ suite('FindController', () => {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.FocusFindInput,
shouldAnimate: false,
updateSearchScope: false
updateSearchScope: false,
loop: true
});
nextMatchFindAction.run(null, editor);
startFindReplaceAction.run(null, editor);
......@@ -301,7 +302,8 @@ suite('FindController', () => {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: false,
updateSearchScope: false
updateSearchScope: false,
loop: true
});
assert.equal(findController.getState().searchScope, null);
......@@ -530,7 +532,8 @@ suite('FindController query options persistence', () => {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: false,
updateSearchScope: true
updateSearchScope: true,
loop: true
});
assert.deepEqual(findController.getState().searchScope, new Selection(1, 1, 2, 1));
......@@ -553,7 +556,8 @@ suite('FindController query options persistence', () => {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: false,
updateSearchScope: true
updateSearchScope: true,
loop: true
});
assert.deepEqual(findController.getState().searchScope, null);
......@@ -576,7 +580,8 @@ suite('FindController query options persistence', () => {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: false,
updateSearchScope: true
updateSearchScope: true,
loop: true
});
assert.deepEqual(findController.getState().searchScope, new Selection(1, 2, 1, 3));
......@@ -600,7 +605,8 @@ suite('FindController query options persistence', () => {
seedSearchStringFromGlobalClipboard: false,
shouldFocus: FindStartFocusAction.NoFocusChange,
shouldAnimate: false,
updateSearchScope: true
updateSearchScope: true,
loop: true
});
assert.deepEqual(findController.getState().searchScope, new Selection(1, 6, 2, 1));
......
......@@ -2089,4 +2089,165 @@ suite('FindModel', () => {
findModel.dispose();
findState.dispose();
});
findTest('issue #3516: Control behavior of "Next" operations (not looping back to beginning)', (editor, cursor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello', loop: false }, false);
let findModel = new FindModelBoundToEditorModel(editor, findState);
assert.equal(findState.matchesCount, 5);
// Test next operations
assert.equal(findState.matchesPosition, 0);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), false);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 2);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 3);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 4);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 5);
assert.equal(findState.canNavigateForward(), false);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 5);
assert.equal(findState.canNavigateForward(), false);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 5);
assert.equal(findState.canNavigateForward(), false);
assert.equal(findState.canNavigateBack(), true);
// Test previous operations
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 4);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 3);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 2);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), false);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), false);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), false);
});
findTest('issue #3516: Control behavior of "Next" operations (looping back to beginning)', (editor, cursor) => {
let findState = new FindReplaceState();
findState.change({ searchString: 'hello' }, false);
let findModel = new FindModelBoundToEditorModel(editor, findState);
assert.equal(findState.matchesCount, 5);
// Test next operations
assert.equal(findState.matchesPosition, 0);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 2);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 3);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 4);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 5);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToNextMatch();
assert.equal(findState.matchesPosition, 2);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
// Test previous operations
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 5);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 4);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 3);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 2);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
findModel.moveToPrevMatch();
assert.equal(findState.matchesPosition, 1);
assert.equal(findState.canNavigateForward(), true);
assert.equal(findState.canNavigateBack(), true);
});
});
......@@ -3275,6 +3275,10 @@ declare namespace monaco.editor {
*/
autoFindInSelection?: 'never' | 'always' | 'multiline';
addExtraSpaceOnTop?: boolean;
/**
* Controls whether the search automatically restarts from the beginning (or the end) when no further matches can be found
*/
loop?: boolean;
}
export type EditorFindOptions = Readonly<Required<IEditorFindOptions>>;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册