diff --git a/packages/core/src/browser/icons/CollapseAll.svg b/packages/core/src/browser/icons/CollapseAll.svg new file mode 100644 index 0000000000000..f44a3b03142ad --- /dev/null +++ b/packages/core/src/browser/icons/CollapseAll.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/core/src/browser/icons/CollapseAll_inverse.svg b/packages/core/src/browser/icons/CollapseAll_inverse.svg new file mode 100644 index 0000000000000..0d65cd8ba6095 --- /dev/null +++ b/packages/core/src/browser/icons/CollapseAll_inverse.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/core/src/browser/icons/Refresh.svg b/packages/core/src/browser/icons/Refresh.svg new file mode 100644 index 0000000000000..82ac7408b2a74 --- /dev/null +++ b/packages/core/src/browser/icons/Refresh.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/core/src/browser/icons/Refresh_inverse.svg b/packages/core/src/browser/icons/Refresh_inverse.svg new file mode 100644 index 0000000000000..b0f180ce1563b --- /dev/null +++ b/packages/core/src/browser/icons/Refresh_inverse.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/core/src/browser/icons/case-sensitive-dark.svg b/packages/core/src/browser/icons/case-sensitive-dark.svg new file mode 100644 index 0000000000000..f4b6fcd6d81d1 --- /dev/null +++ b/packages/core/src/browser/icons/case-sensitive-dark.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/packages/core/src/browser/icons/case-sensitive.svg b/packages/core/src/browser/icons/case-sensitive.svg new file mode 100644 index 0000000000000..94f9b32503fa5 --- /dev/null +++ b/packages/core/src/browser/icons/case-sensitive.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/packages/core/src/browser/icons/clear-search-results-dark.svg b/packages/core/src/browser/icons/clear-search-results-dark.svg new file mode 100644 index 0000000000000..ae0e6809a8c85 --- /dev/null +++ b/packages/core/src/browser/icons/clear-search-results-dark.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/core/src/browser/icons/clear-search-results.svg b/packages/core/src/browser/icons/clear-search-results.svg new file mode 100644 index 0000000000000..24a8a8b1cc6dc --- /dev/null +++ b/packages/core/src/browser/icons/clear-search-results.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/core/src/browser/icons/regex-dark.svg b/packages/core/src/browser/icons/regex-dark.svg new file mode 100644 index 0000000000000..5b0a7c7a8908c --- /dev/null +++ b/packages/core/src/browser/icons/regex-dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/core/src/browser/icons/regex.svg b/packages/core/src/browser/icons/regex.svg new file mode 100644 index 0000000000000..b83e6965ec6e2 --- /dev/null +++ b/packages/core/src/browser/icons/regex.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/core/src/browser/icons/replace-all-inverse.svg b/packages/core/src/browser/icons/replace-all-inverse.svg new file mode 100644 index 0000000000000..cb1b7fca836fd --- /dev/null +++ b/packages/core/src/browser/icons/replace-all-inverse.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/packages/core/src/browser/icons/replace-all.svg b/packages/core/src/browser/icons/replace-all.svg new file mode 100644 index 0000000000000..edd6cc8ef7049 --- /dev/null +++ b/packages/core/src/browser/icons/replace-all.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/packages/core/src/browser/icons/replace-inverse.svg b/packages/core/src/browser/icons/replace-inverse.svg new file mode 100644 index 0000000000000..ec99722f487f3 --- /dev/null +++ b/packages/core/src/browser/icons/replace-inverse.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/packages/core/src/browser/icons/replace.svg b/packages/core/src/browser/icons/replace.svg new file mode 100644 index 0000000000000..82b26f264bfa4 --- /dev/null +++ b/packages/core/src/browser/icons/replace.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/packages/core/src/browser/icons/whole-word-dark.svg b/packages/core/src/browser/icons/whole-word-dark.svg new file mode 100644 index 0000000000000..ae27a3eb68b61 --- /dev/null +++ b/packages/core/src/browser/icons/whole-word-dark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/core/src/browser/icons/whole-word.svg b/packages/core/src/browser/icons/whole-word.svg new file mode 100644 index 0000000000000..10878ec28a6bb --- /dev/null +++ b/packages/core/src/browser/icons/whole-word.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 3d55e5e124c00..b7d7b742634dd 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -1114,6 +1114,13 @@ export class ApplicationShell extends Widget { return toArray(this.bottomPanel.tabBars()); } + /** + * The tab bars contained in all shell areas. + */ + get allTabBars(): TabBar[] { + return [...this.mainAreaTabBars, ...this.bottomAreaTabBars, this.leftPanelHandler.tabBar, this.rightPanelHandler.tabBar]; + } + /* * Activate the next tab in the current tab bar. */ diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css index 414cadfb1fccf..4e33fb9da100d 100644 --- a/packages/core/src/browser/style/index.css +++ b/packages/core/src/browser/style/index.css @@ -87,6 +87,13 @@ body { :focus { outline: none; + border-style: solid; + border-width: 1px; + border-color: var(--theia-accent-color3); +} + +.p-Widget:focus { + border: none; } button, .theia-button { diff --git a/packages/core/src/browser/style/tree.css b/packages/core/src/browser/style/tree.css index b26f4450bb758..549d95e2c1383 100644 --- a/packages/core/src/browser/style/tree.css +++ b/packages/core/src/browser/style/tree.css @@ -23,6 +23,7 @@ line-height: var(--theia-private-horizontal-tab-height); display: flex; align-items: baseline; + margin-right: 10px; } .theia-TreeNode:hover { diff --git a/packages/core/src/browser/style/variables-bright.useable.css b/packages/core/src/browser/style/variables-bright.useable.css index b019e179c5c5d..a1519e7cabd5a 100644 --- a/packages/core/src/browser/style/variables-bright.useable.css +++ b/packages/core/src/browser/style/variables-bright.useable.css @@ -151,11 +151,24 @@ is not optimized for dense, information rich UIs. --theia-highlight-background-color: var(--md-purple-A100); --theia-highlight-color: var(--theia-content-font-color0); + /* Colors to highlight words in widgets like tree or editors */ + + --theia-word-highlight-color0: rgba(168, 172, 148, 0.7); + --theia-word-highlight-color1: rgba(253, 255, 0, 0.2); + /* Icons */ --theia-icon-close: url(../icons/close-bright.svg); --theia-sprite-y-offset: 0px; --theia-icon-circle: url(../icons/circle-bright.svg); --theia-preloader: url(../icons/spinner.gif); + --theia-icon-case-sensitive: url(../icons/case-sensitive.svg); + --theia-icon-regex: url(../icons/regex.svg); + --theia-icon-whole-word: url(../icons/whole-word.svg); + --theia-icon-refresh: url(../icons/Refresh.svg); + --theia-icon-collapse-all: url(../icons/CollapseAll.svg); + --theia-icon-clear: url(../icons/clear-search-results.svg); + --theia-icon-replace: url(../icons/replace.svg); + --theia-icon-replace-all: url(../icons/replace-all.svg); /* Scrollbars */ --theia-scrollbar-width: 6px; diff --git a/packages/core/src/browser/style/variables-dark.useable.css b/packages/core/src/browser/style/variables-dark.useable.css index 065a115c38dbe..7311b946b987a 100644 --- a/packages/core/src/browser/style/variables-dark.useable.css +++ b/packages/core/src/browser/style/variables-dark.useable.css @@ -151,11 +151,24 @@ is not optimized for dense, information rich UIs. --theia-highlight-background-color: var(--md-purple-A400); --theia-highlight-color: var(--theia-content-font-color0); + /* Colors to highlight words in widgets like tree or editors */ + + --theia-word-highlight-color0: rgba(81, 92, 106, 0.7); + --theia-word-highlight-color1: rgba(255, 255, 255, 0.04); + /* Icons */ --theia-icon-close: url(../icons/close-dark.svg); --theia-sprite-y-offset: -20px; --theia-icon-circle: url(../icons/circle-dark.svg); --theia-preloader: url(../icons/spinner.gif); + --theia-icon-case-sensitive: url(../icons/case-sensitive-dark.svg); + --theia-icon-regex: url(../icons/regex-dark.svg); + --theia-icon-whole-word: url(../icons/whole-word-dark.svg); + --theia-icon-refresh: url(../icons/Refresh_inverse.svg); + --theia-icon-collapse-all: url(../icons/CollapseAll_inverse.svg); + --theia-icon-clear: url(../icons/clear-search-results-dark.svg); + --theia-icon-replace: url(../icons/replace-inverse.svg); + --theia-icon-replace-all: url(../icons/replace-all-inverse.svg); /* Scrollbars */ --theia-scrollbar-width: 6px; diff --git a/packages/editor/src/browser/editor.ts b/packages/editor/src/browser/editor.ts index d991551d5abbe..b8073c94c2bbf 100644 --- a/packages/editor/src/browser/editor.ts +++ b/packages/editor/src/browser/editor.ts @@ -81,6 +81,12 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable deltaDecorations(params: DeltaDecorationParams): string[]; getVisibleColumn(position: Position): number; + + /** + * Replaces the text of source given in ReplacetextParams. + * @param params: ReplaceTextParams + */ + replaceText(params: ReplaceTextParams): Promise; } export interface Dimension { @@ -115,6 +121,28 @@ export interface DeltaDecorationParams { newDecorations: EditorDecoration[]; } +export interface ReplaceTextParams { + /** + * the source to edit + */ + source: string; + /** + * the replace operations + */ + replaceOperations: ReplaceOperation[]; +} + +export interface ReplaceOperation { + /** + * the position that shall be replaced + */ + range: Range; + /** + * the text to replace with + */ + text: string; +} + export namespace TextEditorSelection { // tslint:disable-next-line:no-any export function is(e: any): e is TextEditorSelection { diff --git a/packages/monaco/src/browser/monaco-editor.ts b/packages/monaco/src/browser/monaco-editor.ts index 5558717b94808..102092f6a0736 100644 --- a/packages/monaco/src/browser/monaco-editor.ts +++ b/packages/monaco/src/browser/monaco-editor.ts @@ -22,6 +22,7 @@ import { RevealPositionOptions, EditorDecorationsService, DeltaDecorationParams, + ReplaceTextParams, } from '@theia/editor/lib/browser'; import { MonacoEditorModel } from "./monaco-editor-model"; @@ -29,6 +30,7 @@ import IEditorConstructionOptions = monaco.editor.IEditorConstructionOptions; import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration; import IEditorOverrideServices = monaco.editor.IEditorOverrideServices; import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; +import IIdentifiedSingleEditOperation = monaco.editor.IIdentifiedSingleEditOperation; import IBoxSizing = ElementExt.IBoxSizing; import IEditorReference = monaco.editor.IEditorReference; import SuggestController = monaco.suggestController.SuggestController; @@ -338,6 +340,34 @@ export class MonacoEditor implements TextEditor, IEditorReference { lineNumber: position.line + 1 }); } + + async replaceText(params: ReplaceTextParams): Promise { + const edits: IIdentifiedSingleEditOperation[] = params.replaceOperations.map(param => { + const startPos = param.range.start; + const endPos = param.range.end; + const range = this.p2m.asRange({ + start: { + line: startPos.line - 1, + character: startPos.character - 1 + }, + end: { + line: endPos.line - 1, + character: endPos.character - 1 + } + }); + return { + forceMoveMarkers: true, + identifier: { + major: range.startLineNumber, + minor: range.startColumn + }, + range, + text: param.text + }; + }); + return this.editor.executeEdits(params.source, edits); + } + } export namespace MonacoEditor { diff --git a/packages/search-in-workspace/src/browser/in-memory-text-resource.ts b/packages/search-in-workspace/src/browser/in-memory-text-resource.ts new file mode 100644 index 0000000000000..7f519979cbbe2 --- /dev/null +++ b/packages/search-in-workspace/src/browser/in-memory-text-resource.ts @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { injectable } from "inversify"; +import { ResourceResolver, Resource } from "@theia/core"; +import URI from "@theia/core/lib/common/uri"; + +export const MEMORY_TEXT = "mem-txt"; + +export class InMemoryTextResource implements Resource { + + constructor(readonly uri: URI) { } + + async readContents(options?: { encoding?: string | undefined; } | undefined): Promise { + return this.uri.query; + } + + dispose(): void { } +} + +@injectable() +export class InMemoryTextResourceResolver implements ResourceResolver { + resolve(uri: URI): Resource | Promise { + if (uri.scheme !== MEMORY_TEXT) { + throw new Error(`Expected a URI with ${MEMORY_TEXT} scheme. Was: ${uri}.`); + } + return new InMemoryTextResource(uri); + } +} diff --git a/packages/search-in-workspace/src/browser/quick-search-in-workspace.ts b/packages/search-in-workspace/src/browser/quick-search-in-workspace.ts deleted file mode 100644 index 57407d6101963..0000000000000 --- a/packages/search-in-workspace/src/browser/quick-search-in-workspace.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2017-2018 Ericsson and others. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - */ - -import URI from '@theia/core/lib/common/uri'; -import { QuickOpenService, QuickOpenModel, QuickOpenItem, QuickOpenItemOptions } from '@theia/core/lib/browser/quick-open/'; -import { injectable, inject } from 'inversify'; -import { MenuModelRegistry, MenuContribution, CommandContribution, CommandRegistry, ILogger } from '@theia/core'; -import { - CommonMenus, QuickOpenMode, OpenerService, open, Highlight, QuickOpenOptions, - KeybindingContribution, KeybindingRegistry -} from '@theia/core/lib/browser'; -import { SearchInWorkspaceService } from './search-in-workspace-service'; -import { SearchInWorkspaceResult, SearchInWorkspaceOptions } from '../common/search-in-workspace-interface'; -import { Range } from '@theia/editor/lib/browser'; -import { LabelProvider } from '@theia/core/lib/browser/label-provider'; - -@injectable() -export class QuickSearchInWorkspace implements QuickOpenModel { - private currentSearchId: number = -1; - protected MAX_RESULTS = 100; - - constructor( - @inject(QuickOpenService) protected readonly quickOpenService: QuickOpenService, - @inject(SearchInWorkspaceService) protected readonly searchInWorkspaceService: SearchInWorkspaceService, - @inject(OpenerService) protected readonly openerService: OpenerService, - @inject(LabelProvider) protected readonly labelProvider: LabelProvider, - @inject(ILogger) protected readonly logger: ILogger, - ) { } - - isEnabled(): boolean { - return this.searchInWorkspaceService.isEnabled(); - } - - onType(lookFor: string, acceptor: (items: QuickOpenItem[]) => void): void { - // If we have a search pending, it's not relevant anymore, cancel it. - this.cancelCurrentSeach(); - - if (lookFor.length === 0) { - // The user has emptied the search box, call acceptor to - // remove any previously shown results. - acceptor([]); - return; - } - - // Options passed to the search service. - const opts: SearchInWorkspaceOptions = { - maxResults: this.MAX_RESULTS, - }; - - // The array in which we'll keep accumulating search results. - const items: QuickSearchInWorkspaceResultItem[] = []; - - this.searchInWorkspaceService.search(lookFor, { - - onResult: (searchId: number, result: SearchInWorkspaceResult) => { - // Is this result from a previous search? - if (searchId !== this.currentSearchId) { - return; - } - - items.push(new QuickSearchInWorkspaceResultItem(result, this.openerService, this.labelProvider)); - }, - - onDone: (searchId: number, error?: string) => { - if (searchId !== this.currentSearchId) { - this.logger.debug('Search ' + this.currentSearchId + ' has completed, but it\'s not the current search.'); - return; - } - this.logger.debug('Search ' + this.currentSearchId + ' has completed and is the current search.'); - this.currentSearchId = -1; - - if (error) { - this.showFakeResult(error, acceptor); - } else if (items.length !== 0) { - items.sort((a, b) => SearchInWorkspaceResult.compare(a.getResult(), b.getResult())); - acceptor(items); - } else { - this.showFakeResult('No matches :(', acceptor); - } - - }, - }, opts).then(searchId => { - this.currentSearchId = searchId; - }); - } - - showFakeResult(label: string, acceptor: (items: QuickOpenItem[]) => void) { - acceptor([ - new QuickOpenItem({ - label: label, - }), - ]); - } - - // If we have an ongoing search, cancel it. - cancelCurrentSeach() { - if (this.currentSearchId >= 0) { - this.logger.debug('Cancelling search ' + this.currentSearchId); - this.searchInWorkspaceService.cancel(this.currentSearchId); - this.currentSearchId = -1; - } - } - - // Open the quick search in workspace popup. - open() { - const opts: QuickOpenOptions = { - onClose: cancelled => this.cancelCurrentSeach(), - placeholder: 'Search in workspace by regular expression...', - }; - this.quickOpenService.open(this, opts); - } -} - -class QuickSearchInWorkspaceResultItem extends QuickOpenItem { - - private result: SearchInWorkspaceResult; - private openerService: OpenerService; - - constructor(result: SearchInWorkspaceResult, openerService: OpenerService, labelProvider: LabelProvider) { - const resultHl: Highlight = { - start: result.character - 1, - end: result.character + result.length - 1, - }; - - // Show the path relative to the workspace. - const uri = new URI('file://' + result.file); - const file = labelProvider.getName(uri); - const dir = labelProvider.getLongName(uri.parent) + '/'; - - const filenameHl: Highlight = { - start: 0, - end: file.length, - }; - - const opts: QuickOpenItemOptions = { - detail: result.lineText, - detailHighlights: [resultHl], - label: `${file}:${result.line} - ${dir}`, - labelHighlights: [filenameHl], - }; - super(opts); - - this.result = result; - this.openerService = openerService; - } - - run(mode: QuickOpenMode): boolean { - if (mode !== QuickOpenMode.OPEN) { - return false; - } - - // Search results are 1-based, positions in editors are 0-based. - const line = this.result.line - 1; - const character = this.result.character - 1; - const uri = new URI('file://' + this.result.file); - const r = Range.create(line, character, line, character + this.result.length); - open(this.openerService, uri, { selection: r }); - - return true; - } - - getResult(): SearchInWorkspaceResult { - return this.result; - } -} - -const OpenQuickSearchInWorkspaceCommand = { - id: 'QuickSearchInWorkspace.open', - label: "Search in Workspace..." -}; - -@injectable() -export class SearchInWorkspaceContributions implements CommandContribution, MenuContribution, KeybindingContribution { - constructor( - @inject(QuickSearchInWorkspace) protected readonly quickSeachInWorkspace: QuickSearchInWorkspace, - ) { } - - registerCommands(registry: CommandRegistry): void { - registry.registerCommand(OpenQuickSearchInWorkspaceCommand, { - execute: what => this.quickSeachInWorkspace.open(), - isEnabled: () => this.quickSeachInWorkspace.isEnabled(), - }); - } - - registerMenus(menus: MenuModelRegistry): void { - menus.registerMenuAction(CommonMenus.EDIT_FIND, { - commandId: OpenQuickSearchInWorkspaceCommand.id, - label: OpenQuickSearchInWorkspaceCommand.label, - }); - } - - registerKeybindings(keybindings: KeybindingRegistry): void { - keybindings.registerKeybinding({ - command: OpenQuickSearchInWorkspaceCommand.id, - keybinding: 'ctrlcmd+shift+f', - }); - } -} diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts new file mode 100644 index 0000000000000..085a958ded68f --- /dev/null +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { AbstractViewContribution } from "@theia/core/lib/browser"; +import { SearchInWorkspaceWidget } from "./search-in-workspace-widget"; +import { injectable } from "inversify"; + +export namespace SearchInWorkspaceCommands { + export const OPEN_SIW_WIDGET = { + id: "search-in-workspace.open" + }; +} + +@injectable() +export class SearchInWorkspaceFrontendContribution extends AbstractViewContribution { + + constructor() { + super({ + widgetId: SearchInWorkspaceWidget.ID, + widgetName: SearchInWorkspaceWidget.LABEL, + defaultWidgetOptions: { + area: "left" + }, + toggleCommandId: SearchInWorkspaceCommands.OPEN_SIW_WIDGET.id, + toggleKeybinding: "ctrlcmd+shift+f" + }); + } +} diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts index c770202854c3b..0150393e631d0 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-module.ts @@ -5,19 +5,30 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ -import { ContainerModule } from "inversify"; +import { ContainerModule, interfaces } from "inversify"; import { SearchInWorkspaceService, SearchInWorkspaceClientImpl } from './search-in-workspace-service'; import { SearchInWorkspaceServer } from '../common/search-in-workspace-interface'; -import { WebSocketConnectionProvider, KeybindingContribution } from '@theia/core/lib/browser'; -import { QuickSearchInWorkspace, SearchInWorkspaceContributions } from './quick-search-in-workspace'; -import { CommandContribution, MenuContribution } from "@theia/core"; +import { WebSocketConnectionProvider, KeybindingContribution, WidgetFactory, createTreeContainer, TreeWidget } from '@theia/core/lib/browser'; +import { CommandContribution, MenuContribution, ResourceResolver } from "@theia/core"; +import { SearchInWorkspaceWidget } from "./search-in-workspace-widget"; +import { SearchInWorkspaceResultTreeWidget } from "./search-in-workspace-result-tree-widget"; +import { SearchInWorkspaceFrontendContribution } from "./search-in-workspace-frontend-contribution"; +import { InMemoryTextResourceResolver } from "./in-memory-text-resource"; + +import "../../src/browser/styles/index.css"; export default new ContainerModule(bind => { - bind(QuickSearchInWorkspace).toSelf().inSingletonScope(); + bind(SearchInWorkspaceWidget).toSelf(); + bind(WidgetFactory).toDynamicValue(ctx => ({ + id: SearchInWorkspaceWidget.ID, + createWidget: () => ctx.container.get(SearchInWorkspaceWidget) + })); + bind(SearchInWorkspaceResultTreeWidget).toDynamicValue(ctx => createSearchTreeWidget(ctx.container)); - bind(CommandContribution).to(SearchInWorkspaceContributions).inSingletonScope(); - bind(MenuContribution).to(SearchInWorkspaceContributions).inSingletonScope(); - bind(KeybindingContribution).to(SearchInWorkspaceContributions).inSingletonScope(); + bind(SearchInWorkspaceFrontendContribution).toSelf().inSingletonScope(); + for (const identifier of [CommandContribution, MenuContribution, KeybindingContribution]) { + bind(identifier).toService(SearchInWorkspaceFrontendContribution); + } // The object that gets notified of search results. bind(SearchInWorkspaceClientImpl).toSelf().inSingletonScope(); @@ -29,4 +40,16 @@ export default new ContainerModule(bind => { const client = ctx.container.get(SearchInWorkspaceClientImpl); return WebSocketConnectionProvider.createProxy(ctx.container, '/search-in-workspace', client); }).inSingletonScope(); + + bind(InMemoryTextResourceResolver).toSelf().inSingletonScope(); + bind(ResourceResolver).toService(InMemoryTextResourceResolver); }); + +export function createSearchTreeWidget(parent: interfaces.Container): SearchInWorkspaceResultTreeWidget { + const child = createTreeContainer(parent); + + child.unbind(TreeWidget); + child.bind(SearchInWorkspaceResultTreeWidget).toSelf(); + + return child.get(SearchInWorkspaceResultTreeWidget); +} diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.ts b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.ts new file mode 100644 index 0000000000000..ec16b958385b6 --- /dev/null +++ b/packages/search-in-workspace/src/browser/search-in-workspace-result-tree-widget.ts @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2018 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { + TreeWidget, + ContextMenuRenderer, + CompositeTreeNode, + ExpandableTreeNode, + SelectableTreeNode, + TreeModel, + TreeNode, + NodeProps, + LabelProvider, + TreeExpansionService, + ApplicationShell, + DiffUris +} from "@theia/core/lib/browser"; +import { SearchInWorkspaceResult, SearchInWorkspaceOptions } from "../common/search-in-workspace-interface"; +import { SearchInWorkspaceService } from "./search-in-workspace-service"; +import { TreeProps } from "@theia/core/lib/browser"; +import { EditorManager, EditorDecoration, TrackedRangeStickiness, OverviewRulerLane, EditorWidget, ReplaceOperation } from "@theia/editor/lib/browser"; +import { inject, injectable, postConstruct } from "inversify"; +import URI from "@theia/core/lib/common/uri"; +import { Path, CancellationTokenSource, Emitter, Event } from "@theia/core"; +import { WorkspaceService } from "@theia/workspace/lib/browser"; +import { h } from "@phosphor/virtualdom"; +import { MEMORY_TEXT } from "./in-memory-text-resource"; +import { FileResourceResolver } from "@theia/filesystem/lib/browser"; + +export interface SearchInWorkspaceResultNode extends ExpandableTreeNode, SelectableTreeNode { + children: SearchInWorkspaceResultLineNode[]; + path: string; + file: string; +} +export namespace SearchInWorkspaceResultNode { + export function is(node: any): node is SearchInWorkspaceResultNode { + return ExpandableTreeNode.is(node) && SelectableTreeNode.is(node) && "path" in node; + } +} + +export type SearchInWorkspaceResultLineNode = SelectableTreeNode & SearchInWorkspaceResult; +export namespace SearchInWorkspaceResultLineNode { + export function is(node: any): node is SearchInWorkspaceResultLineNode { + return SelectableTreeNode.is(node) && "line" in node && "character" in node && "lineText" in node; + } +} + +@injectable() +export class SearchInWorkspaceResultTreeWidget extends TreeWidget { + + protected resultTree: Map; + protected workspaceRoot: string = ""; + + protected _showReplaceButtons = false; + protected _replaceTerm = ""; + protected searchTerm = ""; + + protected appliedDecorations = new Map(); + + private cancelIndicator = new CancellationTokenSource(); + + protected changeEmitter: Emitter>; + + @inject(SearchInWorkspaceService) protected readonly searchService: SearchInWorkspaceService; + @inject(EditorManager) protected readonly editorManager: EditorManager; + @inject(FileResourceResolver) protected readonly fileResourceResolver: FileResourceResolver; + @inject(ApplicationShell) protected readonly shell: ApplicationShell; + @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(TreeExpansionService) protected readonly expansionService: TreeExpansionService; + + constructor( + @inject(TreeProps) readonly props: TreeProps, + @inject(TreeModel) readonly model: TreeModel, + @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer + ) { + super(props, model, contextMenuRenderer); + + model.root = { + id: "ResultTree", + name: "ResultTree", + parent: undefined, + visible: false, + children: [] + }; + + this.toDispose.push(model.onSelectionChanged(nodes => { + const node = nodes[0]; + if (SearchInWorkspaceResultLineNode.is(node)) { + this.doOpen(node, true); + } + })); + + this.toDispose.push(model.onNodeRefreshed(() => this.changeEmitter.fire(this.resultTree))); + } + + @postConstruct() + protected init() { + this.addClass("resultContainer"); + + this.workspaceService.root.then(rootFileStat => { + if (rootFileStat) { + const uri = new URI(rootFileStat.uri); + this.workspaceRoot = uri.withoutScheme().toString(); + } + }); + + this.changeEmitter = new Emitter(); + + this.toDispose.push(this.editorManager.onActiveEditorChanged(() => { + this.updateCurrentEditorDecorations(); + })); + } + + set showReplaceButtons(srb: boolean) { + this._showReplaceButtons = srb; + this.update(); + } + + set replaceTerm(rt: string) { + this._replaceTerm = rt; + this.update(); + } + + get onChange(): Event> { + return this.changeEmitter.event; + } + + collapseAll() { + this.resultTree.forEach(v => this.expansionService.collapseNode(v)); + } + + async search(searchTerm: string, searchOptions: SearchInWorkspaceOptions): Promise { + this.searchTerm = searchTerm; + this.resultTree = new Map(); + if (searchTerm === "") { + this.refreshModelChildren(); + return; + } + this.cancelIndicator.cancel(); + this.cancelIndicator = new CancellationTokenSource(); + const token = this.cancelIndicator.token; + const searchId = await this.searchService.search(searchTerm, { + onResult: async (aSearchId: number, result: SearchInWorkspaceResult) => { + if (token.isCancellationRequested || aSearchId !== searchId) { + return; + } + const { name, path } = this.filenameAndPath(result.file); + let resultElement = this.resultTree.get(result.file); + + if (resultElement) { + const resultLine = this.createResultLineNode(result, resultElement); + resultElement.children.push(resultLine); + } else { + const children: SearchInWorkspaceResultLineNode[] = []; + const icon = await this.labelProvider.getIcon(new URI(result.file)); + if (CompositeTreeNode.is(this.model.root)) { + resultElement = { + selected: false, + name, + path, + children, + expanded: true, + id: path + "-" + name, + parent: this.model.root, + icon, + file: result.file + }; + resultElement.children.push(this.createResultLineNode(result, resultElement)); + this.resultTree.set(result.file, resultElement); + } + } + }, + onDone: () => { + if (token.isCancellationRequested) { + return; + } + this.refreshModelChildren(); + } + }, searchOptions); + token.onCancellationRequested(() => { + this.searchService.cancel(searchId); + }); + } + + protected refreshModelChildren() { + if (CompositeTreeNode.is(this.model.root)) { + this.model.root.children = Array.from(this.resultTree.values()); + this.model.refresh(); + this.updateCurrentEditorDecorations(); + } + } + + protected updateCurrentEditorDecorations() { + this.shell.allTabBars.map(tb => { + const currentTitle = tb.currentTitle; + if (currentTitle && currentTitle.owner instanceof EditorWidget) { + const widget = currentTitle.owner; + const result = this.resultTree.get(widget.editor.uri.withoutScheme().toString()); + this.decorateEditor(result, widget); + } + }); + + const currentWidget = this.editorManager.currentEditor; + if (currentWidget) { + const result = this.resultTree.get(currentWidget.editor.uri.withoutScheme().toString()); + this.decorateEditor(result, currentWidget); + } + } + + protected createResultLineNode(result: SearchInWorkspaceResult, resultNode: SearchInWorkspaceResultNode): SearchInWorkspaceResultLineNode { + return { + ...result, + selected: false, + id: result.file + "-" + result.line + "-" + result.character + "-" + result.length, + name: result.lineText, + parent: resultNode + }; + } + + protected filenameAndPath(uriStr: string): { name: string, path: string } { + const uri: URI = new URI(uriStr); + const name = uri.displayName; + const path = new Path(uri.toString().substr(this.workspaceRoot.length + 1)).dir.toString(); + return { name, path }; + } + + protected renderCaption(node: TreeNode, props: NodeProps): h.Child { + if (SearchInWorkspaceResultNode.is(node)) { + return this.renderResultNode(node); + } else if (SearchInWorkspaceResultLineNode.is(node)) { + return this.renderResultLineNode(node); + } + return ""; + } + + protected renderTailDecorations(node: TreeNode, props: NodeProps): h.Child[] { + const btns: h.Child[] = []; + if (this._showReplaceButtons) { + btns.push(this.renderReplaceButton(node)); + } + btns.push(this.renderRemoveButton(node)); + return [h.div({ className: "result-node-buttons" }, ...btns)]; + } + + protected renderReplaceButton(node: TreeNode): h.Child { + return h.span({ + className: "replace-result", + onclick: async e => { + this.replaceResult(node); + this.removeNode(node); + e.stopPropagation(); + } + }); + } + + replaceAll(): void { + this.resultTree.forEach(async resultNode => { + await this.replaceResult(resultNode); + }); + this.resultTree.clear(); + this.refreshModelChildren(); + } + + protected updateRightResults(node: SearchInWorkspaceResultLineNode) { + const result = this.resultTree.get(node.file); + if (result) { + const rightPositionedNodes = result.children.filter(rl => rl.line === node.line && rl.character > node.character); + const diff = this._replaceTerm.length - this.searchTerm.length; + rightPositionedNodes.map(r => r.character += diff); + } + } + + protected async replaceResult(node: TreeNode) { + const toReplace: SearchInWorkspaceResultLineNode[] = []; + if (SearchInWorkspaceResultNode.is(node)) { + toReplace.push(...node.children); + } else if (SearchInWorkspaceResultLineNode.is(node)) { + toReplace.push(node); + this.updateRightResults(node); + } + + if (toReplace.length > 0) { + const widget = await this.doOpen(toReplace[0]); + const source = widget.editor.document.getText(); + const replaceOperations = toReplace.map(resultLineNode => { + text: this._replaceTerm, + range: { + start: { + line: resultLineNode.line, + character: resultLineNode.character + }, + end: { + line: resultLineNode.line, + character: resultLineNode.character + resultLineNode.length + } + } + }); + await widget.editor.replaceText({ + source, + replaceOperations + }); + } + } + + protected renderRemoveButton(node: TreeNode): h.Child { + return h.span({ + className: "remove-node", onclick: e => { + this.removeNode(node); + e.stopPropagation(); + } + }); + } + + protected removeNode(node: TreeNode) { + if (SearchInWorkspaceResultNode.is(node)) { + this.resultTree.delete(node.file); + } else if (SearchInWorkspaceResultLineNode.is(node)) { + const result = this.resultTree.get(node.file); + if (result) { + const index = result.children.findIndex(n => n.file === node.file && n.line === node.line && n.character === node.character); + if (index > -1) { + result.children.splice(index, 1); + if (result.children.length === 0) { + this.resultTree.delete(result.file); + } + } + } + } + this.refreshModelChildren(); + } + + protected renderResultNode(node: SearchInWorkspaceResultNode): h.Child { + const icon = node.icon; + const fileIcon = h.span({ className: `file-icon ${icon || ""}` }); + const fileName = h.span({ className: "file-name" }, node.name); + const filePath = h.span({ className: "file-path" }, node.path); + const resultNumber = h.span({ className: "result-number" }, node.children.length.toString()); + const resultHeadInfo = h.div( + { className: `result-head-info noWrapInfo noselect ${node.selected ? 'selected' : ''}` }, + fileIcon, fileName, filePath); + const resultHead = h.div({ className: "result-head" }, resultHeadInfo, resultNumber); + return h.div({ className: "result" }, resultHead); + } + + protected renderResultLineNode(node: SearchInWorkspaceResultLineNode): h.Child { + const prefix = node.character > 26 ? '... ' : ''; + const start = h.span(prefix + node.lineText.substr(0, node.character - 1).substr(-25)); + const match = this.renderMatchLinePart(node); + const end = h.span(node.lineText.substr(node.character - 1 + node.length, 75)); + return h.div( + { + className: `resultLine noWrapInfo ${node.selected ? 'selected' : ''}`, + onclick: () => this.model.selectNode(node) + }, start, ...match, end); + } + + protected renderMatchLinePart(node: SearchInWorkspaceResultLineNode): h.Child[] { + const replaceTerm = this._replaceTerm !== "" && this._showReplaceButtons ? h.span({ className: "replace-term" }, this._replaceTerm) : ""; + const className = `match${this._showReplaceButtons ? " strike-through" : ""}`; + return [h.span({ className }, node.lineText.substr(node.character - 1, node.length)), replaceTerm]; + } + + protected async doOpen(node: SearchInWorkspaceResultLineNode, preview: boolean = false): Promise { + let fileUri: URI; + const resultNode = this.resultTree.get(node.file); + if (resultNode && this._showReplaceButtons && preview) { + const leftUri = new URI(node.file).withScheme("file"); + const rightUri = await this.createReplacePreview(resultNode); + fileUri = DiffUris.encode(leftUri, rightUri); + } else { + fileUri = new URI(node.file).withScheme("file"); + } + const editorWidget = await this.editorManager.open(fileUri, { + selection: { + start: { + line: node.line - 1, + character: node.character - 1 + }, + end: { + line: node.line - 1, + character: node.character - 1 + node.length + } + }, + mode: "reveal" + }); + + this.decorateEditor(resultNode, editorWidget); + + return editorWidget; + } + + protected async createReplacePreview(node: SearchInWorkspaceResultNode): Promise { + const fileUri = new URI(node.file).withScheme("file"); + const uri = fileUri.withoutScheme().toString(); + const resource = await this.fileResourceResolver.resolve(fileUri); + const content = await resource.readContents(); + + const lines = content.split("\n"); + node.children.map(l => { + const leftPositionedNodes = node.children.filter(rl => rl.line === l.line && rl.character < l.character); + const diff = (this._replaceTerm.length - this.searchTerm.length) * leftPositionedNodes.length; + const start = lines[l.line - 1].substr(0, l.character - 1 + diff); + const end = lines[l.line - 1].substr(l.character - 1 + diff + l.length); + lines[l.line - 1] = start + this._replaceTerm + end; + }); + + return new URI(uri).withScheme(MEMORY_TEXT).withQuery(lines.join("\n")); + } + + protected decorateEditor(node: SearchInWorkspaceResultNode | undefined, editorWidget: EditorWidget) { + const key = `${editorWidget.editor.uri.toString()}#search-in-workspace-matches`; + const oldDecorations = this.appliedDecorations.get(key) || []; + const newDecorations = this.createEditorDecorations(node); + const appliedDecorations = editorWidget.editor.deltaDecorations({ + newDecorations, + oldDecorations, + uri: editorWidget.editor.uri.toString() + }); + this.appliedDecorations.set(key, appliedDecorations); + } + + protected createEditorDecorations(resultNode: SearchInWorkspaceResultNode | undefined): EditorDecoration[] { + const decorations: EditorDecoration[] = []; + if (resultNode) { + resultNode.children.map(res => { + decorations.push({ + range: { + start: { + line: res.line - 1, + character: res.character - 1 + }, + end: { + line: res.line - 1, + character: res.character - 1 + res.length + } + }, + options: { + overviewRuler: { + color: "rgba(230, 0, 0, 1)", + position: OverviewRulerLane.Full + }, + className: res.selected ? "current-search-in-workspace-editor-match" : "search-in-workspace-editor-match", + stickiness: TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + } + }); + }); + } + return decorations; + } +} diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-service.ts b/packages/search-in-workspace/src/browser/search-in-workspace-service.ts index d6d6d20fac612..0ce9f06a2b494 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-service.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-service.ts @@ -5,7 +5,7 @@ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ -import { injectable, inject } from "inversify"; +import { injectable, inject, postConstruct } from "inversify"; import { SearchInWorkspaceServer, SearchInWorkspaceClient, SearchInWorkspaceResult, SearchInWorkspaceOptions } from "../common/search-in-workspace-interface"; import { WorkspaceService } from "@theia/workspace/lib/browser"; import URI from "@theia/core/lib/common/uri"; @@ -56,13 +56,14 @@ export class SearchInWorkspaceService implements SearchInWorkspaceClient { private lastKnownSearchId: number = -1; - constructor( - @inject(SearchInWorkspaceServer) protected readonly searchServer: SearchInWorkspaceServer, - @inject(SearchInWorkspaceClientImpl) protected readonly client: SearchInWorkspaceClientImpl, - @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, - @inject(ILogger) protected readonly logger: ILogger, - ) { - client.setService(this); + @inject(SearchInWorkspaceServer) protected readonly searchServer: SearchInWorkspaceServer; + @inject(SearchInWorkspaceClientImpl) protected readonly client: SearchInWorkspaceClientImpl; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(ILogger) protected readonly logger: ILogger; + + @postConstruct() + protected init() { + this.client.setService(this); } isEnabled(): boolean { diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-widget.ts b/packages/search-in-workspace/src/browser/search-in-workspace-widget.ts new file mode 100644 index 0000000000000..f11c79687faff --- /dev/null +++ b/packages/search-in-workspace/src/browser/search-in-workspace-widget.ts @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2018 TypeFox and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + +import { Widget, Message, BaseWidget, VirtualRenderer, Key, StatefulWidget } from "@theia/core/lib/browser"; +import { inject, injectable, postConstruct } from "inversify"; +import { SearchInWorkspaceResultTreeWidget } from "./search-in-workspace-result-tree-widget"; +import { h } from "@phosphor/virtualdom"; +import { SearchInWorkspaceOptions } from "../common/search-in-workspace-interface"; + +export interface SearchFieldState { + className: string; + enabled: boolean; + title: string; +} + +@injectable() +export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidget { + + static ID = "search-in-workspace"; + static LABEL = "Search"; + + protected matchCaseState: SearchFieldState; + protected wholeWordState: SearchFieldState; + protected regExpState: SearchFieldState; + protected includeIgnoredState: SearchFieldState; + + protected showSearchDetails = false; + protected hasResults = false; + + protected searchInWorkspaceOptions: SearchInWorkspaceOptions; + + protected searchTerm = ""; + protected replaceTerm = ""; + + protected showReplaceField = false; + + protected contentNode: HTMLElement; + protected searchFormContainer: HTMLElement; + protected resultContainer: HTMLElement; + + @inject(SearchInWorkspaceResultTreeWidget) protected readonly resultTreeWidget: SearchInWorkspaceResultTreeWidget; + + @postConstruct() + init() { + this.id = SearchInWorkspaceWidget.ID; + this.title.label = SearchInWorkspaceWidget.LABEL; + + this.contentNode = document.createElement('div'); + this.contentNode.classList.add("t-siw-search-container"); + this.searchFormContainer = document.createElement('div'); + this.searchFormContainer.classList.add("searchHeader"); + this.contentNode.appendChild(this.searchFormContainer); + this.node.appendChild(this.contentNode); + + this.matchCaseState = { + className: "match-case", + enabled: false, + title: "Match Case" + }; + this.wholeWordState = { + className: "whole-word", + enabled: false, + title: "Match Whole Word" + }; + this.regExpState = { + className: "use-regexp", + enabled: false, + title: "Use Regular Expression" + }; + this.includeIgnoredState = { + className: "include-ignored fa fa-eye", + enabled: false, + title: "Include Ignored Files" + }; + this.searchInWorkspaceOptions = { + matchCase: false, + matchWholeWord: false, + useRegExp: false, + includeIgnored: false, + include: [], + exclude: [], + maxResults: 500 + }; + this.toDispose.push(this.resultTreeWidget.onChange(r => { + this.hasResults = r.size > 0; + this.update(); + })); + } + + storeState(): object { + return { + matchCaseState: this.matchCaseState, + wholeWordState: this.wholeWordState, + regExpState: this.regExpState, + includeIgnoredState: this.includeIgnoredState, + showSearchDetails: this.showSearchDetails, + searchInWorkspaceOptions: this.searchInWorkspaceOptions, + searchTerm: this.searchTerm, + replaceTerm: this.replaceTerm, + showReplaceField: this.showReplaceField + }; + } + + // tslint:disable-next-line:no-any + restoreState(oldState: any): void { + this.matchCaseState = oldState.matchCaseState; + this.wholeWordState = oldState.wholeWordState; + this.regExpState = oldState.regExpState; + this.includeIgnoredState = oldState.includeIgnoredState; + this.showSearchDetails = oldState.showSearchDetails; + this.searchInWorkspaceOptions = oldState.searchInWorkspaceOptions; + this.searchTerm = oldState.searchTerm; + this.replaceTerm = oldState.replaceTerm; + this.showReplaceField = oldState.showReplaceField; + this.resultTreeWidget.replaceTerm = this.replaceTerm; + this.resultTreeWidget.showReplaceButtons = this.showReplaceField; + this.refresh(); + } + + onAfterAttach(msg: Message) { + super.onAfterAttach(msg); + VirtualRenderer.render(this.renderSearchHeader(), this.searchFormContainer); + Widget.attach(this.resultTreeWidget, this.contentNode); + } + + onUpdateRequest(msg: Message) { + super.onUpdateRequest(msg); + VirtualRenderer.render(this.renderSearchHeader(), this.searchFormContainer); + } + + onAfterShow(msg: Message) { + const f = document.getElementById("search-input-field"); + if (f) { + (f as HTMLInputElement).focus(); + } + } + + protected renderSearchHeader(): h.Child { + const controlButtons = this.renderControlButtons(); + const searchAndReplaceContainer = this.renderSearchAndReplace(); + const searchDetails = this.renderSearchDetails(); + return h.div(controlButtons, searchAndReplaceContainer, searchDetails); + } + + protected refresh = () => { + this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); + this.update(); + } + + protected collapseAll = () => { + this.resultTreeWidget.collapseAll(); + this.update(); + } + + protected clear = () => { + this.searchTerm = ""; + this.replaceTerm = ""; + this.searchInWorkspaceOptions.include = []; + this.searchInWorkspaceOptions.exclude = []; + this.includeIgnoredState.enabled = false; + this.matchCaseState.enabled = false; + this.wholeWordState.enabled = false; + this.regExpState.enabled = false; + const search = document.getElementById("search-input-field"); + const replace = document.getElementById("replace-input-field"); + const include = document.getElementById("include-glob-field"); + const exclude = document.getElementById("exclude-glob-field"); + if (search && replace && include && exclude) { + (search as HTMLInputElement).value = ""; + (replace as HTMLInputElement).value = ""; + (include as HTMLInputElement).value = ""; + (exclude as HTMLInputElement).value = ""; + } + this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); + this.update(); + } + + protected renderControlButtons(): h.Child { + const refreshButton = this.renderControlButton(`refresh${this.hasResults || this.searchTerm !== "" ? " enabled" : ""}`, 'Refresh', this.refresh); + const collapseAllButton = this.renderControlButton(`collapse-all${this.hasResults ? " enabled" : ""}`, 'Collapse All', this.collapseAll); + const clearButton = this.renderControlButton(`clear${this.hasResults ? " enabled" : ""}`, 'Clear', this.clear); + return h.div({ className: "controls button-container" }, refreshButton, collapseAllButton, clearButton); + } + + protected renderControlButton(btnClass: string, title: string, clickHandler: () => void): h.Child { + return h.span({ className: `btn ${btnClass}`, title, onclick: clickHandler }); + } + + protected renderSearchAndReplace(): h.Child { + const toggleContainer = this.renderReplaceFieldToggle(); + const searchField = this.renderSearchField(); + const replaceField = this.renderReplaceField(); + const searchAndReplaceFields = h.div({ className: "search-and-replace-fields" }, searchField, replaceField); + return h.div({ className: "search-and-replace-container" }, toggleContainer, searchAndReplaceFields); + } + + protected renderReplaceFieldToggle(): h.Child { + const toggle = h.span({ className: `fa fa-caret-${this.showReplaceField ? "down" : "right"}` }); + return h.div({ + className: "replace-toggle", + tabindex: "0", + onclick: e => { + const elArr = document.getElementsByClassName("replace-toggle"); + if (elArr && elArr.length > 0) { + (elArr[0] as HTMLElement).focus(); + } + this.showReplaceField = !this.showReplaceField; + this.resultTreeWidget.showReplaceButtons = this.showReplaceField; + this.update(); + } + }, toggle); + } + + protected renderSearchField(): h.Child { + const input = h.input({ + id: "search-input-field", + type: "text", + placeholder: "Search", + value: this.searchTerm, + onfocus: e => { + const elArr = document.getElementsByClassName("search-field-container"); + if (elArr && elArr.length > 0) { + (elArr[0] as HTMLElement).className = "search-field-container focussed"; + } + }, + onblur: e => { + const elArr = document.getElementsByClassName("search-field-container"); + if (elArr && elArr.length > 0) { + (elArr[0] as HTMLElement).className = "search-field-container"; + } + }, + onkeyup: e => { + if (e.target) { + this.searchTerm = (e.target as HTMLInputElement).value; + this.resultTreeWidget.search(this.searchTerm, (this.searchInWorkspaceOptions || {})); + this.update(); + } + } + }); + const optionContainer = this.renderOptionContainer(); + return h.div({ className: "search-field-container" }, h.div({ className: "search-field" }, input, optionContainer)); + } + + protected renderReplaceField(): h.Child { + const input = h.input({ + id: "replace-input-field", + type: "text", + placeholder: "Replace", + value: this.replaceTerm, + onkeyup: e => { + if (e.target) { + if (Key.ENTER.keyCode === e.keyCode) { + this.resultTreeWidget.search(this.searchTerm, (this.searchInWorkspaceOptions || {})); + this.update(); + } else { + this.replaceTerm = (e.target as HTMLInputElement).value; + this.resultTreeWidget.replaceTerm = this.replaceTerm; + } + } + } + }); + const replaceAllButtonContainer = this.renderReplaceAllButtonContainer(); + return h.div({ className: `replace-field${this.showReplaceField ? "" : " hidden"}` }, input, replaceAllButtonContainer); + } + + protected renderReplaceAllButtonContainer(): h.Child { + const replaceButton = h.span({ + className: `replace-all-button${this.searchTerm === "" ? " disabled" : ""}`, + onclick: () => { + this.resultTreeWidget.replaceAll(); + } + }); + return h.div({ + className: "replace-all-button-container" + }, replaceButton); + } + + protected renderOptionContainer(): h.Child { + const matchCaseOption = this.renderOptionElement(this.matchCaseState); + const wholeWordOption = this.renderOptionElement(this.wholeWordState); + const regexOption = this.renderOptionElement(this.regExpState); + const includeIgnoredOption = this.renderOptionElement(this.includeIgnoredState); + return h.div({ className: "option-buttons" }, matchCaseOption, wholeWordOption, regexOption, includeIgnoredOption); + } + + protected renderOptionElement(opt: SearchFieldState): h.Child { + return h.span({ + className: `${opt.className} option ${opt.enabled ? "enabled" : ""}`, + title: opt.title, + onclick: () => this.handleOptionClick(opt) + }); + } + + protected handleOptionClick(option: SearchFieldState): void { + option.enabled = !option.enabled; + this.updateSearchOptions(); + this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); + this.update(); + } + + protected updateSearchOptions() { + this.searchInWorkspaceOptions.matchCase = this.matchCaseState.enabled; + this.searchInWorkspaceOptions.matchWholeWord = this.wholeWordState.enabled; + this.searchInWorkspaceOptions.useRegExp = this.regExpState.enabled; + this.searchInWorkspaceOptions.includeIgnored = this.includeIgnoredState.enabled; + } + + protected renderSearchDetails(): h.Child { + const expandButton = this.renderExpandGlobFieldsButton(); + const globFieldContainer = this.renderGlobFieldContainer(); + return h.div({ className: "search-details" }, expandButton, globFieldContainer); + } + + protected renderGlobFieldContainer(): h.Child { + const includeField = this.renderGlobField("include"); + const excludeField = this.renderGlobField("exclude"); + return h.div({ className: `glob-field-container${!this.showSearchDetails ? " hidden" : ""}` }, includeField, excludeField); + } + + protected renderExpandGlobFieldsButton(): h.Child { + const button = h.span({ className: "fa fa-ellipsis-h btn" }); + return h.div({ + className: "button-container", onclick: () => { + this.showSearchDetails = !this.showSearchDetails; + this.update(); + } + }, button); + } + + protected renderGlobField(kind: "include" | "exclude"): h.Child { + const label = h.div({ className: "label" }, "files to " + kind); + const currentValue = this.searchInWorkspaceOptions[kind]; + const input = h.input({ + type: "text", + value: currentValue && currentValue.join(', ') || '', + id: kind + "-glob-field", + onkeyup: e => { + if (e.target) { + if (Key.ENTER.keyCode === e.keyCode) { + this.resultTreeWidget.search(this.searchTerm, this.searchInWorkspaceOptions); + } else { + this.searchInWorkspaceOptions[kind] = this.splitOnComma((e.target as HTMLInputElement).value); + } + } + } + }); + return h.div({ className: "glob-field" }, label, input); + } + + protected splitOnComma(patterns: string): string[] { + return patterns.split(',').map(s => s.trim()); + } +} diff --git a/packages/search-in-workspace/src/browser/styles/index.css b/packages/search-in-workspace/src/browser/styles/index.css new file mode 100644 index 0000000000000..09c23db17bd03 --- /dev/null +++ b/packages/search-in-workspace/src/browser/styles/index.css @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2017-2018 Ericsson and others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + */ + + :root { + --theia-search-match-color0: rgba(234, 92, 0, 0.33); + --theia-search-match-color1: rgba(234, 92, 0, 0.5); + --theia-match-replace-color: rgba(155, 185, 85, 0.2); + + --theia-current-search-match-color: var(--theia-word-highlight-color0); + --theia-range-highlight: var(--theia-word-highlight-color1); + } + +.t-siw-search-container { + color: var(--theia-ui-font-color1); + padding: 5px; + display: flex; + flex-direction: column; + height: 100%; + box-sizing: border-box; +} + +.t-siw-search-container input[type="text"] { + flex: 1; + line-height: var(--theia-content-line-height); + font-size: var(--theia-ui-font-size1); + padding-left: 8px; + color: var(--theia-ui-font-color1); +} + +.t-siw-search-container input[type="text"]:focus { + outline: none; +} + +.t-siw-search-container #search-input-field:focus { + border-color: var(--theia-layout-color2); +} + +.t-siw-search-container .searchHeader { + width: 100%; + margin-bottom: 10px; +} + +.t-siw-search-container .searchHeader .controls.button-container { + height: 22px; + margin-bottom: 5px; +} + +.t-siw-search-container .searchHeader .controls .refresh { + background: var(--theia-icon-refresh); +} + +.t-siw-search-container .searchHeader .controls .collapse-all { + background: var(--theia-icon-collapse-all); +} + +.t-siw-search-container .searchHeader .controls .clear { + background: var(--theia-icon-clear); +} + +.t-siw-search-container .searchHeader .search-field-container.focussed { + border-style: solid; + border-width: var(--theia-border-width); + border-color: var(--theia-accent-color3); + margin: -1px; +} + +.t-siw-search-container .searchHeader .search-field { + display: flex; + align-items: center; +} + +.t-siw-search-container .searchHeader .search-field .option { + opacity: 0.7; + cursor: pointer; +} + +.t-siw-search-container .searchHeader .search-field .option.enabled { + border: var(--theia-border-width) var(--theia-accent-color3) solid; +} + +.t-siw-search-container .searchHeader .search-field .option:hover { + opacity: 1; +} + +.t-siw-search-container .searchHeader .search-field .option.match-case { + background-image: var(--theia-icon-case-sensitive); +} + +.t-siw-search-container .searchHeader .search-field .option.whole-word { + background-image: var(--theia-icon-whole-word); +} + +.t-siw-search-container .searchHeader .search-field .option.use-regexp { + background-image: var(--theia-icon-regex); +} + +.t-siw-search-container .searchHeader .search-field .option-buttons { + height: 23px; + display: flex; + align-items: center; + background-color: var(--theia-layout-color2); +} + +.t-siw-search-container .searchHeader .button-container { + text-align: right; + padding-right: 5px; + padding-top: 5px; + display: flex; + justify-content: flex-end; +} + +.t-siw-search-container .searchHeader .search-field .option, +.t-siw-search-container .searchHeader .button-container .btn { + width: 21px; + height: 21px; + margin: 0 1px; + display: inline-block; + box-sizing: border-box; + align-items: center; + user-select: none; + background-repeat: no-repeat; + background-position: center; + border: var(--theia-border-width) solid transparent; +} + +.t-siw-search-container .searchHeader .search-field .fa.option { + display: flex; + align-items: center; + justify-content: center; +} + +.t-siw-search-container .searchHeader .controls .btn{ + margin-left: 3px; + opacity: 0.25; + width: 18px +} + +.t-siw-search-container .searchHeader .controls .btn.enabled{ + opacity: 0.7; + cursor: pointer; +} + +.t-siw-search-container .searchHeader .controls .btn.enabled:hover{ + opacity: 1; +} + +.t-siw-search-container .searchHeader .search-details .button-container { + height: 5px; +} + + +.t-siw-search-container .searchHeader .search-details .button-container .btn{ + cursor: pointer; +} + +.t-siw-search-container .searchHeader .glob-field-container.hidden { + display: none; +} + +.t-siw-search-container .searchHeader .glob-field-container .glob-field { + margin-bottom: 8px; + margin-left: 17px; + display: flex; + flex-direction: column; +} + +.t-siw-search-container .searchHeader .glob-field-container .glob-field .label { + margin-bottom: 3px; + user-select: none; + font-size: var(--theia-ui-font-size0); +} + +.t-siw-search-container .result { + overflow: hidden; + width: 100%; +} + +.t-siw-search-container .result .result-head { + display:flex; +} + +.t-siw-search-container .result .result-head .result-no { + background: var(--theia-ui-expand-button-color); + padding: 3px 8px; + border-radius: 7px; + font-size: var(--theia-ui-font-size0); +} + +.t-siw-search-container .result .result-head .expand-icon { + margin: 0 3px; + width: 7px; +} + +.t-siw-search-container .result .result-head .file-icon { + margin: 0 3px; +} + +.t-siw-search-container .result .result-head .file-name { + margin-right: 5px; +} + +.t-siw-search-container .result .result-head .file-path { + color: var(--theia-ui-font-color2); + font-size: var(--theia-ui-font-size0); + margin-left: 3px; +} + +.t-siw-search-container .resultLine .match { + background: var(--theia-search-match-color1); +} + +.t-siw-search-container .resultLine .match.strike-through { + text-decoration: line-through; +} + +.t-siw-search-container .resultLine.selected .match { + background: var(--theia-search-match-color1); +} +.t-siw-search-container .resultLine .replace-term { + background: var(--theia-match-replace-color); +} + +.noWrapInfo { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; +} + +.result-head-info { + align-items: center; +} + +.search-in-workspace-editor-match { + background: var(--theia-search-match-color0); +} + +.current-search-in-workspace-editor-match { + background: var(--theia-current-search-match-color) +} + +.current-match-range-highlight { + background: var(--theia-range-highlight); +} + +.result-node-buttons { + display: none; +} + +.theia-TreeNode:hover .result-node-buttons { + display: flex; + justify-content: flex-end; + flex: 1; + align-items: center; + align-self: center; +} + +.theia-TreeNode:hover .result-number { + display: none; +} + +.theia-TreeNode .result-number { + background-color: var(--theia-ui-font-color2); + border-radius: 10px; + padding: 0 5px; + text-align: center; + font-size: calc(var(--theia-ui-font-size0) * 0.8); + color: var(--theia-inverse-ui-font-color0); + font-weight: 400; + min-width: 7px; + height: 16px; + line-height: calc(var(--theia-private-horizontal-tab-height) * 0.8); + align-self: center; +} + +.result-node-buttons .remove-node { + background-image: var(--theia-icon-close); +} + +.result-node-buttons > span { + width: 15px; + height: 15px; + margin-right: 3px; + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} + +.search-and-replace-container { + display: flex; +} + +.replace-toggle { + display: flex; + align-items: center; + width: 15px; + justify-content: center; + margin-right: 2px; + box-sizing: border-box; +} + +.replace-toggle:hover { + background: var(--theia-layout-color2); +} + +.search-and-replace-fields { + display: flex; + flex-direction: column; + flex: 1; +} + +.replace-field { + display: flex; + margin-top: 5px; +} + +.replace-field.hidden { + display: none; +} + +.replace-all-button-container { + width: 25px; + display: flex; + align-items: center; + justify-content: center; +} + +.replace-all-button { + width: 100%; + height: 100%; + display: inline-block; + background: var(--theia-icon-replace-all) no-repeat center; +} + +.result-node-buttons .replace-result { + background-image: var(--theia-icon-replace); +} + +.replace-all-button.disabled { + opacity: 0.5; +} \ No newline at end of file diff --git a/packages/search-in-workspace/src/common/search-in-workspace-interface.ts b/packages/search-in-workspace/src/common/search-in-workspace-interface.ts index 87fc47afef64a..e19f3cbed84c5 100644 --- a/packages/search-in-workspace/src/common/search-in-workspace-interface.ts +++ b/packages/search-in-workspace/src/common/search-in-workspace-interface.ts @@ -12,6 +12,30 @@ export interface SearchInWorkspaceOptions { * Maximum number of results to return. Defaults to unlimited. */ maxResults?: number; + /** + * Search case sensitively if true. + */ + matchCase?: boolean; + /** + * Search whole words only if true. + */ + matchWholeWord?: boolean; + /** + * Use regular expressions for search if true. + */ + useRegExp?: boolean; + /** + * Include all .gitignored and hidden files. + */ + includeIgnored?: boolean; + /** + * Glob pattern for matching files and directories to include the search. + */ + include?: string[]; + /** + * Glob pattern for matching files and directories to exclude the search. + */ + exclude?: string[] } export interface SearchInWorkspaceResult { diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts index c97238b0d04ab..ca3b4b35d0073 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.slow-spec.ts @@ -52,7 +52,7 @@ class ResultAccumulator implements SearchInWorkspaceClient { // Create a test file relative to rootDir. function createTestFile(filename: string, text: string) { - fs.writeFileSync(rootDir + '/' + filename, text); + fs.writeFileSync(path.join(rootDir, filename), text); fileLines.set(filename, text.split('\n')); } @@ -71,7 +71,7 @@ it's very confusing. `); createTestFile('regexes', `\ -aaa hello x h3lo y hell0h3lllo +aaa hello. x h3lo y hell0h3lllo hello1 `); @@ -177,7 +177,7 @@ describe('ripgrep-search-in-workspace-server', function () { { file: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' }, { file: 'carrots', line: 3, character: 52, length: pattern.length, lineText: '' }, { file: 'carrots', line: 4, character: 1, length: pattern.length, lineText: '' }, - { file: 'potatoes', line: 1, character: 18, length: pattern.length, lineText: '' }, + { file: 'potatoes', line: 1, character: 18, length: pattern.length, lineText: '' } ]; compareSearchResults(expected, client.results); @@ -187,6 +187,66 @@ describe('ripgrep-search-in-workspace-server', function () { ripgrepServer.search(pattern, rootDir); }); + it('returns 5 results when searching for "carrot" case sensitive', function (done) { + const pattern = 'carrot'; + + const client = new ResultAccumulator(() => { + const expected: SearchInWorkspaceResult[] = [ + { file: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, + { file: 'carrots', line: 2, character: 6, length: pattern.length, lineText: '' }, + { file: 'carrots', line: 2, character: 35, length: pattern.length, lineText: '' }, + { file: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' }, + { file: 'potatoes', line: 1, character: 18, length: pattern.length, lineText: '' } + ]; + + compareSearchResults(expected, client.results); + done(); + }); + ripgrepServer.setClient(client); + ripgrepServer.search(pattern, rootDir, { + matchCase: true + }); + }); + + it('returns 4 results when searching for "carrot" matching whole words, case insensitive', function (done) { + const pattern = 'carrot'; + + const client = new ResultAccumulator(() => { + const expected: SearchInWorkspaceResult[] = [ + { file: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, + { file: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' }, + { file: 'carrots', line: 3, character: 52, length: pattern.length, lineText: '' }, + { file: 'carrots', line: 4, character: 1, length: pattern.length, lineText: '' } + ]; + + compareSearchResults(expected, client.results); + done(); + }); + ripgrepServer.setClient(client); + ripgrepServer.search(pattern, rootDir, { + matchWholeWord: true + }); + }); + + it('returns 4 results when searching for "carrot" matching whole words, case sensitive', function (done) { + const pattern = 'carrot'; + + const client = new ResultAccumulator(() => { + const expected: SearchInWorkspaceResult[] = [ + { file: 'carrots', line: 1, character: 11, length: pattern.length, lineText: '' }, + { file: 'carrots', line: 3, character: 28, length: pattern.length, lineText: '' } + ]; + + compareSearchResults(expected, client.results); + done(); + }); + ripgrepServer.setClient(client); + ripgrepServer.search(pattern, rootDir, { + matchWholeWord: true, + matchCase: true + }); + }); + it('returns 1 result when searching for "Carrot"', function (done) { const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceResult[] = [ @@ -197,7 +257,7 @@ describe('ripgrep-search-in-workspace-server', function () { done(); }); ripgrepServer.setClient(client); - ripgrepServer.search('Carrot', rootDir); + ripgrepServer.search('Carrot', rootDir, { matchCase: true }); }); it('returns 0 result when searching for "CarroT"', function (done) { @@ -208,7 +268,7 @@ describe('ripgrep-search-in-workspace-server', function () { done(); }); ripgrepServer.setClient(client); - ripgrepServer.search(pattern, rootDir); + ripgrepServer.search(pattern, rootDir, { matchCase: true }); }); // Try something that we know isn't there. @@ -301,9 +361,9 @@ describe('ripgrep-search-in-workspace-server', function () { const client = new ResultAccumulator(() => { const expected: SearchInWorkspaceResult[] = [ { file: 'regexes', line: 1, character: 5, length: 5, lineText: '' }, - { file: 'regexes', line: 1, character: 13, length: 4, lineText: '' }, - { file: 'regexes', line: 1, character: 20, length: 5, lineText: '' }, - { file: 'regexes', line: 1, character: 25, length: 6, lineText: '' }, + { file: 'regexes', line: 1, character: 14, length: 4, lineText: '' }, + { file: 'regexes', line: 1, character: 21, length: 5, lineText: '' }, + { file: 'regexes', line: 1, character: 26, length: 6, lineText: '' }, { file: 'regexes', line: 2, character: 1, length: 5, lineText: '' }, ]; @@ -311,7 +371,27 @@ describe('ripgrep-search-in-workspace-server', function () { done(); }); ripgrepServer.setClient(client); - ripgrepServer.search(pattern, rootDir); + ripgrepServer.search(pattern, rootDir, { + useRegExp: true + }); + }); + + // Try without regex + it('searches for fixed string', function (done) { + const pattern = 'hello.'; + + const client = new ResultAccumulator(() => { + const expected: SearchInWorkspaceResult[] = [ + { file: 'regexes', line: 1, character: 5, length: 6, lineText: '' } + ]; + + compareSearchResults(expected, client.results); + done(); + }); + ripgrepServer.setClient(client); + ripgrepServer.search(pattern, rootDir, { + useRegExp: false + }); }); // Try with a pattern starting with -, and in filenames containing colons and spaces. @@ -333,7 +413,7 @@ describe('ripgrep-search-in-workspace-server', function () { done(); }); ripgrepServer.setClient(client); - ripgrepServer.search(pattern, rootDir); + ripgrepServer.search(pattern, rootDir, { useRegExp: true }); }); // Try with a pattern starting with --, and in filenames containing colons and spaces. @@ -355,7 +435,7 @@ describe('ripgrep-search-in-workspace-server', function () { done(); }); ripgrepServer.setClient(client); - ripgrepServer.search(pattern, rootDir); + ripgrepServer.search(pattern, rootDir, { useRegExp: true }); }); // Try searching in an UTF-8 file. @@ -390,7 +470,7 @@ describe('ripgrep-search-in-workspace-server', function () { done(); }); ripgrepServer.setClient(client); - ripgrepServer.search(pattern, rootDir); + ripgrepServer.search(pattern, rootDir, { useRegExp: true }); }); // A regex that may match an empty string should not return zero-length diff --git a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts index 7dae101e05cf1..d9fbbcedcf0ee 100644 --- a/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts +++ b/packages/search-in-workspace/src/node/ripgrep-search-in-workspace-server.ts @@ -44,24 +44,53 @@ export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer { this.client = client; } + protected getArgs(options?: SearchInWorkspaceOptions): string[] { + const args = ["--vimgrep", "--color=always", + "--colors=path:none", + "--colors=line:none", + "--colors=column:none", + "--colors=match:none", + "--colors=path:fg:red", + "--colors=line:fg:green", + "--colors=column:fg:yellow", + "--colors=match:fg:blue", + "--sort-files"]; + args.push(options && options.matchCase ? "--case-sensitive" : "--ignore-case"); + if (options && options.matchWholeWord) { + args.push("--word-regexp"); + } + if (options && options.includeIgnored) { + args.push("-uu"); + } + args.push(options && options.useRegExp ? "--regexp" : "--fixed-strings"); + + return args; + } + // Search for the string WHAT in directory ROOT. Return the assigned search id. search(what: string, root: string, opts?: SearchInWorkspaceOptions): Promise { // Start the rg process. Use --vimgrep to get one result per // line, --color=always to get color control characters that // we'll use to parse the lines. const searchId = this.nextSearchId++; + const args = this.getArgs(opts); + const globs = []; + if (opts && opts.include) { + for (const include of opts.include) { + globs.push('--glob="**/' + include + '"'); + } + } + if (opts && opts.exclude) { + for (const exclude of opts.exclude) { + globs.push('--glob="!**/' + exclude + '"'); + } + } const processOptions: RawProcessOptions = { command: rgPath, - args: ["--vimgrep", "-S", "--color=always", - "--colors=path:none", - "--colors=line:none", - "--colors=column:none", - "--colors=match:none", - "--colors=path:fg:red", - "--colors=line:fg:green", - "--colors=column:fg:yellow", - "--colors=match:fg:blue", - "-e", what, root], + args: [...args, '"' + what + '"', ...globs, root], + options: { + shell: true + } }; const process: RawProcess = this.rawProcessFactory(processOptions); this.ongoingSearches.set(searchId, process);