/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { IAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; import * as errors from 'vs/base/common/errors'; import { anyEvent, debounceEvent, Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as paths from 'vs/base/common/paths'; import * as env from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { ITree } from 'vs/base/parts/tree/browser/tree'; import 'vs/css!./media/searchview'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IConfirmation, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TreeResourceNavigator, WorkbenchTree } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { IPatternInfo, IQueryOptions, ISearchComplete, ISearchConfiguration, ISearchHistoryService, ISearchProgressItem, ISearchQuery, VIEW_ID } from 'vs/platform/search/common/search'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { SimpleFileResourceDragAndDrop } from 'vs/workbench/browser/dnd'; import { Viewlet } from 'vs/workbench/browser/viewlet'; import { Scope } from 'vs/workbench/common/memento'; import { IPanel } from 'vs/workbench/common/panel'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/parts/search/browser/patternInputWidget'; import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction } from 'vs/workbench/parts/search/browser/searchActions'; import { SearchAccessibilityProvider, SearchDataSource, SearchFilter, SearchRenderer, SearchSorter, SearchTreeController } from 'vs/workbench/parts/search/browser/searchResultsView'; import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/parts/search/browser/searchWidget'; import * as Constants from 'vs/workbench/parts/search/common/constants'; import { QueryBuilder } from 'vs/workbench/parts/search/common/queryBuilder'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/parts/search/common/search'; import { FileMatch, FileMatchOrMatch, FolderMatch, IChangeEvent, ISearchWorkbenchService, Match, SearchModel } from 'vs/workbench/parts/search/common/searchModel'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; const $ = dom.$; export class SearchView extends Viewlet implements IViewlet, IPanel { private static readonly MAX_TEXT_RESULTS = 10000; private static readonly SHOW_REPLACE_STORAGE_KEY = 'vs.search.show.replace'; private static readonly WIDE_CLASS_NAME = 'wide'; private static readonly WIDE_VIEW_SIZE = 600; private isDisposed: boolean; private queryBuilder: QueryBuilder; private viewModel: SearchModel; private viewletVisible: IContextKey; private viewletFocused: IContextKey; private inputBoxFocused: IContextKey; private inputPatternIncludesFocused: IContextKey; private inputPatternExclusionsFocused: IContextKey; private firstMatchFocused: IContextKey; private fileMatchOrMatchFocused: IContextKey; private fileMatchOrFolderMatchFocus: IContextKey; private fileMatchFocused: IContextKey; private folderMatchFocused: IContextKey; private matchFocused: IContextKey; private hasSearchResultsKey: IContextKey; private searchSubmitted: boolean; private searching: boolean; private actions: (RefreshAction | CollapseDeepestExpandedLevelAction | ClearSearchResultsAction | CancelSearchAction)[] = []; private tree: WorkbenchTree; private viewletSettings: any; private messagesElement: HTMLElement; private messageDisposables: IDisposable[] = []; private searchWidgetsContainerElement: HTMLElement; private searchWidget: SearchWidget; private size: dom.Dimension; private queryDetails: HTMLElement; private toggleQueryDetailsButton: HTMLElement; private inputPatternExcludes: ExcludePatternInputWidget; private inputPatternIncludes: PatternInputWidget; private resultsElement: HTMLElement; private currentSelectedFileMatch: FileMatch; private readonly selectCurrentMatchEmitter: Emitter; private delayedRefresh: Delayer; private changedWhileHidden: boolean; private searchWithoutFolderMessageElement: HTMLElement; constructor( @IPartService partService: IPartService, @ITelemetryService telemetryService: ITelemetryService, @IFileService private fileService: IFileService, @IEditorService private editorService: IEditorService, @IProgressService private progressService: IProgressService, @INotificationService private notificationService: INotificationService, @IDialogService private dialogService: IDialogService, @IStorageService private storageService: IStorageService, @IContextViewService private contextViewService: IContextViewService, @IInstantiationService private instantiationService: IInstantiationService, @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, @ISearchWorkbenchService private searchWorkbenchService: ISearchWorkbenchService, @IContextKeyService private contextKeyService: IContextKeyService, @IReplaceService private replaceService: IReplaceService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IPreferencesService private preferencesService: IPreferencesService, @IThemeService protected themeService: IThemeService, @ISearchHistoryService private searchHistoryService: ISearchHistoryService, @IEditorGroupsService private editorGroupsService: IEditorGroupsService ) { super(VIEW_ID, partService, telemetryService, themeService); this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService); this.viewletFocused = Constants.SearchViewFocusedKey.bindTo(contextKeyService); this.inputBoxFocused = Constants.InputBoxFocusedKey.bindTo(this.contextKeyService); this.inputPatternIncludesFocused = Constants.PatternIncludesFocusedKey.bindTo(this.contextKeyService); this.inputPatternExclusionsFocused = Constants.PatternExcludesFocusedKey.bindTo(this.contextKeyService); this.firstMatchFocused = Constants.FirstMatchFocusKey.bindTo(contextKeyService); this.fileMatchOrMatchFocused = Constants.FileMatchOrMatchFocusKey.bindTo(contextKeyService); this.fileMatchOrFolderMatchFocus = Constants.FileMatchOrFolderMatchFocusKey.bindTo(contextKeyService); this.fileMatchFocused = Constants.FileFocusKey.bindTo(contextKeyService); this.folderMatchFocused = Constants.FolderFocusKey.bindTo(contextKeyService); this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService); this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService); this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); this.viewletSettings = this.getMemento(storageService, Scope.WORKSPACE); this._register(this.fileService.onFileChanges(e => this.onFilesChanged(e))); this._register(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDidChangeDirty(e))); this._register(this.contextService.onDidChangeWorkbenchState(() => this.onDidChangeWorkbenchState())); this._register(this.searchHistoryService.onDidClearHistory(() => this.clearHistory())); this.selectCurrentMatchEmitter = new Emitter(); debounceEvent(this.selectCurrentMatchEmitter.event, (l, e) => e, 100, /*leading=*/true) (() => this.selectCurrentMatch()); this.delayedRefresh = new Delayer(250); } private onDidChangeWorkbenchState(): void { if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.searchWithoutFolderMessageElement) { dom.hide(this.searchWithoutFolderMessageElement); } } public create(parent: HTMLElement): TPromise { super.create(parent); this.viewModel = this._register(this.searchWorkbenchService.searchModel); const containerElement = dom.append(parent, $('.search-view')); this.searchWidgetsContainerElement = dom.append(containerElement, $('.search-widgets-container')); this.createSearchWidget(this.searchWidgetsContainerElement); const history = this.searchHistoryService.load(); const filePatterns = this.viewletSettings['query.filePatterns'] || ''; const patternExclusions = this.viewletSettings['query.folderExclusions'] || ''; const patternExclusionsHistory: string[] = history.exclude || []; const patternIncludes = this.viewletSettings['query.folderIncludes'] || ''; const patternIncludesHistory: string[] = history.include || []; const queryDetailsExpanded = this.viewletSettings['query.queryDetailsExpanded'] || ''; const useExcludesAndIgnoreFiles = typeof this.viewletSettings['query.useExcludesAndIgnoreFiles'] === 'boolean' ? this.viewletSettings['query.useExcludesAndIgnoreFiles'] : true; this.queryDetails = dom.append(this.searchWidgetsContainerElement, $('.query-details')); // Toggle query details button this.toggleQueryDetailsButton = dom.append(this.queryDetails, $('.more', { tabindex: 0, role: 'button', title: nls.localize('moreSearch', "Toggle Search Details") })); this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.CLICK, e => { dom.EventHelper.stop(e); this.toggleQueryDetails(!this.isScreenReaderOptimized()); })); this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.KEY_UP, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { dom.EventHelper.stop(e); this.toggleQueryDetails(false); } })); this._register(dom.addDisposableListener(this.toggleQueryDetailsButton, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyMod.Shift | KeyCode.Tab)) { if (this.searchWidget.isReplaceActive()) { this.searchWidget.focusReplaceAllAction(); } else { this.searchWidget.focusRegexAction(); } dom.EventHelper.stop(e); } })); // folder includes list const folderIncludesList = dom.append(this.queryDetails, $('.file-types.includes')); const filesToIncludeTitle = nls.localize('searchScope.includes', "files to include"); dom.append(folderIncludesList, $('h4', undefined, filesToIncludeTitle)); this.inputPatternIncludes = this._register(this.instantiationService.createInstance(PatternInputWidget, folderIncludesList, this.contextViewService, { ariaLabel: nls.localize('label.includes', 'Search Include Patterns'), history: patternIncludesHistory, })); this.inputPatternIncludes.setValue(patternIncludes); this.inputPatternIncludes.onSubmit(() => this.onQueryChanged(true)); this.inputPatternIncludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget this.trackInputBox(this.inputPatternIncludes.inputFocusTracker, this.inputPatternIncludesFocused); // excludes list const excludesList = dom.append(this.queryDetails, $('.file-types.excludes')); const excludesTitle = nls.localize('searchScope.excludes', "files to exclude"); dom.append(excludesList, $('h4', undefined, excludesTitle)); this.inputPatternExcludes = this._register(this.instantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, { ariaLabel: nls.localize('label.excludes', 'Search Exclude Patterns'), history: patternExclusionsHistory, })); this.inputPatternExcludes.setValue(patternExclusions); this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(useExcludesAndIgnoreFiles); this.inputPatternExcludes.onSubmit(() => this.onQueryChanged(true)); this.inputPatternExcludes.onCancel(() => this.viewModel.cancelSearch()); // Cancel search without focusing the search widget this.trackInputBox(this.inputPatternExcludes.inputFocusTracker, this.inputPatternExclusionsFocused); this.messagesElement = dom.append(containerElement, $('.messages')); if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.showSearchWithoutFolderMessage(); } this.createSearchResultsView(containerElement); this.actions = [ this.instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL), this.instantiationService.createInstance(CollapseDeepestExpandedLevelAction, CollapseDeepestExpandedLevelAction.ID, CollapseDeepestExpandedLevelAction.LABEL), this.instantiationService.createInstance(ClearSearchResultsAction, ClearSearchResultsAction.ID, ClearSearchResultsAction.LABEL) ]; if (filePatterns !== '' || patternExclusions !== '' || patternIncludes !== '' || queryDetailsExpanded !== '' || !useExcludesAndIgnoreFiles) { this.toggleQueryDetails(true, true, true); } this._register(this.viewModel.searchResult.onChange((event) => this.onSearchResultsChanged(event))); this._register(this.onDidFocus(() => this.viewletFocused.set(true))); this._register(this.onDidBlur(() => this.viewletFocused.set(false))); return TPromise.as(null); } public get searchAndReplaceWidget(): SearchWidget { return this.searchWidget; } public get searchIncludePattern(): PatternInputWidget { return this.inputPatternIncludes; } public get searchExcludePattern(): PatternInputWidget { return this.inputPatternExcludes; } private updateActions(): void { for (const action of this.actions) { action.update(); } } private isScreenReaderOptimized() { const detected = browser.getAccessibilitySupport() === env.AccessibilitySupport.Enabled; const config = this.configurationService.getValue('editor').accessibilitySupport; return config === 'on' || (config === 'auto' && detected); } private createSearchWidget(container: HTMLElement): void { let contentPattern = this.viewletSettings['query.contentPattern'] || ''; let isRegex = this.viewletSettings['query.regex'] === true; let isWholeWords = this.viewletSettings['query.wholeWords'] === true; let isCaseSensitive = this.viewletSettings['query.caseSensitive'] === true; const history = this.searchHistoryService.load(); let searchHistory = history.search || this.viewletSettings['query.searchHistory'] || []; let replaceHistory = history.replace || this.viewletSettings['query.replaceHistory'] || []; this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, { value: contentPattern, isRegex: isRegex, isCaseSensitive: isCaseSensitive, isWholeWords: isWholeWords, searchHistory: searchHistory, replaceHistory: replaceHistory })); if (this.storageService.getBoolean(SearchView.SHOW_REPLACE_STORAGE_KEY, StorageScope.WORKSPACE, true)) { this.searchWidget.toggleReplace(true); } this._register(this.searchWidget.onSearchSubmit(() => this.onQueryChanged())); this._register(this.searchWidget.onSearchCancel(() => this.cancelSearch())); this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.onQueryChanged(true))); this._register(this.searchWidget.onReplaceToggled(() => this.onReplaceToggled())); this._register(this.searchWidget.onReplaceStateChange((state) => { this.viewModel.replaceActive = state; this.tree.refresh(); })); this._register(this.searchWidget.onReplaceValueChanged((value) => { this.viewModel.replaceString = this.searchWidget.getReplaceValue(); this.delayedRefresh.trigger(() => this.tree.refresh()); })); this._register(this.searchWidget.onBlur(() => { this.toggleQueryDetailsButton.focus(); })); this._register(this.searchWidget.onReplaceAll(() => this.replaceAll())); this.trackInputBox(this.searchWidget.searchInputFocusTracker); this.trackInputBox(this.searchWidget.replaceInputFocusTracker); } private trackInputBox(inputFocusTracker: dom.IFocusTracker, contextKey?: IContextKey): void { this._register(inputFocusTracker.onDidFocus(() => { this.inputBoxFocused.set(true); if (contextKey) { contextKey.set(true); } })); this._register(inputFocusTracker.onDidBlur(() => { this.inputBoxFocused.set(this.searchWidget.searchInputHasFocus() || this.searchWidget.replaceInputHasFocus() || this.inputPatternIncludes.inputHasFocus() || this.inputPatternExcludes.inputHasFocus()); if (contextKey) { contextKey.set(false); } })); } private onReplaceToggled(): void { this.layout(this.size); const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown(); if (!isReplaceShown) { this.storageService.store(SearchView.SHOW_REPLACE_STORAGE_KEY, false, StorageScope.WORKSPACE); } else { this.storageService.remove(SearchView.SHOW_REPLACE_STORAGE_KEY, StorageScope.WORKSPACE); } } private onSearchResultsChanged(event?: IChangeEvent): TPromise { if (this.isVisible()) { return this.refreshAndUpdateCount(event); } else { this.changedWhileHidden = true; return TPromise.wrap(null); } } private refreshAndUpdateCount(event?: IChangeEvent): TPromise { return this.refreshTree(event).then(() => { this.searchWidget.setReplaceAllActionState(!this.viewModel.searchResult.isEmpty()); this.updateSearchResultCount(); }); } private refreshTree(event?: IChangeEvent): TPromise { if (!event || event.added || event.removed) { return this.tree.refresh(this.viewModel.searchResult); } else { if (event.elements.length === 1) { return this.tree.refresh(event.elements[0]); } else { return this.tree.refresh(event.elements); } } } private replaceAll(): void { if (this.viewModel.searchResult.count() === 0) { return; } let progressRunner = this.progressService.show(100); let occurrences = this.viewModel.searchResult.count(); let fileCount = this.viewModel.searchResult.fileCount(); let replaceValue = this.searchWidget.getReplaceValue() || ''; let afterReplaceAllMessage = this.buildAfterReplaceAllMessage(occurrences, fileCount, replaceValue); let confirmation: IConfirmation = { title: nls.localize('replaceAll.confirmation.title', "Replace All"), message: this.buildReplaceAllConfirmationMessage(occurrences, fileCount, replaceValue), primaryButton: nls.localize('replaceAll.confirm.button', "&&Replace"), type: 'question' }; this.dialogService.confirm(confirmation).then(res => { if (res.confirmed) { this.searchWidget.setReplaceAllActionState(false); this.viewModel.searchResult.replaceAll(progressRunner).then(() => { progressRunner.done(); const messageEl = this.clearMessage(); dom.append(messageEl, $('p', undefined, afterReplaceAllMessage)); }, (error) => { progressRunner.done(); errors.isPromiseCanceledError(error); this.notificationService.error(error); }); } }); } private buildAfterReplaceAllMessage(occurrences: number, fileCount: number, replaceValue?: string) { if (occurrences === 1) { if (fileCount === 1) { if (replaceValue) { return nls.localize('replaceAll.occurrence.file.message', "Replaced {0} occurrence across {1} file with '{2}'.", occurrences, fileCount, replaceValue); } return nls.localize('removeAll.occurrence.file.message', "Replaced {0} occurrence across {1} file'.", occurrences, fileCount); } if (replaceValue) { return nls.localize('replaceAll.occurrence.files.message', "Replaced {0} occurrence across {1} files with '{2}'.", occurrences, fileCount, replaceValue); } return nls.localize('removeAll.occurrence.files.message', "Replaced {0} occurrence across {1} files.", occurrences, fileCount); } if (fileCount === 1) { if (replaceValue) { return nls.localize('replaceAll.occurrences.file.message', "Replaced {0} occurrences across {1} file with '{2}'.", occurrences, fileCount, replaceValue); } return nls.localize('removeAll.occurrences.file.message', "Replaced {0} occurrences across {1} file'.", occurrences, fileCount); } if (replaceValue) { return nls.localize('replaceAll.occurrences.files.message', "Replaced {0} occurrences across {1} files with '{2}'.", occurrences, fileCount, replaceValue); } return nls.localize('removeAll.occurrences.files.message', "Replaced {0} occurrences across {1} files.", occurrences, fileCount); } private buildReplaceAllConfirmationMessage(occurrences: number, fileCount: number, replaceValue?: string) { if (occurrences === 1) { if (fileCount === 1) { if (replaceValue) { return nls.localize('removeAll.occurrence.file.confirmation.message', "Replace {0} occurrence across {1} file with '{2}'?", occurrences, fileCount, replaceValue); } return nls.localize('replaceAll.occurrence.file.confirmation.message', "Replace {0} occurrence across {1} file'?", occurrences, fileCount); } if (replaceValue) { return nls.localize('removeAll.occurrence.files.confirmation.message', "Replace {0} occurrence across {1} files with '{2}'?", occurrences, fileCount, replaceValue); } return nls.localize('replaceAll.occurrence.files.confirmation.message', "Replace {0} occurrence across {1} files?", occurrences, fileCount); } if (fileCount === 1) { if (replaceValue) { return nls.localize('removeAll.occurrences.file.confirmation.message', "Replace {0} occurrences across {1} file with '{2}'?", occurrences, fileCount, replaceValue); } return nls.localize('replaceAll.occurrences.file.confirmation.message', "Replace {0} occurrences across {1} file'?", occurrences, fileCount); } if (replaceValue) { return nls.localize('removeAll.occurrences.files.confirmation.message', "Replace {0} occurrences across {1} files with '{2}'?", occurrences, fileCount, replaceValue); } return nls.localize('replaceAll.occurrences.files.confirmation.message', "Replace {0} occurrences across {1} files?", occurrences, fileCount); } private clearMessage(): HTMLElement { this.searchWithoutFolderMessageElement = void 0; dom.clearNode(this.messagesElement); dom.show(this.messagesElement); dispose(this.messageDisposables); this.messageDisposables = []; return dom.append(this.messagesElement, $('.message')); } private createSearchResultsView(container: HTMLElement): void { this.resultsElement = dom.append(container, $('.results.show-file-icons')); const dataSource = this._register(this.instantiationService.createInstance(SearchDataSource)); const renderer = this._register(this.instantiationService.createInstance(SearchRenderer, this.getActionRunner(), this)); const dnd = this.instantiationService.createInstance(SimpleFileResourceDragAndDrop, (obj: any) => obj instanceof FileMatch ? obj.resource() : void 0); this.tree = this._register(this.instantiationService.createInstance(WorkbenchTree, this.resultsElement, { dataSource: dataSource, renderer: renderer, sorter: new SearchSorter(), filter: new SearchFilter(), controller: this.instantiationService.createInstance(SearchTreeController), accessibilityProvider: this.instantiationService.createInstance(SearchAccessibilityProvider), dnd }, { ariaLabel: nls.localize('treeAriaLabel', "Search Results"), showLoading: false })); this.tree.setInput(this.viewModel.searchResult); const searchResultsNavigator = this._register(new TreeResourceNavigator(this.tree, { openOnFocus: true })); this._register(debounceEvent(searchResultsNavigator.openResource, (last, event) => event, 75, true)(options => { if (options.element instanceof Match) { let selectedMatch: Match = options.element; if (this.currentSelectedFileMatch) { this.currentSelectedFileMatch.setSelectedMatch(null); } this.currentSelectedFileMatch = selectedMatch.parent(); this.currentSelectedFileMatch.setSelectedMatch(selectedMatch); if (!(options.payload && options.payload.preventEditorOpen)) { this.onFocus(selectedMatch, options.editorOptions.preserveFocus, options.sideBySide, options.editorOptions.pinned); } } })); this._register(anyEvent(this.tree.onDidFocus, this.tree.onDidChangeFocus)(() => { if (this.tree.isDOMFocused()) { const focus = this.tree.getFocus(); this.firstMatchFocused.set(this.tree.getNavigator().first() === focus); this.fileMatchOrMatchFocused.set(!!focus); this.fileMatchFocused.set(focus instanceof FileMatch); this.folderMatchFocused.set(focus instanceof FolderMatch); this.matchFocused.set(focus instanceof Match); this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch); } })); this._register(this.tree.onDidBlur(e => { this.firstMatchFocused.reset(); this.fileMatchOrMatchFocused.reset(); this.fileMatchFocused.reset(); this.folderMatchFocused.reset(); this.matchFocused.reset(); this.fileMatchOrFolderMatchFocus.reset(); })); } public selectCurrentMatch(): void { const focused = this.tree.getFocus(); const eventPayload = { focusEditor: true }; this.tree.setSelection([focused], eventPayload); } public selectNextMatch(): void { const [selected]: FileMatchOrMatch[] = this.tree.getSelection(); // Expand the initial selected node, if needed if (selected instanceof FileMatch) { if (!this.tree.isExpanded(selected)) { this.tree.expand(selected); } } let navigator = this.tree.getNavigator(selected, /*subTreeOnly=*/false); let next = navigator.next(); if (!next) { // Reached the end - get a new navigator from the root. // .first and .last only work when subTreeOnly = true. Maybe there's a simpler way. navigator = this.tree.getNavigator(this.tree.getInput(), /*subTreeOnly*/true); next = navigator.first(); } // Expand and go past FileMatch nodes while (!(next instanceof Match)) { if (!this.tree.isExpanded(next)) { this.tree.expand(next); } // Select the FileMatch's first child next = navigator.next(); } // Reveal the newly selected element if (next) { const eventPayload = { preventEditorOpen: true }; this.tree.setFocus(next, eventPayload); this.tree.setSelection([next], eventPayload); this.tree.reveal(next); this.selectCurrentMatchEmitter.fire(); } } public selectPreviousMatch(): void { const [selected]: FileMatchOrMatch[] = this.tree.getSelection(); let navigator = this.tree.getNavigator(selected, /*subTreeOnly=*/false); let prev = navigator.previous(); // Expand and go past FileMatch nodes if (!(prev instanceof Match)) { prev = navigator.previous(); if (!prev) { // Wrap around. Get a new tree starting from the root navigator = this.tree.getNavigator(this.tree.getInput(), /*subTreeOnly*/true); prev = navigator.last(); // This is complicated because .last will set the navigator to the last FileMatch, // so expand it and FF to its last child this.tree.expand(prev); let tmp; while (tmp = navigator.next()) { prev = tmp; } } if (!(prev instanceof Match)) { // There is a second non-Match result, which must be a collapsed FileMatch. // Expand it then select its last child. navigator.next(); this.tree.expand(prev); prev = navigator.previous(); } } // Reveal the newly selected element if (prev) { const eventPayload = { preventEditorOpen: true }; this.tree.setFocus(prev, eventPayload); this.tree.setSelection([prev], eventPayload); this.tree.reveal(prev); this.selectCurrentMatchEmitter.fire(); } } public setVisible(visible: boolean): TPromise { let promise: TPromise; this.viewletVisible.set(visible); if (visible) { if (this.changedWhileHidden) { // Render if results changed while viewlet was hidden - #37818 this.refreshAndUpdateCount(); this.changedWhileHidden = false; } promise = super.setVisible(visible); this.tree.onVisible(); } else { this.tree.onHidden(); promise = super.setVisible(visible); } // Enable highlights if there are searchresults if (this.viewModel) { this.viewModel.searchResult.toggleHighlights(visible); } // Open focused element from results in case the editor area is otherwise empty if (visible && !this.editorService.activeEditor) { let focus = this.tree.getFocus(); if (focus) { this.onFocus(focus, true); } } return promise; } public moveFocusToResults(): void { this.tree.domFocus(); } public focus(): void { super.focus(); const updatedText = this.updateTextFromSelection(); this.searchWidget.focus(undefined, undefined, updatedText); } public updateTextFromSelection(allowUnselectedWord = true): boolean { let updatedText = false; const seedSearchStringFromSelection = this.configurationService.getValue('editor').find.seedSearchStringFromSelection; if (seedSearchStringFromSelection) { let selectedText = this.getSearchTextFromEditor(allowUnselectedWord); if (selectedText) { if (this.searchWidget.searchInput.getRegex()) { selectedText = strings.escapeRegExpCharacters(selectedText); } this.searchWidget.searchInput.setValue(selectedText); updatedText = true; } } return updatedText; } public focusNextInputBox(): void { if (this.searchWidget.searchInputHasFocus()) { if (this.searchWidget.isReplaceShown()) { this.searchWidget.focus(true, true); } else { this.moveFocusFromSearchOrReplace(); } return; } if (this.searchWidget.replaceInputHasFocus()) { this.moveFocusFromSearchOrReplace(); return; } if (this.inputPatternIncludes.inputHasFocus()) { this.inputPatternExcludes.focus(); this.inputPatternExcludes.select(); return; } if (this.inputPatternExcludes.inputHasFocus()) { this.selectTreeIfNotSelected(); return; } } private moveFocusFromSearchOrReplace() { if (this.showsFileTypes()) { this.toggleQueryDetails(true, this.showsFileTypes()); } else { this.selectTreeIfNotSelected(); } } public focusPreviousInputBox(): void { if (this.searchWidget.searchInputHasFocus()) { return; } if (this.searchWidget.replaceInputHasFocus()) { this.searchWidget.focus(true); return; } if (this.inputPatternIncludes.inputHasFocus()) { this.searchWidget.focus(true, true); return; } if (this.inputPatternExcludes.inputHasFocus()) { this.inputPatternIncludes.focus(); this.inputPatternIncludes.select(); return; } if (this.tree.isDOMFocused()) { this.moveFocusFromResults(); return; } } private moveFocusFromResults(): void { if (this.showsFileTypes()) { this.toggleQueryDetails(true, true, false, true); } else { this.searchWidget.focus(true, true); } } private reLayout(): void { if (this.isDisposed) { return; } if (this.size.width >= SearchView.WIDE_VIEW_SIZE) { dom.addClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); } else { dom.removeClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); } this.searchWidget.setWidth(this.size.width - 28 /* container margin */); this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */); this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */); const messagesSize = this.messagesElement.style.display === 'none' ? 0 : dom.getTotalHeight(this.messagesElement); const searchResultContainerSize = this.size.height - messagesSize - dom.getTotalHeight(this.searchWidgetsContainerElement); this.resultsElement.style.height = searchResultContainerSize + 'px'; this.tree.layout(searchResultContainerSize); } public layout(dimension: dom.Dimension): void { this.size = dimension; this.reLayout(); } public getControl(): ITree { return this.tree; } public isSearchSubmitted(): boolean { return this.searchSubmitted; } public isSearching(): boolean { return this.searching; } public hasSearchResults(): boolean { return !this.viewModel.searchResult.isEmpty(); } public clearSearchResults(): void { this.viewModel.searchResult.clear(); this.showEmptyStage(); if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.showSearchWithoutFolderMessage(); } this.searchWidget.clear(); this.viewModel.cancelSearch(); } public cancelSearch(): boolean { if (this.viewModel.cancelSearch()) { this.searchWidget.focus(); return true; } return false; } private selectTreeIfNotSelected(): void { if (this.tree.getInput()) { this.tree.domFocus(); let selection = this.tree.getSelection(); if (selection.length === 0) { this.tree.focusNext(); } } } private getSearchTextFromEditor(allowUnselectedWord: boolean): string { if (!this.editorService.activeEditor) { return null; } if (dom.isAncestor(document.activeElement, this.getContainer())) { return null; } let activeTextEditorWidget = this.editorService.activeTextEditorWidget; if (isDiffEditor(activeTextEditorWidget)) { if (activeTextEditorWidget.getOriginalEditor().hasTextFocus()) { activeTextEditorWidget = activeTextEditorWidget.getOriginalEditor(); } else { activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor(); } } if (!isCodeEditor(activeTextEditorWidget)) { return null; } const range = activeTextEditorWidget.getSelection(); if (!range) { return null; } if (range.isEmpty() && !this.searchWidget.searchInput.getValue() && allowUnselectedWord) { const wordAtPosition = activeTextEditorWidget.getModel().getWordAtPosition(range.getStartPosition()); if (wordAtPosition) { return wordAtPosition.word; } } if (!range.isEmpty() && range.startLineNumber === range.endLineNumber) { let searchText = activeTextEditorWidget.getModel().getLineContent(range.startLineNumber); searchText = searchText.substring(range.startColumn - 1, range.endColumn - 1); return searchText; } return null; } private showsFileTypes(): boolean { return dom.hasClass(this.queryDetails, 'more'); } public toggleCaseSensitive(): void { this.searchWidget.searchInput.setCaseSensitive(!this.searchWidget.searchInput.getCaseSensitive()); this.onQueryChanged(true); } public toggleWholeWords(): void { this.searchWidget.searchInput.setWholeWords(!this.searchWidget.searchInput.getWholeWords()); this.onQueryChanged(true); } public toggleRegex(): void { this.searchWidget.searchInput.setRegex(!this.searchWidget.searchInput.getRegex()); this.onQueryChanged(true); } public toggleQueryDetails(moveFocus = true, show?: boolean, skipLayout?: boolean, reverse?: boolean): void { let cls = 'more'; show = typeof show === 'undefined' ? !dom.hasClass(this.queryDetails, cls) : Boolean(show); this.viewletSettings['query.queryDetailsExpanded'] = show; skipLayout = Boolean(skipLayout); if (show) { this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'true'); dom.addClass(this.queryDetails, cls); if (moveFocus) { if (reverse) { this.inputPatternExcludes.focus(); this.inputPatternExcludes.select(); } else { this.inputPatternIncludes.focus(); this.inputPatternIncludes.select(); } } } else { this.toggleQueryDetailsButton.setAttribute('aria-expanded', 'false'); dom.removeClass(this.queryDetails, cls); if (moveFocus) { this.searchWidget.focus(); } } if (!skipLayout && this.size) { this.layout(this.size); } } public searchInFolders(resources: URI[], pathToRelative: (from: string, to: string) => string): void { const folderPaths: string[] = []; const workspace = this.contextService.getWorkspace(); if (resources) { resources.forEach(resource => { let folderPath: string; if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { // Show relative path from the root for single-root mode folderPath = paths.normalize(pathToRelative(workspace.folders[0].uri.fsPath, resource.fsPath)); if (folderPath && folderPath !== '.') { folderPath = './' + folderPath; } } else { const owningFolder = this.contextService.getWorkspaceFolder(resource); if (owningFolder) { const owningRootName = owningFolder.name; // If this root is the only one with its basename, use a relative ./ path. If there is another, use an absolute path const isUniqueFolder = workspace.folders.filter(folder => folder.name === owningRootName).length === 1; if (isUniqueFolder) { const relativePath = paths.normalize(pathToRelative(owningFolder.uri.fsPath, resource.fsPath)); if (relativePath === '.') { folderPath = `./${owningFolder.name}`; } else { folderPath = `./${owningFolder.name}/${relativePath}`; } } else { folderPath = resource.fsPath; } } } if (folderPath) { folderPaths.push(folderPath); } }); } if (!folderPaths.length || folderPaths.some(folderPath => folderPath === '.')) { this.inputPatternIncludes.setValue(''); this.searchWidget.focus(); return; } // Show 'files to include' box if (!this.showsFileTypes()) { this.toggleQueryDetails(true, true); } this.inputPatternIncludes.setValue(folderPaths.join(', ')); this.searchWidget.focus(false); } public onQueryChanged(preserveFocus?: boolean): void { const isRegex = this.searchWidget.searchInput.getRegex(); const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); const excludePatternText = this.inputPatternExcludes.getValue().trim(); const includePatternText = this.inputPatternIncludes.getValue().trim(); const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); if (contentPattern.length === 0) { return; } // Validate regex is OK if (isRegex) { let regExp: RegExp; try { regExp = new RegExp(contentPattern); } catch (e) { return; // malformed regex } if (strings.regExpLeadsToEndlessLoop(regExp)) { return; // endless regex } } const content: IPatternInfo = { pattern: contentPattern, isRegExp: isRegex, isCaseSensitive: isCaseSensitive, isWordMatch: isWholeWords, isSmartCase: this.configurationService.getValue().search.smartCase }; const excludePattern = this.inputPatternExcludes.getValue(); const includePattern = this.inputPatternIncludes.getValue(); // Need the full match line to correctly calculate replace text, if this is a search/replace with regex group references ($1, $2, ...). // 10000 chars is enough to avoid sending huge amounts of text around, if you do a replace with a longer match, it may or may not resolve the group refs correctly. // https://github.com/Microsoft/vscode/issues/58374 const charsPerLine = content.isRegExp ? 10000 : 250; const options: IQueryOptions = { extraFileResources: getOutOfWorkspaceEditorResources(this.editorService, this.contextService), maxResults: SearchView.MAX_TEXT_RESULTS, disregardIgnoreFiles: !useExcludesAndIgnoreFiles, disregardExcludeSettings: !useExcludesAndIgnoreFiles, excludePattern, includePattern, previewOptions: { matchLines: 1, charsPerLine } }; const folderResources = this.contextService.getWorkspace().folders; const onQueryValidationError = (err: Error) => { this.searchWidget.searchInput.showMessage({ content: err.message, type: MessageType.ERROR }); this.viewModel.searchResult.clear(); }; let query: ISearchQuery; try { query = this.queryBuilder.text(content, folderResources.map(folder => folder.uri), options); } catch (err) { onQueryValidationError(err); return; } this.validateQuery(query).then(() => { this.onQueryTriggered(query, excludePatternText, includePatternText); if (!preserveFocus) { this.searchWidget.focus(false); // focus back to input field } }, onQueryValidationError); } private validateQuery(query: ISearchQuery): TPromise { // Validate folderQueries const folderQueriesExistP = query.folderQueries.map(fq => { return this.fileService.existsFile(fq.folder); }); return TPromise.join(folderQueriesExistP).then(existResults => { // If no folders exist, show an error message about the first one const existingFolderQueries = query.folderQueries.filter((folderQuery, i) => existResults[i]); if (!query.folderQueries.length || existingFolderQueries.length) { query.folderQueries = existingFolderQueries; } else { const nonExistantPath = query.folderQueries[0].folder.fsPath; const searchPathNotFoundError = nls.localize('searchPathNotFoundError', "Search path not found: {0}", nonExistantPath); return TPromise.wrapError(new Error(searchPathNotFoundError)); } return undefined; }); } private onQueryTriggered(query: ISearchQuery, excludePatternText: string, includePatternText: string): void { this.inputPatternExcludes.onSearchSubmit(); this.inputPatternIncludes.onSearchSubmit(); this.viewModel.cancelSearch(); // Progress total is 100.0% for more progress bar granularity let progressTotal = 1000; let progressWorked = 0; let progressRunner = query.useRipgrep ? this.progressService.show(/*infinite=*/true) : this.progressService.show(progressTotal); this.searchWidget.searchInput.clearMessage(); this.searching = true; setTimeout(() => { if (this.searching) { this.changeActionAtPosition(0, this.instantiationService.createInstance(CancelSearchAction, CancelSearchAction.ID, CancelSearchAction.LABEL)); } }, 2000); this.showEmptyStage(); let onComplete = (completed?: ISearchComplete) => { this.searching = false; this.changeActionAtPosition(0, this.instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL)); // Complete up to 100% as needed if (completed && !query.useRipgrep) { progressRunner.worked(progressTotal - progressWorked); setTimeout(() => progressRunner.done(), 200); } else { progressRunner.done(); } // Do final render, then expand if just 1 file with less than 50 matches this.onSearchResultsChanged().then(() => { if (this.viewModel.searchResult.count() === 1) { const onlyMatch = this.viewModel.searchResult.matches()[0]; if (onlyMatch.count() < 50) { return this.tree.expand(onlyMatch); } } return null; }); this.viewModel.replaceString = this.searchWidget.getReplaceValue(); let hasResults = !this.viewModel.searchResult.isEmpty(); this.searchSubmitted = true; this.updateActions(); if (completed && completed.limitHit) { this.searchWidget.searchInput.showMessage({ content: nls.localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Please be more specific in your search to narrow down the results."), type: MessageType.WARNING }); } if (!hasResults) { let hasExcludes = !!excludePatternText; let hasIncludes = !!includePatternText; let message: string; if (!completed) { message = nls.localize('searchCanceled', "Search was canceled before any results could be found - "); } else if (hasIncludes && hasExcludes) { message = nls.localize('noResultsIncludesExcludes', "No results found in '{0}' excluding '{1}' - ", includePatternText, excludePatternText); } else if (hasIncludes) { message = nls.localize('noResultsIncludes', "No results found in '{0}' - ", includePatternText); } else if (hasExcludes) { message = nls.localize('noResultsExcludes', "No results found excluding '{0}' - ", excludePatternText); } else { message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and ignore files - "); } // Indicate as status to ARIA aria.status(message); this.tree.onHidden(); dom.hide(this.resultsElement); const messageEl = this.clearMessage(); const p = dom.append(messageEl, $('p', undefined, message)); if (!completed) { const searchAgainLink = dom.append(p, $('a.pointer.prominent', undefined, nls.localize('rerunSearch.message', "Search again"))); this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); this.onQueryChanged(); })); } else if (hasIncludes || hasExcludes) { const searchAgainLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('rerunSearchInAll.message', "Search again in all files"))); this.messageDisposables.push(dom.addDisposableListener(searchAgainLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); this.inputPatternExcludes.setValue(''); this.inputPatternIncludes.setValue(''); this.onQueryChanged(); })); } else { const openSettingsLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.message', "Open Settings"))); this.messageDisposables.push(dom.addDisposableListener(openSettingsLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); const options: ISettingsEditorOptions = { query: '.exclude' }; this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? this.preferencesService.openWorkspaceSettings(undefined, options) : this.preferencesService.openGlobalSettings(undefined, options); })); } if (completed) { dom.append(p, $('span', undefined, ' - ')); const learnMoreLink = dom.append(p, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openSettings.learnMore', "Learn More"))); this.messageDisposables.push(dom.addDisposableListener(learnMoreLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); window.open('https://go.microsoft.com/fwlink/?linkid=853977'); })); } if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.showSearchWithoutFolderMessage(); } } else { this.viewModel.searchResult.toggleHighlights(this.isVisible()); // show highlights // Indicate final search result count for ARIA aria.status(nls.localize('ariaSearchResultsStatus', "Search returned {0} results in {1} files", this.viewModel.searchResult.count(), this.viewModel.searchResult.fileCount())); } }; let onError = (e: any) => { if (errors.isPromiseCanceledError(e)) { onComplete(null); } else { this.searching = false; this.changeActionAtPosition(0, this.instantiationService.createInstance(RefreshAction, RefreshAction.ID, RefreshAction.LABEL)); progressRunner.done(); this.searchWidget.searchInput.showMessage({ content: e.message, type: MessageType.ERROR }); this.viewModel.searchResult.clear(); } }; let total: number = 0; let worked: number = 0; let visibleMatches = 0; let onProgress = (p: ISearchProgressItem) => { // Progress if (p.total) { total = p.total; } if (p.worked) { worked = p.worked; } }; // Handle UI updates in an interval to show frequent progress and results let uiRefreshHandle = setInterval(() => { if (!this.searching) { window.clearInterval(uiRefreshHandle); return; } if (!query.useRipgrep) { // Progress bar update let fakeProgress = true; if (total > 0 && worked > 0) { let ratio = Math.round((worked / total) * progressTotal); if (ratio > progressWorked) { // never show less progress than what we have already progressRunner.worked(ratio - progressWorked); progressWorked = ratio; fakeProgress = false; } } // Fake progress up to 90%, or when actual progress beats it const fakeMax = 900; const fakeMultiplier = 12; if (fakeProgress && progressWorked < fakeMax) { // Linearly decrease the rate of fake progress. // 1 is the smallest allowed amount of progress. const fakeAmt = Math.round((fakeMax - progressWorked) / fakeMax * fakeMultiplier) || 1; progressWorked += fakeAmt; progressRunner.worked(fakeAmt); } } // Search result tree update const fileCount = this.viewModel.searchResult.fileCount(); if (visibleMatches !== fileCount) { visibleMatches = fileCount; this.tree.refresh(); this.updateSearchResultCount(); } if (fileCount > 0) { this.updateActions(); } }, 100); this.searchWidget.setReplaceAllActionState(false); this.viewModel.search(query, onProgress).then(onComplete, onError); } private updateSearchResultCount(): void { const fileCount = this.viewModel.searchResult.fileCount(); this.hasSearchResultsKey.set(fileCount > 0); const msgWasHidden = this.messagesElement.style.display === 'none'; if (fileCount > 0) { const messageEl = this.clearMessage(); dom.append(messageEl, $('p', undefined, this.buildResultCountMessage(this.viewModel.searchResult.count(), fileCount))); if (msgWasHidden) { this.reLayout(); } } else if (!msgWasHidden) { dom.hide(this.messagesElement); } } private buildResultCountMessage(resultCount: number, fileCount: number): string { if (resultCount === 1 && fileCount === 1) { return nls.localize('search.file.result', "{0} result in {1} file", resultCount, fileCount); } else if (resultCount === 1) { return nls.localize('search.files.result', "{0} result in {1} files", resultCount, fileCount); } else if (fileCount === 1) { return nls.localize('search.file.results', "{0} results in {1} file", resultCount, fileCount); } else { return nls.localize('search.files.results', "{0} results in {1} files", resultCount, fileCount); } } private showSearchWithoutFolderMessage(): void { this.searchWithoutFolderMessageElement = this.clearMessage(); const textEl = dom.append(this.searchWithoutFolderMessageElement, $('p', undefined, nls.localize('searchWithoutFolder', "You have not yet opened a folder. Only open files are currently searched - "))); const openFolderLink = dom.append(textEl, $('a.pointer.prominent', { tabindex: 0 }, nls.localize('openFolder', "Open Folder"))); this.messageDisposables.push(dom.addDisposableListener(openFolderLink, dom.EventType.CLICK, (e: MouseEvent) => { dom.EventHelper.stop(e, false); const actionClass = env.isMacintosh ? OpenFileFolderAction : OpenFolderAction; const action = this.instantiationService.createInstance(actionClass, actionClass.ID, actionClass.LABEL); this.actionRunner.run(action).then(() => { action.dispose(); }, err => { action.dispose(); errors.onUnexpectedError(err); }); })); } private showEmptyStage(): void { // disable 'result'-actions this.searchSubmitted = false; this.updateActions(); // clean up ui // this.replaceService.disposeAllReplacePreviews(); dom.hide(this.messagesElement); dom.show(this.resultsElement); this.tree.onVisible(); this.currentSelectedFileMatch = null; } private onFocus(lineMatch: any, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { if (!(lineMatch instanceof Match)) { this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange(); return TPromise.as(true); } return (this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) ? this.replaceService.openReplacePreview(lineMatch, preserveFocus, sideBySide, pinned) : this.open(lineMatch, preserveFocus, sideBySide, pinned); } public open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { const selection = this.getSelectionFrom(element); const resource = element instanceof Match ? element.parent().resource() : (element).resource(); return this.editorService.openEditor({ resource: resource, options: { preserveFocus, pinned, selection, revealIfVisible: true } }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(editor => { if (editor && element instanceof Match && preserveFocus) { this.viewModel.searchResult.rangeHighlightDecorations.highlightRange( (editor.getControl()).getModel(), element.range() ); } else { this.viewModel.searchResult.rangeHighlightDecorations.removeHighlightRange(); } return this.editorGroupsService.activateGroup(editor.group); }, errors.onUnexpectedError); } private getSelectionFrom(element: FileMatchOrMatch): any { let match: Match = null; if (element instanceof Match) { match = element; } if (element instanceof FileMatch && element.count() > 0) { match = element.matches()[element.matches().length - 1]; } if (match) { let range = match.range(); if (this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) { let replaceString = match.replaceString; return { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.startLineNumber, endColumn: range.startColumn + replaceString.length }; } return range; } return void 0; } private onUntitledDidChangeDirty(resource: URI): void { if (!this.viewModel) { return; } // remove search results from this resource as it got disposed if (!this.untitledEditorService.isDirty(resource)) { let matches = this.viewModel.searchResult.matches(); for (let i = 0, len = matches.length; i < len; i++) { if (resource.toString() === matches[i].resource().toString()) { this.viewModel.searchResult.remove(matches[i]); } } } } private onFilesChanged(e: FileChangesEvent): void { if (!this.viewModel) { return; } let matches = this.viewModel.searchResult.matches(); for (let i = 0, len = matches.length; i < len; i++) { if (e.contains(matches[i].resource(), FileChangeType.DELETED)) { this.viewModel.searchResult.remove(matches[i]); } } } public getActions(): IAction[] { return this.actions; } private changeActionAtPosition(index: number, newAction: ClearSearchResultsAction | CancelSearchAction | RefreshAction | CollapseDeepestExpandedLevelAction): void { this.actions.splice(index, 1, newAction); this.updateTitleArea(); } private clearHistory(): void { this.searchWidget.clearHistory(); this.inputPatternExcludes.clearHistory(); this.inputPatternIncludes.clearHistory(); } public shutdown(): void { const isRegex = this.searchWidget.searchInput.getRegex(); const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); const patternExcludes = this.inputPatternExcludes.getValue().trim(); const patternIncludes = this.inputPatternIncludes.getValue().trim(); const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles(); // store memento this.viewletSettings['query.contentPattern'] = contentPattern; this.viewletSettings['query.regex'] = isRegex; this.viewletSettings['query.wholeWords'] = isWholeWords; this.viewletSettings['query.caseSensitive'] = isCaseSensitive; this.viewletSettings['query.folderExclusions'] = patternExcludes; this.viewletSettings['query.folderIncludes'] = patternIncludes; this.viewletSettings['query.useExcludesAndIgnoreFiles'] = useExcludesAndIgnoreFiles; const searchHistory = this.searchWidget.getSearchHistory(); const replaceHistory = this.searchWidget.getReplaceHistory(); const patternExcludesHistory = this.inputPatternExcludes.getHistory(); const patternIncludesHistory = this.inputPatternIncludes.getHistory(); this.searchHistoryService.save({ search: searchHistory, replace: replaceHistory, exclude: patternExcludesHistory, include: patternIncludesHistory }); super.shutdown(); } public dispose(): void { this.isDisposed = true; super.dispose(); } } registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const matchHighlightColor = theme.getColor(editorFindMatchHighlight); if (matchHighlightColor) { collector.addRule(`.monaco-workbench .search-view .findInFileMatch { background-color: ${matchHighlightColor}; }`); } const diffInsertedColor = theme.getColor(diffInserted); if (diffInsertedColor) { collector.addRule(`.monaco-workbench .search-view .replaceMatch { background-color: ${diffInsertedColor}; }`); } const diffRemovedColor = theme.getColor(diffRemoved); if (diffRemovedColor) { collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { background-color: ${diffRemovedColor}; }`); } const diffInsertedOutlineColor = theme.getColor(diffInsertedOutline); if (diffInsertedOutlineColor) { collector.addRule(`.monaco-workbench .search-view .replaceMatch:not(:empty) { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffInsertedOutlineColor}; }`); } const diffRemovedOutlineColor = theme.getColor(diffRemovedOutline); if (diffRemovedOutlineColor) { collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffRemovedOutlineColor}; }`); } const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder); if (findMatchHighlightBorder) { collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`); } const outlineSelectionColor = theme.getColor(listActiveSelectionForeground); if (outlineSelectionColor) { collector.addRule(`.monaco-workbench .search-view .monaco-tree.focused .monaco-tree-row.focused.selected:not(.highlighted) .action-label:focus { outline-color: ${outlineSelectionColor} }`); } });