SQL Operations Studio Public Preview 1 (0.23) release source code
55
src/vs/editor/contrib/find/browser/find.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { FindWidget, IFindController } from 'vs/editor/contrib/find/browser/findWidget';
|
||||
import { FindOptionsWidget } from 'vs/editor/contrib/find/browser/findOptionsWidget';
|
||||
import { CommonFindController, FindStartFocusAction, IFindStartOptions } from 'vs/editor/contrib/find/common/findController';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
@editorContribution
|
||||
export class FindController extends CommonFindController implements IFindController {
|
||||
|
||||
private _widget: FindWidget;
|
||||
private _findOptionsWidget: FindOptionsWidget;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(editor, contextKeyService, storageService);
|
||||
|
||||
this._widget = this._register(new FindWidget(editor, this, this._state, contextViewService, keybindingService, contextKeyService, themeService));
|
||||
this._findOptionsWidget = this._register(new FindOptionsWidget(editor, this._state, keybindingService, themeService));
|
||||
}
|
||||
|
||||
protected _start(opts: IFindStartOptions): void {
|
||||
super._start(opts);
|
||||
|
||||
if (opts.shouldFocus === FindStartFocusAction.FocusReplaceInput) {
|
||||
this._widget.focusReplaceInput();
|
||||
} else if (opts.shouldFocus === FindStartFocusAction.FocusFindInput) {
|
||||
this._widget.focusFindInput();
|
||||
}
|
||||
}
|
||||
|
||||
public highlightFindOptions(): void {
|
||||
if (this._state.isRevealed) {
|
||||
this._widget.highlightFindOptions();
|
||||
} else {
|
||||
this._findOptionsWidget.highlightFindOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
207
src/vs/editor/contrib/find/browser/findOptionsWidget.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { FIND_IDS } from 'vs/editor/contrib/find/common/findModel';
|
||||
import { FindReplaceState } from 'vs/editor/contrib/find/common/findState';
|
||||
import { CaseSensitiveCheckbox, WholeWordsCheckbox, RegexCheckbox } from 'vs/base/browser/ui/findinput/findInputCheckboxes';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IThemeService, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { inputActiveOptionBorder, editorWidgetBackground, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
export class FindOptionsWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
private static ID = 'editor.contrib.findOptionsWidget';
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _state: FindReplaceState;
|
||||
private _keybindingService: IKeybindingService;
|
||||
|
||||
private _domNode: HTMLElement;
|
||||
private regex: RegexCheckbox;
|
||||
private wholeWords: WholeWordsCheckbox;
|
||||
private caseSensitive: CaseSensitiveCheckbox;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
state: FindReplaceState,
|
||||
keybindingService: IKeybindingService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editor = editor;
|
||||
this._state = state;
|
||||
this._keybindingService = keybindingService;
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'findOptionsWidget';
|
||||
this._domNode.style.display = 'none';
|
||||
this._domNode.style.top = '10px';
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
let inputActiveOptionBorderColor = themeService.getTheme().getColor(inputActiveOptionBorder);
|
||||
|
||||
this.caseSensitive = this._register(new CaseSensitiveCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand),
|
||||
isChecked: this._state.matchCase,
|
||||
onChange: (viaKeyboard) => {
|
||||
this._state.change({
|
||||
matchCase: this.caseSensitive.checked
|
||||
}, false);
|
||||
},
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor
|
||||
}));
|
||||
this._domNode.appendChild(this.caseSensitive.domNode);
|
||||
|
||||
this.wholeWords = this._register(new WholeWordsCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand),
|
||||
isChecked: this._state.wholeWord,
|
||||
onChange: (viaKeyboard) => {
|
||||
this._state.change({
|
||||
wholeWord: this.wholeWords.checked
|
||||
}, false);
|
||||
},
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor
|
||||
}));
|
||||
this._domNode.appendChild(this.wholeWords.domNode);
|
||||
|
||||
this.regex = this._register(new RegexCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand),
|
||||
isChecked: this._state.isRegex,
|
||||
onChange: (viaKeyboard) => {
|
||||
this._state.change({
|
||||
isRegex: this.regex.checked
|
||||
}, false);
|
||||
},
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor
|
||||
}));
|
||||
this._domNode.appendChild(this.regex.domNode);
|
||||
|
||||
this._editor.addOverlayWidget(this);
|
||||
|
||||
this._register(this._state.addChangeListener((e) => {
|
||||
let somethingChanged = false;
|
||||
if (e.isRegex) {
|
||||
this.regex.checked = this._state.isRegex;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (e.wholeWord) {
|
||||
this.wholeWords.checked = this._state.wholeWord;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (e.matchCase) {
|
||||
this.caseSensitive.checked = this._state.matchCase;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (!this._state.isRevealed && somethingChanged) {
|
||||
this._revealTemporarily();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(dom.addDisposableNonBubblingMouseOutListener(this._domNode, (e) => this._onMouseOut()));
|
||||
this._register(dom.addDisposableListener(this._domNode, 'mouseover', (e) => this._onMouseOver()));
|
||||
|
||||
this._applyTheme(themeService.getTheme());
|
||||
this._register(themeService.onThemeChange(this._applyTheme.bind(this)));
|
||||
}
|
||||
|
||||
private _keybindingLabelFor(actionId: string): string {
|
||||
let kb = this._keybindingService.lookupKeybinding(actionId);
|
||||
if (!kb) {
|
||||
return '';
|
||||
}
|
||||
return ` (${kb.getLabel()})`;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.removeOverlayWidget(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ----- IOverlayWidget API
|
||||
|
||||
public getId(): string {
|
||||
return FindOptionsWidget.ID;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public getPosition(): IOverlayWidgetPosition {
|
||||
return {
|
||||
preference: OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
|
||||
};
|
||||
}
|
||||
|
||||
public highlightFindOptions(): void {
|
||||
this._revealTemporarily();
|
||||
}
|
||||
|
||||
private _hideSoon = this._register(new RunOnceScheduler(() => this._hide(), 1000));
|
||||
|
||||
private _revealTemporarily(): void {
|
||||
this._show();
|
||||
this._hideSoon.schedule();
|
||||
}
|
||||
|
||||
private _onMouseOut(): void {
|
||||
this._hideSoon.schedule();
|
||||
}
|
||||
|
||||
private _onMouseOver(): void {
|
||||
this._hideSoon.cancel();
|
||||
}
|
||||
|
||||
private _isVisible: boolean = false;
|
||||
|
||||
private _show(): void {
|
||||
if (this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = true;
|
||||
this._domNode.style.display = 'block';
|
||||
}
|
||||
|
||||
private _hide(): void {
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = false;
|
||||
this._domNode.style.display = 'none';
|
||||
}
|
||||
|
||||
private _applyTheme(theme: ITheme) {
|
||||
let inputStyles = { inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder) };
|
||||
this.caseSensitive.style(inputStyles);
|
||||
this.wholeWords.style(inputStyles);
|
||||
this.regex.style(inputStyles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let widgetBackground = theme.getColor(editorWidgetBackground);
|
||||
if (widgetBackground) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { background-color: ${widgetBackground}; }`);
|
||||
}
|
||||
|
||||
let widgetShadowColor = theme.getColor(widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { box-shadow: 0 2px 8px ${widgetShadowColor}; }`);
|
||||
}
|
||||
|
||||
let hcBorder = theme.getColor(contrastBorder);
|
||||
if (hcBorder) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { border: 2px solid ${hcBorder}; }`);
|
||||
}
|
||||
});
|
||||
356
src/vs/editor/contrib/find/browser/findWidget.css
Normal file
@@ -0,0 +1,356 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Checkbox */
|
||||
|
||||
.monaco-checkbox .label {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid black;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-checkbox .checkbox {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
/* Find widget */
|
||||
.monaco-editor .find-widget {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: -44px; /* find input height + shadow (10px) */
|
||||
height: 34px; /* find input height */
|
||||
overflow: hidden;
|
||||
line-height: 19px;
|
||||
|
||||
-webkit-transition: top 200ms linear;
|
||||
-o-transition: top 200ms linear;
|
||||
-moz-transition: top 200ms linear;
|
||||
-ms-transition: top 200ms linear;
|
||||
transition: top 200ms linear;
|
||||
|
||||
padding: 0 4px;
|
||||
}
|
||||
/* Find widget when replace is toggled on */
|
||||
.monaco-editor .find-widget.replaceToggled {
|
||||
top: -74px; /* find input height + replace input height + shadow (10px) */
|
||||
height: 64px; /* find input height + replace input height */
|
||||
}
|
||||
.monaco-editor .find-widget.replaceToggled > .replace-part {
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.visible,
|
||||
.monaco-editor .find-widget.replaceToggled.visible {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-inputbox .input {
|
||||
background-color: transparent;
|
||||
/* Style to compensate for //winjs */
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .replace-input .input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.visible.noanimation {
|
||||
-webkit-transition: none;
|
||||
-o-transition: none;
|
||||
-moz-transition: none;
|
||||
-ms-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part,
|
||||
.monaco-editor .find-widget > .replace-part {
|
||||
margin: 4px 0 0 17px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox,
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input {
|
||||
width: 100% !important;
|
||||
padding-right: 66px;
|
||||
}
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input,
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-findInput {
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .matchesCount {
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
flex: initial;
|
||||
margin: 0 1px 0 3px;
|
||||
padding: 2px 2px 0 2px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
flex: initial;
|
||||
margin-left: 3px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button:not(.disabled):hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.left {
|
||||
margin-left: 0;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.wide {
|
||||
width: auto;
|
||||
padding: 1px 6px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.toggle.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .previous {
|
||||
background-image: url('images/previous.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .next {
|
||||
background-image: url('images/next.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .label {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
background-image: url('images/cancelSelectionFind.svg');
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:disabled + .label {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:not(:disabled) + .label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:not(:disabled):hover:before + .label {
|
||||
background-color: #DDD;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: rgba(100, 100, 100, 0.2);
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .close-fw {
|
||||
background-image: url('images/close.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .expand {
|
||||
background-image: url('images/expando-expanded.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .collapse {
|
||||
background-image: url('images/expando-collapsed.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .replace {
|
||||
background-image: url('images/replace.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .replace-all {
|
||||
background-image: url('images/replace-all.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part > .replace-input {
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
vertical-align: middle;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* REDUCED */
|
||||
.monaco-editor .find-widget.reduced-find-widget .matchesCount,
|
||||
.monaco-editor .find-widget.reduced-find-widget .monaco-checkbox {
|
||||
display:none;
|
||||
}
|
||||
|
||||
/* NARROW (SMALLER THAN REDUCED) */
|
||||
.monaco-editor .find-widget.narrow-find-widget {
|
||||
max-width: 257px !important;
|
||||
}
|
||||
|
||||
/* COLLAPSED (SMALLER THAN NARROW) */
|
||||
.monaco-editor .find-widget.collapsed-find-widget {
|
||||
max-width: 111px !important;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.previous,
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.next,
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.replace,
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.replace-all,
|
||||
.monaco-editor .find-widget.collapsed-find-widget > .find-part .monaco-findInput .controls {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.collapsed-find-widget > .find-part .monaco-inputbox > .wrapper > .input {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.monaco-editor .findMatch {
|
||||
-webkit-animation-duration: 0;
|
||||
-webkit-animation-name: inherit !important;
|
||||
-moz-animation-duration: 0;
|
||||
-moz-animation-name: inherit !important;
|
||||
-ms-animation-duration: 0;
|
||||
-ms-animation-name: inherit !important;
|
||||
animation-duration: 0;
|
||||
animation-name: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-sash {
|
||||
width: 2px !important;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .previous,
|
||||
.monaco-editor.vs-dark .find-widget .previous {
|
||||
background-image: url('images/previous-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .next,
|
||||
.monaco-editor.vs-dark .find-widget .next {
|
||||
background-image: url('images/next-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .monaco-checkbox .label,
|
||||
.monaco-editor.vs-dark .find-widget .monaco-checkbox .label {
|
||||
background-image: url('images/cancelSelectionFind-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .find-widget .monaco-checkbox .checkbox:not(:disabled):hover:before + .label {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .find-widget .monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .close-fw,
|
||||
.monaco-editor.vs-dark .find-widget .close-fw {
|
||||
background-image: url('images/close-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .replace,
|
||||
.monaco-editor.vs-dark .find-widget .replace {
|
||||
background-image: url('images/replace-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .replace-all,
|
||||
.monaco-editor.vs-dark .find-widget .replace-all {
|
||||
background-image: url('images/replace-all-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .expand,
|
||||
.monaco-editor.vs-dark .find-widget .expand {
|
||||
background-image: url('images/expando-expanded-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .collapse,
|
||||
.monaco-editor.vs-dark .find-widget .collapse {
|
||||
background-image: url('images/expando-collapsed-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .button:not(.disabled):hover,
|
||||
.monaco-editor.vs-dark .find-widget .button:not(.disabled):hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .button:before {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
1079
src/vs/editor/contrib/find/browser/findWidget.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<g transform="translate(0,-1032.3622)">
|
||||
<rect width="9" height="2" x="2" y="1046.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
<rect width="13" height="2" x="2" y="1043.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
<rect width="6" height="2" x="2" y="1040.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
<rect width="12" height="2" x="2" y="1037.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<g transform="translate(0,-1032.3622)">
|
||||
<rect width="9" height="2" x="2" y="1046.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
<rect width="13" height="2" x="2" y="1043.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
<rect width="6" height="2" x="2" y="1040.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
<rect width="12" height="2" x="2" y="1037.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
1
src/vs/editor/contrib/find/browser/images/close-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
1
src/vs/editor/contrib/find/browser/images/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#424242" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
|
||||
|
After Width: | Height: | Size: 151 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
|
||||
|
After Width: | Height: | Size: 151 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
|
||||
|
After Width: | Height: | Size: 131 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
|
||||
|
After Width: | Height: | Size: 131 B |
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<path fill="#C5C5C5" d="M1,4h7L5,1h3l4,4L8,9H5l3-3H1V4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
5
src/vs/editor/contrib/find/browser/images/next.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<path fill="#424242" d="M1,4h7L5,1h3l4,4L8,9H5l3-3H1V4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<polygon fill="#C5C5C5" points="13,4 6,4 9,1 6,1 2,5 6,9 9,9 6,6 13,6 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 305 B |
5
src/vs/editor/contrib/find/browser/images/previous.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<polygon fill="#424242" points="13,4 6,4 9,1 6,1 2,5 6,9 9,9 6,6 13,6 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 305 B |
@@ -0,0 +1,11 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<path fill="#C5C5C5" d="M11,15V9H1v6H11z M2,14v-2h1v-1H2v-1h3v4H2z M10,11H8v2h2v1H7v-4h3V11z M3,13v-1h1v1H3z M13,7v6h-1V8H5V7
|
||||
H13z M13,2V1h-1v5h3V2H13z M14,5h-1V3h1V5z M11,2v4H8V4h1v1h1V4H9V3H8V2H11z"/>
|
||||
</g>
|
||||
<g id="color_x5F_action">
|
||||
<path fill="#75BEFF" d="M1.979,3.5L2,6L1,5v1.5L2.5,8L4,6.5V5L3,6L2.979,3.5c0-0.275,0.225-0.5,0.5-0.5H7V2H3.479
|
||||
C2.651,2,1.979,2.673,1.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
11
src/vs/editor/contrib/find/browser/images/replace-all.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<path fill="#424242" d="M11,15V9H1v6H11z M2,14v-2h1v-1H2v-1h3v4H2z M10,11H8v2h2v1H7v-4h3V11z M3,13v-1h1v1H3z M13,7v6h-1V8H5V7
|
||||
H13z M13,2V1h-1v5h3V2H13z M14,5h-1V3h1V5z M11,2v4H8V4h1v1h1V4H9V3H8V2H11z"/>
|
||||
</g>
|
||||
<g id="color_x5F_action">
|
||||
<path fill="#00539C" d="M1.979,3.5L2,6L1,5v1.5L2.5,8L4,6.5V5L3,6L2.979,3.5c0-0.275,0.225-0.5,0.5-0.5H7V2H3.479
|
||||
C2.651,2,1.979,2.673,1.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
@@ -0,0 +1,13 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<g>
|
||||
<path fill="#C5C5C5" d="M11,3V1h-1v5v1h1h2h1V4V3H11z M13,6h-2V4h2V6z"/>
|
||||
<path fill="#C5C5C5" d="M2,15h7V9H2V15z M4,10h3v1H5v2h2v1H4V10z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="color_x5F_importance">
|
||||
<path fill="#75BEFF" d="M3.979,3.5L4,6L3,5v1.5L4.5,8L6,6.5V5L5,6L4.979,3.5c0-0.275,0.225-0.5,0.5-0.5H9V2H5.479
|
||||
C4.651,2,3.979,2.673,3.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
13
src/vs/editor/contrib/find/browser/images/replace.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<g>
|
||||
<path fill="#424242" d="M11,3V1h-1v5v1h1h2h1V4V3H11z M13,6h-2V4h2V6z"/>
|
||||
<path fill="#424242" d="M2,15h7V9H2V15z M4,10h3v1H5v2h2v1H4V10z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="color_x5F_importance">
|
||||
<path fill="#00539C" d="M3.979,3.5L4,6L3,5v1.5L4.5,8L6,6.5V5L5,6L4.979,3.5c0-0.275,0.225-0.5,0.5-0.5H9V2H5.479
|
||||
C4.651,2,3.979,2.673,3.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
82
src/vs/editor/contrib/find/browser/simpleFindWidget.css
Normal file
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .simple-find-part {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 28px;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
width: 220px;
|
||||
max-width: calc(100% - 28px - 28px - 8px);
|
||||
|
||||
-webkit-transition: top 200ms linear;
|
||||
-o-transition: top 200ms linear;
|
||||
-moz-transition: top 200ms linear;
|
||||
-ms-transition: top 200ms linear;
|
||||
transition: top 200ms linear;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part.visible {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .monaco-findInput {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Temporarily we don't show match numbers */
|
||||
.monaco-workbench .simple-find-part .monaco-findInput .controls {
|
||||
display: none;
|
||||
}
|
||||
.monaco-workbench .simple-find-part .monaco-findInput .monaco-inputbox .wrapper .input {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
flex: initial;
|
||||
margin-left: 3px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button.previous {
|
||||
background-image: url('images/previous.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button.next {
|
||||
background-image: url('images/next.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button.close-fw {
|
||||
background-image: url('images/close.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-find-part .button.previous,
|
||||
.vs-dark .monaco-workbench .simple-find-part .button.previous {
|
||||
background-image: url('images/previous-inverse.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-find-part .button.next,
|
||||
.vs-dark .monaco-workbench .simple-find-part .button.next {
|
||||
background-image: url('images/next-inverse.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-find-part .button.close-fw,
|
||||
.vs-dark .monaco-workbench .simple-find-part .button.close-fw {
|
||||
background-image: url('images/close-dark.svg');
|
||||
}
|
||||
|
||||
monaco-workbench .simple-find-part .button.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
223
src/vs/editor/contrib/find/browser/simpleFindWidget.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./simpleFindWidget';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { HistoryNavigator } from 'vs/base/common/history';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FindInput } from 'vs/base/browser/ui/findinput/findInput';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { registerThemingParticipant, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { inputBackground, inputActiveOptionBorder, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorBorder, editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { SimpleButton } from './findWidget';
|
||||
|
||||
const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find");
|
||||
const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
|
||||
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match");
|
||||
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match");
|
||||
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
|
||||
|
||||
export abstract class SimpleFindWidget extends Widget {
|
||||
protected _findInput: FindInput;
|
||||
protected _domNode: HTMLElement;
|
||||
protected _isVisible: boolean;
|
||||
protected _focusTracker: dom.IFocusTracker;
|
||||
protected _findInputFocusTracker: dom.IFocusTracker;
|
||||
protected _findHistory: HistoryNavigator<string>;
|
||||
protected _updateHistoryDelayer: Delayer<void>;
|
||||
|
||||
constructor(
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
private animate: boolean = true
|
||||
) {
|
||||
super();
|
||||
this._findInput = this._register(new FindInput(null, this._contextViewService, {
|
||||
label: NLS_FIND_INPUT_LABEL,
|
||||
placeholder: NLS_FIND_INPUT_PLACEHOLDER,
|
||||
}));
|
||||
|
||||
// Find History with update delayer
|
||||
this._findHistory = new HistoryNavigator<string>();
|
||||
this._updateHistoryDelayer = new Delayer<void>(500);
|
||||
|
||||
this.oninput(this._findInput.domNode, (e) => {
|
||||
this.onInputChanged();
|
||||
this._delayedUpdateHistory();
|
||||
});
|
||||
|
||||
this._register(this._findInput.onKeyDown((e) => {
|
||||
if (e.equals(KeyCode.Enter)) {
|
||||
this.find(false);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.equals(KeyMod.Shift | KeyCode.Enter)) {
|
||||
this.find(true);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}));
|
||||
|
||||
let prevBtn = new SimpleButton({
|
||||
label: NLS_PREVIOUS_MATCH_BTN_LABEL,
|
||||
className: 'previous',
|
||||
onTrigger: () => {
|
||||
this.find(true);
|
||||
},
|
||||
onKeyDown: (e) => { }
|
||||
});
|
||||
|
||||
let nextBtn = new SimpleButton({
|
||||
label: NLS_NEXT_MATCH_BTN_LABEL,
|
||||
className: 'next',
|
||||
onTrigger: () => {
|
||||
this.find(false);
|
||||
},
|
||||
onKeyDown: (e) => { }
|
||||
});
|
||||
|
||||
let closeBtn = new SimpleButton({
|
||||
label: NLS_CLOSE_BTN_LABEL,
|
||||
className: 'close-fw',
|
||||
onTrigger: () => {
|
||||
this.hide();
|
||||
},
|
||||
onKeyDown: (e) => { }
|
||||
});
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.classList.add('simple-find-part');
|
||||
this._domNode.appendChild(this._findInput.domNode);
|
||||
this._domNode.appendChild(prevBtn.domNode);
|
||||
this._domNode.appendChild(nextBtn.domNode);
|
||||
this._domNode.appendChild(closeBtn.domNode);
|
||||
|
||||
this.onkeyup(this._domNode, e => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.hide();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this._focusTracker = this._register(dom.trackFocus(this._domNode));
|
||||
this._register(this._focusTracker.addFocusListener(this.onFocusTrackerFocus.bind(this)));
|
||||
this._register(this._focusTracker.addBlurListener(this.onFocusTrackerBlur.bind(this)));
|
||||
|
||||
this._findInputFocusTracker = this._register(dom.trackFocus(this._findInput.domNode));
|
||||
this._register(this._findInputFocusTracker.addFocusListener(this.onFindInputFocusTrackerFocus.bind(this)));
|
||||
this._register(this._findInputFocusTracker.addBlurListener(this.onFindInputFocusTrackerBlur.bind(this)));
|
||||
|
||||
this._register(dom.addDisposableListener(this._domNode, 'click', (event) => {
|
||||
event.stopPropagation();
|
||||
}));
|
||||
}
|
||||
|
||||
protected abstract onInputChanged();
|
||||
protected abstract find(previous: boolean);
|
||||
protected abstract onFocusTrackerFocus();
|
||||
protected abstract onFocusTrackerBlur();
|
||||
protected abstract onFindInputFocusTrackerFocus();
|
||||
protected abstract onFindInputFocusTrackerBlur();
|
||||
|
||||
protected get inputValue() {
|
||||
return this._findInput.getValue();
|
||||
}
|
||||
|
||||
public updateTheme(theme?: ITheme): void {
|
||||
let inputStyles = {
|
||||
inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder),
|
||||
inputBackground: theme.getColor(inputBackground),
|
||||
inputForeground: theme.getColor(inputForeground),
|
||||
inputBorder: theme.getColor(inputBorder),
|
||||
inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground),
|
||||
inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder),
|
||||
inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground),
|
||||
inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder),
|
||||
inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground),
|
||||
inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder)
|
||||
};
|
||||
this._findInput.style(inputStyles);
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public reveal(initialInput?: string): void {
|
||||
if (initialInput) {
|
||||
this._findInput.setValue(initialInput);
|
||||
}
|
||||
|
||||
if (this._isVisible) {
|
||||
this._findInput.select();
|
||||
return;
|
||||
}
|
||||
|
||||
this._isVisible = true;
|
||||
|
||||
setTimeout(() => {
|
||||
dom.addClass(this._domNode, 'visible');
|
||||
this._domNode.setAttribute('aria-hidden', 'false');
|
||||
if (!this.animate) {
|
||||
dom.addClass(this._domNode, 'noanimation');
|
||||
}
|
||||
setTimeout(() => {
|
||||
dom.removeClass(this._domNode, 'noanimation');
|
||||
this._findInput.select();
|
||||
}, 200);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this._isVisible) {
|
||||
this._isVisible = false;
|
||||
|
||||
dom.removeClass(this._domNode, 'visible');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
protected _delayedUpdateHistory() {
|
||||
this._updateHistoryDelayer.trigger(this._updateHistory.bind(this));
|
||||
}
|
||||
|
||||
protected _updateHistory() {
|
||||
if (this.inputValue) {
|
||||
this._findHistory.add(this._findInput.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public showNextFindTerm() {
|
||||
let next = this._findHistory.next();
|
||||
if (next) {
|
||||
this._findInput.setValue(next);
|
||||
}
|
||||
}
|
||||
|
||||
public showPreviousFindTerm() {
|
||||
let previous = this._findHistory.previous();
|
||||
if (previous) {
|
||||
this._findInput.setValue(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// theming
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const findWidgetBGColor = theme.getColor(editorWidgetBackground);
|
||||
if (findWidgetBGColor) {
|
||||
collector.addRule(`.monaco-workbench .simple-find-part { background-color: ${findWidgetBGColor} !important; }`);
|
||||
}
|
||||
|
||||
let widgetShadowColor = theme.getColor(widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
collector.addRule(`.monaco-workbench .simple-find-part { box-shadow: 0 2px 8px ${widgetShadowColor}; }`);
|
||||
}
|
||||
});
|
||||
25
src/vs/editor/contrib/find/common/find.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
|
||||
export function getSelectionSearchString(editor: editorCommon.ICommonCodeEditor): string {
|
||||
let selection = editor.getSelection();
|
||||
|
||||
// if selection spans multiple lines, default search string to empty
|
||||
if (selection.startLineNumber === selection.endLineNumber) {
|
||||
if (selection.isEmpty()) {
|
||||
let wordAtPosition = editor.getModel().getWordAtPosition(selection.getStartPosition());
|
||||
if (wordAtPosition) {
|
||||
return wordAtPosition.word;
|
||||
}
|
||||
} else {
|
||||
return editor.getModel().getValueInRange(selection);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
1277
src/vs/editor/contrib/find/common/findController.ts
Normal file
192
src/vs/editor/contrib/find/common/findDecorations.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
import { editorFindMatchHighlight, editorFindMatch } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class FindDecorations implements IDisposable {
|
||||
|
||||
private _editor: editorCommon.ICommonCodeEditor;
|
||||
private _decorations: string[];
|
||||
private _findScopeDecorationId: string;
|
||||
private _rangeHighlightDecorationId: string;
|
||||
private _highlightedDecorationId: string;
|
||||
private _startPosition: Position;
|
||||
|
||||
constructor(editor: editorCommon.ICommonCodeEditor) {
|
||||
this._editor = editor;
|
||||
this._decorations = [];
|
||||
this._findScopeDecorationId = null;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
this._startPosition = this._editor.getPosition();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.deltaDecorations(this._allDecorations(), []);
|
||||
|
||||
this._editor = null;
|
||||
this._decorations = [];
|
||||
this._findScopeDecorationId = null;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
this._startPosition = null;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._decorations = [];
|
||||
this._findScopeDecorationId = null;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
|
||||
public getCount(): number {
|
||||
return this._decorations.length;
|
||||
}
|
||||
|
||||
public getFindScope(): Range {
|
||||
if (this._findScopeDecorationId) {
|
||||
return this._editor.getModel().getDecorationRange(this._findScopeDecorationId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getStartPosition(): Position {
|
||||
return this._startPosition;
|
||||
}
|
||||
|
||||
public setStartPosition(newStartPosition: Position): void {
|
||||
this._startPosition = newStartPosition;
|
||||
this.setCurrentFindMatch(null);
|
||||
}
|
||||
|
||||
public getCurrentMatchesPosition(desiredRange: Range): number {
|
||||
for (let i = 0, len = this._decorations.length; i < len; i++) {
|
||||
let range = this._editor.getModel().getDecorationRange(this._decorations[i]);
|
||||
if (desiredRange.equalsRange(range)) {
|
||||
return (i + 1);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public setCurrentFindMatch(nextMatch: Range): number {
|
||||
let newCurrentDecorationId: string = null;
|
||||
let matchPosition = 0;
|
||||
if (nextMatch) {
|
||||
for (let i = 0, len = this._decorations.length; i < len; i++) {
|
||||
let range = this._editor.getModel().getDecorationRange(this._decorations[i]);
|
||||
if (nextMatch.equalsRange(range)) {
|
||||
newCurrentDecorationId = this._decorations[i];
|
||||
matchPosition = (i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._highlightedDecorationId !== null || newCurrentDecorationId !== null) {
|
||||
this._editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => {
|
||||
if (this._highlightedDecorationId !== null) {
|
||||
changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(false));
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
if (newCurrentDecorationId !== null) {
|
||||
this._highlightedDecorationId = newCurrentDecorationId;
|
||||
changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(true));
|
||||
}
|
||||
if (this._rangeHighlightDecorationId !== null) {
|
||||
changeAccessor.removeDecoration(this._rangeHighlightDecorationId);
|
||||
this._rangeHighlightDecorationId = null;
|
||||
}
|
||||
if (newCurrentDecorationId !== null) {
|
||||
let rng = this._editor.getModel().getDecorationRange(newCurrentDecorationId);
|
||||
this._rangeHighlightDecorationId = changeAccessor.addDecoration(rng, FindDecorations._RANGE_HIGHLIGHT_DECORATION);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return matchPosition;
|
||||
}
|
||||
|
||||
public set(matches: Range[], findScope: Range): void {
|
||||
let newDecorations: editorCommon.IModelDeltaDecoration[] = matches.map((match) => {
|
||||
return {
|
||||
range: match,
|
||||
options: FindDecorations.createFindMatchDecorationOptions(false)
|
||||
};
|
||||
});
|
||||
if (findScope) {
|
||||
newDecorations.unshift({
|
||||
range: findScope,
|
||||
options: FindDecorations._FIND_SCOPE_DECORATION
|
||||
});
|
||||
}
|
||||
let tmpDecorations = this._editor.deltaDecorations(this._allDecorations(), newDecorations);
|
||||
|
||||
if (findScope) {
|
||||
this._findScopeDecorationId = tmpDecorations.shift();
|
||||
} else {
|
||||
this._findScopeDecorationId = null;
|
||||
}
|
||||
this._decorations = tmpDecorations;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
|
||||
private _allDecorations(): string[] {
|
||||
let result: string[] = [];
|
||||
result = result.concat(this._decorations);
|
||||
if (this._findScopeDecorationId) {
|
||||
result.push(this._findScopeDecorationId);
|
||||
}
|
||||
if (this._rangeHighlightDecorationId) {
|
||||
result.push(this._rangeHighlightDecorationId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static createFindMatchDecorationOptions(isCurrent: boolean): ModelDecorationOptions {
|
||||
return (isCurrent ? this._CURRENT_FIND_MATCH_DECORATION : this._FIND_MATCH_DECORATION);
|
||||
}
|
||||
|
||||
private static _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'currentFindMatch',
|
||||
showIfCollapsed: true,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(editorFindMatch),
|
||||
darkColor: themeColorFromId(editorFindMatch),
|
||||
position: editorCommon.OverviewRulerLane.Center
|
||||
}
|
||||
});
|
||||
|
||||
private static _FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'findMatch',
|
||||
showIfCollapsed: true,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(editorFindMatchHighlight),
|
||||
darkColor: themeColorFromId(editorFindMatchHighlight),
|
||||
position: editorCommon.OverviewRulerLane.Center
|
||||
}
|
||||
});
|
||||
|
||||
private static _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'rangeHighlight',
|
||||
isWholeLine: true
|
||||
});
|
||||
|
||||
private static _FIND_SCOPE_DECORATION = ModelDecorationOptions.register({
|
||||
className: 'findScope',
|
||||
isWholeLine: true
|
||||
});
|
||||
}
|
||||
478
src/vs/editor/contrib/find/common/findModel.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/common/replacePattern';
|
||||
import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { FindDecorations } from './findDecorations';
|
||||
import { FindReplaceState, FindReplaceStateChangedEvent } from './findState';
|
||||
import { ReplaceAllCommand } from './replaceAllCommand';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Constants } from 'vs/editor/common/core/uint';
|
||||
import { SearchParams } from 'vs/editor/common/model/textModelSearch';
|
||||
import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
|
||||
export const ToggleCaseSensitiveKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_C,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }
|
||||
};
|
||||
export const ToggleWholeWordKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_W,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W }
|
||||
};
|
||||
export const ToggleRegexKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_R,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }
|
||||
};
|
||||
export const ToggleSearchScopeKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_L,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L }
|
||||
};
|
||||
export const ShowPreviousFindTermKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.UpArrow
|
||||
};
|
||||
export const ShowNextFindTermKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.DownArrow
|
||||
};
|
||||
|
||||
export const FIND_IDS = {
|
||||
StartFindAction: 'actions.find',
|
||||
NextMatchFindAction: 'editor.action.nextMatchFindAction',
|
||||
PreviousMatchFindAction: 'editor.action.previousMatchFindAction',
|
||||
NextSelectionMatchFindAction: 'editor.action.nextSelectionMatchFindAction',
|
||||
PreviousSelectionMatchFindAction: 'editor.action.previousSelectionMatchFindAction',
|
||||
AddSelectionToNextFindMatchAction: 'editor.action.addSelectionToNextFindMatch',
|
||||
AddSelectionToPreviousFindMatchAction: 'editor.action.addSelectionToPreviousFindMatch',
|
||||
MoveSelectionToNextFindMatchAction: 'editor.action.moveSelectionToNextFindMatch',
|
||||
MoveSelectionToPreviousFindMatchAction: 'editor.action.moveSelectionToPreviousFindMatch',
|
||||
StartFindReplaceAction: 'editor.action.startFindReplaceAction',
|
||||
CloseFindWidgetCommand: 'closeFindWidget',
|
||||
ToggleCaseSensitiveCommand: 'toggleFindCaseSensitive',
|
||||
ToggleWholeWordCommand: 'toggleFindWholeWord',
|
||||
ToggleRegexCommand: 'toggleFindRegex',
|
||||
ToggleSearchScopeCommand: 'toggleFindInSelection',
|
||||
ReplaceOneAction: 'editor.action.replaceOne',
|
||||
ReplaceAllAction: 'editor.action.replaceAll',
|
||||
SelectAllMatchesAction: 'editor.action.selectAllMatches',
|
||||
ShowPreviousFindTermAction: 'find.history.showPrevious',
|
||||
ShowNextFindTermAction: 'find.history.showNext'
|
||||
};
|
||||
|
||||
export const MATCHES_LIMIT = 999;
|
||||
|
||||
export class FindModelBoundToEditorModel {
|
||||
|
||||
private _editor: editorCommon.ICommonCodeEditor;
|
||||
private _state: FindReplaceState;
|
||||
private _toDispose: IDisposable[];
|
||||
private _decorations: FindDecorations;
|
||||
private _ignoreModelContentChanged: boolean;
|
||||
|
||||
private _updateDecorationsScheduler: RunOnceScheduler;
|
||||
private _isDisposed: boolean;
|
||||
|
||||
constructor(editor: editorCommon.ICommonCodeEditor, state: FindReplaceState) {
|
||||
this._editor = editor;
|
||||
this._state = state;
|
||||
this._toDispose = [];
|
||||
this._isDisposed = false;
|
||||
|
||||
this._decorations = new FindDecorations(editor);
|
||||
this._toDispose.push(this._decorations);
|
||||
|
||||
this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100);
|
||||
this._toDispose.push(this._updateDecorationsScheduler);
|
||||
|
||||
this._toDispose.push(this._editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
|
||||
if (
|
||||
e.reason === CursorChangeReason.Explicit
|
||||
|| e.reason === CursorChangeReason.Undo
|
||||
|| e.reason === CursorChangeReason.Redo
|
||||
) {
|
||||
this._decorations.setStartPosition(this._editor.getPosition());
|
||||
}
|
||||
}));
|
||||
|
||||
this._ignoreModelContentChanged = false;
|
||||
this._toDispose.push(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.push(this._state.addChangeListener((e) => this._onStateChanged(e)));
|
||||
|
||||
this.research(false, this._state.searchScope);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
}
|
||||
|
||||
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
|
||||
if (this._isDisposed) {
|
||||
// The find model is disposed during a find state changed event
|
||||
return;
|
||||
}
|
||||
if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) {
|
||||
if (e.searchScope) {
|
||||
this.research(e.moveCursor, this._state.searchScope);
|
||||
} else {
|
||||
this.research(e.moveCursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static _getSearchRange(model: editorCommon.IModel, searchOnlyEditableRange: boolean, findScope: Range): Range {
|
||||
let searchRange: Range;
|
||||
|
||||
if (searchOnlyEditableRange) {
|
||||
searchRange = model.getEditableRange();
|
||||
} else {
|
||||
searchRange = model.getFullModelRange();
|
||||
}
|
||||
|
||||
// If we have set now or before a find scope, use it for computing the search range
|
||||
if (findScope) {
|
||||
searchRange = searchRange.intersectRanges(findScope);
|
||||
}
|
||||
|
||||
return searchRange;
|
||||
}
|
||||
|
||||
private research(moveCursor: boolean, newFindScope?: Range): void {
|
||||
let findScope: Range = null;
|
||||
if (typeof newFindScope !== 'undefined') {
|
||||
findScope = newFindScope;
|
||||
} else {
|
||||
findScope = this._decorations.getFindScope();
|
||||
}
|
||||
if (findScope !== null) {
|
||||
if (findScope.startLineNumber !== findScope.endLineNumber) {
|
||||
// multiline find scope => expand to line starts / ends
|
||||
findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber));
|
||||
}
|
||||
}
|
||||
|
||||
let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT);
|
||||
this._decorations.set(findMatches.map(match => match.range), findScope);
|
||||
|
||||
this._state.changeMatchInfo(
|
||||
this._decorations.getCurrentMatchesPosition(this._editor.getSelection()),
|
||||
this._decorations.getCount(),
|
||||
undefined
|
||||
);
|
||||
|
||||
if (moveCursor) {
|
||||
this._moveToNextMatch(this._decorations.getStartPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private _hasMatches(): boolean {
|
||||
return (this._state.matchesCount > 0);
|
||||
}
|
||||
|
||||
private _cannotFind(): boolean {
|
||||
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, editorCommon.ScrollType.Smooth);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _setCurrentFindMatch(match: Range): void {
|
||||
let matchesPosition = this._decorations.setCurrentFindMatch(match);
|
||||
this._state.changeMatchInfo(
|
||||
matchesPosition,
|
||||
this._decorations.getCount(),
|
||||
match
|
||||
);
|
||||
|
||||
this._editor.setSelection(match);
|
||||
this._editor.revealRangeInCenterIfOutsideViewport(match, editorCommon.ScrollType.Smooth);
|
||||
}
|
||||
|
||||
private _moveToPrevMatch(before: Position, isRecursed: boolean = false): void {
|
||||
if (this._cannotFind()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), this._state.isReplaceRevealed, 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.getConfiguration().wordSeparators : null, false);
|
||||
|
||||
if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) {
|
||||
// Looks like we're stuck at this position, unacceptable!
|
||||
|
||||
let isUsingLineStops = this._state.isRegex && (
|
||||
this._state.searchString.indexOf('^') >= 0
|
||||
|| this._state.searchString.indexOf('$') >= 0
|
||||
);
|
||||
|
||||
if (isUsingLineStops || column === 1) {
|
||||
if (lineNumber === 1) {
|
||||
lineNumber = model.getLineCount();
|
||||
} else {
|
||||
lineNumber--;
|
||||
}
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
} else {
|
||||
column--;
|
||||
}
|
||||
|
||||
position = new Position(lineNumber, column);
|
||||
prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false);
|
||||
}
|
||||
|
||||
if (!prevMatch) {
|
||||
// there is precisely one match and selection is on top of it
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isRecursed && !searchRange.containsRange(prevMatch.range)) {
|
||||
return this._moveToPrevMatch(prevMatch.range.getStartPosition(), true);
|
||||
}
|
||||
|
||||
this._setCurrentFindMatch(prevMatch.range);
|
||||
}
|
||||
|
||||
public moveToPrevMatch(): void {
|
||||
this._moveToPrevMatch(this._editor.getSelection().getStartPosition());
|
||||
}
|
||||
|
||||
private _moveToNextMatch(after: Position): void {
|
||||
let nextMatch = this._getNextMatch(after, false, true);
|
||||
if (nextMatch) {
|
||||
this._setCurrentFindMatch(nextMatch.range);
|
||||
}
|
||||
}
|
||||
|
||||
private _getNextMatch(after: Position, captureMatches: boolean, forceMove: boolean, isRecursed: boolean = false): editorCommon.FindMatch {
|
||||
if (this._cannotFind()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), this._state.isReplaceRevealed, 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.getConfiguration().wordSeparators : null, captureMatches);
|
||||
|
||||
if (forceMove && nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) {
|
||||
// Looks like we're stuck at this position, unacceptable!
|
||||
|
||||
let isUsingLineStops = this._state.isRegex && (
|
||||
this._state.searchString.indexOf('^') >= 0
|
||||
|| this._state.searchString.indexOf('$') >= 0
|
||||
);
|
||||
|
||||
if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) {
|
||||
if (lineNumber === model.getLineCount()) {
|
||||
lineNumber = 1;
|
||||
} else {
|
||||
lineNumber++;
|
||||
}
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
|
||||
position = new Position(lineNumber, column);
|
||||
nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().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;
|
||||
}
|
||||
|
||||
public moveToNextMatch(): void {
|
||||
this._moveToNextMatch(this._editor.getSelection().getEndPosition());
|
||||
}
|
||||
|
||||
private _getReplacePattern(): ReplacePattern {
|
||||
if (this._state.isRegex) {
|
||||
return parseReplaceString(this._state.replaceString);
|
||||
}
|
||||
return ReplacePattern.fromStaticValue(this._state.replaceString);
|
||||
}
|
||||
|
||||
public replace(): void {
|
||||
if (!this._hasMatches()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let replacePattern = this._getReplacePattern();
|
||||
let selection = this._editor.getSelection();
|
||||
let nextMatch = this._getNextMatch(selection.getStartPosition(), replacePattern.hasReplacementPatterns, false);
|
||||
if (nextMatch) {
|
||||
if (selection.equalsRange(nextMatch.range)) {
|
||||
// selection sits on a find match => replace it!
|
||||
let replaceString = replacePattern.buildReplaceString(nextMatch.matches);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _findMatches(findScope: Range, captureMatches: boolean, limitResultCount: number): editorCommon.FindMatch[] {
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), this._state.isReplaceRevealed, findScope);
|
||||
return this._editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, captureMatches, limitResultCount);
|
||||
}
|
||||
|
||||
public replaceAll(): void {
|
||||
if (!this._hasMatches()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const findScope = this._decorations.getFindScope();
|
||||
|
||||
if (findScope === null && this._state.matchesCount >= MATCHES_LIMIT) {
|
||||
// Doing a replace on the entire file that is over 1k matches
|
||||
this._largeReplaceAll();
|
||||
} else {
|
||||
this._regularReplaceAll(findScope);
|
||||
}
|
||||
|
||||
this.research(false);
|
||||
}
|
||||
|
||||
private _largeReplaceAll(): void {
|
||||
const searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null);
|
||||
const searchData = searchParams.parseSearchRequest();
|
||||
if (!searchData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const modelText = model.getValue(editorCommon.EndOfLinePreference.LF);
|
||||
const fullModelRange = model.getFullModelRange();
|
||||
|
||||
const replacePattern = this._getReplacePattern();
|
||||
let resultText: string;
|
||||
if (replacePattern.hasReplacementPatterns) {
|
||||
resultText = modelText.replace(searchData.regex, function () {
|
||||
return replacePattern.buildReplaceString(<string[]><any>arguments);
|
||||
});
|
||||
} else {
|
||||
resultText = modelText.replace(searchData.regex, replacePattern.buildReplaceString(null));
|
||||
}
|
||||
|
||||
let command = new ReplaceCommandThatPreservesSelection(fullModelRange, resultText, this._editor.getSelection());
|
||||
this._executeEditorCommand('replaceAll', command);
|
||||
}
|
||||
|
||||
private _regularReplaceAll(findScope: Range): void {
|
||||
const replacePattern = this._getReplacePattern();
|
||||
// Get all the ranges (even more than the highlighted ones)
|
||||
let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns, Constants.MAX_SAFE_SMALL_INTEGER);
|
||||
|
||||
let replaceStrings: string[] = [];
|
||||
for (let i = 0, len = matches.length; i < len; i++) {
|
||||
replaceStrings[i] = replacePattern.buildReplaceString(matches[i].matches);
|
||||
}
|
||||
|
||||
let command = new ReplaceAllCommand(this._editor.getSelection(), matches.map(m => m.range), replaceStrings);
|
||||
this._executeEditorCommand('replaceAll', command);
|
||||
}
|
||||
|
||||
public selectAllMatches(): void {
|
||||
if (!this._hasMatches()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
|
||||
// Get all the ranges (even more than the highlighted ones)
|
||||
let matches = this._findMatches(findScope, false, Constants.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);
|
||||
}
|
||||
|
||||
private _executeEditorCommand(source: string, command: editorCommon.ICommand): void {
|
||||
try {
|
||||
this._ignoreModelContentChanged = true;
|
||||
this._editor.pushUndoStop();
|
||||
this._editor.executeCommand(source, command);
|
||||
this._editor.pushUndoStop();
|
||||
} finally {
|
||||
this._ignoreModelContentChanged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
219
src/vs/editor/contrib/find/common/findState.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { EventEmitter } from 'vs/base/common/eventEmitter';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
export interface FindReplaceStateChangedEvent {
|
||||
moveCursor: boolean;
|
||||
updateHistory: boolean;
|
||||
|
||||
searchString: boolean;
|
||||
replaceString: boolean;
|
||||
isRevealed: boolean;
|
||||
isReplaceRevealed: boolean;
|
||||
isRegex: boolean;
|
||||
wholeWord: boolean;
|
||||
matchCase: boolean;
|
||||
searchScope: boolean;
|
||||
matchesPosition: boolean;
|
||||
matchesCount: boolean;
|
||||
currentMatch: boolean;
|
||||
}
|
||||
|
||||
export interface INewFindReplaceState {
|
||||
searchString?: string;
|
||||
replaceString?: string;
|
||||
isRevealed?: boolean;
|
||||
isReplaceRevealed?: boolean;
|
||||
isRegex?: boolean;
|
||||
wholeWord?: boolean;
|
||||
matchCase?: boolean;
|
||||
searchScope?: Range;
|
||||
}
|
||||
|
||||
export class FindReplaceState implements IDisposable {
|
||||
|
||||
private static _CHANGED_EVENT = 'changed';
|
||||
|
||||
private _searchString: string;
|
||||
private _replaceString: string;
|
||||
private _isRevealed: boolean;
|
||||
private _isReplaceRevealed: boolean;
|
||||
private _isRegex: boolean;
|
||||
private _wholeWord: boolean;
|
||||
private _matchCase: boolean;
|
||||
private _searchScope: Range;
|
||||
private _matchesPosition: number;
|
||||
private _matchesCount: number;
|
||||
private _currentMatch: Range;
|
||||
private _eventEmitter: EventEmitter;
|
||||
|
||||
public get searchString(): string { return this._searchString; }
|
||||
public get replaceString(): string { return this._replaceString; }
|
||||
public get isRevealed(): boolean { return this._isRevealed; }
|
||||
public get isReplaceRevealed(): boolean { return this._isReplaceRevealed; }
|
||||
public get isRegex(): boolean { return this._isRegex; }
|
||||
public get wholeWord(): boolean { return this._wholeWord; }
|
||||
public get matchCase(): boolean { return this._matchCase; }
|
||||
public get searchScope(): Range { return this._searchScope; }
|
||||
public get matchesPosition(): number { return this._matchesPosition; }
|
||||
public get matchesCount(): number { return this._matchesCount; }
|
||||
public get currentMatch(): Range { return this._currentMatch; }
|
||||
|
||||
constructor() {
|
||||
this._searchString = '';
|
||||
this._replaceString = '';
|
||||
this._isRevealed = false;
|
||||
this._isReplaceRevealed = false;
|
||||
this._isRegex = false;
|
||||
this._wholeWord = false;
|
||||
this._matchCase = false;
|
||||
this._searchScope = null;
|
||||
this._matchesPosition = 0;
|
||||
this._matchesCount = 0;
|
||||
this._currentMatch = null;
|
||||
this._eventEmitter = new EventEmitter();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._eventEmitter.dispose();
|
||||
}
|
||||
|
||||
public addChangeListener(listener: (e: FindReplaceStateChangedEvent) => void): IDisposable {
|
||||
return this._eventEmitter.addListener(FindReplaceState._CHANGED_EVENT, listener);
|
||||
}
|
||||
|
||||
public changeMatchInfo(matchesPosition: number, matchesCount: number, currentMatch: Range): void {
|
||||
let changeEvent: FindReplaceStateChangedEvent = {
|
||||
moveCursor: false,
|
||||
updateHistory: false,
|
||||
searchString: false,
|
||||
replaceString: false,
|
||||
isRevealed: false,
|
||||
isReplaceRevealed: false,
|
||||
isRegex: false,
|
||||
wholeWord: false,
|
||||
matchCase: false,
|
||||
searchScope: false,
|
||||
matchesPosition: false,
|
||||
matchesCount: false,
|
||||
currentMatch: false
|
||||
};
|
||||
let somethingChanged = false;
|
||||
|
||||
if (matchesCount === 0) {
|
||||
matchesPosition = 0;
|
||||
}
|
||||
if (matchesPosition > matchesCount) {
|
||||
matchesPosition = matchesCount;
|
||||
}
|
||||
|
||||
if (this._matchesPosition !== matchesPosition) {
|
||||
this._matchesPosition = matchesPosition;
|
||||
changeEvent.matchesPosition = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (this._matchesCount !== matchesCount) {
|
||||
this._matchesCount = matchesCount;
|
||||
changeEvent.matchesCount = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
|
||||
if (typeof currentMatch !== 'undefined') {
|
||||
if (!Range.equalsRange(this._currentMatch, currentMatch)) {
|
||||
this._currentMatch = currentMatch;
|
||||
changeEvent.currentMatch = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
this._eventEmitter.emit(FindReplaceState._CHANGED_EVENT, changeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public change(newState: INewFindReplaceState, moveCursor: boolean, updateHistory: boolean = true): void {
|
||||
let changeEvent: FindReplaceStateChangedEvent = {
|
||||
moveCursor: moveCursor,
|
||||
updateHistory: updateHistory,
|
||||
searchString: false,
|
||||
replaceString: false,
|
||||
isRevealed: false,
|
||||
isReplaceRevealed: false,
|
||||
isRegex: false,
|
||||
wholeWord: false,
|
||||
matchCase: false,
|
||||
searchScope: false,
|
||||
matchesPosition: false,
|
||||
matchesCount: false,
|
||||
currentMatch: false
|
||||
};
|
||||
let somethingChanged = false;
|
||||
|
||||
if (typeof newState.searchString !== 'undefined') {
|
||||
if (this._searchString !== newState.searchString) {
|
||||
this._searchString = newState.searchString;
|
||||
changeEvent.searchString = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.replaceString !== 'undefined') {
|
||||
if (this._replaceString !== newState.replaceString) {
|
||||
this._replaceString = newState.replaceString;
|
||||
changeEvent.replaceString = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.isRevealed !== 'undefined') {
|
||||
if (this._isRevealed !== newState.isRevealed) {
|
||||
this._isRevealed = newState.isRevealed;
|
||||
changeEvent.isRevealed = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.isReplaceRevealed !== 'undefined') {
|
||||
if (this._isReplaceRevealed !== newState.isReplaceRevealed) {
|
||||
this._isReplaceRevealed = newState.isReplaceRevealed;
|
||||
changeEvent.isReplaceRevealed = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.isRegex !== 'undefined') {
|
||||
if (this._isRegex !== newState.isRegex) {
|
||||
this._isRegex = newState.isRegex;
|
||||
changeEvent.isRegex = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.wholeWord !== 'undefined') {
|
||||
if (this._wholeWord !== newState.wholeWord) {
|
||||
this._wholeWord = newState.wholeWord;
|
||||
changeEvent.wholeWord = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.matchCase !== 'undefined') {
|
||||
if (this._matchCase !== newState.matchCase) {
|
||||
this._matchCase = newState.matchCase;
|
||||
changeEvent.matchCase = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.searchScope !== 'undefined') {
|
||||
if (!Range.equalsRange(this._searchScope, newState.searchScope)) {
|
||||
this._searchScope = newState.searchScope;
|
||||
changeEvent.searchScope = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
this._eventEmitter.emit(FindReplaceState._CHANGED_EVENT, changeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/vs/editor/contrib/find/common/replaceAllCommand.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
|
||||
interface IEditOperation {
|
||||
range: Range;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class ReplaceAllCommand implements editorCommon.ICommand {
|
||||
|
||||
private _editorSelection: Selection;
|
||||
private _trackedEditorSelectionId: string;
|
||||
private _ranges: Range[];
|
||||
private _replaceStrings: string[];
|
||||
|
||||
constructor(editorSelection: Selection, ranges: Range[], replaceStrings: string[]) {
|
||||
this._editorSelection = editorSelection;
|
||||
this._ranges = ranges;
|
||||
this._replaceStrings = replaceStrings;
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
if (this._ranges.length > 0) {
|
||||
// Collect all edit operations
|
||||
var ops: IEditOperation[] = [];
|
||||
for (var i = 0; i < this._ranges.length; i++) {
|
||||
ops.push({
|
||||
range: this._ranges[i],
|
||||
text: this._replaceStrings[i]
|
||||
});
|
||||
}
|
||||
|
||||
// Sort them in ascending order by range starts
|
||||
ops.sort((o1, o2) => {
|
||||
return Range.compareRangesUsingStarts(o1.range, o2.range);
|
||||
});
|
||||
|
||||
// Merge operations that touch each other
|
||||
var resultOps: IEditOperation[] = [];
|
||||
var previousOp = ops[0];
|
||||
for (var i = 1; i < ops.length; i++) {
|
||||
if (previousOp.range.endLineNumber === ops[i].range.startLineNumber && previousOp.range.endColumn === ops[i].range.startColumn) {
|
||||
// These operations are one after another and can be merged
|
||||
previousOp.range = previousOp.range.plusRange(ops[i].range);
|
||||
previousOp.text = previousOp.text + ops[i].text;
|
||||
} else {
|
||||
resultOps.push(previousOp);
|
||||
previousOp = ops[i];
|
||||
}
|
||||
}
|
||||
resultOps.push(previousOp);
|
||||
|
||||
for (var i = 0; i < resultOps.length; i++) {
|
||||
builder.addEditOperation(resultOps[i].range, resultOps[i].text);
|
||||
}
|
||||
}
|
||||
|
||||
this._trackedEditorSelectionId = builder.trackSelection(this._editorSelection);
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
return helper.getTrackedSelection(this._trackedEditorSelectionId);
|
||||
}
|
||||
}
|
||||
266
src/vs/editor/contrib/find/common/replacePattern.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
|
||||
export class ReplacePattern {
|
||||
|
||||
public static fromStaticValue(value: string): ReplacePattern {
|
||||
return new ReplacePattern([ReplacePiece.staticValue(value)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigned when the replace pattern is entirely static.
|
||||
*/
|
||||
private readonly _staticValue: string;
|
||||
|
||||
public get hasReplacementPatterns(): boolean {
|
||||
return this._staticValue === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigned when the replace pattern has replacemend patterns.
|
||||
*/
|
||||
private readonly _pieces: ReplacePiece[];
|
||||
|
||||
constructor(pieces: ReplacePiece[]) {
|
||||
if (!pieces || pieces.length === 0) {
|
||||
this._staticValue = '';
|
||||
this._pieces = null;
|
||||
} else if (pieces.length === 1 && pieces[0].staticValue !== null) {
|
||||
this._staticValue = pieces[0].staticValue;
|
||||
this._pieces = null;
|
||||
} else {
|
||||
this._staticValue = null;
|
||||
this._pieces = pieces;
|
||||
}
|
||||
}
|
||||
|
||||
public buildReplaceString(matches: string[]): string {
|
||||
if (this._staticValue !== null) {
|
||||
return this._staticValue;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (let i = 0, len = this._pieces.length; i < len; i++) {
|
||||
let piece = this._pieces[i];
|
||||
if (piece.staticValue !== null) {
|
||||
// static value ReplacePiece
|
||||
result += piece.staticValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// match index ReplacePiece
|
||||
result += ReplacePattern._substitute(piece.matchIndex, matches);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _substitute(matchIndex: number, matches: string[]): string {
|
||||
if (matchIndex === 0) {
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
let remainder = '';
|
||||
while (matchIndex > 0) {
|
||||
if (matchIndex < matches.length) {
|
||||
// A match can be undefined
|
||||
let match = (matches[matchIndex] || '');
|
||||
return match + remainder;
|
||||
}
|
||||
remainder = String(matchIndex % 10) + remainder;
|
||||
matchIndex = Math.floor(matchIndex / 10);
|
||||
}
|
||||
return '$' + remainder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A replace piece can either be a static string or an index to a specific match.
|
||||
*/
|
||||
export class ReplacePiece {
|
||||
|
||||
public static staticValue(value: string): ReplacePiece {
|
||||
return new ReplacePiece(value, -1);
|
||||
}
|
||||
|
||||
public static matchIndex(index: number): ReplacePiece {
|
||||
return new ReplacePiece(null, index);
|
||||
}
|
||||
|
||||
public readonly staticValue: string;
|
||||
public readonly matchIndex: number;
|
||||
|
||||
private constructor(staticValue: string, matchIndex: number) {
|
||||
this.staticValue = staticValue;
|
||||
this.matchIndex = matchIndex;
|
||||
}
|
||||
}
|
||||
|
||||
class ReplacePieceBuilder {
|
||||
|
||||
private readonly _source: string;
|
||||
private _lastCharIndex: number;
|
||||
private readonly _result: ReplacePiece[];
|
||||
private _resultLen: number;
|
||||
private _currentStaticPiece: string;
|
||||
|
||||
constructor(source: string) {
|
||||
this._source = source;
|
||||
this._lastCharIndex = 0;
|
||||
this._result = [];
|
||||
this._resultLen = 0;
|
||||
this._currentStaticPiece = '';
|
||||
}
|
||||
|
||||
public emitUnchanged(toCharIndex: number): void {
|
||||
this._emitStatic(this._source.substring(this._lastCharIndex, toCharIndex));
|
||||
this._lastCharIndex = toCharIndex;
|
||||
}
|
||||
|
||||
public emitStatic(value: string, toCharIndex: number): void {
|
||||
this._emitStatic(value);
|
||||
this._lastCharIndex = toCharIndex;
|
||||
}
|
||||
|
||||
private _emitStatic(value: string): void {
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._currentStaticPiece += value;
|
||||
}
|
||||
|
||||
public emitMatchIndex(index: number, toCharIndex: number): void {
|
||||
if (this._currentStaticPiece.length !== 0) {
|
||||
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
|
||||
this._currentStaticPiece = '';
|
||||
}
|
||||
this._result[this._resultLen++] = ReplacePiece.matchIndex(index);
|
||||
this._lastCharIndex = toCharIndex;
|
||||
}
|
||||
|
||||
|
||||
public finalize(): ReplacePattern {
|
||||
this.emitUnchanged(this._source.length);
|
||||
if (this._currentStaticPiece.length !== 0) {
|
||||
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
|
||||
this._currentStaticPiece = '';
|
||||
}
|
||||
return new ReplacePattern(this._result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \n => inserts a LF
|
||||
* \t => inserts a TAB
|
||||
* \\ => inserts a "\".
|
||||
* $$ => inserts a "$".
|
||||
* $& and $0 => inserts the matched substring.
|
||||
* $n => Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string
|
||||
* everything else stays untouched
|
||||
*
|
||||
* Also see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
|
||||
*/
|
||||
export function parseReplaceString(replaceString: string): ReplacePattern {
|
||||
if (!replaceString || replaceString.length === 0) {
|
||||
return new ReplacePattern(null);
|
||||
}
|
||||
|
||||
let result = new ReplacePieceBuilder(replaceString);
|
||||
|
||||
for (let i = 0, len = replaceString.length; i < len; i++) {
|
||||
let chCode = replaceString.charCodeAt(i);
|
||||
|
||||
if (chCode === CharCode.Backslash) {
|
||||
|
||||
// move to next char
|
||||
i++;
|
||||
|
||||
if (i >= len) {
|
||||
// string ends with a \
|
||||
break;
|
||||
}
|
||||
|
||||
let nextChCode = replaceString.charCodeAt(i);
|
||||
// let replaceWithCharacter: string = null;
|
||||
|
||||
switch (nextChCode) {
|
||||
case CharCode.Backslash:
|
||||
// \\ => inserts a "\"
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('\\', i + 1);
|
||||
break;
|
||||
case CharCode.n:
|
||||
// \n => inserts a LF
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('\n', i + 1);
|
||||
break;
|
||||
case CharCode.t:
|
||||
// \t => inserts a TAB
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('\t', i + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chCode === CharCode.DollarSign) {
|
||||
|
||||
// move to next char
|
||||
i++;
|
||||
|
||||
if (i >= len) {
|
||||
// string ends with a $
|
||||
break;
|
||||
}
|
||||
|
||||
let nextChCode = replaceString.charCodeAt(i);
|
||||
|
||||
if (nextChCode === CharCode.DollarSign) {
|
||||
// $$ => inserts a "$"
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('$', i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) {
|
||||
// $& and $0 => inserts the matched substring.
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitMatchIndex(0, i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) {
|
||||
// $n
|
||||
|
||||
let matchIndex = nextChCode - CharCode.Digit0;
|
||||
|
||||
// peek next char to probe for $nn
|
||||
if (i + 1 < len) {
|
||||
let nextNextChCode = replaceString.charCodeAt(i + 1);
|
||||
if (CharCode.Digit0 <= nextNextChCode && nextNextChCode <= CharCode.Digit9) {
|
||||
// $nn
|
||||
|
||||
// move to next char
|
||||
i++;
|
||||
matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0);
|
||||
|
||||
result.emitUnchanged(i - 2);
|
||||
result.emitMatchIndex(matchIndex, i + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitMatchIndex(matchIndex, i + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.finalize();
|
||||
}
|
||||
89
src/vs/editor/contrib/find/test/common/find.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import {
|
||||
getSelectionSearchString
|
||||
} from 'vs/editor/contrib/find/common/find';
|
||||
import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
|
||||
|
||||
|
||||
suite('Find', () => {
|
||||
|
||||
test('search string at position', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor, cursor) => {
|
||||
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let searchStringAtTop = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringAtTop, 'ABC');
|
||||
|
||||
// Move cursor to the end of ABC
|
||||
editor.setPosition(new Position(1, 3));
|
||||
let searchStringAfterABC = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringAfterABC, 'ABC');
|
||||
|
||||
// Move cursor to DEF
|
||||
editor.setPosition(new Position(1, 5));
|
||||
let searchStringInsideDEF = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringInsideDEF, 'DEF');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test('search string with selection', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor, cursor) => {
|
||||
|
||||
// Select A of ABC
|
||||
editor.setSelection(new Range(1, 1, 1, 2));
|
||||
let searchStringSelectionA = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionA, 'A');
|
||||
|
||||
// Select BC of ABC
|
||||
editor.setSelection(new Range(1, 2, 1, 4));
|
||||
let searchStringSelectionBC = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionBC, 'BC');
|
||||
|
||||
// Select BC DE
|
||||
editor.setSelection(new Range(1, 2, 1, 7));
|
||||
let searchStringSelectionBCDE = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionBCDE, 'BC DE');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test('search string with multiline selection', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor, cursor) => {
|
||||
|
||||
// Select first line and newline
|
||||
editor.setSelection(new Range(1, 1, 2, 1));
|
||||
let searchStringSelectionWholeLine = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionWholeLine, null);
|
||||
|
||||
// Select first line and chunk of second
|
||||
editor.setSelection(new Range(1, 1, 2, 4));
|
||||
let searchStringSelectionTwoLines = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionTwoLines, null);
|
||||
|
||||
// Select end of first line newline and and chunk of second
|
||||
editor.setSelection(new Range(1, 7, 2, 4));
|
||||
let searchStringSelectionSpanLines = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionSpanLines, null);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
871
src/vs/editor/contrib/find/test/common/findController.test.ts
Normal file
@@ -0,0 +1,871 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { EndOfLineSequence, ICommonCodeEditor, Handler } from 'vs/editor/common/editorCommon';
|
||||
import {
|
||||
CommonFindController, FindStartFocusAction, IFindStartOptions,
|
||||
NextMatchFindAction, StartFindAction, SelectHighlightsAction,
|
||||
AddSelectionToNextFindMatchAction
|
||||
} from 'vs/editor/contrib/find/common/findController';
|
||||
import { MockCodeEditor, withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
|
||||
import { HistoryNavigator } from 'vs/base/common/history';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
|
||||
class TestFindController extends CommonFindController {
|
||||
|
||||
public hasFocus: boolean;
|
||||
public delayUpdateHistory: boolean = false;
|
||||
public delayedUpdateHistoryPromise: TPromise<void>;
|
||||
|
||||
private _delayedUpdateHistoryEvent: Emitter<void> = new Emitter<void>();
|
||||
|
||||
constructor(editor: ICommonCodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @IStorageService storageService: IStorageService) {
|
||||
super(editor, contextKeyService, storageService);
|
||||
this._updateHistoryDelayer = new Delayer<void>(50);
|
||||
}
|
||||
|
||||
protected _start(opts: IFindStartOptions): void {
|
||||
super._start(opts);
|
||||
|
||||
if (opts.shouldFocus !== FindStartFocusAction.NoFocusChange) {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected _delayedUpdateHistory() {
|
||||
if (!this.delayedUpdateHistoryPromise) {
|
||||
this.delayedUpdateHistoryPromise = new TPromise<void>((c, e) => {
|
||||
const disposable = this._delayedUpdateHistoryEvent.event(() => {
|
||||
disposable.dispose();
|
||||
this.delayedUpdateHistoryPromise = null;
|
||||
c(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (this.delayUpdateHistory) {
|
||||
super._delayedUpdateHistory();
|
||||
} else {
|
||||
this._updateHistory();
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateHistory() {
|
||||
super._updateHistory();
|
||||
this._delayedUpdateHistoryEvent.fire();
|
||||
}
|
||||
}
|
||||
|
||||
function fromRange(rng: Range): number[] {
|
||||
return [rng.startLineNumber, rng.startColumn, rng.endLineNumber, rng.endColumn];
|
||||
}
|
||||
|
||||
suite('FindController', () => {
|
||||
let queryState: { [key: string]: any; } = {};
|
||||
let serviceCollection = new ServiceCollection();
|
||||
serviceCollection.set(IStorageService, <any>{
|
||||
get: (key: string) => queryState[key],
|
||||
getBoolean: (key: string) => !!queryState[key],
|
||||
store: (key: string, value: any) => { queryState[key] = value; }
|
||||
});
|
||||
|
||||
test('issue #1857: F3, Find Next, acts like "Find Under Cursor"', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
// I type ABC.
|
||||
findState.change({ searchString: 'A' }, true);
|
||||
findState.change({ searchString: 'AB' }, true);
|
||||
findState.change({ searchString: 'ABC' }, true);
|
||||
|
||||
// The first ABC is highlighted.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 1, 1, 4]);
|
||||
|
||||
// I hit Esc to exit the Find dialog.
|
||||
findController.closeFindWidget();
|
||||
findController.hasFocus = false;
|
||||
|
||||
// The cursor is now at end of the first line, with ABC on that line highlighted.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 1, 1, 4]);
|
||||
|
||||
// I hit delete to remove it and change the text to XYZ.
|
||||
editor.pushUndoStop();
|
||||
editor.executeEdits('test', [EditOperation.delete(new Range(1, 1, 1, 4))]);
|
||||
editor.executeEdits('test', [EditOperation.insert(new Position(1, 1), 'XYZ')]);
|
||||
editor.pushUndoStop();
|
||||
|
||||
// At this point the text editor looks like this:
|
||||
// XYZ
|
||||
// ABC
|
||||
// XYZ
|
||||
// ABC
|
||||
assert.equal(editor.getModel().getLineContent(1), 'XYZ');
|
||||
|
||||
// The cursor is at end of the first line.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 4, 1, 4]);
|
||||
|
||||
// I hit F3 to "Find Next" to find the next occurrence of ABC, but instead it searches for XYZ.
|
||||
nextMatchFindAction.run(null, editor);
|
||||
|
||||
assert.equal(findState.searchString, 'ABC');
|
||||
assert.equal(findController.hasFocus, false);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #3090: F3 does not loop with two matches on a single line', () => {
|
||||
withMockCodeEditor([
|
||||
'import nls = require(\'vs/nls\');'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
editor.setPosition({
|
||||
lineNumber: 1,
|
||||
column: 9
|
||||
});
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 26, 1, 29]);
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 8, 1, 11]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #6149: Auto-escape highlighted text for search and replace regex mode', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let startFindAction = new StartFindAction();
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
editor.setSelection(new Selection(1, 9, 1, 13));
|
||||
|
||||
findController.toggleRegex();
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 9, 2, 13]);
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 9, 1, 13]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #8817: Cursor position changes when you cancel multicursor', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let selectHighlightsAction = new SelectHighlightsAction();
|
||||
|
||||
editor.setSelection(new Selection(2, 9, 2, 16));
|
||||
|
||||
selectHighlightsAction.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 9, 2, 16],
|
||||
[1, 9, 1, 16],
|
||||
[3, 9, 3, 16],
|
||||
]);
|
||||
|
||||
editor.trigger('test', 'removeSecondaryCursors', null);
|
||||
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 9, 2, 16]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #5400: "Select All Occurrences of Find Match" does not select all if find uses regex', () => {
|
||||
withMockCodeEditor([
|
||||
'something',
|
||||
'someething',
|
||||
'someeething',
|
||||
'nothing'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let selectHighlightsAction = new SelectHighlightsAction();
|
||||
|
||||
editor.setSelection(new Selection(1, 1, 1, 1));
|
||||
findController.getState().change({ searchString: 'some+thing', isRegex: true, isRevealed: true }, false);
|
||||
|
||||
selectHighlightsAction.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 1, 1, 10],
|
||||
[2, 1, 2, 11],
|
||||
[3, 1, 3, 12],
|
||||
]);
|
||||
|
||||
assert.equal(findController.getState().searchString, 'some+thing');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #9043: Clear search scope when find widget is hidden', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false
|
||||
});
|
||||
|
||||
assert.equal(findController.getState().searchScope, null);
|
||||
|
||||
findController.getState().change({
|
||||
searchScope: new Range(1, 1, 1, 5)
|
||||
}, false);
|
||||
|
||||
assert.deepEqual(findController.getState().searchScope, new Range(1, 1, 1, 5));
|
||||
|
||||
findController.closeFindWidget();
|
||||
assert.equal(findController.getState().searchScope, null);
|
||||
});
|
||||
});
|
||||
|
||||
test('find term is added to history on state change', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
assert.deepEqual(['1', '2', '3'], toArray(findController.getHistory()));
|
||||
});
|
||||
});
|
||||
|
||||
test('find term is added with delay', (done) => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.delayUpdateHistory = true;
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
findController.delayedUpdateHistoryPromise.then(() => {
|
||||
assert.deepEqual(['3'], toArray(findController.getHistory()));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('show previous find term', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
assert.deepEqual('2', findController.getState().searchString);
|
||||
});
|
||||
});
|
||||
|
||||
test('show previous find term do not update history', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
assert.deepEqual(['1', '2', '3'], toArray(findController.getHistory()));
|
||||
});
|
||||
});
|
||||
|
||||
test('show next find term', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
findController.getState().change({ searchString: '4' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showNextFindTerm();
|
||||
assert.deepEqual('3', findController.getState().searchString);
|
||||
});
|
||||
});
|
||||
|
||||
test('show next find term do not update history', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
findController.getState().change({ searchString: '4' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showNextFindTerm();
|
||||
assert.deepEqual(['1', '2', '3', '4'], toArray(findController.getHistory()));
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction can work with multiline', () => {
|
||||
withMockCodeEditor([
|
||||
'',
|
||||
'qwe',
|
||||
'rty',
|
||||
'',
|
||||
'qwe',
|
||||
'',
|
||||
'rty',
|
||||
'qwe',
|
||||
'rty'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
editor.setSelection(new Selection(2, 1, 3, 4));
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 1, 3, 4],
|
||||
[8, 1, 9, 4]
|
||||
]);
|
||||
|
||||
editor.trigger('test', 'removeSecondaryCursors', null);
|
||||
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #6661: AddSelectionToNextFindMatchAction can work with touching ranges', () => {
|
||||
withMockCodeEditor([
|
||||
'abcabc',
|
||||
'abc',
|
||||
'abcabc',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
editor.setSelection(new Selection(1, 1, 1, 4));
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 1, 1, 4],
|
||||
[1, 4, 1, 7]
|
||||
]);
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 1, 1, 4],
|
||||
[1, 4, 1, 7],
|
||||
[2, 1, 2, 4],
|
||||
[3, 1, 3, 4],
|
||||
[3, 4, 3, 7]
|
||||
]);
|
||||
|
||||
editor.trigger('test', Handler.Type, { text: 'z' });
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 2, 1, 2],
|
||||
[1, 3, 1, 3],
|
||||
[2, 2, 2, 2],
|
||||
[3, 2, 3, 2],
|
||||
[3, 3, 3, 3]
|
||||
]);
|
||||
assert.equal(editor.getValue(), [
|
||||
'zz',
|
||||
'z',
|
||||
'zz',
|
||||
].join('\n'));
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #23541: Multiline Ctrl+D does not work in CRLF files', () => {
|
||||
withMockCodeEditor([
|
||||
'',
|
||||
'qwe',
|
||||
'rty',
|
||||
'',
|
||||
'qwe',
|
||||
'',
|
||||
'rty',
|
||||
'qwe',
|
||||
'rty'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
editor.getModel().setEOL(EndOfLineSequence.CRLF);
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
editor.setSelection(new Selection(2, 1, 3, 4));
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 1, 3, 4],
|
||||
[8, 1, 9, 4]
|
||||
]);
|
||||
|
||||
editor.trigger('test', 'removeSecondaryCursors', null);
|
||||
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #18111: Regex replace with single space replaces with no space', () => {
|
||||
withMockCodeEditor([
|
||||
'HRESULT OnAmbientPropertyChange(DISPID dispid);'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
|
||||
let startFindAction = new StartFindAction();
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false);
|
||||
findController.moveToNextMatch();
|
||||
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 39, 1, 42]
|
||||
]);
|
||||
|
||||
findController.replace();
|
||||
|
||||
assert.deepEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #24714: Regular expression with ^ in search & replace', () => {
|
||||
withMockCodeEditor([
|
||||
'',
|
||||
'line2',
|
||||
'line3'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
|
||||
let startFindAction = new StartFindAction();
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
findController.getState().change({ searchString: '^', replaceString: 'x', isRegex: true }, false);
|
||||
findController.moveToNextMatch();
|
||||
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 1, 2, 1]
|
||||
]);
|
||||
|
||||
findController.replace();
|
||||
|
||||
assert.deepEqual(editor.getValue(), '\nxline2\nline3');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
function toArray(historyNavigator: HistoryNavigator<string>): string[] {
|
||||
let result = [];
|
||||
historyNavigator.first();
|
||||
if (historyNavigator.current()) {
|
||||
do {
|
||||
result.push(historyNavigator.current());
|
||||
} while (historyNavigator.next());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function testAddSelectionToNextFindMatchAction(text: string[], callback: (editor: MockCodeEditor, action: AddSelectionToNextFindMatchAction, findController: TestFindController) => void): void {
|
||||
withMockCodeEditor(text, { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
|
||||
let action = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
callback(editor, action, findController);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with single collapsed selection', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 2, 1, 2),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 1)', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 2, 2, 2),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 2)', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 2, 1, 2),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with all collapsed selections', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 2, 1, 2),
|
||||
new Selection(2, 2, 2, 2),
|
||||
new Selection(3, 1, 3, 1),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with all collapsed selections on different words', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 6, 1, 6),
|
||||
new Selection(2, 6, 2, 6),
|
||||
new Selection(3, 6, 3, 6),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 5, 1, 10),
|
||||
new Selection(2, 5, 2, 10),
|
||||
new Selection(3, 5, 3, 8),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 5, 1, 10),
|
||||
new Selection(2, 5, 2, 10),
|
||||
new Selection(3, 5, 3, 8),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #20651: AddSelectionToNextFindMatchAction case insensitive', () => {
|
||||
const text = [
|
||||
'test',
|
||||
'testte',
|
||||
'Test',
|
||||
'testte',
|
||||
'test'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 1, 1, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
new Selection(4, 1, 4, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
new Selection(4, 1, 4, 5),
|
||||
new Selection(5, 1, 5, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
new Selection(4, 1, 4, 5),
|
||||
new Selection(5, 1, 5, 5),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('FindController query options persistence', () => {
|
||||
let queryState: { [key: string]: any; } = {};
|
||||
queryState['editor.isRegex'] = false;
|
||||
queryState['editor.matchCase'] = false;
|
||||
queryState['editor.wholeWord'] = false;
|
||||
let serviceCollection = new ServiceCollection();
|
||||
serviceCollection.set(IStorageService, <any>{
|
||||
get: (key: string) => queryState[key],
|
||||
getBoolean: (key: string) => !!queryState[key],
|
||||
store: (key: string, value: any) => { queryState[key] = value; }
|
||||
});
|
||||
|
||||
test('matchCase', () => {
|
||||
withMockCodeEditor([
|
||||
'abc',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': true, 'editor.wholeWord': false };
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
// I type ABC.
|
||||
findState.change({ searchString: 'ABC' }, true);
|
||||
// The second ABC is highlighted as matchCase is true.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 2, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true };
|
||||
|
||||
test('wholeWord', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC',
|
||||
'AB',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true };
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
// I type AB.
|
||||
findState.change({ searchString: 'AB' }, true);
|
||||
// The second AB is highlighted as wholeWord is true.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 2, 3]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('toggling options is saved', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC',
|
||||
'AB',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true };
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.toggleRegex();
|
||||
assert.equal(queryState['editor.isRegex'], true);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
2037
src/vs/editor/contrib/find/test/common/findModel.test.ts
Normal file
157
src/vs/editor/contrib/find/test/common/replacePattern.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { parseReplaceString, ReplacePattern, ReplacePiece } from 'vs/editor/contrib/find/common/replacePattern';
|
||||
|
||||
suite('Replace Pattern test', () => {
|
||||
|
||||
test('parse replace string', () => {
|
||||
let testParse = (input: string, expectedPieces: ReplacePiece[]) => {
|
||||
let actual = parseReplaceString(input);
|
||||
let expected = new ReplacePattern(expectedPieces);
|
||||
assert.deepEqual(actual, expected, 'Parsing ' + input);
|
||||
};
|
||||
|
||||
// no backslash => no treatment
|
||||
testParse('hello', [ReplacePiece.staticValue('hello')]);
|
||||
|
||||
// \t => TAB
|
||||
testParse('\\thello', [ReplacePiece.staticValue('\thello')]);
|
||||
testParse('h\\tello', [ReplacePiece.staticValue('h\tello')]);
|
||||
testParse('hello\\t', [ReplacePiece.staticValue('hello\t')]);
|
||||
|
||||
// \n => LF
|
||||
testParse('\\nhello', [ReplacePiece.staticValue('\nhello')]);
|
||||
|
||||
// \\t => \t
|
||||
testParse('\\\\thello', [ReplacePiece.staticValue('\\thello')]);
|
||||
testParse('h\\\\tello', [ReplacePiece.staticValue('h\\tello')]);
|
||||
testParse('hello\\\\t', [ReplacePiece.staticValue('hello\\t')]);
|
||||
|
||||
// \\\t => \TAB
|
||||
testParse('\\\\\\thello', [ReplacePiece.staticValue('\\\thello')]);
|
||||
|
||||
// \\\\t => \\t
|
||||
testParse('\\\\\\\\thello', [ReplacePiece.staticValue('\\\\thello')]);
|
||||
|
||||
// \ at the end => no treatment
|
||||
testParse('hello\\', [ReplacePiece.staticValue('hello\\')]);
|
||||
|
||||
// \ with unknown char => no treatment
|
||||
testParse('hello\\x', [ReplacePiece.staticValue('hello\\x')]);
|
||||
|
||||
// \ with back reference => no treatment
|
||||
testParse('hello\\0', [ReplacePiece.staticValue('hello\\0')]);
|
||||
|
||||
testParse('hello$&', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0)]);
|
||||
testParse('hello$0', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0)]);
|
||||
testParse('hello$02', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0), ReplacePiece.staticValue('2')]);
|
||||
testParse('hello$1', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1)]);
|
||||
testParse('hello$2', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(2)]);
|
||||
testParse('hello$9', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(9)]);
|
||||
testParse('$9hello', [ReplacePiece.matchIndex(9), ReplacePiece.staticValue('hello')]);
|
||||
|
||||
testParse('hello$12', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(12)]);
|
||||
testParse('hello$99', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(99)]);
|
||||
testParse('hello$99a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(99), ReplacePiece.staticValue('a')]);
|
||||
testParse('hello$1a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1), ReplacePiece.staticValue('a')]);
|
||||
testParse('hello$100', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('0')]);
|
||||
testParse('hello$100a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('0a')]);
|
||||
testParse('hello$10a0', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('a0')]);
|
||||
testParse('hello$$', [ReplacePiece.staticValue('hello$')]);
|
||||
testParse('hello$$0', [ReplacePiece.staticValue('hello$0')]);
|
||||
|
||||
testParse('hello$`', [ReplacePiece.staticValue('hello$`')]);
|
||||
testParse('hello$\'', [ReplacePiece.staticValue('hello$\'')]);
|
||||
});
|
||||
|
||||
test('replace has JavaScript semantics', () => {
|
||||
let testJSReplaceSemantics = (target: string, search: RegExp, replaceString: string, expected: string) => {
|
||||
let replacePattern = parseReplaceString(replaceString);
|
||||
let m = search.exec(target);
|
||||
let actual = replacePattern.buildReplaceString(m);
|
||||
|
||||
assert.deepEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`);
|
||||
};
|
||||
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello', 'hi'.replace(/hi/, 'hello'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\t', 'hi'.replace(/hi/, '\t'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\n', 'hi'.replace(/hi/, '\n'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\\\t', 'hi'.replace(/hi/, '\\t'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\\\n', 'hi'.replace(/hi/, '\\n'));
|
||||
|
||||
// implicit capture group 0
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$&', 'hi'.replace(/hi/, 'hello$&'));
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$0', 'hi'.replace(/hi/, 'hello$&'));
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$&1', 'hi'.replace(/hi/, 'hello$&1'));
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$01', 'hi'.replace(/hi/, 'hello$&1'));
|
||||
|
||||
// capture groups have funny semantics in replace strings
|
||||
// the replace string interprets $nn as a captured group only if it exists in the search regex
|
||||
testJSReplaceSemantics('hi', /(hi)/, 'hello$10', 'hi'.replace(/(hi)/, 'hello$10'));
|
||||
testJSReplaceSemantics('hi', /(hi)()()()()()()()()()/, 'hello$10', 'hi'.replace(/(hi)()()()()()()()()()/, 'hello$10'));
|
||||
testJSReplaceSemantics('hi', /(hi)/, 'hello$100', 'hi'.replace(/(hi)/, 'hello$100'));
|
||||
testJSReplaceSemantics('hi', /(hi)/, 'hello$20', 'hi'.replace(/(hi)/, 'hello$20'));
|
||||
});
|
||||
|
||||
test('get replace string if given text is a complete match', () => {
|
||||
function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void {
|
||||
let replacePattern = parseReplaceString(replaceString);
|
||||
let m = search.exec(target);
|
||||
let actual = replacePattern.buildReplaceString(m);
|
||||
|
||||
assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`);
|
||||
}
|
||||
|
||||
assertReplace('bla', /bla/, 'hello', 'hello');
|
||||
assertReplace('bla', /(bla)/, 'hello', 'hello');
|
||||
assertReplace('bla', /(bla)/, 'hello$0', 'hellobla');
|
||||
|
||||
let searchRegex = /let\s+(\w+)\s*=\s*require\s*\(\s*['"]([\w\.\-/]+)\s*['"]\s*\)\s*/;
|
||||
assertReplace('let fs = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as fs from \'fs\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as something from \'fs\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $1 from \'$1\';', 'import * as something from \'something\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $2 from \'$1\';', 'import * as fs from \'something\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $0 from \'$0\';', 'import * as let something = require(\'fs\') from \'let something = require(\'fs\')\';');
|
||||
assertReplace('let fs = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as fs from \'fs\';');
|
||||
assertReplace('for ()', /for(.*)/, 'cat$1', 'cat ()');
|
||||
|
||||
// issue #18111
|
||||
assertReplace('HRESULT OnAmbientPropertyChange(DISPID dispid);', /\b\s{3}\b/, ' ', ' ');
|
||||
});
|
||||
|
||||
test('get replace string if match is sub-string of the text', () => {
|
||||
function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void {
|
||||
let replacePattern = parseReplaceString(replaceString);
|
||||
let m = search.exec(target);
|
||||
let actual = replacePattern.buildReplaceString(m);
|
||||
|
||||
assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`);
|
||||
}
|
||||
assertReplace('this is a bla text', /bla/, 'hello', 'hello');
|
||||
assertReplace('this is a bla text', /this(?=.*bla)/, 'that', 'that');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1at', 'that');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1e', 'the');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1ere', 'there');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1', 'th');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, 'ma$1', 'math');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, 'ma$1s', 'maths');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$0', 'this');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$0$1', 'thisth');
|
||||
assertReplace('this is a bla text', /bla(?=\stext$)/, 'foo', 'foo');
|
||||
assertReplace('this is a bla text', /b(la)(?=\stext$)/, 'f$1', 'fla');
|
||||
assertReplace('this is a bla text', /b(la)(?=\stext$)/, 'f$0', 'fbla');
|
||||
assertReplace('this is a bla text', /b(la)(?=\stext$)/, '$0ah', 'blaah');
|
||||
});
|
||||
|
||||
test('issue #19740 Find and replace capture group/backreference inserts `undefined` instead of empty string', () => {
|
||||
let replacePattern = parseReplaceString('a{$1}');
|
||||
let matches = /a(z)?/.exec('abcd');
|
||||
let actual = replacePattern.buildReplaceString(matches);
|
||||
assert.equal(actual, 'a{}');
|
||||
});
|
||||
});
|
||||