mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-27 15:20:30 -04:00
Merge from vscode 2b0b9136329c181a9e381463a1f7dc3a2d105a34 (#4880)
This commit is contained in:
@@ -53,7 +53,7 @@ export interface ISearchResultProvider {
|
||||
clearCache(cacheKey: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFolderQuery<U extends UriComponents=URI> {
|
||||
export interface IFolderQuery<U extends UriComponents = URI> {
|
||||
folder: U;
|
||||
excludePattern?: glob.IExpression;
|
||||
includePattern?: glob.IExpression;
|
||||
|
||||
413
src/vs/workbench/services/search/common/searchExtTypes.ts
Normal file
413
src/vs/workbench/services/search/common/searchExtTypes.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IProgress } from 'vs/platform/progress/common/progress';
|
||||
|
||||
export class Position {
|
||||
constructor(readonly line: number, readonly character: number) { }
|
||||
|
||||
isBefore(other: Position): boolean { return false; }
|
||||
isBeforeOrEqual(other: Position): boolean { return false; }
|
||||
isAfter(other: Position): boolean { return false; }
|
||||
isAfterOrEqual(other: Position): boolean { return false; }
|
||||
isEqual(other: Position): boolean { return false; }
|
||||
compareTo(other: Position): number { return 0; }
|
||||
translate(lineDelta?: number, characterDelta?: number): Position;
|
||||
translate(change: { lineDelta?: number; characterDelta?: number; }): Position;
|
||||
translate(_?: any, _2?: any): Position { return new Position(0, 0); }
|
||||
with(line?: number, character?: number): Position;
|
||||
with(change: { line?: number; character?: number; }): Position;
|
||||
with(_: any): Position { return new Position(0, 0); }
|
||||
}
|
||||
|
||||
export class Range {
|
||||
readonly start: Position;
|
||||
readonly end: Position;
|
||||
|
||||
constructor(startLine: number, startCol: number, endLine: number, endCol: number) {
|
||||
this.start = new Position(startLine, startCol);
|
||||
this.end = new Position(endLine, endCol);
|
||||
}
|
||||
|
||||
isEmpty: boolean;
|
||||
isSingleLine: boolean;
|
||||
contains(positionOrRange: Position | Range): boolean { return false; }
|
||||
isEqual(other: Range): boolean { return false; }
|
||||
intersection(range: Range): Range | undefined { return undefined; }
|
||||
union(other: Range): Range { return new Range(0, 0, 0, 0); }
|
||||
|
||||
with(start?: Position, end?: Position): Range;
|
||||
with(change: { start?: Position, end?: Position }): Range;
|
||||
with(_: any): Range { return new Range(0, 0, 0, 0); }
|
||||
}
|
||||
|
||||
export type ProviderResult<T> = T | undefined | null | Thenable<T | undefined | null>;
|
||||
|
||||
/**
|
||||
* A relative pattern is a helper to construct glob patterns that are matched
|
||||
* relatively to a base path. The base path can either be an absolute file path
|
||||
* or a [workspace folder](#WorkspaceFolder).
|
||||
*/
|
||||
export interface RelativePattern {
|
||||
|
||||
/**
|
||||
* A base file path to which this pattern will be matched against relatively.
|
||||
*/
|
||||
base: string;
|
||||
|
||||
/**
|
||||
* A file glob pattern like `*.{ts,js}` that will be matched on file paths
|
||||
* relative to the base path.
|
||||
*
|
||||
* Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
|
||||
* the file glob pattern will match on `index.js`.
|
||||
*/
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file glob pattern to match file paths against. This can either be a glob pattern string
|
||||
* (like `**/*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern).
|
||||
*
|
||||
* Glob patterns can have the following syntax:
|
||||
* * `*` to match one or more characters in a path segment
|
||||
* * `?` to match on one character in a path segment
|
||||
* * `**` to match any number of path segments, including none
|
||||
* * `{}` to group conditions (e.g. `**/*.{ts,js}` matches all TypeScript and JavaScript files)
|
||||
* * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
|
||||
* * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
|
||||
*
|
||||
* Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file
|
||||
* path to match against, consider to use the [relative pattern](#RelativePattern) support
|
||||
* that takes care of converting any backslash into slash. Otherwise, make sure to convert
|
||||
* any backslash to slash when creating the glob pattern.
|
||||
*/
|
||||
export type GlobPattern = string | RelativePattern;
|
||||
|
||||
/**
|
||||
* The parameters of a query for text search.
|
||||
*/
|
||||
export interface TextSearchQuery {
|
||||
/**
|
||||
* The text pattern to search for.
|
||||
*/
|
||||
pattern: string;
|
||||
|
||||
/**
|
||||
* Whether or not `pattern` should match multiple lines of text.
|
||||
*/
|
||||
isMultiline?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not `pattern` should be interpreted as a regular expression.
|
||||
*/
|
||||
isRegExp?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the search should be case-sensitive.
|
||||
*/
|
||||
isCaseSensitive?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to search for whole word matches only.
|
||||
*/
|
||||
isWordMatch?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A file glob pattern to match file paths against.
|
||||
* TODO@roblou - merge this with the GlobPattern docs/definition in vscode.d.ts.
|
||||
* @see [GlobPattern](#GlobPattern)
|
||||
*/
|
||||
export type GlobString = string;
|
||||
|
||||
/**
|
||||
* Options common to file and text search
|
||||
*/
|
||||
export interface SearchOptions {
|
||||
/**
|
||||
* The root folder to search within.
|
||||
*/
|
||||
folder: URI;
|
||||
|
||||
/**
|
||||
* Files that match an `includes` glob pattern should be included in the search.
|
||||
*/
|
||||
includes: GlobString[];
|
||||
|
||||
/**
|
||||
* Files that match an `excludes` glob pattern should be excluded from the search.
|
||||
*/
|
||||
excludes: GlobString[];
|
||||
|
||||
/**
|
||||
* Whether external files that exclude files, like .gitignore, should be respected.
|
||||
* See the vscode setting `"search.useIgnoreFiles"`.
|
||||
*/
|
||||
useIgnoreFiles: boolean;
|
||||
|
||||
/**
|
||||
* Whether symlinks should be followed while searching.
|
||||
* See the vscode setting `"search.followSymlinks"`.
|
||||
*/
|
||||
followSymlinks: boolean;
|
||||
|
||||
/**
|
||||
* Whether global files that exclude files, like .gitignore, should be respected.
|
||||
* See the vscode setting `"search.useGlobalIgnoreFiles"`.
|
||||
*/
|
||||
useGlobalIgnoreFiles: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to specify the size of the result text preview.
|
||||
* These options don't affect the size of the match itself, just the amount of preview text.
|
||||
*/
|
||||
export interface TextSearchPreviewOptions {
|
||||
/**
|
||||
* The maximum number of lines in the preview.
|
||||
* Only search providers that support multiline search will ever return more than one line in the match.
|
||||
*/
|
||||
matchLines: number;
|
||||
|
||||
/**
|
||||
* The maximum number of characters included per line.
|
||||
*/
|
||||
charsPerLine: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that apply to text search.
|
||||
*/
|
||||
export interface TextSearchOptions extends SearchOptions {
|
||||
/**
|
||||
* The maximum number of results to be returned.
|
||||
*/
|
||||
maxResults: number;
|
||||
|
||||
/**
|
||||
* Options to specify the size of the result text preview.
|
||||
*/
|
||||
previewOptions?: TextSearchPreviewOptions;
|
||||
|
||||
/**
|
||||
* Exclude files larger than `maxFileSize` in bytes.
|
||||
*/
|
||||
maxFileSize?: number;
|
||||
|
||||
/**
|
||||
* Interpret files using this encoding.
|
||||
* See the vscode setting `"files.encoding"`
|
||||
*/
|
||||
encoding?: string;
|
||||
|
||||
/**
|
||||
* Number of lines of context to include before each match.
|
||||
*/
|
||||
beforeContext?: number;
|
||||
|
||||
/**
|
||||
* Number of lines of context to include after each match.
|
||||
*/
|
||||
afterContext?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information collected when text search is complete.
|
||||
*/
|
||||
export interface TextSearchComplete {
|
||||
/**
|
||||
* Whether the search hit the limit on the maximum number of search results.
|
||||
* `maxResults` on [`TextSearchOptions`](#TextSearchOptions) specifies the max number of results.
|
||||
* - If exactly that number of matches exist, this should be false.
|
||||
* - If `maxResults` matches are returned and more exist, this should be true.
|
||||
* - If search hits an internal limit which is less than `maxResults`, this should be true.
|
||||
*/
|
||||
limitHit?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The parameters of a query for file search.
|
||||
*/
|
||||
export interface FileSearchQuery {
|
||||
/**
|
||||
* The search pattern to match against file paths.
|
||||
*/
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that apply to file search.
|
||||
*/
|
||||
export interface FileSearchOptions extends SearchOptions {
|
||||
/**
|
||||
* The maximum number of results to be returned.
|
||||
*/
|
||||
maxResults?: number;
|
||||
|
||||
/**
|
||||
* A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache,
|
||||
* and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared.
|
||||
*/
|
||||
session?: CancellationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* A preview of the text result.
|
||||
*/
|
||||
export interface TextSearchMatchPreview {
|
||||
/**
|
||||
* The matching lines of text, or a portion of the matching line that contains the match.
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* The Range within `text` corresponding to the text of the match.
|
||||
* The number of matches must match the TextSearchMatch's range property.
|
||||
*/
|
||||
matches: Range | Range[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A match from a text search
|
||||
*/
|
||||
export interface TextSearchMatch {
|
||||
/**
|
||||
* The uri for the matching document.
|
||||
*/
|
||||
uri: URI;
|
||||
|
||||
/**
|
||||
* The range of the match within the document, or multiple ranges for multiple matches.
|
||||
*/
|
||||
ranges: Range | Range[];
|
||||
|
||||
/**
|
||||
* A preview of the text match.
|
||||
*/
|
||||
preview: TextSearchMatchPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* A line of context surrounding a TextSearchMatch.
|
||||
*/
|
||||
export interface TextSearchContext {
|
||||
/**
|
||||
* The uri for the matching document.
|
||||
*/
|
||||
uri: URI;
|
||||
|
||||
/**
|
||||
* One line of text.
|
||||
* previewOptions.charsPerLine applies to this
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* The line number of this line of context.
|
||||
*/
|
||||
lineNumber: number;
|
||||
}
|
||||
|
||||
export type TextSearchResult = TextSearchMatch | TextSearchContext;
|
||||
|
||||
/**
|
||||
* A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions.
|
||||
*
|
||||
* A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for
|
||||
* all files that match the user's query.
|
||||
*
|
||||
* The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string,
|
||||
* and in that case, every file in the folder should be returned.
|
||||
*/
|
||||
export interface FileSearchProvider {
|
||||
/**
|
||||
* Provide the set of files that match a certain file path pattern.
|
||||
* @param query The parameters for this query.
|
||||
* @param options A set of options to consider while searching files.
|
||||
* @param progress A progress callback that must be invoked for all results.
|
||||
* @param token A cancellation token.
|
||||
*/
|
||||
provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult<URI[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A TextSearchProvider provides search results for text results inside files in the workspace.
|
||||
*/
|
||||
export interface TextSearchProvider {
|
||||
/**
|
||||
* Provide results that match the given text pattern.
|
||||
* @param query The parameters for this query.
|
||||
* @param options A set of options to consider while searching.
|
||||
* @param progress A progress callback that must be invoked for all results.
|
||||
* @param token A cancellation token.
|
||||
*/
|
||||
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: IProgress<TextSearchResult>, token: CancellationToken): ProviderResult<TextSearchComplete>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be set on a findTextInFiles search.
|
||||
*/
|
||||
export interface FindTextInFilesOptions {
|
||||
/**
|
||||
* A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern
|
||||
* will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern)
|
||||
* to restrict the search results to a [workspace folder](#WorkspaceFolder).
|
||||
*/
|
||||
include?: GlobPattern;
|
||||
|
||||
/**
|
||||
* A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern
|
||||
* will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will
|
||||
* apply, when `null` no excludes will apply.
|
||||
*/
|
||||
exclude?: GlobPattern | null;
|
||||
|
||||
/**
|
||||
* The maximum number of results to search for
|
||||
*/
|
||||
maxResults?: number;
|
||||
|
||||
/**
|
||||
* Whether external files that exclude files, like .gitignore, should be respected.
|
||||
* See the vscode setting `"search.useIgnoreFiles"`.
|
||||
*/
|
||||
useIgnoreFiles?: boolean;
|
||||
|
||||
/**
|
||||
* Whether global files that exclude files, like .gitignore, should be respected.
|
||||
* See the vscode setting `"search.useGlobalIgnoreFiles"`.
|
||||
*/
|
||||
useGlobalIgnoreFiles?: boolean;
|
||||
|
||||
/**
|
||||
* Whether symlinks should be followed while searching.
|
||||
* See the vscode setting `"search.followSymlinks"`.
|
||||
*/
|
||||
followSymlinks?: boolean;
|
||||
|
||||
/**
|
||||
* Interpret files using this encoding.
|
||||
* See the vscode setting `"files.encoding"`
|
||||
*/
|
||||
encoding?: string;
|
||||
|
||||
/**
|
||||
* Options to specify the size of the result text preview.
|
||||
*/
|
||||
previewOptions?: TextSearchPreviewOptions;
|
||||
|
||||
/**
|
||||
* Number of lines of context to include before each match.
|
||||
*/
|
||||
beforeContext?: number;
|
||||
|
||||
/**
|
||||
* Number of lines of context to include after each match.
|
||||
*/
|
||||
afterContext?: number;
|
||||
}
|
||||
@@ -19,8 +19,7 @@ import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import * as flow from 'vs/base/node/flow';
|
||||
import { readdir } from 'vs/base/node/pfs';
|
||||
import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search';
|
||||
import { spawnRipgrepCmd } from './ripgrepFileSearch';
|
||||
|
||||
@@ -128,7 +127,7 @@ export class FileWalker {
|
||||
this.cmdSW = StopWatch.create(false);
|
||||
|
||||
// For each root folder
|
||||
flow.parallel<IFolderQuery, void>(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => {
|
||||
this.parallel<IFolderQuery, void>(folderQueries, (folderQuery: IFolderQuery, rootFolderDone: (err: Error | null, result: void) => void) => {
|
||||
this.call(this.cmdTraversal, this, folderQuery, onResult, onMessage, (err?: Error) => {
|
||||
if (err) {
|
||||
const errorMessage = toErrorMessage(err);
|
||||
@@ -146,6 +145,34 @@ export class FileWalker {
|
||||
});
|
||||
}
|
||||
|
||||
private parallel<T, E>(list: T[], fn: (item: T, callback: (err: Error | null, result: E | null) => void) => void, callback: (err: Array<Error | null> | null, result: E[]) => void): void {
|
||||
const results = new Array(list.length);
|
||||
const errors = new Array<Error | null>(list.length);
|
||||
let didErrorOccur = false;
|
||||
let doneCount = 0;
|
||||
|
||||
if (list.length === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
list.forEach((item, index) => {
|
||||
fn(item, (error, result) => {
|
||||
if (error) {
|
||||
didErrorOccur = true;
|
||||
results[index] = null;
|
||||
errors[index] = error;
|
||||
} else {
|
||||
results[index] = result;
|
||||
errors[index] = null;
|
||||
}
|
||||
|
||||
if (++doneCount === list.length) {
|
||||
return callback(didErrorOccur ? errors : null, results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private call<F extends Function>(fun: F, that: any, ...args: any[]): void {
|
||||
try {
|
||||
fun.apply(that, args);
|
||||
@@ -440,7 +467,7 @@ export class FileWalker {
|
||||
|
||||
// Execute tasks on each file in parallel to optimize throughput
|
||||
const hasSibling = glob.hasSiblingFn(() => files);
|
||||
flow.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => {
|
||||
this.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => {
|
||||
|
||||
// Check canceled
|
||||
if (this.isCanceled || this.isLimitHit) {
|
||||
@@ -489,12 +516,14 @@ export class FileWalker {
|
||||
this.walkedPaths[realpath] = true; // remember as walked
|
||||
|
||||
// Continue walking
|
||||
return extfs.readdir(currentAbsolutePath, (error: Error, children: string[]): void => {
|
||||
if (error || this.isCanceled || this.isLimitHit) {
|
||||
return readdir(currentAbsolutePath).then(children => {
|
||||
if (this.isCanceled || this.isLimitHit) {
|
||||
return clb(null);
|
||||
}
|
||||
|
||||
this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err || null));
|
||||
}, error => {
|
||||
clb(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
|
||||
import * as vscode from 'vscode';
|
||||
import { FileSearchProvider, FileSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
|
||||
export interface IInternalFileMatch {
|
||||
base: URI;
|
||||
@@ -45,7 +45,7 @@ class FileSearchEngine {
|
||||
|
||||
private globalExcludePattern?: glob.ParsedExpression;
|
||||
|
||||
constructor(private config: IFileQuery, private provider: vscode.FileSearchProvider, private sessionToken?: CancellationToken) {
|
||||
constructor(private config: IFileQuery, private provider: FileSearchProvider, private sessionToken?: CancellationToken) {
|
||||
this.filePattern = config.filePattern;
|
||||
this.includePattern = config.includePattern && glob.parse(config.includePattern);
|
||||
this.maxResults = config.maxResults || undefined;
|
||||
@@ -172,7 +172,7 @@ class FileSearchEngine {
|
||||
});
|
||||
}
|
||||
|
||||
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): vscode.FileSearchOptions {
|
||||
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): FileSearchOptions {
|
||||
const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern);
|
||||
const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern);
|
||||
|
||||
@@ -283,7 +283,7 @@ export class FileSearchManager {
|
||||
|
||||
private readonly sessions = new Map<string, CancellationTokenSource>();
|
||||
|
||||
fileSearch(config: IFileQuery, provider: vscode.FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
|
||||
fileSearch(config: IFileQuery, provider: FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
|
||||
const sessionTokenSource = this.getSessionTokenSource(config.cacheKey);
|
||||
const engine = new FileSearchEngine(config, provider, sessionTokenSource && sessionTokenSource.token);
|
||||
|
||||
|
||||
@@ -144,7 +144,8 @@ function globExprsToRgGlobs(patterns: glob.IExpression, folder?: string, exclude
|
||||
}
|
||||
|
||||
globArgs.push(fixDriveC(key));
|
||||
} else if (value && value.when) {
|
||||
// {{SQL CARBON EDIT}} @todo anthonydresser cast value because we aren't using strict null checks
|
||||
} else if (value && (<glob.SiblingClause>value).when) {
|
||||
siblingClauses[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,24 +3,25 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils';
|
||||
import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
|
||||
import * as vscode from 'vscode';
|
||||
import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
import { Progress } from 'vs/platform/progress/common/progress';
|
||||
|
||||
export class RipgrepSearchProvider implements vscode.TextSearchProvider {
|
||||
private inProgress: Set<vscode.CancellationTokenSource> = new Set();
|
||||
export class RipgrepSearchProvider implements TextSearchProvider {
|
||||
private inProgress: Set<CancellationTokenSource> = new Set();
|
||||
|
||||
constructor(private outputChannel: OutputChannel) {
|
||||
process.once('exit', () => this.dispose());
|
||||
}
|
||||
|
||||
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
|
||||
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress<TextSearchResult>, token: CancellationToken): Promise<TextSearchComplete> {
|
||||
const engine = new RipgrepTextSearchEngine(this.outputChannel);
|
||||
return this.withToken(token, token => engine.provideTextSearchResults(query, options, progress, token));
|
||||
}
|
||||
|
||||
private async withToken<T>(token: vscode.CancellationToken, fn: (token: vscode.CancellationToken) => Promise<T>): Promise<T> {
|
||||
private async withToken<T>(token: CancellationToken, fn: (token: CancellationToken) => Promise<T>): Promise<T> {
|
||||
const merged = mergedTokenSource(token);
|
||||
this.inProgress.add(merged);
|
||||
const result = await fn(merged.token);
|
||||
@@ -34,7 +35,7 @@ export class RipgrepSearchProvider implements vscode.TextSearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
function mergedTokenSource(token: vscode.CancellationToken): vscode.CancellationTokenSource {
|
||||
function mergedTokenSource(token: CancellationToken): CancellationTokenSource {
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
token.onCancellationRequested(() => tokenSource.cancel());
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { SearchRange, TextSearchMatch } from 'vs/workbench/services/search/common/search';
|
||||
import * as vscode from 'vscode';
|
||||
import { mapArrayOrNot } from 'vs/base/common/arrays';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as searchExtTypes from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
|
||||
export type Maybe<T> = T | null | undefined;
|
||||
|
||||
@@ -16,9 +17,9 @@ export function anchorGlob(glob: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a vscode.TextSearchResult by using our internal TextSearchResult type for its previewOptions logic.
|
||||
* Create a vscode.TextSearchMatch by using our internal TextSearchMatch type for its previewOptions logic.
|
||||
*/
|
||||
export function createTextSearchResult(uri: vscode.Uri, text: string, range: Range | Range[], previewOptions?: vscode.TextSearchPreviewOptions): vscode.TextSearchMatch {
|
||||
export function createTextSearchResult(uri: URI, text: string, range: searchExtTypes.Range | searchExtTypes.Range[], previewOptions?: searchExtTypes.TextSearchPreviewOptions): searchExtTypes.TextSearchMatch {
|
||||
const searchRange = mapArrayOrNot(range, rangeToSearchRange);
|
||||
|
||||
const internalResult = new TextSearchMatch(text, searchRange, previewOptions);
|
||||
@@ -33,50 +34,12 @@ export function createTextSearchResult(uri: vscode.Uri, text: string, range: Ran
|
||||
};
|
||||
}
|
||||
|
||||
function rangeToSearchRange(range: Range): SearchRange {
|
||||
function rangeToSearchRange(range: searchExtTypes.Range): SearchRange {
|
||||
return new SearchRange(range.start.line, range.start.character, range.end.line, range.end.character);
|
||||
}
|
||||
|
||||
function searchRangeToRange(range: SearchRange): Range {
|
||||
return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
|
||||
}
|
||||
|
||||
export class Position {
|
||||
constructor(readonly line: number, readonly character: number) { }
|
||||
|
||||
isBefore(other: Position): boolean { return false; }
|
||||
isBeforeOrEqual(other: Position): boolean { return false; }
|
||||
isAfter(other: Position): boolean { return false; }
|
||||
isAfterOrEqual(other: Position): boolean { return false; }
|
||||
isEqual(other: Position): boolean { return false; }
|
||||
compareTo(other: Position): number { return 0; }
|
||||
translate(lineDelta?: number, characterDelta?: number): Position;
|
||||
translate(change: { lineDelta?: number; characterDelta?: number; }): Position;
|
||||
translate(_?: any, _2?: any): Position { return new Position(0, 0); }
|
||||
with(line?: number, character?: number): Position;
|
||||
with(change: { line?: number; character?: number; }): Position;
|
||||
with(_: any): Position { return new Position(0, 0); }
|
||||
}
|
||||
|
||||
export class Range {
|
||||
readonly start: Position;
|
||||
readonly end: Position;
|
||||
|
||||
constructor(startLine: number, startCol: number, endLine: number, endCol: number) {
|
||||
this.start = new Position(startLine, startCol);
|
||||
this.end = new Position(endLine, endCol);
|
||||
}
|
||||
|
||||
isEmpty: boolean;
|
||||
isSingleLine: boolean;
|
||||
contains(positionOrRange: Position | Range): boolean { return false; }
|
||||
isEqual(other: Range): boolean { return false; }
|
||||
intersection(range: Range): Range | undefined { return undefined; }
|
||||
union(other: Range): Range { return new Range(0, 0, 0, 0); }
|
||||
|
||||
with(start?: Position, end?: Position): Range;
|
||||
with(change: { start?: Position, end?: Position }): Range;
|
||||
with(_: any): Range { return new Range(0, 0, 0, 0); }
|
||||
function searchRangeToRange(range: SearchRange): searchExtTypes.Range {
|
||||
return new searchExtTypes.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
|
||||
}
|
||||
|
||||
export interface IOutputChannel {
|
||||
|
||||
@@ -10,12 +10,14 @@ import { NodeStringDecoder, StringDecoder } from 'string_decoder';
|
||||
import { createRegExp, startsWith, startsWithUTF8BOM, stripUTF8BOM, escapeRegExpCharacters, endsWith } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtendedExtensionSearchOptions, SearchError, SearchErrorCode, serializeSearchError } from 'vs/workbench/services/search/common/search';
|
||||
import * as vscode from 'vscode';
|
||||
import { rgPath } from 'vscode-ripgrep';
|
||||
import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe, Range } from './ripgrepSearchUtils';
|
||||
import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe } from './ripgrepSearchUtils';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { splitGlobAware } from 'vs/base/common/glob';
|
||||
import { groupBy } from 'vs/base/common/collections';
|
||||
import { TextSearchQuery, TextSearchOptions, TextSearchResult, TextSearchComplete, TextSearchPreviewOptions, TextSearchContext, TextSearchMatch, Range } from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
import { Progress } from 'vs/platform/progress/common/progress';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
// If vscode-ripgrep is in an .asar file, then the binary is unpacked.
|
||||
const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
|
||||
@@ -24,7 +26,7 @@ export class RipgrepTextSearchEngine {
|
||||
|
||||
constructor(private outputChannel: IOutputChannel) { }
|
||||
|
||||
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
|
||||
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress<TextSearchResult>, token: CancellationToken): Promise<TextSearchComplete> {
|
||||
this.outputChannel.appendLine(`provideTextSearchResults ${query.pattern}, ${JSON.stringify({
|
||||
...options,
|
||||
...{
|
||||
@@ -53,7 +55,7 @@ export class RipgrepTextSearchEngine {
|
||||
|
||||
let gotResult = false;
|
||||
const ripgrepParser = new RipgrepParser(options.maxResults, cwd, options.previewOptions);
|
||||
ripgrepParser.on('result', (match: vscode.TextSearchResult) => {
|
||||
ripgrepParser.on('result', (match: TextSearchResult) => {
|
||||
gotResult = true;
|
||||
progress.report(match);
|
||||
});
|
||||
@@ -155,7 +157,7 @@ export class RipgrepParser extends EventEmitter {
|
||||
|
||||
private numResults = 0;
|
||||
|
||||
constructor(private maxResults: number, private rootFolder: string, private previewOptions?: vscode.TextSearchPreviewOptions) {
|
||||
constructor(private maxResults: number, private rootFolder: string, private previewOptions?: TextSearchPreviewOptions) {
|
||||
super();
|
||||
this.stringDecoder = new StringDecoder();
|
||||
}
|
||||
@@ -169,7 +171,7 @@ export class RipgrepParser extends EventEmitter {
|
||||
}
|
||||
|
||||
|
||||
on(event: 'result', listener: (result: vscode.TextSearchResult) => void): this;
|
||||
on(event: 'result', listener: (result: TextSearchResult) => void): this;
|
||||
on(event: 'hitLimit', listener: () => void): this;
|
||||
on(event: string, listener: (...args: any[]) => void): this {
|
||||
super.on(event, listener);
|
||||
@@ -240,7 +242,7 @@ export class RipgrepParser extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private createTextSearchMatch(data: IRgMatch, uri: vscode.Uri): vscode.TextSearchMatch {
|
||||
private createTextSearchMatch(data: IRgMatch, uri: URI): TextSearchMatch {
|
||||
const lineNumber = data.line_number - 1;
|
||||
let isBOMStripped = false;
|
||||
let fullText = bytesOrTextToString(data.lines);
|
||||
@@ -290,7 +292,7 @@ export class RipgrepParser extends EventEmitter {
|
||||
return createTextSearchResult(uri, fullText, <Range[]>ranges, this.previewOptions);
|
||||
}
|
||||
|
||||
private createTextSearchContext(data: IRgMatch, uri: URI): vscode.TextSearchContext[] {
|
||||
private createTextSearchContext(data: IRgMatch, uri: URI): TextSearchContext[] {
|
||||
const text = bytesOrTextToString(data.lines);
|
||||
const startLine = data.line_number;
|
||||
return text
|
||||
@@ -305,7 +307,7 @@ export class RipgrepParser extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
private onResult(match: vscode.TextSearchResult): void {
|
||||
private onResult(match: TextSearchResult): void {
|
||||
this.emit('result', match);
|
||||
}
|
||||
}
|
||||
@@ -333,7 +335,7 @@ function getNumLinesAndLastNewlineLength(text: string): { numLines: number, last
|
||||
return { numLines, lastLineLength };
|
||||
}
|
||||
|
||||
function getRgArgs(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions): string[] {
|
||||
function getRgArgs(query: TextSearchQuery, options: TextSearchOptions): string[] {
|
||||
const args = ['--hidden'];
|
||||
args.push(query.isCaseSensitive ? '--case-sensitive' : '--ignore-case');
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { IFileMatch, IProgressMessage, ITextQuery, ITextSearchStats, ITextSearchMatch, ISerializedFileMatch, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search';
|
||||
import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
|
||||
import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
|
||||
@@ -30,7 +30,7 @@ export class TextSearchEngineAdapter {
|
||||
onMessage({ message: msg });
|
||||
}
|
||||
};
|
||||
const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), extfs);
|
||||
const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs);
|
||||
return new Promise((resolve, reject) => {
|
||||
return textSearchManager
|
||||
.search(
|
||||
|
||||
@@ -11,9 +11,9 @@ import * as glob from 'vs/base/common/glob';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { toCanonicalName } from 'vs/base/node/encoding';
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
|
||||
import * as vscode from 'vscode';
|
||||
import { TextSearchProvider, TextSearchResult, TextSearchMatch, TextSearchComplete, Range, TextSearchOptions, TextSearchQuery } from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
|
||||
export class TextSearchManager {
|
||||
|
||||
@@ -22,7 +22,7 @@ export class TextSearchManager {
|
||||
private isLimitHit: boolean;
|
||||
private resultCount = 0;
|
||||
|
||||
constructor(private query: ITextQuery, private provider: vscode.TextSearchProvider, private _extfs: typeof extfs = extfs) {
|
||||
constructor(private query: ITextQuery, private provider: TextSearchProvider, private _pfs: typeof pfs = pfs) {
|
||||
}
|
||||
|
||||
search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
|
||||
@@ -34,7 +34,7 @@ export class TextSearchManager {
|
||||
this.collector = new TextSearchResultsCollector(onProgress);
|
||||
|
||||
let isCanceled = false;
|
||||
const onResult = (result: vscode.TextSearchResult, folderIdx: number) => {
|
||||
const onResult = (result: TextSearchResult, folderIdx: number) => {
|
||||
if (isCanceled) {
|
||||
return;
|
||||
}
|
||||
@@ -79,14 +79,14 @@ export class TextSearchManager {
|
||||
});
|
||||
}
|
||||
|
||||
private resultSize(result: vscode.TextSearchResult): number {
|
||||
const match = <vscode.TextSearchMatch>result;
|
||||
private resultSize(result: TextSearchResult): number {
|
||||
const match = <TextSearchMatch>result;
|
||||
return Array.isArray(match.ranges) ?
|
||||
match.ranges.length :
|
||||
1;
|
||||
}
|
||||
|
||||
private trimResultToSize(result: vscode.TextSearchMatch, size: number): vscode.TextSearchMatch {
|
||||
private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch {
|
||||
const rangesArr = Array.isArray(result.ranges) ? result.ranges : [result.ranges];
|
||||
const matchesArr = Array.isArray(result.preview.matches) ? result.preview.matches : [result.preview.matches];
|
||||
|
||||
@@ -100,11 +100,11 @@ export class TextSearchManager {
|
||||
};
|
||||
}
|
||||
|
||||
private searchInFolder(folderQuery: IFolderQuery<URI>, onResult: (result: vscode.TextSearchResult) => void, token: CancellationToken): Promise<vscode.TextSearchComplete | null | undefined> {
|
||||
private searchInFolder(folderQuery: IFolderQuery<URI>, onResult: (result: TextSearchResult) => void, token: CancellationToken): Promise<TextSearchComplete | null | undefined> {
|
||||
const queryTester = new QueryGlobTester(this.query, folderQuery);
|
||||
const testingPs: Promise<void>[] = [];
|
||||
const progress = {
|
||||
report: (result: vscode.TextSearchResult) => {
|
||||
report: (result: TextSearchResult) => {
|
||||
if (!this.validateProviderResult(result)) {
|
||||
return;
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export class TextSearchManager {
|
||||
});
|
||||
}
|
||||
|
||||
private validateProviderResult(result: vscode.TextSearchResult): boolean {
|
||||
private validateProviderResult(result: TextSearchResult): boolean {
|
||||
if (extensionResultIsMatch(result)) {
|
||||
if (Array.isArray(result.ranges)) {
|
||||
if (!Array.isArray(result.preview.matches)) {
|
||||
@@ -143,7 +143,7 @@ export class TextSearchManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((<vscode.Range[]>result.preview.matches).length !== result.ranges.length) {
|
||||
if ((<Range[]>result.preview.matches).length !== result.ranges.length) {
|
||||
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
|
||||
return false;
|
||||
}
|
||||
@@ -159,22 +159,14 @@ export class TextSearchManager {
|
||||
}
|
||||
|
||||
private readdir(dirname: string): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._extfs.readdir(dirname, (err, files) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
return this._pfs.readdir(dirname);
|
||||
}
|
||||
|
||||
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): vscode.TextSearchOptions {
|
||||
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): TextSearchOptions {
|
||||
const includes = resolvePatternsForProvider(this.query.includePattern, fq.includePattern);
|
||||
const excludes = resolvePatternsForProvider(this.query.excludePattern, fq.excludePattern);
|
||||
|
||||
const options = <vscode.TextSearchOptions>{
|
||||
const options = <TextSearchOptions>{
|
||||
folder: URI.from(fq.folder),
|
||||
excludes,
|
||||
includes,
|
||||
@@ -193,8 +185,8 @@ export class TextSearchManager {
|
||||
}
|
||||
}
|
||||
|
||||
function patternInfoToQuery(patternInfo: IPatternInfo): vscode.TextSearchQuery {
|
||||
return <vscode.TextSearchQuery>{
|
||||
function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQuery {
|
||||
return <TextSearchQuery>{
|
||||
isCaseSensitive: patternInfo.isCaseSensitive || false,
|
||||
isRegExp: patternInfo.isRegExp || false,
|
||||
isWordMatch: patternInfo.isWordMatch || false,
|
||||
@@ -214,7 +206,7 @@ export class TextSearchResultsCollector {
|
||||
this._batchedCollector = new BatchedCollector<IFileMatch>(512, items => this.sendItems(items));
|
||||
}
|
||||
|
||||
add(data: vscode.TextSearchResult, folderIdx: number): void {
|
||||
add(data: TextSearchResult, folderIdx: number): void {
|
||||
// Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector.
|
||||
// This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search
|
||||
// providers that send results in random order. We could do this step afterwards instead.
|
||||
@@ -251,8 +243,8 @@ export class TextSearchResultsCollector {
|
||||
}
|
||||
}
|
||||
|
||||
function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSearchResult {
|
||||
// Warning: result from RipgrepTextSearchEH has fake vscode.Range. Don't depend on any other props beyond these...
|
||||
function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchResult {
|
||||
// Warning: result from RipgrepTextSearchEH has fake Range. Don't depend on any other props beyond these...
|
||||
if (extensionResultIsMatch(data)) {
|
||||
return <ITextSearchMatch>{
|
||||
preview: {
|
||||
@@ -279,8 +271,8 @@ function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSe
|
||||
}
|
||||
}
|
||||
|
||||
export function extensionResultIsMatch(data: vscode.TextSearchResult): data is vscode.TextSearchMatch {
|
||||
return !!(<vscode.TextSearchMatch>data).preview;
|
||||
export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch {
|
||||
return !!(<TextSearchMatch>data).preview;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
import * as assert from 'assert';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range } from 'vs/workbench/services/search/node/ripgrepSearchUtils';
|
||||
import { fixRegexCRMatchingNonWordClass, fixRegexCRMatchingWhitespaceClass, fixRegexEndingPattern, fixRegexNewline, IRgMatch, IRgMessage, RipgrepParser, unicodeEscapesToPCRE2, fixNewline } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
|
||||
import { TextSearchResult } from 'vscode';
|
||||
import { Range, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
|
||||
suite('RipgrepTextSearchEngine', () => {
|
||||
test('unicodeEscapesToPCRE2', async () => {
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Progress } from 'vs/platform/progress/common/progress';
|
||||
import { ITextQuery, QueryType } from 'vs/workbench/services/search/common/search';
|
||||
import { ProviderResult, TextSearchComplete, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
suite('TextSearchManager', () => {
|
||||
test('fixes encoding', async () => {
|
||||
let correctEncoding = false;
|
||||
const provider: vscode.TextSearchProvider = {
|
||||
provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.TextSearchComplete> {
|
||||
const provider: TextSearchProvider = {
|
||||
provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress<TextSearchResult>, token: CancellationToken): ProviderResult<TextSearchComplete> {
|
||||
correctEncoding = options.encoding === 'windows-1252';
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user