deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/search-in-workspace/.eslintrc.js
Normal file
10
packages/search-in-workspace/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
42
packages/search-in-workspace/README.md
Normal file
42
packages/search-in-workspace/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - SEARCH-IN-WORKSPACE EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/search-in-workspace` extension provides the ability to perform searches over all files in a given workspace using different search techniques.
|
||||
|
||||
## Search Widget
|
||||
|
||||
The `@theia/search-in-workspace` extension contributes the `Search` widget which is capable of performing different types of searches include the possibility to:
|
||||
|
||||
- Perform standard searches
|
||||
- Perform searches using regular expressions
|
||||
- Perform searches within an `include` list (search for specific types of files (ex: `*.ts`))
|
||||
- Perform searches excluding files or directories (using `exclude`)
|
||||
- Perform searches ignoring hidden or excluded files/folders
|
||||
- Perform search and replace (to quickly update multiple occurrences of a search term)
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/search-in-workspace`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_search-in-workspace.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
57
packages/search-in-workspace/package.json
Normal file
57
packages/search-in-workspace/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@theia/search-in-workspace",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Search in workspace",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/editor": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@theia/navigator": "1.68.0",
|
||||
"@theia/process": "1.68.0",
|
||||
"@theia/workspace": "1.68.0",
|
||||
"@vscode/ripgrep": "^1.14.2",
|
||||
"minimatch": "^10.0.3",
|
||||
"react-textarea-autosize": "^8.5.5",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/search-in-workspace-frontend-module",
|
||||
"backend": "lib/node/search-in-workspace-backend-module"
|
||||
},
|
||||
{
|
||||
"frontendOnly": "lib/browser-only/search-in-workspace-frontend-only-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 Maksim Kachurin 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, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { SearchInWorkspaceService } from '../browser/search-in-workspace-service';
|
||||
|
||||
@injectable()
|
||||
export class BrowserOnlySearchInWorkspaceService extends SearchInWorkspaceService {
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
|
||||
if (this.searchServer && typeof this.searchServer.setClient === 'function') {
|
||||
this.searchServer.setClient(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 Maksim Kachurin 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, named } from '@theia/core/shared/inversify';
|
||||
import type {
|
||||
SearchInWorkspaceClient,
|
||||
SearchInWorkspaceOptions,
|
||||
SearchInWorkspaceResult,
|
||||
SearchInWorkspaceServer,
|
||||
SearchMatch
|
||||
} from '../common/search-in-workspace-interface';
|
||||
import { FileUri } from '@theia/core/lib/common/file-uri';
|
||||
import { URI, ILogger } from '@theia/core';
|
||||
import { FileService, TextFileOperationError, TextFileOperationResult } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { normalizeGlob, matchesPattern, createIgnoreMatcher, getIgnorePatterns } from '@theia/filesystem/lib/browser-only/file-search';
|
||||
import { escapeRegExpCharacters } from '@theia/core/lib/common/strings';
|
||||
import { BinarySize, type FileStatWithMetadata } from '@theia/filesystem/lib/common/files';
|
||||
|
||||
interface SearchController {
|
||||
regex: RegExp;
|
||||
searchPaths: URI[];
|
||||
options: SearchInWorkspaceOptions;
|
||||
isAborted: () => boolean;
|
||||
abort: () => void;
|
||||
};
|
||||
|
||||
const minimatchOpts = {
|
||||
dot: true,
|
||||
matchBase: true,
|
||||
nocase: true
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class BrowserSearchInWorkspaceServer implements SearchInWorkspaceServer {
|
||||
@inject(ILogger) @named('search-in-workspace')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fs: FileService;
|
||||
|
||||
private client: SearchInWorkspaceClient | undefined;
|
||||
private ongoingSearches: Map<number, SearchController> = new Map();
|
||||
private nextSearchId: number = 1;
|
||||
|
||||
/**
|
||||
* Sets the client for receiving search results
|
||||
*/
|
||||
setClient(client: SearchInWorkspaceClient | undefined): void {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a search operation and returns a search ID.
|
||||
* @param what - The search term or pattern
|
||||
* @param rootUris - Array of root URIs to search in
|
||||
* @param opts - Search options including filters and limits
|
||||
* @returns Promise resolving to the search ID
|
||||
*/
|
||||
async search(what: string, rootUris: string[], opts: SearchInWorkspaceOptions = {}): Promise<number> {
|
||||
const searchId = this.nextSearchId++;
|
||||
const controller = new AbortController();
|
||||
|
||||
const { regex, searchPaths, options } = await this.processSearchOptions(
|
||||
what,
|
||||
rootUris,
|
||||
opts,
|
||||
);
|
||||
|
||||
this.ongoingSearches.set(searchId, {
|
||||
regex,
|
||||
searchPaths,
|
||||
options,
|
||||
isAborted: () => controller.signal.aborted,
|
||||
abort: () => controller.abort()
|
||||
});
|
||||
|
||||
// Start search asynchronously and return searchId immediately
|
||||
this.doSearch(searchId).catch((error: Error) => {
|
||||
const errorStr = `An error happened while searching (${error.message}).`;
|
||||
|
||||
this.client?.onDone(searchId, errorStr);
|
||||
}).finally(() => {
|
||||
this.ongoingSearches.delete(searchId);
|
||||
});
|
||||
|
||||
return searchId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an ongoing search operation.
|
||||
* @param searchId - The ID of the search to cancel
|
||||
*/
|
||||
cancel(searchId: number): Promise<void> {
|
||||
const controller = this.ongoingSearches.get(searchId);
|
||||
|
||||
if (controller) {
|
||||
this.ongoingSearches.delete(searchId);
|
||||
|
||||
controller.abort();
|
||||
this.client?.onDone(searchId);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the service by aborting all ongoing searches.
|
||||
*/
|
||||
dispose(): void {
|
||||
this.ongoingSearches.forEach(controller => controller.abort());
|
||||
this.ongoingSearches.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to perform the search.
|
||||
* @param searchId - The ID of the search to perform.
|
||||
* @returns A promise that resolves when the search is complete.
|
||||
*/
|
||||
private async doSearch(searchId: number): Promise<void> {
|
||||
const ctx = this.ongoingSearches.get(searchId);
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { regex, searchPaths, options, isAborted } = ctx;
|
||||
|
||||
const maxFileSize = options.maxFileSize ? BinarySize.parseSize(options.maxFileSize) : 20 * BinarySize.MB;
|
||||
const matcher = createIgnoreMatcher();
|
||||
|
||||
let remaining = options.maxResults ?? Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const root of searchPaths) {
|
||||
if (isAborted()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const pathsStack: URI[] = [root];
|
||||
let index = 0;
|
||||
|
||||
while (index < pathsStack.length && !isAborted() && remaining > 0) {
|
||||
const current = pathsStack[index++];
|
||||
const relPath = current.path.toString().replace(/^\/|^\.\//, '');
|
||||
|
||||
// Skip excluded paths
|
||||
if (this.shouldExcludePath(current, options.exclude)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip ignored files unless explicitly included
|
||||
if (!options.includeIgnored && relPath && matcher.ignores(relPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stat: FileStatWithMetadata;
|
||||
|
||||
try {
|
||||
stat = await this.fs.resolve(current, { resolveMetadata: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip files not matching include patterns
|
||||
if (stat.isFile && !this.shouldIncludePath(current, options.include)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip files exceeding size limit
|
||||
if (stat.isFile && stat.size > maxFileSize) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process directory contents
|
||||
if (stat.isDirectory) {
|
||||
if (Array.isArray(stat.children)) {
|
||||
// Load ignore patterns from files
|
||||
if (!options.includeIgnored) {
|
||||
const patterns = await getIgnorePatterns(
|
||||
current,
|
||||
uri => this.fs.read(uri).then(content => content.value)
|
||||
);
|
||||
|
||||
matcher.add(patterns);
|
||||
}
|
||||
|
||||
for (const child of stat.children) {
|
||||
pathsStack.push(child.resource);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = await this.searchFileByLines(current, regex, isAborted, {
|
||||
autoGuessEncoding: true,
|
||||
acceptTextOnly: true
|
||||
}, remaining);
|
||||
|
||||
if (matches.length > 0) {
|
||||
const result: SearchInWorkspaceResult = {
|
||||
root: root.path.toString(),
|
||||
fileUri: current.path.toString(),
|
||||
matches
|
||||
};
|
||||
|
||||
this.client?.onResult(searchId, result);
|
||||
|
||||
remaining -= matches.length;
|
||||
if (remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof TextFileOperationError && err.textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.error(`Error reading file ${current.path.toString()}: ${err.message}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining <= 0 || isAborted()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.client?.onDone(searchId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for matches within a file by processing it line by line.
|
||||
* @param uri - The file URI to search
|
||||
* @param re - The regex pattern to match
|
||||
* @param isAborted - Function to check if search was aborted
|
||||
* @param opts - File reading options
|
||||
* @param limit - Maximum number of matches to return
|
||||
* @returns Array of search matches found in the file
|
||||
*/
|
||||
private async searchFileByLines(
|
||||
uri: URI,
|
||||
re: RegExp,
|
||||
isAborted: () => boolean,
|
||||
opts: { autoGuessEncoding: boolean; acceptTextOnly: boolean },
|
||||
limit: number
|
||||
): Promise<SearchMatch[]> {
|
||||
const { value: stream } = await this.fs.readStream(uri, opts);
|
||||
|
||||
let leftover = '';
|
||||
let lineNo = 0;
|
||||
const matches: SearchMatch[] = [];
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
stream.on('data', chunk => {
|
||||
if (isAborted()) {
|
||||
stream.pause();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = leftover + chunk;
|
||||
const lines = data.split(/\r?\n/);
|
||||
leftover = lines.pop() ?? '';
|
||||
|
||||
for (const line of lines) {
|
||||
lineNo += 1; // 1-based
|
||||
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset regex lastIndex for global patterns
|
||||
if (re.global) {
|
||||
re.lastIndex = 0;
|
||||
}
|
||||
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = re.exec(line))) {
|
||||
matches.push({
|
||||
line: lineNo,
|
||||
character: m.index + 1, // 1-based
|
||||
length: m[0].length,
|
||||
lineText: line
|
||||
});
|
||||
|
||||
if (matches.length >= limit) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', err => reject(err));
|
||||
|
||||
stream.on('end', () => {
|
||||
if (leftover.length && matches.length < limit) {
|
||||
lineNo += 1;
|
||||
const line = leftover;
|
||||
|
||||
// Reset regex lastIndex for global patterns
|
||||
if (re.global) {
|
||||
re.lastIndex = 0;
|
||||
}
|
||||
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = re.exec(line))) {
|
||||
matches.push({
|
||||
line: lineNo,
|
||||
character: m.index + 1,
|
||||
length: m[0].length,
|
||||
lineText: line
|
||||
});
|
||||
|
||||
if (matches.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes search options and returns clean paths and processed options.
|
||||
* This method consolidates the path processing logic and matchWholeWord handling for better readability.
|
||||
*/
|
||||
private async processSearchOptions(_searchTerm: string, _searchPaths: string[], _options: SearchInWorkspaceOptions): Promise<{
|
||||
regex: RegExp,
|
||||
searchPaths: URI[],
|
||||
options: SearchInWorkspaceOptions,
|
||||
}> {
|
||||
const options = { ..._options };
|
||||
|
||||
options.maxResults = typeof options.maxResults === 'number' && options.maxResults > 0 ? options.maxResults : Number.POSITIVE_INFINITY;
|
||||
options.include = (options.include ?? []).map(glob => normalizeGlob(glob));
|
||||
options.exclude = (options.exclude ?? []).map(glob => normalizeGlob(glob));
|
||||
|
||||
// If there are absolute paths in `include` we will remove them and use
|
||||
// those as paths to search from
|
||||
const paths = await this.extractSearchPathsFromIncludes(
|
||||
_searchPaths.map(p => FileUri.fsPath(p)),
|
||||
options.include
|
||||
);
|
||||
|
||||
// Build regex with consideration of useRegExp/matchCase/matchWholeWord
|
||||
const useRegExp = !!options.useRegExp;
|
||||
const matchCase = !!options.matchCase;
|
||||
const matchWholeWord = !!options.matchWholeWord;
|
||||
|
||||
const flags = 'g' + (matchCase ? '' : 'i') + 'u';
|
||||
let source = useRegExp ? _searchTerm : escapeRegExpCharacters(_searchTerm);
|
||||
|
||||
// Unicode word boundaries: letters/numbers/underscore
|
||||
if (matchWholeWord) {
|
||||
const wbL = '(?<![\\p{L}\\p{N}_])';
|
||||
const wbR = '(?![\\p{L}\\p{N}_])';
|
||||
source = `${wbL}${source}${wbR}`;
|
||||
}
|
||||
|
||||
const regex = new RegExp(source, flags);
|
||||
|
||||
const searchPaths = paths.map(p => URI.fromFilePath(p));
|
||||
|
||||
return { regex, searchPaths, options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path should be excluded based on exclude patterns.
|
||||
* @param uri - The URI to check
|
||||
* @param exclude - Array of exclude patterns
|
||||
* @returns True if the path should be excluded
|
||||
*/
|
||||
protected shouldExcludePath(uri: URI, exclude: string[] | undefined): boolean {
|
||||
if (!exclude?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return matchesPattern(uri.path.toString(), exclude, minimatchOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path should be included based on include patterns.
|
||||
* @param uri - The URI to check
|
||||
* @param include - Array of include patterns
|
||||
* @returns True if the path should be included
|
||||
*/
|
||||
private shouldIncludePath(uri: URI, include: string[] | undefined): boolean {
|
||||
if (!include?.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return matchesPattern(uri.path.toString(), include, minimatchOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* The default search paths are set to be the root paths associated to a workspace
|
||||
* however the search scope can be further refined with the include paths available in the search options.
|
||||
* This method will replace the searching paths to the ones specified in the 'include' options but as long
|
||||
* as the 'include' paths can be successfully validated as existing.
|
||||
*
|
||||
* Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
|
||||
* derived from the include options which can be used to perform the search.
|
||||
*
|
||||
* Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
|
||||
* provided as an equivalent search path instead.
|
||||
*/
|
||||
protected async extractSearchPathsFromIncludes(searchPaths: string[], include: string[]): Promise<string[]> {
|
||||
if (!include) {
|
||||
return searchPaths;
|
||||
}
|
||||
|
||||
const resolvedPaths = new Set<string>();
|
||||
const searchPathsUris = searchPaths.map(p => new URI(p));
|
||||
|
||||
for (const pattern of include) {
|
||||
const [base, _] = getGlobBase(pattern);
|
||||
const baseUri = new URI(base);
|
||||
|
||||
for (const rootUri of searchPathsUris) {
|
||||
if (rootUri.isEqualOrParent(baseUri) && await this.fs.exists(baseUri)) {
|
||||
resolvedPaths.add(baseUri.path.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPaths.size ? Array.from(resolvedPaths) : searchPaths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base + rest of a glob pattern.
|
||||
*
|
||||
* @param pattern - The glob pattern to get the base of (like 'workspace2/foo/*.md')
|
||||
* @returns The base + rest of the glob pattern. (like ['workspace2/foo/', '*.md'])
|
||||
*/
|
||||
function getGlobBase(pattern: string): [string, string] {
|
||||
const isAbsolute = pattern.startsWith('/');
|
||||
const parts = pattern.replace(/^\//, '').split('/');
|
||||
const magic = /[*?[\]{}]/;
|
||||
|
||||
const staticParts: string[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (magic.test(part)) { break; }
|
||||
staticParts.push(part);
|
||||
}
|
||||
|
||||
const base = (isAbsolute ? '/' : '') + staticParts.join('/');
|
||||
|
||||
return [base, pattern.substring(base.length)];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 Maksim Kachurin 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 { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { SearchInWorkspaceServer } from '../common/search-in-workspace-interface';
|
||||
import { BrowserSearchInWorkspaceServer } from './browser-search-in-workspace-server';
|
||||
import { SearchInWorkspaceService } from '../browser/search-in-workspace-service';
|
||||
import { BrowserOnlySearchInWorkspaceService } from './browser-only-search-in-workspace-service';
|
||||
|
||||
export default new ContainerModule((bind, _unbind, isBound, rebind) => {
|
||||
if (isBound(SearchInWorkspaceServer)) {
|
||||
rebind(SearchInWorkspaceServer).to(BrowserSearchInWorkspaceServer).inSingletonScope();
|
||||
} else {
|
||||
bind(SearchInWorkspaceServer).to(BrowserSearchInWorkspaceServer).inSingletonScope();
|
||||
}
|
||||
|
||||
if (isBound(SearchInWorkspaceService)) {
|
||||
rebind(SearchInWorkspaceService).to(BrowserOnlySearchInWorkspaceService).inSingletonScope();
|
||||
} else {
|
||||
bind(SearchInWorkspaceService).to(BrowserOnlySearchInWorkspaceService).inSingletonScope();
|
||||
}
|
||||
});
|
||||
@@ -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 |
@@ -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 { RpcServer } from '@theia/core';
|
||||
|
||||
export interface SearchInWorkspaceOptions {
|
||||
/**
|
||||
* Maximum number of results to return. Defaults to unlimited.
|
||||
*/
|
||||
maxResults?: number;
|
||||
/**
|
||||
* accepts suffixes of K, M or G which correspond to kilobytes,
|
||||
* megabytes and gigabytes, respectively. If no suffix is provided the input is
|
||||
* treated as bytes.
|
||||
*
|
||||
* defaults to '20M'
|
||||
*/
|
||||
maxFileSize?: string;
|
||||
/**
|
||||
* Search case sensitively if true.
|
||||
*/
|
||||
matchCase?: boolean;
|
||||
/**
|
||||
* Search whole words only if true.
|
||||
*/
|
||||
matchWholeWord?: boolean;
|
||||
/**
|
||||
* Use regular expressions for search if true.
|
||||
*/
|
||||
useRegExp?: boolean;
|
||||
/**
|
||||
* Use multiline search if true.
|
||||
*/
|
||||
multiline?: boolean;
|
||||
/**
|
||||
* Include all .gitignored and hidden files.
|
||||
*/
|
||||
includeIgnored?: boolean;
|
||||
/**
|
||||
* Glob pattern for matching files and directories to include the search.
|
||||
*/
|
||||
include?: string[];
|
||||
/**
|
||||
* Glob pattern for matching files and directories to exclude the search.
|
||||
*/
|
||||
exclude?: string[];
|
||||
/**
|
||||
* Whether symlinks should be followed while searching.
|
||||
*/
|
||||
followSymlinks?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchInWorkspaceResult {
|
||||
/**
|
||||
* The string uri to the root folder that the search was performed.
|
||||
*/
|
||||
root: string;
|
||||
|
||||
/**
|
||||
* The string uri to the file containing the result.
|
||||
*/
|
||||
fileUri: string;
|
||||
|
||||
/**
|
||||
* matches found in the file
|
||||
*/
|
||||
matches: SearchMatch[];
|
||||
}
|
||||
|
||||
export interface SearchMatch {
|
||||
/**
|
||||
* The (1-based) line number of the result.
|
||||
*/
|
||||
line: number;
|
||||
|
||||
/**
|
||||
* The (1-based) character number in the result line. For UTF-8 files,
|
||||
* one multi-byte character counts as one character.
|
||||
*/
|
||||
character: number;
|
||||
|
||||
/**
|
||||
* The length of the match, in characters. For UTF-8 files, one
|
||||
* multi-byte character counts as one character.
|
||||
*/
|
||||
length: number;
|
||||
|
||||
/**
|
||||
* The text of the line containing the result.
|
||||
*/
|
||||
lineText: string | LinePreview;
|
||||
|
||||
}
|
||||
|
||||
export interface LinePreview {
|
||||
text: string;
|
||||
character: number;
|
||||
}
|
||||
|
||||
export namespace SearchInWorkspaceResult {
|
||||
/**
|
||||
* Sort search in workspace results according to file, line, character position
|
||||
* and then length.
|
||||
*/
|
||||
export function compare(a: SearchInWorkspaceResult, b: SearchInWorkspaceResult): number {
|
||||
if (a.fileUri !== b.fileUri) {
|
||||
return a.fileUri < b.fileUri ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const SearchInWorkspaceClient = Symbol('SearchInWorkspaceClient');
|
||||
export interface SearchInWorkspaceClient {
|
||||
/**
|
||||
* Called by the server for every search match.
|
||||
*/
|
||||
onResult(searchId: number, result: SearchInWorkspaceResult): void;
|
||||
|
||||
/**
|
||||
* Called when no more search matches will come.
|
||||
*/
|
||||
onDone(searchId: number, error?: string): void;
|
||||
}
|
||||
|
||||
export const SIW_WS_PATH = '/services/search-in-workspace';
|
||||
export const SearchInWorkspaceServer = Symbol('SearchInWorkspaceServer');
|
||||
export interface SearchInWorkspaceServer extends RpcServer<SearchInWorkspaceClient> {
|
||||
/**
|
||||
* Start a search for WHAT in directories ROOTURIS. Return a unique search id.
|
||||
*/
|
||||
search(what: string, rootUris: string[], opts?: SearchInWorkspaceOptions): Promise<number>;
|
||||
|
||||
/**
|
||||
* Cancel an ongoing search.
|
||||
*/
|
||||
cancel(searchId: number): Promise<void>;
|
||||
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 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 { nls } from '@theia/core/lib/common/nls';
|
||||
import { PreferenceProxy, PreferenceScope, PreferenceService, createPreferenceProxy } from '@theia/core/lib/common/preferences';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
|
||||
|
||||
export const searchInWorkspacePreferencesSchema: PreferenceSchema = {
|
||||
scope: PreferenceScope.Folder,
|
||||
properties: {
|
||||
'search.lineNumbers': {
|
||||
description: nls.localizeByDefault('Controls whether to show line numbers for search results.'),
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
'search.collapseResults': {
|
||||
description: nls.localizeByDefault('Controls whether the search results will be collapsed or expanded.'),
|
||||
default: 'auto',
|
||||
type: 'string',
|
||||
enum: ['auto', 'alwaysCollapse', 'alwaysExpand'],
|
||||
},
|
||||
'search.quickOpen.includeHistory': {
|
||||
description: nls.localizeByDefault('Whether to include results from recently opened files in the file results for Quick Open.'),
|
||||
default: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
'search.searchOnType': {
|
||||
description: nls.localizeByDefault('Search all files as you type.'),
|
||||
default: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
'search.searchOnTypeDebouncePeriod': {
|
||||
// eslint-disable-next-line max-len
|
||||
markdownDescription: nls.localizeByDefault('When {0} is enabled, controls the timeout in milliseconds between a character being typed and the search starting. Has no effect when {0} is disabled.', '`#search.searchOnType#`'),
|
||||
default: 300,
|
||||
type: 'number',
|
||||
},
|
||||
'search.searchOnEditorModification': {
|
||||
description: nls.localize('theia/search-in-workspace/searchOnEditorModification', 'Search the active editor when modified.'),
|
||||
default: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
'search.smartCase': {
|
||||
// eslint-disable-next-line max-len
|
||||
description: nls.localizeByDefault('Search case-insensitively if the pattern is all lowercase, otherwise, search case-sensitively.'),
|
||||
default: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
'search.followSymlinks': {
|
||||
description: nls.localizeByDefault('Controls whether to follow symlinks while searching.'),
|
||||
default: true,
|
||||
type: 'boolean',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class SearchInWorkspaceConfiguration {
|
||||
'search.lineNumbers': boolean;
|
||||
'search.collapseResults': string;
|
||||
'search.searchOnType': boolean;
|
||||
'search.searchOnTypeDebouncePeriod': number;
|
||||
'search.searchOnEditorModification': boolean;
|
||||
'search.smartCase': boolean;
|
||||
'search.followSymlinks': boolean;
|
||||
}
|
||||
|
||||
export const SearchInWorkspacePreferenceContribution = Symbol('SearchInWorkspacePreferenceContribution');
|
||||
export const SearchInWorkspacePreferences = Symbol('SearchInWorkspacePreferences');
|
||||
export type SearchInWorkspacePreferences = PreferenceProxy<SearchInWorkspaceConfiguration>;
|
||||
|
||||
export function createSearchInWorkspacePreferences(preferences: PreferenceService, schema: PreferenceSchema = searchInWorkspacePreferencesSchema): SearchInWorkspacePreferences {
|
||||
return createPreferenceProxy(preferences, schema);
|
||||
}
|
||||
|
||||
export function bindSearchInWorkspacePreferences(bind: interfaces.Bind): void {
|
||||
bind(SearchInWorkspacePreferences).toDynamicValue(ctx => {
|
||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||
const contribution = ctx.container.get<PreferenceContribution>(SearchInWorkspacePreferenceContribution);
|
||||
return createSearchInWorkspacePreferences(preferences, contribution.schema);
|
||||
}).inSingletonScope();
|
||||
bind(SearchInWorkspacePreferenceContribution).toConstantValue({ schema: searchInWorkspacePreferencesSchema });
|
||||
bind(PreferenceContribution).toService(SearchInWorkspacePreferenceContribution);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,490 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017-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 fs from '@theia/core/shared/fs-extra';
|
||||
import * as path from 'path';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { RawProcess, RawProcessFactory, RawProcessOptions } from '@theia/process/lib/node';
|
||||
import { FileUri } from '@theia/core/lib/common/file-uri';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { SearchInWorkspaceServer, SearchInWorkspaceOptions, SearchInWorkspaceResult, SearchInWorkspaceClient, LinePreview } from '../common/search-in-workspace-interface';
|
||||
|
||||
export const RgPath = Symbol('RgPath');
|
||||
|
||||
/**
|
||||
* Typing for ripgrep's arbitrary data object:
|
||||
*
|
||||
* https://docs.rs/grep-printer/0.1.0/grep_printer/struct.JSON.html#object-arbitrary-data
|
||||
*/
|
||||
export type IRgBytesOrText = { bytes: string } | { text: string };
|
||||
|
||||
function bytesOrTextToString(obj: IRgBytesOrText): string {
|
||||
return 'bytes' in obj ?
|
||||
Buffer.from(obj.bytes, 'base64').toString() :
|
||||
obj.text;
|
||||
}
|
||||
|
||||
type IRgMessage = IRgMatch | IRgBegin | IRgEnd;
|
||||
|
||||
interface IRgMatch {
|
||||
type: 'match';
|
||||
data: {
|
||||
path: IRgBytesOrText;
|
||||
lines: IRgBytesOrText;
|
||||
line_number: number;
|
||||
absolute_offset: number;
|
||||
submatches: IRgSubmatch[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRgSubmatch {
|
||||
match: IRgBytesOrText;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface IRgBegin {
|
||||
type: 'begin';
|
||||
data: {
|
||||
path: IRgBytesOrText;
|
||||
lines: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface IRgEnd {
|
||||
type: 'end';
|
||||
data: {
|
||||
path: IRgBytesOrText;
|
||||
};
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class RipgrepSearchInWorkspaceServer implements SearchInWorkspaceServer {
|
||||
|
||||
// List of ongoing searches, maps search id to a the started rg process.
|
||||
private ongoingSearches: Map<number, RawProcess> = new Map();
|
||||
|
||||
// Each incoming search is given a unique id, returned to the client. This is the next id we will assigned.
|
||||
private nextSearchId: number = 1;
|
||||
|
||||
private client: SearchInWorkspaceClient | undefined;
|
||||
|
||||
@inject(RgPath)
|
||||
protected readonly rgPath: string;
|
||||
|
||||
constructor(
|
||||
@inject(ILogger) protected readonly logger: ILogger,
|
||||
@inject(RawProcessFactory) protected readonly rawProcessFactory: RawProcessFactory,
|
||||
) { }
|
||||
|
||||
setClient(client: SearchInWorkspaceClient | undefined): void {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
protected getArgs(options?: SearchInWorkspaceOptions): string[] {
|
||||
const args = new Set<string>();
|
||||
|
||||
args.add('--hidden');
|
||||
args.add('--json');
|
||||
|
||||
if (options?.multiline) {
|
||||
args.add('--multiline');
|
||||
}
|
||||
|
||||
if (options?.matchCase) {
|
||||
args.add('--case-sensitive');
|
||||
} else {
|
||||
args.add('--ignore-case');
|
||||
}
|
||||
|
||||
if (options?.includeIgnored) {
|
||||
args.add('--no-ignore');
|
||||
}
|
||||
if (options?.maxFileSize) {
|
||||
args.add('--max-filesize=' + options.maxFileSize.trim());
|
||||
} else {
|
||||
args.add('--max-filesize=20M');
|
||||
}
|
||||
|
||||
if (options?.include) {
|
||||
this.addGlobArgs(args, options.include, false);
|
||||
}
|
||||
|
||||
if (options?.exclude) {
|
||||
this.addGlobArgs(args, options.exclude, true);
|
||||
}
|
||||
|
||||
if (options?.followSymlinks) {
|
||||
args.add('--follow');
|
||||
}
|
||||
|
||||
if (options?.useRegExp || options?.matchWholeWord) {
|
||||
args.add('--regexp');
|
||||
} else {
|
||||
args.add('--fixed-strings');
|
||||
args.add('--');
|
||||
}
|
||||
|
||||
return Array.from(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add glob patterns to ripgrep's arguments
|
||||
* @param args ripgrep set of arguments
|
||||
* @param patterns patterns to include as globs
|
||||
* @param exclude whether to negate the glob pattern or not
|
||||
*/
|
||||
protected addGlobArgs(args: Set<string>, patterns: string[], exclude: boolean = false): void {
|
||||
const sanitizedPatterns = patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0);
|
||||
for (let pattern of sanitizedPatterns) {
|
||||
// make sure the pattern always starts with `**/`
|
||||
if (pattern.startsWith('/')) {
|
||||
pattern = '**' + pattern;
|
||||
} else if (!pattern.startsWith('**/')) {
|
||||
pattern = '**/' + pattern;
|
||||
}
|
||||
// add the exclusion prefix
|
||||
if (exclude) {
|
||||
pattern = '!' + pattern;
|
||||
}
|
||||
args.add(`--glob=${pattern}`);
|
||||
// add a generic glob cli argument entry to include files inside a given directory
|
||||
if (!pattern.endsWith('*')) {
|
||||
// ensure the new pattern ends with `/*`
|
||||
pattern += pattern.endsWith('/') ? '*' : '/*';
|
||||
args.add(`--glob=${pattern}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms relative patterns to absolute paths, one for each given search path.
|
||||
* The resulting paths are not validated in the file system as the pattern keeps glob information.
|
||||
*
|
||||
* @returns The resulting list may be larger than the received patterns as a relative pattern may
|
||||
* resolve to multiple absolute patterns up to the number of search paths.
|
||||
*/
|
||||
protected replaceRelativeToAbsolute(roots: string[], patterns: string[] = []): string[] {
|
||||
const expandedPatterns = new Set<string>();
|
||||
for (const pattern of patterns) {
|
||||
if (this.isPatternRelative(pattern)) {
|
||||
// create new patterns using the absolute form for each root
|
||||
for (const root of roots) {
|
||||
expandedPatterns.add(path.resolve(root, pattern));
|
||||
}
|
||||
} else {
|
||||
expandedPatterns.add(pattern);
|
||||
}
|
||||
}
|
||||
return Array.from(expandedPatterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the pattern is relative and should/can be made absolute.
|
||||
*/
|
||||
protected isPatternRelative(pattern: string): boolean {
|
||||
return pattern.replace(/\\/g, '/').startsWith('./');
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, sets the search directories for the string WHAT to the provided ROOTURIS directories
|
||||
* and returns the assigned search id.
|
||||
*
|
||||
* The include / exclude (options in SearchInWorkspaceOptions) are lists of patterns for files to
|
||||
* include / exclude during search (glob characters are allowed).
|
||||
*
|
||||
* include patterns successfully recognized as absolute paths will override the default search and set
|
||||
* the search directories to the ones provided as includes.
|
||||
* Relative paths are allowed, the application will attempt to translate them to valid absolute paths
|
||||
* based on the applicable search directories.
|
||||
*/
|
||||
async search(what: string, rootUris: string[], options: SearchInWorkspaceOptions = {}): Promise<number> {
|
||||
// Start the rg process. Use --vimgrep to get one result per
|
||||
// line, --color=always to get color control characters that
|
||||
// we'll use to parse the lines.
|
||||
const searchId = this.nextSearchId++;
|
||||
const rootPaths = rootUris.map(root => FileUri.fsPath(root));
|
||||
// If there are absolute paths in `include` we will remove them and use
|
||||
// those as paths to search from.
|
||||
const searchPaths = await this.extractSearchPathsFromIncludes(rootPaths, options);
|
||||
options.include = this.replaceRelativeToAbsolute(searchPaths, options.include);
|
||||
options.exclude = this.replaceRelativeToAbsolute(searchPaths, options.exclude);
|
||||
const rgArgs = this.getArgs(options);
|
||||
// If we use matchWholeWord we use regExp internally, so we need
|
||||
// to escape regexp characters if we actually not set regexp true in UI.
|
||||
if (options?.matchWholeWord && !options.useRegExp) {
|
||||
what = what.replace(/[\-\\\{\}\*\+\?\|\^\$\.\[\]\(\)\#]/g, '\\$&');
|
||||
if (!/\B/.test(what.charAt(0))) {
|
||||
what = '\\b' + what;
|
||||
}
|
||||
if (!/\B/.test(what.charAt(what.length - 1))) {
|
||||
what = what + '\\b';
|
||||
}
|
||||
}
|
||||
|
||||
const args = [...rgArgs, what, ...searchPaths];
|
||||
const processOptions: RawProcessOptions = {
|
||||
command: this.rgPath,
|
||||
args
|
||||
};
|
||||
|
||||
// TODO: Use child_process directly instead of rawProcessFactory?
|
||||
const rgProcess: RawProcess = this.rawProcessFactory(processOptions);
|
||||
this.ongoingSearches.set(searchId, rgProcess);
|
||||
|
||||
rgProcess.onError(error => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let errorCode = (error as any).code;
|
||||
|
||||
// Try to provide somewhat clearer error messages, if possible.
|
||||
if (errorCode === 'ENOENT') {
|
||||
errorCode = 'could not find the ripgrep (rg) binary';
|
||||
} else if (errorCode === 'EACCES') {
|
||||
errorCode = 'could not execute the ripgrep (rg) binary';
|
||||
}
|
||||
|
||||
const errorStr = `An error happened while searching (${errorCode}).`;
|
||||
this.wrapUpSearch(searchId, errorStr);
|
||||
});
|
||||
|
||||
// Running counter of results.
|
||||
let numResults = 0;
|
||||
|
||||
// Buffer to accumulate incoming output.
|
||||
let databuf: string = '';
|
||||
|
||||
let currentSearchResult: SearchInWorkspaceResult | undefined;
|
||||
|
||||
rgProcess.outputStream.on('data', (chunk: Buffer) => {
|
||||
// We might have already reached the max number of
|
||||
// results, sent a TERM signal to rg, but we still get
|
||||
// the data that was already output in the mean time.
|
||||
// It's not necessary to return early here (the check
|
||||
// for maxResults below would avoid sending extra
|
||||
// results), but it avoids doing unnecessary work.
|
||||
if (options?.maxResults && numResults >= options.maxResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
databuf += chunk;
|
||||
|
||||
while (1) {
|
||||
// Check if we have a complete line.
|
||||
const eolIdx = databuf.indexOf('\n');
|
||||
if (eolIdx < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Get and remove the line from the data buffer.
|
||||
const lineBuf = databuf.slice(0, eolIdx);
|
||||
databuf = databuf.slice(eolIdx + 1);
|
||||
|
||||
const obj = JSON.parse(lineBuf) as IRgMessage;
|
||||
if (obj.type === 'begin') {
|
||||
const file = bytesOrTextToString(obj.data.path);
|
||||
if (file) {
|
||||
currentSearchResult = {
|
||||
fileUri: FileUri.create(file).toString(),
|
||||
root: this.getRoot(file, rootUris).toString(),
|
||||
matches: []
|
||||
};
|
||||
} else {
|
||||
this.logger.error('Begin message without path. ' + JSON.stringify(obj));
|
||||
}
|
||||
} else if (obj.type === 'end') {
|
||||
if (currentSearchResult && this.client) {
|
||||
this.client.onResult(searchId, currentSearchResult);
|
||||
}
|
||||
currentSearchResult = undefined;
|
||||
} else if (obj.type === 'match') {
|
||||
if (!currentSearchResult) {
|
||||
continue;
|
||||
}
|
||||
const data = obj.data;
|
||||
const file = bytesOrTextToString(data.path);
|
||||
const line = data.line_number;
|
||||
const lineText = bytesOrTextToString(data.lines);
|
||||
|
||||
if (file === undefined || lineText === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineInBytes = Buffer.from(lineText);
|
||||
|
||||
for (const submatch of data.submatches) {
|
||||
const startOffset = lineInBytes.slice(0, submatch.start).toString().length;
|
||||
const match = bytesOrTextToString(submatch.match);
|
||||
let lineInfo: string | LinePreview = lineText.trimRight();
|
||||
if (lineInfo.length > 300) {
|
||||
const prefixLength = 25;
|
||||
const start = Math.max(startOffset - prefixLength, 0);
|
||||
const length = prefixLength + match.length + 70;
|
||||
let prefix = '';
|
||||
if (start >= prefixLength) {
|
||||
prefix = '...';
|
||||
}
|
||||
const character = (start < prefixLength ? start : prefixLength) + prefix.length + 1;
|
||||
lineInfo = <LinePreview>{
|
||||
text: prefix + lineInfo.substring(start, start + length),
|
||||
character
|
||||
};
|
||||
}
|
||||
currentSearchResult.matches.push({
|
||||
line,
|
||||
character: startOffset + 1,
|
||||
length: match.length,
|
||||
lineText: lineInfo
|
||||
});
|
||||
numResults++;
|
||||
|
||||
// Did we reach the maximum number of results?
|
||||
if (options?.maxResults && numResults >= options.maxResults) {
|
||||
rgProcess.kill();
|
||||
if (currentSearchResult && this.client) {
|
||||
this.client.onResult(searchId, currentSearchResult);
|
||||
}
|
||||
currentSearchResult = undefined;
|
||||
this.wrapUpSearch(searchId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rgProcess.outputStream.on('end', () => {
|
||||
// If we reached maxResults, we should have already
|
||||
// wrapped up the search. Returning early avoids
|
||||
// logging a warning message in wrapUpSearch.
|
||||
if (options?.maxResults && numResults >= options.maxResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.wrapUpSearch(searchId);
|
||||
});
|
||||
|
||||
return searchId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default search paths are set to be the root paths associated to a workspace
|
||||
* however the search scope can be further refined with the include paths available in the search options.
|
||||
* This method will replace the searching paths to the ones specified in the 'include' options but as long
|
||||
* as the 'include' paths can be successfully validated as existing.
|
||||
*
|
||||
* Therefore the returned array of paths can be either the workspace root paths or a set of validated paths
|
||||
* derived from the include options which can be used to perform the search.
|
||||
*
|
||||
* Any pattern that resulted in a valid search path will be removed from the 'include' list as it is
|
||||
* provided as an equivalent search path instead.
|
||||
*/
|
||||
protected async extractSearchPathsFromIncludes(rootPaths: string[], options: SearchInWorkspaceOptions): Promise<string[]> {
|
||||
if (!options.include) {
|
||||
return rootPaths;
|
||||
}
|
||||
const resolvedPaths = new Set<string>();
|
||||
const include: string[] = [];
|
||||
for (const pattern of options.include) {
|
||||
let keep = true;
|
||||
for (const root of rootPaths) {
|
||||
const absolutePath = await this.getAbsolutePathFromPattern(root, pattern);
|
||||
// undefined means the pattern cannot be converted into an absolute path
|
||||
if (absolutePath) {
|
||||
resolvedPaths.add(absolutePath);
|
||||
keep = false;
|
||||
}
|
||||
}
|
||||
if (keep) {
|
||||
include.push(pattern);
|
||||
}
|
||||
}
|
||||
options.include = include;
|
||||
return resolvedPaths.size > 0
|
||||
? Array.from(resolvedPaths)
|
||||
: rootPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform include/exclude option patterns from relative patterns to absolute patterns.
|
||||
* E.g. './abc/foo.*' to '${root}/abc/foo.*', the transformation does not validate the
|
||||
* pattern against the file system as glob suffixes remain.
|
||||
*
|
||||
* @returns undefined if the pattern cannot be converted into an absolute path.
|
||||
*/
|
||||
protected async getAbsolutePathFromPattern(root: string, pattern: string): Promise<string | undefined> {
|
||||
pattern = pattern.replace(/\\/g, '/');
|
||||
// The pattern is not referring to a single file or folder, i.e. not to be converted
|
||||
if (!path.isAbsolute(pattern) && !pattern.startsWith('./')) {
|
||||
return undefined;
|
||||
}
|
||||
// remove the `/**` suffix if present
|
||||
if (pattern.endsWith('/**')) {
|
||||
pattern = pattern.substring(0, pattern.length - 3);
|
||||
}
|
||||
// if `pattern` is absolute then `root` will be ignored by `path.resolve()`
|
||||
const targetPath = path.resolve(root, pattern);
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
return targetPath;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root folder uri that a file belongs to.
|
||||
* In case that a file belongs to more than one root folders, returns the root folder that is closest to the file.
|
||||
* If the file is not from the current workspace, returns empty string.
|
||||
* @param filePath string path of the file
|
||||
* @param rootUris string URIs of the root folders in the current workspace
|
||||
*/
|
||||
private getRoot(filePath: string, rootUris: string[]): URI {
|
||||
const roots = rootUris.filter(root => new URI(root).withScheme('file').isEqualOrParent(FileUri.create(filePath).withScheme('file')));
|
||||
if (roots.length > 0) {
|
||||
return FileUri.create(FileUri.fsPath(roots.sort((r1, r2) => r2.length - r1.length)[0]));
|
||||
}
|
||||
return new URI();
|
||||
}
|
||||
|
||||
// Cancel an ongoing search. Trying to cancel a search that doesn't exist isn't an
|
||||
// error, otherwise we'd have to deal with race conditions, where a client cancels a
|
||||
// search that finishes normally at the same time.
|
||||
cancel(searchId: number): Promise<void> {
|
||||
const process = this.ongoingSearches.get(searchId);
|
||||
if (process) {
|
||||
process.kill();
|
||||
this.wrapUpSearch(searchId);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Send onDone to the client and clean up what we know about search searchId.
|
||||
private wrapUpSearch(searchId: number, error?: string): void {
|
||||
if (this.ongoingSearches.delete(searchId)) {
|
||||
if (this.client) {
|
||||
this.logger.debug('Sending onDone for ' + searchId, error);
|
||||
this.client.onDone(searchId, error);
|
||||
} else {
|
||||
this.logger.debug('Wrapping up search ' + searchId + ' but no client');
|
||||
}
|
||||
} else {
|
||||
this.logger.debug("Trying to wrap up a search we don't know about " + searchId);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// *****************************************************************************
|
||||
// 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 { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common';
|
||||
import { SearchInWorkspaceServer, SearchInWorkspaceClient, SIW_WS_PATH } from '../common/search-in-workspace-interface';
|
||||
import { RipgrepSearchInWorkspaceServer, RgPath } from './ripgrep-search-in-workspace-server';
|
||||
import { rgPath } from '@vscode/ripgrep';
|
||||
import { bindSearchInWorkspacePreferences } from '../common/search-in-workspace-preferences';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(SearchInWorkspaceServer).to(RipgrepSearchInWorkspaceServer);
|
||||
bind(ConnectionHandler).toDynamicValue(ctx =>
|
||||
new RpcConnectionHandler<SearchInWorkspaceClient>(SIW_WS_PATH, client => {
|
||||
const server = ctx.container.get<SearchInWorkspaceServer>(SearchInWorkspaceServer);
|
||||
server.setClient(client);
|
||||
client.onDidCloseConnection(() => server.dispose());
|
||||
return server;
|
||||
}));
|
||||
bind(RgPath).toConstantValue(rgPath);
|
||||
bindSearchInWorkspacePreferences(bind);
|
||||
});
|
||||
31
packages/search-in-workspace/tsconfig.json
Normal file
31
packages/search-in-workspace/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../editor"
|
||||
},
|
||||
{
|
||||
"path": "../filesystem"
|
||||
},
|
||||
{
|
||||
"path": "../navigator"
|
||||
},
|
||||
{
|
||||
"path": "../process"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user