deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { Key, KeyCode } from '@theia/core/lib/browser';
import debounce = require('@theia/core/shared/lodash.debounce');
interface HistoryState {
history: string[];
index: number;
};
type InputAttributes = React.InputHTMLAttributes<HTMLInputElement>;
export class SearchInWorkspaceInput extends React.Component<InputAttributes, HistoryState> {
static LIMIT = 100;
private input = React.createRef<HTMLInputElement>();
constructor(props: InputAttributes) {
super(props);
this.state = {
history: [],
index: 0,
};
}
updateState(index: number, history?: string[]): void {
this.value = history ? history[index] : this.state.history[index];
this.setState(prevState => {
const newState = {
...prevState,
index,
};
if (history) {
newState.history = history;
}
return newState;
});
}
get value(): string {
return this.input.current?.value ?? '';
}
set value(value: string) {
if (this.input.current) {
this.input.current.value = value;
}
}
/**
* Handle history navigation without overriding the parent's onKeyDown handler, if any.
*/
protected readonly onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (Key.ARROW_UP.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) {
e.preventDefault();
this.previousValue();
} else if (Key.ARROW_DOWN.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) {
e.preventDefault();
this.nextValue();
}
this.props.onKeyDown?.(e);
};
/**
* Switch the input's text to the previous value, if any.
*/
previousValue(): void {
const { history, index } = this.state;
if (!this.value) {
this.value = history[index];
} else if (index > 0 && index < history.length) {
this.updateState(index - 1);
}
}
/**
* Switch the input's text to the next value, if any.
*/
nextValue(): void {
const { history, index } = this.state;
if (index === history.length - 1) {
this.value = '';
} else if (!this.value) {
this.value = history[index];
} else if (index >= 0 && index < history.length - 1) {
this.updateState(index + 1);
}
}
/**
* Handle history collection without overriding the parent's onChange handler, if any.
*/
protected readonly onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.addToHistory();
this.props.onChange?.(e);
};
/**
* Add a nonempty current value to the history, if not already present. (Debounced, 1 second delay.)
*/
readonly addToHistory = debounce(this.doAddToHistory, 1000);
private doAddToHistory(): void {
if (!this.value) {
return;
}
const history = this.state.history
.filter(term => term !== this.value)
.concat(this.value)
.slice(-SearchInWorkspaceInput.LIMIT);
this.updateState(history.length - 1, history);
}
override render(): React.ReactNode {
return (
<input
{...this.props}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
spellCheck={false}
ref={this.input}
/>
);
}
}

View File

@@ -0,0 +1,159 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Key, KeyCode } from '@theia/core/lib/browser';
import * as React from '@theia/core/shared/react';
import TextareaAutosize from 'react-textarea-autosize';
import debounce = require('@theia/core/shared/lodash.debounce');
interface HistoryState {
history: string[];
index: number;
};
type TextareaAttributes = Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'style'>;
export class SearchInWorkspaceTextArea extends React.Component<TextareaAttributes, HistoryState> {
static LIMIT = 100;
textarea = React.createRef<HTMLTextAreaElement>();
constructor(props: TextareaAttributes) {
super(props);
this.state = {
history: [],
index: 0,
};
}
updateState(index: number, history?: string[]): void {
this.value = history ? history[index] : this.state.history[index];
this.setState(prevState => {
const newState = {
...prevState,
index,
};
if (history) {
newState.history = history;
}
return newState;
});
}
get value(): string {
return this.textarea.current?.value ?? '';
}
set value(value: string) {
if (this.textarea.current) {
this.textarea.current.value = value;
}
}
/**
* Handle history navigation without overriding the parent's onKeyDown handler, if any.
*/
protected readonly onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>): void => {
// Navigate history only when cursor is at first or last position of the textarea
if (Key.ARROW_UP.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && e.currentTarget.selectionStart === 0) {
e.preventDefault();
this.previousValue();
} else if (Key.ARROW_DOWN.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && e.currentTarget.selectionEnd === e.currentTarget.value.length) {
e.preventDefault();
this.nextValue();
}
// Prevent newline on enter
if (Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode && !e.nativeEvent.shiftKey) {
e.preventDefault();
}
setTimeout(() => {
this.forceUpdate();
}, 0);
this.props.onKeyDown?.(e);
};
/**
* Switch the textarea's text to the previous value, if any.
*/
previousValue(): void {
const { history, index } = this.state;
if (!this.value) {
this.value = history[index];
} else if (index > 0 && index < history.length) {
this.updateState(index - 1);
}
}
/**
* Switch the textarea's text to the next value, if any.
*/
nextValue(): void {
const { history, index } = this.state;
if (index === history.length - 1) {
this.value = '';
} else if (!this.value) {
this.value = history[index];
} else if (index >= 0 && index < history.length - 1) {
this.updateState(index + 1);
}
}
/**
* Handle history collection and textarea resizing without overriding the parent's onChange handler, if any.
*/
protected readonly onChange = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
this.addToHistory();
this.forceUpdate();
this.props.onChange?.(e);
};
/**
* Add a nonempty current value to the history, if not already present. (Debounced, 1 second delay.)
*/
readonly addToHistory = debounce(this.doAddToHistory, 1000);
private doAddToHistory(): void {
if (!this.value) {
return;
}
const history = this.state.history
.filter(term => term !== this.value)
.concat(this.value)
.slice(-SearchInWorkspaceTextArea.LIMIT);
this.updateState(history.length - 1, history);
}
override render(): React.ReactNode {
/* One row for an empty search input box (fixes bug #15229), seven rows for the normal state (from VS Code) */
const maxRows = this.value.length ? 7 : 1;
return (
<TextareaAutosize
{...this.props}
autoCapitalize="off"
autoCorrect="off"
maxRows={maxRows}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
ref={this.textarea}
rows={1}
spellCheck={false}
/>
);
}
}

View File

@@ -0,0 +1,93 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
@injectable()
export class SearchInWorkspaceContextKeyService {
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
protected _searchViewletVisible: ContextKey<boolean>;
get searchViewletVisible(): ContextKey<boolean> {
return this._searchViewletVisible;
}
protected _searchViewletFocus: ContextKey<boolean>;
get searchViewletFocus(): ContextKey<boolean> {
return this._searchViewletFocus;
}
protected searchInputBoxFocus: ContextKey<boolean>;
setSearchInputBoxFocus(searchInputBoxFocus: boolean): void {
this.searchInputBoxFocus.set(searchInputBoxFocus);
this.updateInputBoxFocus();
}
protected replaceInputBoxFocus: ContextKey<boolean>;
setReplaceInputBoxFocus(replaceInputBoxFocus: boolean): void {
this.replaceInputBoxFocus.set(replaceInputBoxFocus);
this.updateInputBoxFocus();
}
protected patternIncludesInputBoxFocus: ContextKey<boolean>;
setPatternIncludesInputBoxFocus(patternIncludesInputBoxFocus: boolean): void {
this.patternIncludesInputBoxFocus.set(patternIncludesInputBoxFocus);
this.updateInputBoxFocus();
}
protected patternExcludesInputBoxFocus: ContextKey<boolean>;
setPatternExcludesInputBoxFocus(patternExcludesInputBoxFocus: boolean): void {
this.patternExcludesInputBoxFocus.set(patternExcludesInputBoxFocus);
this.updateInputBoxFocus();
}
protected inputBoxFocus: ContextKey<boolean>;
protected updateInputBoxFocus(): void {
this.inputBoxFocus.set(
this.searchInputBoxFocus.get() ||
this.replaceInputBoxFocus.get() ||
this.patternIncludesInputBoxFocus.get() ||
this.patternExcludesInputBoxFocus.get()
);
}
protected _replaceActive: ContextKey<boolean>;
get replaceActive(): ContextKey<boolean> {
return this._replaceActive;
}
protected _hasSearchResult: ContextKey<boolean>;
get hasSearchResult(): ContextKey<boolean> {
return this._hasSearchResult;
}
@postConstruct()
protected init(): void {
this._searchViewletVisible = this.contextKeyService.createKey<boolean>('searchViewletVisible', false);
this._searchViewletFocus = this.contextKeyService.createKey<boolean>('searchViewletFocus', false);
this.inputBoxFocus = this.contextKeyService.createKey<boolean>('inputBoxFocus', false);
this.searchInputBoxFocus = this.contextKeyService.createKey<boolean>('searchInputBoxFocus', false);
this.replaceInputBoxFocus = this.contextKeyService.createKey<boolean>('replaceInputBoxFocus', false);
this.patternIncludesInputBoxFocus = this.contextKeyService.createKey<boolean>('patternIncludesInputBoxFocus', false);
this.patternExcludesInputBoxFocus = this.contextKeyService.createKey<boolean>('patternExcludesInputBoxFocus', false);
this._replaceActive = this.contextKeyService.createKey<boolean>('replaceActive', false);
this._hasSearchResult = this.contextKeyService.createKey<boolean>('hasSearchResult', false);
}
}

View File

@@ -0,0 +1,59 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import {
codicon,
ViewContainer,
ViewContainerTitleOptions,
WidgetFactory,
WidgetManager
} from '@theia/core/lib/browser';
import { SearchInWorkspaceWidget } from './search-in-workspace-widget';
import { nls } from '@theia/core/lib/common/nls';
export const SEARCH_VIEW_CONTAINER_ID = 'search-view-container';
export const SEARCH_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = {
label: nls.localizeByDefault('Search'),
iconClass: codicon('search'),
closeable: true
};
@injectable()
export class SearchInWorkspaceFactory implements WidgetFactory {
readonly id = SEARCH_VIEW_CONTAINER_ID;
protected searchWidgetOptions: ViewContainer.Factory.WidgetOptions = {
canHide: false,
initiallyCollapsed: false
};
@inject(ViewContainer.Factory)
protected readonly viewContainerFactory: ViewContainer.Factory;
@inject(WidgetManager) protected readonly widgetManager: WidgetManager;
async createWidget(): Promise<ViewContainer> {
const viewContainer = this.viewContainerFactory({
id: SEARCH_VIEW_CONTAINER_ID,
progressLocationId: 'search'
});
viewContainer.setTitleOptions(SEARCH_VIEW_CONTAINER_TITLE_OPTIONS);
const widget = await this.widgetManager.getOrCreateWidget(SearchInWorkspaceWidget.ID);
viewContainer.addWidget(widget, this.searchWidgetOptions);
return viewContainer;
}
}

View File

@@ -0,0 +1,510 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
AbstractViewContribution, KeybindingRegistry, LabelProvider, CommonMenus, FrontendApplication,
FrontendApplicationContribution, CommonCommands, StylingParticipant, ColorTheme, CssStyleCollector
} from '@theia/core/lib/browser';
import { SearchInWorkspaceWidget } from './search-in-workspace-widget';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { CommandRegistry, MenuModelRegistry, SelectionService, Command, isOSX, nls } from '@theia/core';
import { codicon, Widget } from '@theia/core/lib/browser/widgets';
import { FileNavigatorCommands, NavigatorContextMenu } from '@theia/navigator/lib/browser/navigator-contribution';
import { UriCommandHandler, UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import URI from '@theia/core/lib/common/uri';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { SEARCH_VIEW_CONTAINER_ID } from './search-in-workspace-factory';
import { SearchInWorkspaceFileNode, SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result-tree-widget';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { isHighContrast } from '@theia/core/lib/common/theme';
export namespace SearchInWorkspaceCommands {
const SEARCH_CATEGORY = 'Search';
export const TOGGLE_SIW_WIDGET = {
id: 'search-in-workspace.toggle'
};
export const OPEN_SIW_WIDGET = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.open',
category: SEARCH_CATEGORY,
label: 'Find in Files'
});
export const REPLACE_IN_FILES = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.replace',
category: SEARCH_CATEGORY,
label: 'Replace in Files'
});
export const FIND_IN_FOLDER = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.in-folder',
category: SEARCH_CATEGORY,
label: 'Find in Folder...'
});
export const FOCUS_NEXT_RESULT = Command.toDefaultLocalizedCommand({
id: 'search.action.focusNextSearchResult',
category: SEARCH_CATEGORY,
label: 'Focus Next Search Result'
});
export const FOCUS_PREV_RESULT = Command.toDefaultLocalizedCommand({
id: 'search.action.focusPreviousSearchResult',
category: SEARCH_CATEGORY,
label: 'Focus Previous Search Result'
});
export const REFRESH_RESULTS = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.refresh',
category: SEARCH_CATEGORY,
label: 'Refresh',
iconClass: codicon('refresh')
});
export const CANCEL_SEARCH = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.cancel',
category: SEARCH_CATEGORY,
label: 'Cancel Search',
iconClass: codicon('search-stop')
});
export const COLLAPSE_ALL = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.collapse-all',
category: SEARCH_CATEGORY,
label: 'Collapse All',
iconClass: codicon('collapse-all')
});
export const EXPAND_ALL = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.expand-all',
category: SEARCH_CATEGORY,
label: 'Expand All',
iconClass: codicon('expand-all')
});
export const CLEAR_ALL = Command.toDefaultLocalizedCommand({
id: 'search-in-workspace.clear-all',
category: SEARCH_CATEGORY,
label: 'Clear Search Results',
iconClass: codicon('clear-all')
});
export const COPY_ALL = Command.toDefaultLocalizedCommand({
id: 'search.action.copyAll',
category: SEARCH_CATEGORY,
label: 'Copy All',
});
export const COPY_ONE = Command.toDefaultLocalizedCommand({
id: 'search.action.copyMatch',
category: SEARCH_CATEGORY,
label: 'Copy',
});
export const DISMISS_RESULT = Command.toDefaultLocalizedCommand({
id: 'search.action.remove',
category: SEARCH_CATEGORY,
label: 'Dismiss',
});
export const REPLACE_RESULT = Command.toDefaultLocalizedCommand({
id: 'search.action.replace',
});
export const REPLACE_ALL_RESULTS = Command.toDefaultLocalizedCommand({
id: 'search.action.replaceAll'
});
}
@injectable()
export class SearchInWorkspaceFrontendContribution extends AbstractViewContribution<SearchInWorkspaceWidget> implements
FrontendApplicationContribution,
TabBarToolbarContribution,
StylingParticipant {
@inject(SelectionService) protected readonly selectionService: SelectionService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(FileService) protected readonly fileService: FileService;
@inject(EditorManager) protected readonly editorManager: EditorManager;
@inject(ClipboardService) protected readonly clipboardService: ClipboardService;
@inject(SearchInWorkspaceContextKeyService)
protected readonly contextKeyService: SearchInWorkspaceContextKeyService;
constructor() {
super({
viewContainerId: SEARCH_VIEW_CONTAINER_ID,
widgetId: SearchInWorkspaceWidget.ID,
widgetName: SearchInWorkspaceWidget.LABEL,
defaultWidgetOptions: {
area: 'left',
rank: 200
},
toggleCommandId: SearchInWorkspaceCommands.TOGGLE_SIW_WIDGET.id
});
}
@postConstruct()
protected init(): void {
const updateFocusContextKey = () =>
this.contextKeyService.searchViewletFocus.set(this.shell.activeWidget instanceof SearchInWorkspaceWidget);
updateFocusContextKey();
this.shell.onDidChangeActiveWidget(updateFocusContextKey);
}
async initializeLayout(app: FrontendApplication): Promise<void> {
await this.openView({ activate: false });
}
override async registerCommands(commands: CommandRegistry): Promise<void> {
super.registerCommands(commands);
commands.registerCommand(SearchInWorkspaceCommands.OPEN_SIW_WIDGET, {
isEnabled: () => this.workspaceService.tryGetRoots().length > 0,
execute: async () => {
const widget = await this.openView({ activate: true });
widget.updateSearchTerm(this.getSearchTerm());
}
});
commands.registerCommand(SearchInWorkspaceCommands.REPLACE_IN_FILES, {
isEnabled: () => this.workspaceService.tryGetRoots().length > 0,
execute: async () => {
const widget = await this.openView({ activate: true });
widget.updateSearchTerm(this.getSearchTerm(), true);
}
});
commands.registerCommand(SearchInWorkspaceCommands.FOCUS_NEXT_RESULT, {
isEnabled: () => this.withWidget(undefined, widget => widget.hasResultList()),
execute: async () => {
const widget = await this.openView({ reveal: true });
widget.resultTreeWidget.selectNextResult();
}
});
commands.registerCommand(SearchInWorkspaceCommands.FOCUS_PREV_RESULT, {
isEnabled: () => this.withWidget(undefined, widget => widget.hasResultList()),
execute: async () => {
const widget = await this.openView({ reveal: true });
widget.resultTreeWidget.selectPreviousResult();
}
});
commands.registerCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER, this.newMultiUriAwareCommandHandler({
execute: async uris => {
const resources: string[] = [];
for (const { stat } of await this.fileService.resolveAll(uris.map(resource => ({ resource })))) {
if (stat) {
const uri = stat.resource;
let uriStr = this.labelProvider.getLongName(uri);
if (stat && !stat.isDirectory) {
uriStr = this.labelProvider.getLongName(uri.parent);
}
resources.push(uriStr);
}
}
const widget = await this.openView({ activate: true });
widget.findInFolder(resources);
}
}));
commands.registerCommand(SearchInWorkspaceCommands.CANCEL_SEARCH, {
execute: w => this.withWidget(w, widget => widget.getCancelIndicator() && widget.getCancelIndicator()!.cancel()),
isEnabled: w => this.withWidget(w, widget => widget.getCancelIndicator() !== undefined),
isVisible: w => this.withWidget(w, widget => widget.getCancelIndicator() !== undefined)
});
commands.registerCommand(SearchInWorkspaceCommands.REFRESH_RESULTS, {
execute: w => this.withWidget(w, widget => widget.refresh()),
isEnabled: w => this.withWidget(w, widget => (widget.hasResultList() || widget.hasSearchTerm()) && this.workspaceService.tryGetRoots().length > 0),
isVisible: w => this.withWidget(w, () => true)
});
commands.registerCommand(SearchInWorkspaceCommands.COLLAPSE_ALL, {
execute: w => this.withWidget(w, widget => widget.collapseAll()),
isEnabled: w => this.withWidget(w, widget => widget.hasResultList()),
isVisible: w => this.withWidget(w, widget => !widget.areResultsCollapsed())
});
commands.registerCommand(SearchInWorkspaceCommands.EXPAND_ALL, {
execute: w => this.withWidget(w, widget => widget.expandAll()),
isEnabled: w => this.withWidget(w, widget => widget.hasResultList()),
isVisible: w => this.withWidget(w, widget => widget.areResultsCollapsed())
});
commands.registerCommand(SearchInWorkspaceCommands.CLEAR_ALL, {
execute: w => this.withWidget(w, widget => widget.clear()),
isEnabled: w => this.withWidget(w, widget => widget.hasResultList()),
isVisible: w => this.withWidget(w, () => true)
});
commands.registerCommand(SearchInWorkspaceCommands.DISMISS_RESULT, {
isEnabled: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0;
}),
isVisible: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0;
}),
execute: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
if (TreeWidgetSelection.is(selection)) {
selection.forEach(n => widget.resultTreeWidget.removeNode(n));
}
})
});
commands.registerCommand(SearchInWorkspaceCommands.REPLACE_RESULT, {
isEnabled: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0 && !SearchInWorkspaceFileNode.is(selection[0]);
}),
isVisible: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0 && !SearchInWorkspaceFileNode.is(selection[0]);
}),
execute: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
if (TreeWidgetSelection.is(selection)) {
selection.forEach(n => widget.resultTreeWidget.replace(n));
}
}),
});
commands.registerCommand(SearchInWorkspaceCommands.REPLACE_ALL_RESULTS, {
isEnabled: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0
&& SearchInWorkspaceFileNode.is(selection[0]);
}),
isVisible: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0
&& SearchInWorkspaceFileNode.is(selection[0]);
}),
execute: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
if (TreeWidgetSelection.is(selection)) {
selection.forEach(n => widget.resultTreeWidget.replace(n));
}
}),
});
commands.registerCommand(SearchInWorkspaceCommands.COPY_ONE, {
isEnabled: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0;
}),
isVisible: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0;
}),
execute: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
if (TreeWidgetSelection.is(selection)) {
const string = widget.resultTreeWidget.nodeToString(selection[0], true);
if (string.length !== 0) {
this.clipboardService.writeText(string);
}
}
})
});
commands.registerCommand(SearchInWorkspaceCommands.COPY_ALL, {
isEnabled: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0;
}),
isVisible: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
return TreeWidgetSelection.isSource(selection, widget.resultTreeWidget) && selection.length > 0;
}),
execute: () => this.withWidget(undefined, widget => {
const { selection } = this.selectionService;
if (TreeWidgetSelection.is(selection)) {
const string = widget.resultTreeWidget.treeToString();
if (string.length !== 0) {
this.clipboardService.writeText(string);
}
}
})
});
}
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), fn: (widget: SearchInWorkspaceWidget) => T): T | false {
if (widget instanceof SearchInWorkspaceWidget && widget.id === SearchInWorkspaceWidget.ID) {
return fn(widget);
}
return false;
}
/**
* Get the search term based on current editor selection.
* @returns the selection if available.
*/
protected getSearchTerm(): string {
if (!this.editorManager.currentEditor) {
return '';
}
// Get the current editor selection.
const selection = this.editorManager.currentEditor.editor.selection;
// Compute the selection range.
const selectedRange: Range = Range.create(
selection.start.line,
selection.start.character,
selection.end.line,
selection.end.character
);
// Return the selection text if available, else return empty.
return this.editorManager.currentEditor
? this.editorManager.currentEditor.editor.document.getText(selectedRange)
: '';
}
override registerKeybindings(keybindings: KeybindingRegistry): void {
super.registerKeybindings(keybindings);
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.OPEN_SIW_WIDGET.id,
keybinding: 'ctrlcmd+shift+f'
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.FIND_IN_FOLDER.id,
keybinding: 'shift+alt+f',
when: 'explorerResourceIsFolder'
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.FOCUS_NEXT_RESULT.id,
keybinding: 'f4',
when: 'hasSearchResult'
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.FOCUS_PREV_RESULT.id,
keybinding: 'shift+f4',
when: 'hasSearchResult'
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.DISMISS_RESULT.id,
keybinding: isOSX ? 'cmd+backspace' : 'del',
when: 'searchViewletFocus && !inputBoxFocus'
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.REPLACE_RESULT.id,
keybinding: 'ctrlcmd+shift+1',
when: 'searchViewletFocus && replaceActive',
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.REPLACE_ALL_RESULTS.id,
keybinding: 'ctrlcmd+shift+1',
when: 'searchViewletFocus && replaceActive',
});
keybindings.registerKeybinding({
command: SearchInWorkspaceCommands.COPY_ONE.id,
keybinding: 'ctrlcmd+c',
when: 'searchViewletFocus && !inputBoxFocus'
});
}
override registerMenus(menus: MenuModelRegistry): void {
super.registerMenus(menus);
menus.registerMenuAction(NavigatorContextMenu.SEARCH, {
commandId: SearchInWorkspaceCommands.FIND_IN_FOLDER.id,
when: 'explorerResourceIsFolder'
});
menus.registerMenuAction(CommonMenus.EDIT_FIND, {
commandId: SearchInWorkspaceCommands.OPEN_SIW_WIDGET.id,
order: '2'
});
menus.registerMenuAction(CommonMenus.EDIT_FIND, {
commandId: SearchInWorkspaceCommands.REPLACE_IN_FILES.id,
order: '3'
});
menus.registerMenuAction(SearchInWorkspaceResultTreeWidget.Menus.INTERNAL, {
commandId: SearchInWorkspaceCommands.REPLACE_RESULT.id,
label: nls.localizeByDefault('Replace'),
order: '1',
when: 'replaceActive',
});
menus.registerMenuAction(SearchInWorkspaceResultTreeWidget.Menus.INTERNAL, {
commandId: SearchInWorkspaceCommands.REPLACE_ALL_RESULTS.id,
label: nls.localizeByDefault('Replace All'),
order: '1',
when: 'replaceActive',
});
menus.registerMenuAction(SearchInWorkspaceResultTreeWidget.Menus.INTERNAL, {
commandId: SearchInWorkspaceCommands.DISMISS_RESULT.id,
order: '1'
});
menus.registerMenuAction(SearchInWorkspaceResultTreeWidget.Menus.COPY, {
commandId: SearchInWorkspaceCommands.COPY_ONE.id,
order: '1',
});
menus.registerMenuAction(SearchInWorkspaceResultTreeWidget.Menus.COPY, {
commandId: CommonCommands.COPY_PATH.id,
order: '2',
});
menus.registerMenuAction(SearchInWorkspaceResultTreeWidget.Menus.COPY, {
commandId: SearchInWorkspaceCommands.COPY_ALL.id,
order: '3',
});
menus.registerMenuAction(SearchInWorkspaceResultTreeWidget.Menus.EXTERNAL, {
commandId: FileNavigatorCommands.REVEAL_IN_NAVIGATOR.id,
order: '1',
});
}
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
const widget = await this.widget;
const onDidChange = widget.onDidUpdate;
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.CANCEL_SEARCH.id,
command: SearchInWorkspaceCommands.CANCEL_SEARCH.id,
tooltip: SearchInWorkspaceCommands.CANCEL_SEARCH.label,
priority: 0,
onDidChange
});
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.REFRESH_RESULTS.id,
command: SearchInWorkspaceCommands.REFRESH_RESULTS.id,
tooltip: SearchInWorkspaceCommands.REFRESH_RESULTS.label,
priority: 1,
onDidChange
});
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.CLEAR_ALL.id,
command: SearchInWorkspaceCommands.CLEAR_ALL.id,
tooltip: SearchInWorkspaceCommands.CLEAR_ALL.label,
priority: 2,
onDidChange
});
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.COLLAPSE_ALL.id,
command: SearchInWorkspaceCommands.COLLAPSE_ALL.id,
tooltip: SearchInWorkspaceCommands.COLLAPSE_ALL.label,
priority: 3,
onDidChange
});
toolbarRegistry.registerItem({
id: SearchInWorkspaceCommands.EXPAND_ALL.id,
command: SearchInWorkspaceCommands.EXPAND_ALL.id,
tooltip: SearchInWorkspaceCommands.EXPAND_ALL.label,
priority: 3,
onDidChange
});
}
protected newUriAwareCommandHandler(handler: UriCommandHandler<URI>): UriAwareCommandHandler<URI> {
return UriAwareCommandHandler.MonoSelect(this.selectionService, handler);
}
protected newMultiUriAwareCommandHandler(handler: UriCommandHandler<URI[]>): UriAwareCommandHandler<URI[]> {
return UriAwareCommandHandler.MultiSelect(this.selectionService, handler);
}
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
const contrastBorder = theme.getColor('contrastBorder');
if (contrastBorder && isHighContrast(theme.type)) {
collector.addRule(`
.t-siw-search-container .searchHeader .search-field-container {
border-color: ${contrastBorder};
}
`);
}
}
}

View File

@@ -0,0 +1,84 @@
// *****************************************************************************
// Copyright (C) 2017-2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import '../../src/browser/styles/index.css';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { SearchInWorkspaceService, SearchInWorkspaceClientImpl } from './search-in-workspace-service';
import { SearchInWorkspaceServer, SIW_WS_PATH } from '../common/search-in-workspace-interface';
import {
WidgetFactory, createTreeContainer, bindViewContribution, FrontendApplicationContribution, LabelProviderContribution,
ApplicationShellLayoutMigration,
StylingParticipant, RemoteConnectionProvider, ServiceConnectionProvider
} from '@theia/core/lib/browser';
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 { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { bindSearchInWorkspacePreferences } from '../common/search-in-workspace-preferences';
import { SearchInWorkspaceLabelProvider } from './search-in-workspace-label-provider';
import { SearchInWorkspaceFactory } from './search-in-workspace-factory';
import { SearchLayoutVersion3Migration } from './search-layout-migrations';
export default new ContainerModule(bind => {
bind(SearchInWorkspaceContextKeyService).toSelf().inSingletonScope();
bind(SearchInWorkspaceWidget).toSelf();
bind<WidgetFactory>(WidgetFactory).toDynamicValue(ctx => ({
id: SearchInWorkspaceWidget.ID,
createWidget: () => ctx.container.get(SearchInWorkspaceWidget)
}));
bind(SearchInWorkspaceResultTreeWidget).toDynamicValue(ctx => createSearchTreeWidget(ctx.container));
bind(SearchInWorkspaceFactory).toSelf().inSingletonScope();
bind(WidgetFactory).toService(SearchInWorkspaceFactory);
bind(ApplicationShellLayoutMigration).to(SearchLayoutVersion3Migration).inSingletonScope();
bindViewContribution(bind, SearchInWorkspaceFrontendContribution);
bind(FrontendApplicationContribution).toService(SearchInWorkspaceFrontendContribution);
bind(TabBarToolbarContribution).toService(SearchInWorkspaceFrontendContribution);
bind(StylingParticipant).toService(SearchInWorkspaceFrontendContribution);
// The object that gets notified of search results.
bind(SearchInWorkspaceClientImpl).toSelf().inSingletonScope();
bind(SearchInWorkspaceService).toSelf().inSingletonScope();
// The object to call methods on the backend.
bind(SearchInWorkspaceServer).toDynamicValue(ctx => {
const client = ctx.container.get(SearchInWorkspaceClientImpl);
const provider = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
return provider.createProxy<SearchInWorkspaceServer>(SIW_WS_PATH, client);
}).inSingletonScope();
bindSearchInWorkspacePreferences(bind);
bind(SearchInWorkspaceLabelProvider).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(SearchInWorkspaceLabelProvider);
});
export function createSearchTreeWidget(parent: interfaces.Container): SearchInWorkspaceResultTreeWidget {
const child = createTreeContainer(parent, {
widget: SearchInWorkspaceResultTreeWidget,
props: {
contextMenuPath: SearchInWorkspaceResultTreeWidget.Menus.BASE,
multiSelect: true,
globalSelection: true
}
});
return child.get(SearchInWorkspaceResultTreeWidget);
}

View File

@@ -0,0 +1,48 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { LabelProviderContribution, LabelProvider, DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider';
import { SearchInWorkspaceRootFolderNode, SearchInWorkspaceFileNode } from './search-in-workspace-result-tree-widget';
import URI from '@theia/core/lib/common/uri';
@injectable()
export class SearchInWorkspaceLabelProvider implements LabelProviderContribution {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
canHandle(element: object): number {
return SearchInWorkspaceRootFolderNode.is(element) || SearchInWorkspaceFileNode.is(element) ? 100 : 0;
}
getIcon(node: SearchInWorkspaceRootFolderNode | SearchInWorkspaceFileNode): string {
if (SearchInWorkspaceFileNode.is(node)) {
return this.labelProvider.getIcon(new URI(node.fileUri).withScheme('file'));
}
return this.labelProvider.folderIcon;
}
getName(node: SearchInWorkspaceRootFolderNode | SearchInWorkspaceFileNode): string {
const uri = SearchInWorkspaceFileNode.is(node) ? node.fileUri : node.folderUri;
return new URI(uri).displayName;
}
affects(node: SearchInWorkspaceRootFolderNode | SearchInWorkspaceFileNode, event: DidChangeLabelEvent): boolean {
return SearchInWorkspaceFileNode.is(node) && event.affects(new URI(node.fileUri).withScheme('file'));
}
}

View File

@@ -0,0 +1,153 @@
// *****************************************************************************
// Copyright (C) 2017-2018 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import {
SearchInWorkspaceServer,
SearchInWorkspaceClient,
SearchInWorkspaceResult,
SearchInWorkspaceOptions
} from '../common/search-in-workspace-interface';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { ILogger } from '@theia/core';
/**
* Class that will receive the search results from the server. This is separate
* from the SearchInWorkspaceService class only to avoid a cycle in the
* dependency injection.
*/
@injectable()
export class SearchInWorkspaceClientImpl implements SearchInWorkspaceClient {
private service: SearchInWorkspaceClient;
onResult(searchId: number, result: SearchInWorkspaceResult): void {
this.service.onResult(searchId, result);
}
onDone(searchId: number, error?: string): void {
this.service.onDone(searchId, error);
}
setService(service: SearchInWorkspaceClient): void {
this.service = service;
}
}
export type SearchInWorkspaceCallbacks = SearchInWorkspaceClient;
/**
* Service to search text in the workspace files.
*/
@injectable()
export class SearchInWorkspaceService implements SearchInWorkspaceClient {
// All the searches that we have started, that are not done yet (onDone
// with that searchId has not been called).
protected pendingSearches = new Map<number, SearchInWorkspaceCallbacks>();
// Due to the asynchronicity of the node backend, it's possible that we
// start a search, receive an event for that search, and then receive
// the search id for that search.We therefore need to keep those
// events until we get the search id and return it to the caller.
// Otherwise the caller would discard the event because it doesn't know
// the search id yet.
protected pendingOnDones: Map<number, string | undefined> = new Map();
protected lastKnownSearchId: number = -1;
@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(): void {
this.client.setService(this);
}
isEnabled(): boolean {
return this.workspaceService.opened;
}
onResult(searchId: number, result: SearchInWorkspaceResult): void {
const callbacks = this.pendingSearches.get(searchId);
if (callbacks) {
callbacks.onResult(searchId, result);
}
}
onDone(searchId: number, error?: string): void {
const callbacks = this.pendingSearches.get(searchId);
if (callbacks) {
this.pendingSearches.delete(searchId);
callbacks.onDone(searchId, error);
} else {
if (searchId > this.lastKnownSearchId) {
this.logger.debug(`Got an onDone for a searchId we don't know about (${searchId}), stashing it for later with error = `, error);
this.pendingOnDones.set(searchId, error);
} else {
// It's possible to receive an onDone for a search we have cancelled. Just ignore it.
this.logger.debug(`Got an onDone for a searchId we don't know about (${searchId}), but it's probably an old one, error = `, error);
}
}
}
// Start a search of the string "what" in the workspace.
async search(what: string, callbacks: SearchInWorkspaceCallbacks, opts?: SearchInWorkspaceOptions): Promise<number> {
if (!this.workspaceService.opened) {
throw new Error('Search failed: no workspace root.');
}
const roots = await this.workspaceService.roots;
return this.doSearch(what, roots.map(r => r.resource.toString()), callbacks, opts);
}
protected async doSearch(what: string, rootUris: string[], callbacks: SearchInWorkspaceCallbacks, opts?: SearchInWorkspaceOptions): Promise<number> {
const searchId = await this.searchServer.search(what, rootUris, opts);
this.pendingSearches.set(searchId, callbacks);
this.lastKnownSearchId = searchId;
this.logger.debug('Service launched search ' + searchId);
// Check if we received an onDone before search() returned.
if (this.pendingOnDones.has(searchId)) {
this.logger.debug('Ohh, we have a stashed onDone for that searchId');
const error = this.pendingOnDones.get(searchId);
this.pendingOnDones.delete(searchId);
// Call the client's searchId, but first give it a
// chance to record the returned searchId.
setTimeout(() => {
this.onDone(searchId, error);
}, 0);
}
return searchId;
}
async searchWithCallback(what: string, rootUris: string[], callbacks: SearchInWorkspaceClient, opts?: SearchInWorkspaceOptions | undefined): Promise<number> {
return this.doSearch(what, rootUris, callbacks, opts);
}
// Cancel an ongoing search.
cancel(searchId: number): void {
this.pendingSearches.delete(searchId);
this.searchServer.cancel(searchId);
}
}

View File

@@ -0,0 +1,732 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Widget, Message, BaseWidget, Key, StatefulWidget, MessageLoop, KeyCode, codicon } from '@theia/core/lib/browser';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { SearchInWorkspaceResultTreeWidget } from './search-in-workspace-result-tree-widget';
import { SearchInWorkspaceOptions } from '../common/search-in-workspace-interface';
import * as React from '@theia/core/shared/react';
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
import { Event, Emitter, Disposable } from '@theia/core/lib/common';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { SearchInWorkspaceContextKeyService } from './search-in-workspace-context-key-service';
import { CancellationTokenSource } from '@theia/core';
import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory';
import { EditorManager } from '@theia/editor/lib/browser';
import { SearchInWorkspacePreferences } from '../common/search-in-workspace-preferences';
import { SearchInWorkspaceInput } from './components/search-in-workspace-input';
import { SearchInWorkspaceTextArea } from './components/search-in-workspace-textarea';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
export interface SearchFieldState {
className: string;
enabled: boolean;
title: string;
}
@injectable()
export class SearchInWorkspaceWidget extends BaseWidget implements StatefulWidget {
static ID = 'search-in-workspace';
static LABEL = nls.localizeByDefault('Search');
protected matchCaseState: SearchFieldState;
protected wholeWordState: SearchFieldState;
protected regExpState: SearchFieldState;
protected includeIgnoredState: SearchFieldState;
protected showSearchDetails = false;
protected _hasResults = false;
protected get hasResults(): boolean {
return this._hasResults;
}
protected set hasResults(hasResults: boolean) {
this.contextKeyService.hasSearchResult.set(hasResults);
this._hasResults = hasResults;
}
protected resultNumber = 0;
protected searchFieldContainerIsFocused = false;
protected searchInWorkspaceOptions: SearchInWorkspaceOptions;
protected searchTerm = '';
protected replaceTerm = '';
private searchRef = React.createRef<SearchInWorkspaceTextArea>();
private replaceRef = React.createRef<SearchInWorkspaceTextArea>();
private includeRef = React.createRef<SearchInWorkspaceInput>();
private excludeRef = React.createRef<SearchInWorkspaceInput>();
private refsAreSet = new Deferred();
protected _showReplaceField = false;
protected get showReplaceField(): boolean {
return this._showReplaceField;
}
protected set showReplaceField(showReplaceField: boolean) {
this.contextKeyService.replaceActive.set(showReplaceField);
this._showReplaceField = showReplaceField;
}
protected contentNode: HTMLElement;
protected searchFormContainer: HTMLElement;
protected resultContainer: HTMLElement;
protected readonly onDidUpdateEmitter = new Emitter<void>();
readonly onDidUpdate: Event<void> = this.onDidUpdateEmitter.event;
@inject(SearchInWorkspaceResultTreeWidget) readonly resultTreeWidget: SearchInWorkspaceResultTreeWidget;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(SearchInWorkspaceContextKeyService)
protected readonly contextKeyService: SearchInWorkspaceContextKeyService;
@inject(ProgressBarFactory)
protected readonly progressBarFactory: ProgressBarFactory;
@inject(EditorManager) protected readonly editorManager: EditorManager;
@inject(SearchInWorkspacePreferences)
protected readonly searchInWorkspacePreferences: SearchInWorkspacePreferences;
protected searchFormContainerRoot: Root;
@postConstruct()
protected init(): void {
this.id = SearchInWorkspaceWidget.ID;
this.title.label = SearchInWorkspaceWidget.LABEL;
this.title.caption = SearchInWorkspaceWidget.LABEL;
this.title.iconClass = codicon('search');
this.title.closable = true;
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.searchFormContainerRoot = createRoot(this.searchFormContainer);
this.node.tabIndex = 0;
this.node.appendChild(this.contentNode);
this.matchCaseState = {
className: codicon('case-sensitive'),
enabled: false,
title: nls.localizeByDefault('Match Case')
};
this.wholeWordState = {
className: codicon('whole-word'),
enabled: false,
title: nls.localizeByDefault('Match Whole Word')
};
this.regExpState = {
className: codicon('regex'),
enabled: false,
title: nls.localizeByDefault('Use Regular Expression')
};
this.includeIgnoredState = {
className: codicon('eye'),
enabled: false,
title: nls.localize('theia/search-in-workspace/includeIgnoredFiles', 'Include Ignored Files')
};
this.searchInWorkspaceOptions = {
matchCase: false,
matchWholeWord: false,
useRegExp: false,
multiline: false,
includeIgnored: false,
include: [],
exclude: [],
maxResults: 2000
};
this.toDispose.push(this.resultTreeWidget.onChange(r => {
this.hasResults = r.size > 0;
this.resultNumber = 0;
const results = Array.from(r.values());
results.forEach(rootFolder =>
rootFolder.children.forEach(file => this.resultNumber += file.children.length)
);
this.update();
}));
this.toDispose.push(this.resultTreeWidget.onFocusInput(b => {
this.focusInputField();
}));
this.toDispose.push(this.searchInWorkspacePreferences.onPreferenceChanged(e => {
if (e.preferenceName === 'search.smartCase') {
this.performSearch();
}
}));
this.toDispose.push(this.resultTreeWidget);
this.toDispose.push(this.resultTreeWidget.onExpansionChanged(() => {
this.onDidUpdateEmitter.fire();
}));
this.toDispose.push(this.progressBarFactory({ container: this.node, insertMode: 'prepend', locationId: 'search' }));
}
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,
searchHistoryState: this.searchRef.current?.state,
replaceHistoryState: this.replaceRef.current?.state,
includeHistoryState: this.includeRef.current?.state,
excludeHistoryState: this.excludeRef.current?.state,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
restoreState(oldState: any): void {
this.matchCaseState = oldState.matchCaseState;
this.wholeWordState = oldState.wholeWordState;
this.regExpState = oldState.regExpState;
this.includeIgnoredState = oldState.includeIgnoredState;
// Override the title of the restored state, as we could have changed languages in between
this.matchCaseState.title = nls.localizeByDefault('Match Case');
this.wholeWordState.title = nls.localizeByDefault('Match Whole Word');
this.regExpState.title = nls.localizeByDefault('Use Regular Expression');
this.includeIgnoredState.title = nls.localize('theia/search-in-workspace/includeIgnoredFiles', 'Include Ignored Files');
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.searchRef.current?.setState(oldState.searchHistoryState);
this.replaceRef.current?.setState(oldState.replaceHistoryState);
this.includeRef.current?.setState(oldState.includeHistoryState);
this.excludeRef.current?.setState(oldState.excludeHistoryState);
this.refresh();
}
findInFolder(uris: string[]): void {
this.showSearchDetails = true;
const values = Array.from(new Set(uris.map(uri => `${uri}/**`)));
const value = values.join(', ');
this.searchInWorkspaceOptions.include = values;
if (this.includeRef.current) {
this.includeRef.current.value = value;
this.includeRef.current.addToHistory();
}
this.update();
}
/**
* Update the search term and input field.
* @param term the search term.
* @param showReplaceField controls if the replace field should be displayed.
*/
updateSearchTerm(term: string, showReplaceField?: boolean): void {
this.searchTerm = term;
if (this.searchRef.current) {
this.searchRef.current.value = term;
this.searchRef.current.addToHistory();
}
if (showReplaceField) {
this.showReplaceField = true;
}
this.refresh();
}
hasResultList(): boolean {
return this.hasResults;
}
hasSearchTerm(): boolean {
return this.searchTerm !== '';
}
refresh(): void {
this.performSearch();
this.update();
}
getCancelIndicator(): CancellationTokenSource | undefined {
return this.resultTreeWidget.cancelIndicator;
}
collapseAll(): void {
this.resultTreeWidget.collapseAll();
this.update();
}
expandAll(): void {
this.resultTreeWidget.expandAll();
this.update();
}
areResultsCollapsed(): boolean {
return this.resultTreeWidget.areResultsCollapsed();
}
clear(): void {
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;
if (this.searchRef.current) {
this.searchRef.current.value = '';
}
if (this.replaceRef.current) {
this.replaceRef.current.value = '';
}
if (this.includeRef.current) {
this.includeRef.current.value = '';
}
if (this.excludeRef.current) {
this.excludeRef.current.value = '';
}
this.performSearch();
this.update();
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.searchFormContainerRoot.render(<React.Fragment>{this.renderSearchHeader()}{this.renderSearchInfo()}</React.Fragment>);
Widget.attach(this.resultTreeWidget, this.contentNode);
this.toDisposeOnDetach.push(Disposable.create(() => {
Widget.detach(this.resultTreeWidget);
}));
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
const searchInfo = this.renderSearchInfo();
if (searchInfo) {
this.searchFormContainerRoot.render(<React.Fragment>{this.renderSearchHeader()}{searchInfo}</React.Fragment>);
this.onDidUpdateEmitter.fire(undefined);
}
}
protected override onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.searchRef.current?.forceUpdate();
this.replaceRef.current?.forceUpdate();
MessageLoop.sendMessage(this.resultTreeWidget, Widget.ResizeMessage.UnknownSize);
}
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.focusInputField();
this.contextKeyService.searchViewletVisible.set(true);
}
protected override onAfterHide(msg: Message): void {
super.onAfterHide(msg);
this.contextKeyService.searchViewletVisible.set(false);
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.focusInputField();
}
protected async focusInputField(): Promise<void> {
// Wait until React rendering is sufficiently progressed before trying to focus the input field.
await this.refsAreSet.promise;
if (this.searchRef.current?.textarea.current) {
this.searchRef.current.textarea.current.focus();
this.searchRef.current.textarea.current.select();
}
}
protected renderSearchHeader(): React.ReactNode {
const searchAndReplaceContainer = this.renderSearchAndReplace();
const searchDetails = this.renderSearchDetails();
return <div ref={() => this.refsAreSet.resolve()}>{searchAndReplaceContainer}{searchDetails}</div>;
}
protected renderSearchAndReplace(): React.ReactNode {
const toggleContainer = this.renderReplaceFieldToggle();
const searchField = this.renderSearchField();
const replaceField = this.renderReplaceField();
return <div className='search-and-replace-container'>
{toggleContainer}
<div className='search-and-replace-fields'>
{searchField}
{replaceField}
</div>
</div>;
}
protected renderReplaceFieldToggle(): React.ReactNode {
const toggle = <span className={codicon(this.showReplaceField ? 'chevron-down' : 'chevron-right')}></span>;
return <div
title={nls.localizeByDefault('Toggle Replace')}
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}
</div>;
}
protected renderNotification(): React.ReactNode {
if (this.workspaceService.tryGetRoots().length <= 0 && this.editorManager.all.length <= 0) {
return <div className='search-notification show'>
<div>{nls.localize('theia/search-in-workspace/noFolderSpecified', 'You have not opened or specified a folder. Only open files are currently searched.')}</div>
</div>;
}
return <div
className={`search-notification ${this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'show' : ''}`}>
<div>{nls.localize('theia/search-in-workspace/resultSubset',
'This is only a subset of all results. Use a more specific search term to narrow down the result list.')}</div>
</div>;
}
protected readonly focusSearchFieldContainer = () => this.doFocusSearchFieldContainer();
protected doFocusSearchFieldContainer(): void {
this.searchFieldContainerIsFocused = true;
this.update();
}
protected readonly blurSearchFieldContainer = () => this.doBlurSearchFieldContainer();
protected doBlurSearchFieldContainer(): void {
this.searchFieldContainerIsFocused = false;
this.update();
}
private _searchTimeout: number;
protected readonly search = (e: React.KeyboardEvent) => {
e.persist();
const searchOnType = this.searchInWorkspacePreferences['search.searchOnType'];
if (searchOnType) {
const delay = this.searchInWorkspacePreferences['search.searchOnTypeDebouncePeriod'] || 0;
window.clearTimeout(this._searchTimeout);
this._searchTimeout = window.setTimeout(() => this.doSearch(e), delay);
}
};
protected readonly onKeyDownSearch = (e: React.KeyboardEvent) => {
if (Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode) {
this.performSearch();
}
};
protected doSearch(e: React.KeyboardEvent): void {
if (e.target) {
const searchValue = (e.target as HTMLInputElement).value;
if (this.searchTerm === searchValue) {
return;
} else {
this.searchTerm = searchValue;
this.performSearch();
}
}
}
protected performSearch(): void {
const searchOptions: SearchInWorkspaceOptions = {
...this.searchInWorkspaceOptions,
followSymlinks: this.shouldFollowSymlinks(),
matchCase: this.shouldMatchCase(),
multiline: this.searchTerm.includes('\n')
};
this.resultTreeWidget.search(this.searchTerm, searchOptions);
}
protected shouldFollowSymlinks(): boolean {
return this.searchInWorkspacePreferences['search.followSymlinks'];
}
/**
* Determine if search should be case sensitive.
*/
protected shouldMatchCase(): boolean {
if (this.matchCaseState.enabled) {
return this.matchCaseState.enabled;
}
// search.smartCase makes siw search case-sensitive if the search term contains uppercase letter(s).
return (
!!this.searchInWorkspacePreferences['search.smartCase']
&& this.searchTerm !== this.searchTerm.toLowerCase()
);
}
protected renderSearchField(): React.ReactNode {
const input = <SearchInWorkspaceTextArea
id='search-input-field'
className='theia-input'
title={SearchInWorkspaceWidget.LABEL}
placeholder={SearchInWorkspaceWidget.LABEL}
defaultValue={this.searchTerm}
autoComplete='off'
onKeyUp={this.search}
onKeyDown={this.onKeyDownSearch}
onFocus={this.handleFocusSearchInputBox}
onBlur={this.handleBlurSearchInputBox}
ref={this.searchRef}
/>;
const notification = this.renderNotification();
const optionContainer = this.renderOptionContainer();
const tooMany = this.searchInWorkspaceOptions.maxResults && this.resultNumber >= this.searchInWorkspaceOptions.maxResults ? 'tooManyResults' : '';
const className = `search-field-container ${tooMany} ${this.searchFieldContainerIsFocused ? 'focused' : ''}`;
return <div className={className}>
<div className='search-field' tabIndex={-1} onFocus={this.focusSearchFieldContainer} onBlur={this.blurSearchFieldContainer}>
{input}
{optionContainer}
</div>
{notification}
</div>;
}
protected handleFocusSearchInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
event.target.placeholder = SearchInWorkspaceWidget.LABEL + nls.localizeByDefault(' ({0} for history)', '⇅');
this.contextKeyService.setSearchInputBoxFocus(true);
};
protected handleBlurSearchInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
event.target.placeholder = SearchInWorkspaceWidget.LABEL;
this.contextKeyService.setSearchInputBoxFocus(false);
};
protected readonly updateReplaceTerm = (e: React.KeyboardEvent) => this.doUpdateReplaceTerm(e);
protected doUpdateReplaceTerm(e: React.KeyboardEvent): void {
if (e.target) {
this.replaceTerm = (e.target as HTMLInputElement).value;
this.resultTreeWidget.replaceTerm = this.replaceTerm;
if (KeyCode.createKeyCode(e.nativeEvent).key?.keyCode === Key.ENTER.keyCode) { this.performSearch(); }
this.update();
}
}
protected renderReplaceField(): React.ReactNode {
const replaceAllButtonContainer = this.renderReplaceAllButtonContainer();
const replace = nls.localizeByDefault('Replace');
return <div className={`replace-field${this.showReplaceField ? '' : ' hidden'}`}>
<SearchInWorkspaceTextArea
id='replace-input-field'
className='theia-input'
title={replace}
placeholder={replace}
defaultValue={this.replaceTerm}
autoComplete='off'
onKeyUp={this.updateReplaceTerm}
onFocus={this.handleFocusReplaceInputBox}
onBlur={this.handleBlurReplaceInputBox}
ref={this.replaceRef}
/>
{replaceAllButtonContainer}
</div>;
}
protected handleFocusReplaceInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
event.target.placeholder = nls.localizeByDefault('Replace') + nls.localizeByDefault(' ({0} for history)', '⇅');
this.contextKeyService.setReplaceInputBoxFocus(true);
};
protected handleBlurReplaceInputBox = (event: React.FocusEvent<HTMLTextAreaElement>) => {
event.target.placeholder = nls.localizeByDefault('Replace');
this.contextKeyService.setReplaceInputBoxFocus(false);
};
protected renderReplaceAllButtonContainer(): React.ReactNode {
// The `Replace All` button is enabled if there is a search term present with results.
const enabled: boolean = this.searchTerm !== '' && this.resultNumber > 0;
return <div className='replace-all-button-container'>
<span
title={nls.localizeByDefault('Replace All')}
className={`${codicon('replace-all', true)} ${enabled ? ' ' : ' disabled'}`}
onClick={() => {
if (enabled) {
this.resultTreeWidget.replace(undefined);
}
}}>
</span>
</div>;
}
protected renderOptionContainer(): React.ReactNode {
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 <div className='option-buttons'>{matchCaseOption}{wholeWordOption}{regexOption}{includeIgnoredOption}</div>;
}
protected renderOptionElement(opt: SearchFieldState): React.ReactNode {
return <span
className={`${opt.className} option action-label ${opt.enabled ? 'enabled' : ''}`}
title={opt.title}
onClick={() => this.handleOptionClick(opt)}></span>;
}
protected handleOptionClick(option: SearchFieldState): void {
option.enabled = !option.enabled;
this.updateSearchOptions();
this.searchFieldContainerIsFocused = true;
this.performSearch();
this.update();
}
protected updateSearchOptions(): void {
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(): React.ReactNode {
const expandButton = this.renderExpandGlobFieldsButton();
const globFieldContainer = this.renderGlobFieldContainer();
return <div className='search-details'>{expandButton}{globFieldContainer}</div>;
}
protected renderGlobFieldContainer(): React.ReactNode {
const includeField = this.renderGlobField('include');
const excludeField = this.renderGlobField('exclude');
return <div className={`glob-field-container${!this.showSearchDetails ? ' hidden' : ''}`}>{includeField}{excludeField}</div>;
}
protected renderExpandGlobFieldsButton(): React.ReactNode {
return <div className='button-container'>
<span
title={nls.localizeByDefault('Toggle Search Details')}
className={codicon('ellipsis')}
onClick={() => {
this.showSearchDetails = !this.showSearchDetails;
this.update();
}}></span>
</div>;
}
protected renderGlobField(kind: 'include' | 'exclude'): React.ReactNode {
const currentValue = this.searchInWorkspaceOptions[kind];
const value = currentValue && currentValue.join(', ') || '';
return <div className='glob-field'>
<div className='label'>{nls.localizeByDefault('files to ' + kind)}</div>
<SearchInWorkspaceInput
className='theia-input'
type='text'
size={1}
defaultValue={value}
autoComplete='off'
id={kind + '-glob-field'}
placeholder={kind === 'include'
? nls.localizeByDefault('e.g. *.ts, src/**/include')
: nls.localizeByDefault('e.g. *.ts, src/**/exclude')
}
onKeyUp={e => {
if (e.target) {
const targetValue = (e.target as HTMLInputElement).value || '';
let shouldSearch = Key.ENTER.keyCode === KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
const currentOptions = (this.searchInWorkspaceOptions[kind] || []).slice().map(s => s.trim()).sort();
const candidateOptions = this.splitOnComma(targetValue).map(s => s.trim()).sort();
const sameAs = (left: string[], right: string[]) => {
if (left.length !== right.length) {
return false;
}
for (let i = 0; i < left.length; i++) {
if (left[i] !== right[i]) {
return false;
}
}
return true;
};
if (!sameAs(currentOptions, candidateOptions)) {
this.searchInWorkspaceOptions[kind] = this.splitOnComma(targetValue);
shouldSearch = true;
}
if (shouldSearch) {
this.performSearch();
}
}
}}
onFocus={kind === 'include' ? this.handleFocusIncludesInputBox : this.handleFocusExcludesInputBox}
onBlur={kind === 'include' ? this.handleBlurIncludesInputBox : this.handleBlurExcludesInputBox}
ref={kind === 'include' ? this.includeRef : this.excludeRef}
/>
</div>;
}
protected handleFocusIncludesInputBox = () => this.contextKeyService.setPatternIncludesInputBoxFocus(true);
protected handleBlurIncludesInputBox = () => this.contextKeyService.setPatternIncludesInputBoxFocus(false);
protected handleFocusExcludesInputBox = () => this.contextKeyService.setPatternExcludesInputBoxFocus(true);
protected handleBlurExcludesInputBox = () => this.contextKeyService.setPatternExcludesInputBoxFocus(false);
protected splitOnComma(patterns: string): string[] {
return patterns.length > 0 ? patterns.split(',').map(s => s.trim()) : [];
}
protected renderSearchInfo(): React.ReactNode {
const message = this.getSearchResultMessage() || '';
return <div className='search-info'>{message}</div>;
}
protected getSearchResultMessage(): string | undefined {
if (!this.searchTerm) {
return undefined;
}
if (this.resultNumber === 0) {
const isIncludesPresent = this.searchInWorkspaceOptions.include && this.searchInWorkspaceOptions.include.length > 0;
const isExcludesPresent = this.searchInWorkspaceOptions.exclude && this.searchInWorkspaceOptions.exclude.length > 0;
let message: string;
if (isIncludesPresent && isExcludesPresent) {
message = nls.localizeByDefault("No results found in '{0}' excluding '{1}' - ",
this.searchInWorkspaceOptions.include!.toString(), this.searchInWorkspaceOptions.exclude!.toString());
} else if (isIncludesPresent) {
message = nls.localizeByDefault("No results found in '{0}' - ",
this.searchInWorkspaceOptions.include!.toString());
} else if (isExcludesPresent) {
message = nls.localizeByDefault("No results found excluding '{0}' - ",
this.searchInWorkspaceOptions.exclude!.toString());
} else {
message = nls.localizeByDefault('No results found') + ' - ';
}
// We have to trim here as vscode will always add a trailing " - " string
return message.substring(0, message.length - 2).trim();
} else {
if (this.resultNumber === 1 && this.resultTreeWidget.fileNumber === 1) {
return nls.localizeByDefault('{0} result in {1} file',
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
} else if (this.resultTreeWidget.fileNumber === 1) {
return nls.localizeByDefault('{0} results in {1} file',
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
} else if (this.resultTreeWidget.fileNumber > 0) {
return nls.localizeByDefault('{0} results in {1} files',
this.resultNumber.toString(), this.resultTreeWidget.fileNumber.toString());
} else {
// if fileNumber === 0, return undefined so that `onUpdateRequest()` would not re-render component
return undefined;
}
}
}
}

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { ApplicationShellLayoutMigration, WidgetDescription, ApplicationShellLayoutMigrationContext } from '@theia/core/lib/browser/shell/shell-layout-restorer';
import { SearchInWorkspaceWidget } from './search-in-workspace-widget';
import { SEARCH_VIEW_CONTAINER_ID, SEARCH_VIEW_CONTAINER_TITLE_OPTIONS } from './search-in-workspace-factory';
@injectable()
export class SearchLayoutVersion3Migration implements ApplicationShellLayoutMigration {
readonly layoutVersion = 6.0;
onWillInflateWidget(desc: WidgetDescription, { parent }: ApplicationShellLayoutMigrationContext): WidgetDescription | undefined {
if (desc.constructionOptions.factoryId === SearchInWorkspaceWidget.ID && !parent) {
return {
constructionOptions: {
factoryId: SEARCH_VIEW_CONTAINER_ID
},
innerWidgetState: {
parts: [
{
widget: {
constructionOptions: {
factoryId: SearchInWorkspaceWidget.ID
},
innerWidgetState: desc.innerWidgetState
},
partId: {
factoryId: SearchInWorkspaceWidget.ID
},
collapsed: false,
hidden: false
}
],
title: SEARCH_VIEW_CONTAINER_TITLE_OPTIONS
}
};
}
return undefined;
}
}

View File

@@ -0,0 +1,400 @@
/********************************************************************************
* Copyright (C) 2017-2018 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
#search-in-workspace {
height: 100%;
display: flex;
flex-flow: column nowrap;
}
#search-in-workspace .theia-TreeContainer.empty {
overflow: hidden;
}
.t-siw-search-container {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
flex: 1 1 auto;
}
.t-siw-search-container .theia-ExpansionToggle {
padding-right: 4px;
min-width: 6px;
}
.t-siw-search-container .theia-input {
box-sizing: border-box;
flex: 1;
line-height: var(--theia-content-line-height);
max-height: calc(2 * 3px + 7 * var(--theia-content-line-height));
min-width: 16px;
min-height: 26px;
resize: none;
width: 100%;
}
.t-siw-search-container #search-input-field:focus {
border: none;
outline: none;
}
.t-siw-search-container #search-input-field {
background: none;
border: none;
padding-block: 2px;
}
.t-siw-search-container .searchHeader {
padding: var(--theia-ui-padding)
max(var(--theia-scrollbar-width), var(--theia-ui-padding))
calc(var(--theia-ui-padding) * 2)
0;
}
#theia-main-content-panel .t-siw-search-container .searchHeader {
padding-top: 10px;
}
.t-siw-search-container .searchHeader .controls.button-container {
height: var(--theia-content-line-height);
margin-bottom: 5px;
}
.t-siw-search-container .searchHeader .search-field-container {
background: var(--theia-input-background);
border-style: solid;
border-width: var(--theia-border-width);
border-color: var(--theia-input-background);
border-radius: 2px;
}
.t-siw-search-container .searchHeader .search-field-container.focused {
border-color: var(--theia-focusBorder);
}
.t-siw-search-container .searchHeader .search-field {
display: flex;
align-items: center;
}
.t-siw-search-container .searchHeader .search-field:focus {
border: none;
outline: none;
}
.t-siw-search-container .searchHeader .search-field .option {
opacity: 0.7;
cursor: pointer;
}
.t-siw-search-container .searchHeader .search-field .option.enabled {
color: var(--theia-inputOption-activeForeground);
border: var(--theia-border-width) var(--theia-inputOption-activeBorder) solid;
background-color: var(--theia-inputOption-activeBackground);
opacity: 1;
}
.t-siw-search-container .searchHeader .search-field .option:hover {
opacity: 1;
}
.t-siw-search-container .searchHeader .search-field .option-buttons {
display: flex;
align-items: center;
align-self: flex-start;
background-color: unset;
margin: auto 2px;
}
.t-siw-search-container .searchHeader .search-field-container.tooManyResults {
border-style: solid;
border-width: var(--theia-border-width);
border-color: var(--theia-inputValidation-warningBorder);
}
.t-siw-search-container
.searchHeader
.search-field-container
.search-notification {
height: 0;
display: none;
width: 100%;
position: relative;
}
.t-siw-search-container
.searchHeader
.search-field-container.focused
.search-notification.show {
display: block;
}
.t-siw-search-container .searchHeader .search-notification div {
background-color: var(--theia-inputValidation-warningBackground);
width: calc(100% + 2px);
margin-left: -1px;
color: var(--theia-inputValidation-warningForeground);
z-index: 1000;
position: absolute;
border: 1px solid var(--theia-inputValidation-warningBorder);
box-sizing: border-box;
padding: 3px;
}
.t-siw-search-container .searchHeader .button-container {
text-align: center;
display: flex;
justify-content: center;
}
.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 .search-details {
position: relative;
margin-top: var(--theia-ui-padding);
}
.t-siw-search-container .searchHeader .search-details .button-container {
position: absolute;
width: 25px;
top: 0;
right: 0;
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: var(--theia-ui-padding);
margin-left: calc(var(--theia-ui-padding) * 3);
display: flex;
flex-direction: column;
}
.t-siw-search-container .searchHeader .glob-field-container .glob-field .label {
margin-bottom: 4px;
user-select: none;
font-size: var(--theia-ui-font-size0);
}
.t-siw-search-container
.searchHeader
.glob-field-container
.glob-field
.theia-input:not(:focus)::placeholder {
color: transparent;
}
.t-siw-search-container .resultContainer {
height: 100%;
}
.t-siw-search-container .result {
overflow: hidden;
width: 100%;
flex: 1;
}
.t-siw-search-container .result .result-head {
display: flex;
}
.t-siw-search-container .result .result-head .fa,
.t-siw-search-container .result .result-head .theia-file-icons-js {
margin: 0 3px;
}
.t-siw-search-container .result .result-head .file-name {
margin-right: 5px;
}
.t-siw-search-container .result .result-head .file-path {
font-size: var(--theia-ui-font-size0);
margin-left: 3px;
}
.t-siw-search-container .resultLine {
flex-grow: 1;
}
.t-siw-search-container .resultLine .match {
white-space: pre;
background: var(--theia-editor-findMatchHighlightBackground);
border: 1px solid var(--theia-editor-findMatchHighlightBorder);
}
.theia-hc .t-siw-search-container .resultLine .match {
border-style: dashed;
}
.t-siw-search-container .resultLine .match.strike-through {
text-decoration: line-through;
background: var(--theia-diffEditor-removedTextBackground);
border-color: var(--theia-diffEditor-removedTextBorder);
}
.t-siw-search-container .resultLine .replace-term {
background: var(--theia-diffEditor-insertedTextBackground);
border: 1px solid var(--theia-diffEditor-insertedTextBorder);
}
.theia-hc .t-siw-search-container .resultLine .replace-term {
border-style: dashed;
}
.t-siw-search-container .match-line-num {
font-size: .9em;
margin-left: 7px;
margin-right: 4px;
opacity: .7;
}
.t-siw-search-container .result-head-info {
align-items: center;
display: inline-flex;
flex-grow: 1;
}
.search-in-workspace-editor-match {
background: var(--theia-editor-findMatchHighlightBackground);
}
.current-search-in-workspace-editor-match {
background: var(--theia-editor-findMatchBackground);
}
.current-match-range-highlight {
background: var(--theia-editor-findRangeHighlightBackground);
}
.result-node-buttons {
display: none;
}
.theia-TreeNode:hover .result-node-buttons {
display: flex;
justify-content: flex-end;
align-items: center;
align-self: center;
}
.theia-TreeNode:hover .result-head .notification-count-container {
display: none;
}
.result-node-buttons > span {
padding: 2px;
margin-left: var(--theia-ui-padding);
border-radius: 5px;
}
.result-node-buttons > span:hover {
background-color: var(--theia-toolbar-hoverBackground);
}
.search-and-replace-container {
display: flex;
}
.replace-toggle {
display: flex;
align-items: center;
width: 14px;
min-width: 14px;
justify-content: center;
margin-left: 2px;
margin-right: 2px;
box-sizing: border-box;
}
.theia-side-panel .replace-toggle .codicon {
padding: 0px;
}
.replace-toggle:hover {
background: rgba(50%, 50%, 50%, 0.2);
}
.search-and-replace-fields {
display: flex;
flex-direction: column;
flex: 1;
}
.replace-field {
display: flex;
margin-top: var(--theia-ui-padding);
gap: var(--theia-ui-padding);
}
.replace-field.hidden {
display: none;
}
.replace-all-button-container {
display: flex;
align-items: center;
justify-content: center;
}
.result-node-buttons .replace-result {
background-image: var(--theia-icon-replace);
}
.result-node-buttons .replace-all-result {
background-image: var(--theia-icon-replace-all);
}
.replace-all-button-container .action-label.disabled {
opacity: var(--theia-mod-disabled-opacity);
background: transparent;
cursor: default;
}
.highlighted-count-container {
background-color: var(--theia-list-activeSelectionBackground);
color: var(--theia-list-activeSelectionForeground);
}
.t-siw-search-container .searchHeader .search-info {
color: var(--theia-descriptionForeground);
margin-left: 18px;
margin-top: 10px;
}
.theia-siw-lineNumber {
opacity: 0.7;
padding-right: 4px;
}

View File

@@ -0,0 +1,6 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2019 TypeFox and others.-->
<!--Licensed under the MIT License. See License.txt in the project root for license information.-->
<svg fill="#F6F6F6" height="28" viewBox="0 0 28 28" width="28" xmlns="http://www.w3.org/2000/svg">
<path d="m17.1249 2c-4.9127 0-8.89701 3.98533-8.89701 8.899 0 1.807.54686 3.4801 1.47014 4.8853 0 0-5.562 5.5346-7.20564 7.2056-1.644662 1.6701 1.0156 4.1304 2.63997 2.4442 1.62538-1.6832 7.10824-7.1072 7.10824-7.1072 1.4042.9243 3.0793 1.4711 4.8843 1.4711 4.9157 0 8.9-3.9873 8.9-8.899.001-4.91469-3.9843-8.899-8.9-8.899zm0 15.2544c-3.5095 0-6.3565-2.8449-6.3565-6.3554 0-3.51049 2.846-6.35643 6.3565-6.35643 3.5125 0 6.3574 2.84493 6.3574 6.35643 0 3.5105-2.8449 6.3554-6.3574 6.3554z" fill="#F6F6F6" />
</svg>

After

Width:  |  Height:  |  Size: 828 B