deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
400
packages/search-in-workspace/src/browser/styles/index.css
Normal file
400
packages/search-in-workspace/src/browser/styles/index.css
Normal 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;
|
||||
}
|
||||
@@ -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 |
Reference in New Issue
Block a user