Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
徽霖
Vscode
提交
c26d6e6c
V
Vscode
项目概览
徽霖
/
Vscode
通知
9
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
V
Vscode
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
c26d6e6c
编写于
10月 15, 2018
作者:
R
Rob Lourens
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Refactor text seach
To allow better code sharing between EH and search proc
上级
ee40fa3e
变更
15
隐藏空白更改
内联
并排
Showing
15 changed file
with
734 addition
and
1133 deletion
+734
-1133
src/vs/vscode.proposed.d.ts
src/vs/vscode.proposed.d.ts
+5
-0
src/vs/workbench/api/node/extHostSearch.fileIndex.ts
src/vs/workbench/api/node/extHostSearch.fileIndex.ts
+1
-99
src/vs/workbench/api/node/extHostSearch.ts
src/vs/workbench/api/node/extHostSearch.ts
+30
-296
src/vs/workbench/services/search/node/fileSearch.ts
src/vs/workbench/services/search/node/fileSearch.ts
+31
-1
src/vs/workbench/services/search/node/rawSearchService.ts
src/vs/workbench/services/search/node/rawSearchService.ts
+4
-99
src/vs/workbench/services/search/node/ripgrepFileSearch.ts
src/vs/workbench/services/search/node/ripgrepFileSearch.ts
+105
-10
src/vs/workbench/services/search/node/ripgrepSearchUtils.ts
src/vs/workbench/services/search/node/ripgrepSearchUtils.ts
+5
-9
src/vs/workbench/services/search/node/ripgrepTextSearch.ts
src/vs/workbench/services/search/node/ripgrepTextSearch.ts
+0
-567
src/vs/workbench/services/search/node/ripgrepTextSearchEH.ts
src/vs/workbench/services/search/node/ripgrepTextSearchEH.ts
+9
-5
src/vs/workbench/services/search/node/search.ts
src/vs/workbench/services/search/node/search.ts
+105
-6
src/vs/workbench/services/search/node/textSearchAdapter.ts
src/vs/workbench/services/search/node/textSearchAdapter.ts
+78
-0
src/vs/workbench/services/search/node/textSearchEngine.ts
src/vs/workbench/services/search/node/textSearchEngine.ts
+294
-0
src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts
...bench/services/search/test/node/ripgrepFileSearch.test.ts
+48
-0
src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts
...bench/services/search/test/node/ripgrepTextSearch.test.ts
+12
-35
src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts
...h/services/search/test/node/textSearch.integrationTest.ts
+7
-6
未找到文件。
src/vs/vscode.proposed.d.ts
浏览文件 @
c26d6e6c
...
...
@@ -46,6 +46,11 @@ declare module 'vscode' {
*/
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.
*/
...
...
src/vs/workbench/api/node/extHostSearch.fileIndex.ts
浏览文件 @
c26d6e6c
...
...
@@ -18,6 +18,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import
{
compareItemsByScore
,
IItemAccessor
,
prepareQuery
,
ScorerCache
}
from
'
vs/base/parts/quickopen/common/quickOpenScorer
'
;
import
{
ICachedSearchStats
,
IFileIndexProviderStats
,
IFileMatch
,
IFileSearchStats
,
IFolderQuery
,
IRawSearchQuery
,
ISearchCompleteStats
,
ISearchQuery
}
from
'
vs/platform/search/common/search
'
;
import
*
as
vscode
from
'
vscode
'
;
import
{
resolvePatternsForProvider
,
QueryGlobTester
}
from
'
vs/workbench/services/search/node/search
'
;
export
interface
IInternalFileMatch
{
base
:
URI
;
...
...
@@ -27,105 +28,6 @@ export interface IInternalFileMatch {
size
?:
number
;
}
/**
* Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns
*/
export
function
resolvePatternsForProvider
(
globalPattern
:
glob
.
IExpression
,
folderPattern
:
glob
.
IExpression
):
string
[]
{
const
merged
=
{
...(
globalPattern
||
{}),
...(
folderPattern
||
{})
};
return
Object
.
keys
(
merged
)
.
filter
(
key
=>
{
const
value
=
merged
[
key
];
return
typeof
value
===
'
boolean
'
&&
value
;
});
}
export
class
QueryGlobTester
{
private
_excludeExpression
:
glob
.
IExpression
;
private
_parsedExcludeExpression
:
glob
.
ParsedExpression
;
private
_parsedIncludeExpression
:
glob
.
ParsedExpression
;
constructor
(
config
:
ISearchQuery
,
folderQuery
:
IFolderQuery
)
{
this
.
_excludeExpression
=
{
...(
config
.
excludePattern
||
{}),
...(
folderQuery
.
excludePattern
||
{})
};
this
.
_parsedExcludeExpression
=
glob
.
parse
(
this
.
_excludeExpression
);
// Empty includeExpression means include nothing, so no {} shortcuts
let
includeExpression
:
glob
.
IExpression
=
config
.
includePattern
;
if
(
folderQuery
.
includePattern
)
{
if
(
includeExpression
)
{
includeExpression
=
{
...
includeExpression
,
...
folderQuery
.
includePattern
};
}
else
{
includeExpression
=
folderQuery
.
includePattern
;
}
}
if
(
includeExpression
)
{
this
.
_parsedIncludeExpression
=
glob
.
parse
(
includeExpression
);
}
}
/**
* Guaranteed sync - siblingsFn should not return a promise.
*/
public
includedInQuerySync
(
testPath
:
string
,
basename
?:
string
,
hasSibling
?:
(
name
:
string
)
=>
boolean
):
boolean
{
if
(
this
.
_parsedExcludeExpression
&&
this
.
_parsedExcludeExpression
(
testPath
,
basename
,
hasSibling
))
{
return
false
;
}
if
(
this
.
_parsedIncludeExpression
&&
!
this
.
_parsedIncludeExpression
(
testPath
,
basename
,
hasSibling
))
{
return
false
;
}
return
true
;
}
/**
* Guaranteed async.
*/
public
includedInQuery
(
testPath
:
string
,
basename
?:
string
,
hasSibling
?:
(
name
:
string
)
=>
boolean
|
TPromise
<
boolean
>
):
TPromise
<
boolean
>
{
const
excludeP
=
this
.
_parsedExcludeExpression
?
TPromise
.
as
(
this
.
_parsedExcludeExpression
(
testPath
,
basename
,
hasSibling
)).
then
(
result
=>
!!
result
)
:
TPromise
.
wrap
(
false
);
return
excludeP
.
then
(
excluded
=>
{
if
(
excluded
)
{
return
false
;
}
return
this
.
_parsedIncludeExpression
?
TPromise
.
as
(
this
.
_parsedIncludeExpression
(
testPath
,
basename
,
hasSibling
)).
then
(
result
=>
!!
result
)
:
TPromise
.
wrap
(
true
);
}).
then
(
included
=>
{
return
included
;
});
}
public
hasSiblingExcludeClauses
():
boolean
{
return
hasSiblingClauses
(
this
.
_excludeExpression
);
}
}
function
hasSiblingClauses
(
pattern
:
glob
.
IExpression
):
boolean
{
for
(
let
key
in
pattern
)
{
if
(
typeof
pattern
[
key
]
!==
'
boolean
'
)
{
return
true
;
}
}
return
false
;
}
export
interface
IDirectoryEntry
{
base
:
URI
;
relativePath
:
string
;
...
...
src/vs/workbench/api/node/extHostSearch.ts
浏览文件 @
c26d6e6c
...
...
@@ -7,20 +7,22 @@ import * as path from 'path';
import
{
CancellationToken
,
CancellationTokenSource
}
from
'
vs/base/common/cancellation
'
;
import
{
toErrorMessage
}
from
'
vs/base/common/errorMessage
'
;
import
*
as
glob
from
'
vs/base/common/glob
'
;
import
{
toDisposable
}
from
'
vs/base/common/lifecycle
'
;
import
{
IDisposable
,
toDisposable
}
from
'
vs/base/common/lifecycle
'
;
import
*
as
resources
from
'
vs/base/common/resources
'
;
import
{
StopWatch
}
from
'
vs/base/common/stopwatch
'
;
import
{
URI
,
UriComponents
}
from
'
vs/base/common/uri
'
;
import
{
TPromise
}
from
'
vs/base/common/winjs.base
'
;
import
*
as
extfs
from
'
vs/base/node/extfs
'
;
import
{
ILogService
}
from
'
vs/platform/log/common/log
'
;
import
{
IFileMatch
,
IFileSearchProviderStats
,
IFolderQuery
,
IPatternInfo
,
IRawSearchQuery
,
ISearchCompleteStats
,
ISearchQuery
,
ITextSearchResult
}
from
'
vs/platform/search/common/search
'
;
import
{
IFileMatch
,
IFileSearchProviderStats
,
IFolderQuery
,
IPatternInfo
,
IRawSearchQuery
,
ISearchCompleteStats
,
ISearchQuery
}
from
'
vs/platform/search/common/search
'
;
import
{
ExtHostConfiguration
}
from
'
vs/workbench/api/node/extHostConfiguration
'
;
import
{
FileIndexSearchManager
,
IDirectoryEntry
,
IDirectoryTree
,
IInternalFileMatch
,
QueryGlobTester
,
resolvePatternsForProvider
}
from
'
vs/workbench/api/node/extHostSearch.fileIndex
'
;
import
{
FileIndexSearchManager
,
IDirectoryEntry
,
IDirectoryTree
,
IInternalFileMatch
}
from
'
vs/workbench/api/node/extHostSearch.fileIndex
'
;
import
{
RipgrepSearchProvider
}
from
'
vs/workbench/services/search/node/ripgrepSearchEH
'
;
import
{
OutputChannel
}
from
'
vs/workbench/services/search/node/ripgrepSearchUtils
'
;
import
{
QueryGlobTester
,
resolvePatternsForProvider
}
from
'
vs/workbench/services/search/node/search
'
;
import
{
TextSearchEngine
}
from
'
vs/workbench/services/search/node/textSearchEngine
'
;
import
*
as
vscode
from
'
vscode
'
;
import
{
ExtHostSearchShape
,
IMainContext
,
MainContext
,
MainThreadSearchShape
}
from
'
./extHost.protocol
'
;
import
{
RipgrepSearchProvider
}
from
'
vs/workbench/services/search/node/ripgrepSearchEH
'
;
export
interface
ISchemeTransformer
{
transformOutgoing
(
scheme
:
string
):
string
;
...
...
@@ -29,8 +31,9 @@ export interface ISchemeTransformer {
export
class
ExtHostSearch
implements
ExtHostSearchShape
{
private
readonly
_proxy
:
MainThreadSearchShape
;
private
readonly
_fileSearchProvider
=
new
Map
<
number
,
vscode
.
FileSearchProvider
>
();
private
readonly
_textSearchProvider
=
new
Map
<
number
,
vscode
.
TextSearchProvider
>
();
private
readonly
_fileSearchProvider
=
new
Map
<
number
,
vscode
.
FileSearchProvider
>
();
private
_internalFileSearchProvider
;
private
readonly
_fileIndexProvider
=
new
Map
<
number
,
vscode
.
FileIndexProvider
>
();
private
_handlePool
:
number
=
0
;
...
...
@@ -52,7 +55,17 @@ export class ExtHostSearch implements ExtHostSearchShape {
return
scheme
;
}
registerFileSearchProvider
(
scheme
:
string
,
provider
:
vscode
.
FileSearchProvider
)
{
registerTextSearchProvider
(
scheme
:
string
,
provider
:
vscode
.
TextSearchProvider
):
IDisposable
{
const
handle
=
this
.
_handlePool
++
;
this
.
_textSearchProvider
.
set
(
handle
,
provider
);
this
.
_proxy
.
$registerTextSearchProvider
(
handle
,
this
.
_transformScheme
(
scheme
));
return
toDisposable
(()
=>
{
this
.
_textSearchProvider
.
delete
(
handle
);
this
.
_proxy
.
$unregisterProvider
(
handle
);
});
}
registerFileSearchProvider
(
scheme
:
string
,
provider
:
vscode
.
FileSearchProvider
):
IDisposable
{
const
handle
=
this
.
_handlePool
++
;
this
.
_fileSearchProvider
.
set
(
handle
,
provider
);
this
.
_proxy
.
$registerFileSearchProvider
(
handle
,
this
.
_transformScheme
(
scheme
));
...
...
@@ -62,17 +75,17 @@ export class ExtHostSearch implements ExtHostSearchShape {
});
}
register
TextSearchProvider
(
scheme
:
string
,
provider
:
vscode
.
TextSearchProvider
)
{
register
InternalFileSearchProvider
(
scheme
:
string
,
provider
):
IDisposable
{
const
handle
=
this
.
_handlePool
++
;
this
.
_
textSearchProvider
.
set
(
handle
,
provider
)
;
this
.
_proxy
.
$register
Text
SearchProvider
(
handle
,
this
.
_transformScheme
(
scheme
));
this
.
_
internalFileSearchProvider
=
provider
;
this
.
_proxy
.
$register
File
SearchProvider
(
handle
,
this
.
_transformScheme
(
scheme
));
return
toDisposable
(()
=>
{
this
.
_
textSearchProvider
.
delete
(
handle
)
;
this
.
_
internalFileSearchProvider
=
null
;
this
.
_proxy
.
$unregisterProvider
(
handle
);
});
}
registerFileIndexProvider
(
scheme
:
string
,
provider
:
vscode
.
FileIndexProvider
)
{
registerFileIndexProvider
(
scheme
:
string
,
provider
:
vscode
.
FileIndexProvider
)
:
IDisposable
{
const
handle
=
this
.
_handlePool
++
;
this
.
_fileIndexProvider
.
set
(
handle
,
provider
);
this
.
_proxy
.
$registerFileIndexProvider
(
handle
,
this
.
_transformScheme
(
scheme
));
...
...
@@ -98,6 +111,10 @@ export class ExtHostSearch implements ExtHostSearchShape {
}
$clearCache
(
cacheKey
:
string
):
Thenable
<
void
>
{
if
(
this
.
_internalFileSearchProvider
)
{
this
.
_internalFileSearchProvider
.
clearCache
();
}
// Actually called once per provider.
// Only relevant to file index search.
return
this
.
_fileIndexSearchManager
.
clearCache
(
cacheKey
);
...
...
@@ -117,11 +134,9 @@ export class ExtHostSearch implements ExtHostSearchShape {
function
registerEHProviders
(
extHostSearch
:
ExtHostSearch
,
logService
:
ILogService
,
configService
:
ExtHostConfiguration
)
{
if
(
configService
.
getConfiguration
(
'
searchRipgrep
'
).
enable
)
{
console
.
log
(
`enabled`
);
const
outputChannel
=
new
OutputChannel
(
logService
);
extHostSearch
.
registerTextSearchProvider
(
'
file
'
,
new
RipgrepSearchProvider
(
outputChannel
));
}
const
outputChannel
=
new
OutputChannel
(
logService
);
extHostSearch
.
registerTextSearchProvider
(
'
file
'
,
new
RipgrepSearchProvider
(
outputChannel
));
}
function
reviveQuery
(
rawQuery
:
IRawSearchQuery
):
ISearchQuery
{
...
...
@@ -141,287 +156,6 @@ function reviveFolderQuery(rawFolderQuery: IFolderQuery<UriComponents>): IFolder
};
}
class
TextSearchResultsCollector
{
private
_batchedCollector
:
BatchedCollector
<
IFileMatch
>
;
private
_currentFolderIdx
:
number
;
private
_currentUri
:
URI
;
private
_currentFileMatch
:
IFileMatch
;
constructor
(
private
_onResult
:
(
result
:
IFileMatch
[])
=>
void
)
{
this
.
_batchedCollector
=
new
BatchedCollector
<
IFileMatch
>
(
512
,
items
=>
this
.
sendItems
(
items
));
}
add
(
data
:
vscode
.
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.
if
(
this
.
_currentFileMatch
&&
(
this
.
_currentFolderIdx
!==
folderIdx
||
!
resources
.
isEqual
(
this
.
_currentUri
,
data
.
uri
)))
{
this
.
pushToCollector
();
this
.
_currentFileMatch
=
null
;
}
if
(
!
this
.
_currentFileMatch
)
{
this
.
_currentFolderIdx
=
folderIdx
;
this
.
_currentFileMatch
=
{
resource
:
data
.
uri
,
matches
:
[]
};
}
this
.
_currentFileMatch
.
matches
.
push
(
extensionResultToFrontendResult
(
data
));
}
private
pushToCollector
():
void
{
const
size
=
this
.
_currentFileMatch
?
this
.
_currentFileMatch
.
matches
.
length
:
0
;
this
.
_batchedCollector
.
addItem
(
this
.
_currentFileMatch
,
size
);
}
flush
():
void
{
this
.
pushToCollector
();
this
.
_batchedCollector
.
flush
();
}
private
sendItems
(
items
:
IFileMatch
[]):
void
{
this
.
_onResult
(
items
);
}
}
function
extensionResultToFrontendResult
(
data
:
vscode
.
TextSearchResult
):
ITextSearchResult
{
// Warning: result from RipgrepTextSearchEH has fake vscode.Range. Don't depend on any other props beyond these...
return
{
preview
:
{
match
:
{
startLineNumber
:
data
.
preview
.
match
.
start
.
line
,
startColumn
:
data
.
preview
.
match
.
start
.
character
,
endLineNumber
:
data
.
preview
.
match
.
end
.
line
,
endColumn
:
data
.
preview
.
match
.
end
.
character
},
text
:
data
.
preview
.
text
},
range
:
{
startLineNumber
:
data
.
range
.
start
.
line
,
startColumn
:
data
.
range
.
start
.
character
,
endLineNumber
:
data
.
range
.
end
.
line
,
endColumn
:
data
.
range
.
end
.
character
}
};
}
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
* But after that point, the callback is called with batches of maxBatchSize.
* If the batch isn't filled within some time, the callback is also called.
*/
class
BatchedCollector
<
T
>
{
private
static
readonly
TIMEOUT
=
4000
;
// After START_BATCH_AFTER_COUNT items have been collected, stop flushing on timeout
private
static
readonly
START_BATCH_AFTER_COUNT
=
50
;
private
totalNumberCompleted
=
0
;
private
batch
:
T
[]
=
[];
private
batchSize
=
0
;
private
timeoutHandle
:
any
;
constructor
(
private
maxBatchSize
:
number
,
private
cb
:
(
items
:
T
[])
=>
void
)
{
}
addItem
(
item
:
T
,
size
:
number
):
void
{
if
(
!
item
)
{
return
;
}
this
.
addItemToBatch
(
item
,
size
);
}
addItems
(
items
:
T
[],
size
:
number
):
void
{
if
(
!
items
)
{
return
;
}
if
(
this
.
maxBatchSize
>
0
)
{
this
.
addItemsToBatch
(
items
,
size
);
}
else
{
this
.
cb
(
items
);
}
}
private
addItemToBatch
(
item
:
T
,
size
:
number
):
void
{
this
.
batch
.
push
(
item
);
this
.
batchSize
+=
size
;
this
.
onUpdate
();
}
private
addItemsToBatch
(
item
:
T
[],
size
:
number
):
void
{
this
.
batch
=
this
.
batch
.
concat
(
item
);
this
.
batchSize
+=
size
;
this
.
onUpdate
();
}
private
onUpdate
():
void
{
if
(
this
.
totalNumberCompleted
<
BatchedCollector
.
START_BATCH_AFTER_COUNT
)
{
// Flush because we aren't batching yet
this
.
flush
();
}
else
if
(
this
.
batchSize
>=
this
.
maxBatchSize
)
{
// Flush because the batch is full
this
.
flush
();
}
else
if
(
!
this
.
timeoutHandle
)
{
// No timeout running, start a timeout to flush
this
.
timeoutHandle
=
setTimeout
(()
=>
{
this
.
flush
();
},
BatchedCollector
.
TIMEOUT
);
}
}
flush
():
void
{
if
(
this
.
batchSize
)
{
this
.
totalNumberCompleted
+=
this
.
batchSize
;
this
.
cb
(
this
.
batch
);
this
.
batch
=
[];
this
.
batchSize
=
0
;
if
(
this
.
timeoutHandle
)
{
clearTimeout
(
this
.
timeoutHandle
);
this
.
timeoutHandle
=
0
;
}
}
}
}
class
TextSearchEngine
{
private
collector
:
TextSearchResultsCollector
;
private
isLimitHit
:
boolean
;
private
resultCount
=
0
;
constructor
(
private
pattern
:
IPatternInfo
,
private
config
:
ISearchQuery
,
private
provider
:
vscode
.
TextSearchProvider
,
private
_extfs
:
typeof
extfs
)
{
}
public
search
(
onProgress
:
(
matches
:
IFileMatch
[])
=>
void
,
token
:
CancellationToken
):
TPromise
<
ISearchCompleteStats
>
{
const
folderQueries
=
this
.
config
.
folderQueries
;
const
tokenSource
=
new
CancellationTokenSource
();
token
.
onCancellationRequested
(()
=>
tokenSource
.
cancel
());
return
new
TPromise
<
ISearchCompleteStats
>
((
resolve
,
reject
)
=>
{
this
.
collector
=
new
TextSearchResultsCollector
(
onProgress
);
let
isCanceled
=
false
;
const
onResult
=
(
match
:
vscode
.
TextSearchResult
,
folderIdx
:
number
)
=>
{
if
(
isCanceled
)
{
return
;
}
if
(
this
.
resultCount
>=
this
.
config
.
maxResults
)
{
this
.
isLimitHit
=
true
;
isCanceled
=
true
;
tokenSource
.
cancel
();
}
if
(
!
this
.
isLimitHit
)
{
this
.
resultCount
++
;
this
.
collector
.
add
(
match
,
folderIdx
);
}
};
// For each root folder
TPromise
.
join
(
folderQueries
.
map
((
fq
,
i
)
=>
{
return
this
.
searchInFolder
(
fq
,
r
=>
onResult
(
r
,
i
),
tokenSource
.
token
);
})).
then
(
results
=>
{
tokenSource
.
dispose
();
this
.
collector
.
flush
();
const
someFolderHitLImit
=
results
.
some
(
result
=>
result
&&
result
.
limitHit
);
resolve
({
limitHit
:
this
.
isLimitHit
||
someFolderHitLImit
,
stats
:
{
type
:
'
textSearchProvider
'
}
});
},
(
errs
:
Error
[])
=>
{
tokenSource
.
dispose
();
const
errMsg
=
errs
.
map
(
err
=>
toErrorMessage
(
err
))
.
filter
(
msg
=>
!!
msg
)[
0
];
reject
(
new
Error
(
errMsg
));
});
});
}
private
searchInFolder
(
folderQuery
:
IFolderQuery
<
URI
>
,
onResult
:
(
result
:
vscode
.
TextSearchResult
)
=>
void
,
token
:
CancellationToken
):
TPromise
<
vscode
.
TextSearchComplete
>
{
const
queryTester
=
new
QueryGlobTester
(
this
.
config
,
folderQuery
);
const
testingPs
=
[];
const
progress
=
{
report
:
(
result
:
vscode
.
TextSearchResult
)
=>
{
const
hasSibling
=
folderQuery
.
folder
.
scheme
===
'
file
'
&&
glob
.
hasSiblingPromiseFn
(()
=>
{
return
this
.
readdir
(
path
.
dirname
(
result
.
uri
.
fsPath
));
});
const
relativePath
=
path
.
relative
(
folderQuery
.
folder
.
fsPath
,
result
.
uri
.
fsPath
);
testingPs
.
push
(
queryTester
.
includedInQuery
(
relativePath
,
path
.
basename
(
relativePath
),
hasSibling
)
.
then
(
included
=>
{
if
(
included
)
{
onResult
(
result
);
}
}));
}
};
const
searchOptions
=
this
.
getSearchOptionsForFolder
(
folderQuery
);
return
new
TPromise
(
resolve
=>
process
.
nextTick
(
resolve
))
.
then
(()
=>
this
.
provider
.
provideTextSearchResults
(
patternInfoToQuery
(
this
.
pattern
),
searchOptions
,
progress
,
token
))
.
then
(
result
=>
{
return
TPromise
.
join
(
testingPs
)
.
then
(()
=>
result
);
});
}
private
readdir
(
dirname
:
string
):
TPromise
<
string
[]
>
{
return
new
TPromise
((
resolve
,
reject
)
=>
{
this
.
_extfs
.
readdir
(
dirname
,
(
err
,
files
)
=>
{
if
(
err
)
{
return
reject
(
err
);
}
resolve
(
files
);
});
});
}
private
getSearchOptionsForFolder
(
fq
:
IFolderQuery
<
URI
>
):
vscode
.
TextSearchOptions
{
const
includes
=
resolvePatternsForProvider
(
this
.
config
.
includePattern
,
fq
.
includePattern
);
const
excludes
=
resolvePatternsForProvider
(
this
.
config
.
excludePattern
,
fq
.
excludePattern
);
return
{
folder
:
URI
.
from
(
fq
.
folder
),
excludes
,
includes
,
useIgnoreFiles
:
!
this
.
config
.
disregardIgnoreFiles
,
useGlobalIgnoreFiles
:
!
this
.
config
.
disregardGlobalIgnoreFiles
,
followSymlinks
:
!
this
.
config
.
ignoreSymlinks
,
encoding
:
this
.
config
.
fileEncoding
,
maxFileSize
:
this
.
config
.
maxFileSize
,
maxResults
:
this
.
config
.
maxResults
,
previewOptions
:
this
.
config
.
previewOptions
};
}
}
function
patternInfoToQuery
(
patternInfo
:
IPatternInfo
):
vscode
.
TextSearchQuery
{
return
<
vscode
.
TextSearchQuery
>
{
isCaseSensitive
:
patternInfo
.
isCaseSensitive
||
false
,
isRegExp
:
patternInfo
.
isRegExp
||
false
,
isWordMatch
:
patternInfo
.
isWordMatch
||
false
,
pattern
:
patternInfo
.
pattern
};
}
class
FileSearchEngine
{
private
filePattern
:
string
;
private
includePattern
:
glob
.
ParsedExpression
;
...
...
src/vs/workbench/services/search/node/fileSearch.ts
浏览文件 @
c26d6e6c
...
...
@@ -22,7 +22,6 @@ import * as extfs from 'vs/base/node/extfs';
import
*
as
flow
from
'
vs/base/node/flow
'
;
import
{
IProgress
,
ISearchEngineStats
}
from
'
vs/platform/search/common/search
'
;
import
{
spawnRipgrepCmd
}
from
'
./ripgrepFileSearch
'
;
import
{
rgErrorMsgForDisplay
}
from
'
./ripgrepTextSearch
'
;
import
{
IFolderSearch
,
IRawFileMatch
,
IRawSearch
,
ISearchEngine
,
ISearchEngineSuccess
}
from
'
./search
'
;
import
{
StopWatch
}
from
'
vs/base/common/stopwatch
'
;
...
...
@@ -754,3 +753,34 @@ class AbsoluteAndRelativeParsedExpression {
return
pathTerms
;
}
}
export
function
rgErrorMsgForDisplay
(
msg
:
string
):
string
|
undefined
{
const
lines
=
msg
.
trim
().
split
(
'
\n
'
);
const
firstLine
=
lines
[
0
].
trim
();
if
(
strings
.
startsWith
(
firstLine
,
'
Error parsing regex
'
))
{
return
firstLine
;
}
if
(
strings
.
startsWith
(
firstLine
,
'
regex parse error
'
))
{
return
strings
.
uppercaseFirstLetter
(
lines
[
lines
.
length
-
1
].
trim
());
}
if
(
strings
.
startsWith
(
firstLine
,
'
error parsing glob
'
)
||
strings
.
startsWith
(
firstLine
,
'
unsupported encoding
'
))
{
// Uppercase first letter
return
firstLine
.
charAt
(
0
).
toUpperCase
()
+
firstLine
.
substr
(
1
);
}
if
(
firstLine
===
`Literal '\\n' not allowed.`
)
{
// I won't localize this because none of the Ripgrep error messages are localized
return
`Literal '\\n' currently not supported`
;
}
if
(
strings
.
startsWith
(
firstLine
,
'
Literal
'
))
{
// Other unsupported chars
return
firstLine
;
}
return
undefined
;
}
src/vs/workbench/services/search/node/rawSearchService.ts
浏览文件 @
c26d6e6c
...
...
@@ -19,10 +19,11 @@ import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'v
import
{
MAX_FILE_SIZE
}
from
'
vs/platform/files/node/files
'
;
import
{
ICachedSearchStats
,
IFileSearchStats
,
IProgress
}
from
'
vs/platform/search/common/search
'
;
import
{
Engine
as
FileSearchEngine
,
FileWalker
}
from
'
vs/workbench/services/search/node/fileSearch
'
;
import
{
RipgrepEngine
}
from
'
vs/workbench/services/search/node/ripgrepTextSearch
'
;
import
{
TextSearchEngineAdapter
}
from
'
vs/workbench/services/search/node/textSearchAdapter
'
;
import
{
Engine
as
TextSearchEngine
}
from
'
vs/workbench/services/search/node/textSearch
'
;
import
{
TextSearchWorkerProvider
}
from
'
vs/workbench/services/search/node/textSearchWorkerProvider
'
;
import
{
IFileSearchProgressItem
,
IRawFileMatch
,
IRawSearch
,
IRawSearchService
,
ISearchEngine
,
ISearchEngineSuccess
,
ISerializedFileMatch
,
ISerializedSearchComplete
,
ISerializedSearchProgressItem
,
ISerializedSearchSuccess
}
from
'
./search
'
;
import
{
BatchedCollector
}
from
'
vs/workbench/services/search/node/textSearchEngine
'
;
gracefulFs
.
gracefulify
(
fs
);
...
...
@@ -81,20 +82,10 @@ export class SearchService implements IRawSearchService {
private
ripgrepTextSearch
(
config
:
IRawSearch
,
progressCallback
:
IProgressCallback
,
token
:
CancellationToken
):
Promise
<
ISerializedSearchSuccess
>
{
config
.
maxFilesize
=
MAX_FILE_SIZE
;
let
engine
=
new
RipgrepEngine
(
config
);
token
.
onCancellationRequested
(()
=>
engine
.
cancel
());
const
engine
=
new
TextSearchEngineAdapter
(
config
);
return
new
Promise
<
ISerializedSearchSuccess
>
((
c
,
e
)
=>
{
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
const
collector
=
new
BatchedCollector
<
ISerializedFileMatch
>
(
SearchService
.
BATCH_SIZE
,
progressCallback
);
engine
.
search
((
match
)
=>
{
collector
.
addItem
(
match
,
match
.
numMatches
);
},
(
message
)
=>
{
progressCallback
(
message
);
},
(
error
,
stats
)
=>
{
collector
.
flush
();
engine
.
search
(
token
,
progressCallback
,
progressCallback
,
(
error
,
stats
)
=>
{
if
(
error
)
{
e
(
error
);
}
else
{
...
...
@@ -486,89 +477,3 @@ const FileMatchItemAccessor = new class implements IItemAccessor<IRawFileMatch>
return
match
.
relativePath
;
// e.g. some/path/to/file/myFile.txt
}
};
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
* But after that point, the callback is called with batches of maxBatchSize.
* If the batch isn't filled within some time, the callback is also called.
*/
class
BatchedCollector
<
T
>
{
private
static
readonly
TIMEOUT
=
4000
;
// After RUN_TIMEOUT_UNTIL_COUNT items have been collected, stop flushing on timeout
private
static
readonly
START_BATCH_AFTER_COUNT
=
50
;
private
totalNumberCompleted
=
0
;
private
batch
:
T
[]
=
[];
private
batchSize
=
0
;
private
timeoutHandle
:
any
;
constructor
(
private
maxBatchSize
:
number
,
private
cb
:
(
items
:
T
|
T
[])
=>
void
)
{
}
addItem
(
item
:
T
,
size
:
number
):
void
{
if
(
!
item
)
{
return
;
}
if
(
this
.
maxBatchSize
>
0
)
{
this
.
addItemToBatch
(
item
,
size
);
}
else
{
this
.
cb
(
item
);
}
}
addItems
(
items
:
T
[],
size
:
number
):
void
{
if
(
!
items
)
{
return
;
}
if
(
this
.
maxBatchSize
>
0
)
{
this
.
addItemsToBatch
(
items
,
size
);
}
else
{
this
.
cb
(
items
);
}
}
private
addItemToBatch
(
item
:
T
,
size
:
number
):
void
{
this
.
batch
.
push
(
item
);
this
.
batchSize
+=
size
;
this
.
onUpdate
();
}
private
addItemsToBatch
(
item
:
T
[],
size
:
number
):
void
{
this
.
batch
=
this
.
batch
.
concat
(
item
);
this
.
batchSize
+=
size
;
this
.
onUpdate
();
}
private
onUpdate
():
void
{
if
(
this
.
totalNumberCompleted
<
BatchedCollector
.
START_BATCH_AFTER_COUNT
)
{
// Flush because we aren't batching yet
this
.
flush
();
}
else
if
(
this
.
batchSize
>=
this
.
maxBatchSize
)
{
// Flush because the batch is full
this
.
flush
();
}
else
if
(
!
this
.
timeoutHandle
)
{
// No timeout running, start a timeout to flush
this
.
timeoutHandle
=
setTimeout
(()
=>
{
this
.
flush
();
},
BatchedCollector
.
TIMEOUT
);
}
}
flush
():
void
{
if
(
this
.
batchSize
)
{
this
.
totalNumberCompleted
+=
this
.
batchSize
;
this
.
cb
(
this
.
batch
);
this
.
batch
=
[];
this
.
batchSize
=
0
;
if
(
this
.
timeoutHandle
)
{
clearTimeout
(
this
.
timeoutHandle
);
this
.
timeoutHandle
=
0
;
}
}
}
}
src/vs/workbench/services/search/node/ripgrepFileSearch.ts
浏览文件 @
c26d6e6c
...
...
@@ -4,15 +4,16 @@
*--------------------------------------------------------------------------------------------*/
import
*
as
cp
from
'
child_process
'
;
import
{
rgPath
}
from
'
vscode-ripgrep
'
;
import
{
isMacintosh
as
isMac
}
from
'
vs/base/common/platform
'
;
import
*
as
path
from
'
path
'
;
import
*
as
glob
from
'
vs/base/common/glob
'
;
import
{
startsWith
}
from
'
vs/base/common/strings
'
;
import
{
normalizeNFD
}
from
'
vs/base/common/normalization
'
;
import
*
as
objects
from
'
vs/base/common/objects
'
;
import
*
as
paths
from
'
vs/base/common/paths
'
;
import
{
isMacintosh
as
isMac
}
from
'
vs/base/common/platform
'
;
import
*
as
strings
from
'
vs/base/common/strings
'
;
import
{
rgPath
}
from
'
vscode-ripgrep
'
;
import
{
IFolderSearch
,
IRawSearch
}
from
'
./search
'
;
import
{
foldersToIncludeGlobs
,
foldersToRgExcludeGlobs
}
from
'
./ripgrepTextSearch
'
;
import
{
anchorGlob
}
from
'
vs/workbench/services/search/node/ripgrepSearchUtils
'
;
// If vscode-ripgrep is in an .asar file, then the binary is unpacked.
const
rgDiskPath
=
rgPath
.
replace
(
/
\b
node_modules
\.
asar
\b
/
,
'
node_modules.asar.unpacked
'
);
...
...
@@ -33,7 +34,7 @@ function getRgArgs(config: IRawSearch, folderQuery: IFolderSearch, includePatter
// includePattern can't have siblingClauses
foldersToIncludeGlobs
([
folderQuery
],
includePattern
,
false
).
forEach
(
globArg
=>
{
const
inclusion
=
anchor
(
globArg
);
const
inclusion
=
anchor
Glob
(
globArg
);
args
.
push
(
'
-g
'
,
inclusion
);
if
(
isMac
)
{
const
normalized
=
normalizeNFD
(
inclusion
);
...
...
@@ -47,7 +48,7 @@ function getRgArgs(config: IRawSearch, folderQuery: IFolderSearch, includePatter
const
rgGlobs
=
foldersToRgExcludeGlobs
([
folderQuery
],
excludePattern
,
undefined
,
false
);
rgGlobs
.
globArgs
.
forEach
(
globArg
=>
{
const
exclusion
=
`!
${
anchor
(
globArg
)}
`
;
const
exclusion
=
`!
${
anchor
Glob
(
globArg
)}
`
;
args
.
push
(
'
-g
'
,
exclusion
);
if
(
isMac
)
{
const
normalized
=
normalizeNFD
(
exclusion
);
...
...
@@ -82,6 +83,100 @@ function getRgArgs(config: IRawSearch, folderQuery: IFolderSearch, includePatter
return
{
args
,
siblingClauses
};
}
function
anchor
(
glob
:
string
)
{
return
startsWith
(
glob
,
'
**
'
)
||
startsWith
(
glob
,
'
/
'
)
?
glob
:
`/
${
glob
}
`
;
export
interface
IRgGlobResult
{
globArgs
:
string
[];
siblingClauses
:
glob
.
IExpression
;
}
export
function
foldersToRgExcludeGlobs
(
folderQueries
:
IFolderSearch
[],
globalExclude
:
glob
.
IExpression
,
excludesToSkip
?:
Set
<
string
>
,
absoluteGlobs
=
true
):
IRgGlobResult
{
const
globArgs
:
string
[]
=
[];
let
siblingClauses
:
glob
.
IExpression
=
{};
folderQueries
.
forEach
(
folderQuery
=>
{
const
totalExcludePattern
=
objects
.
assign
({},
folderQuery
.
excludePattern
||
{},
globalExclude
||
{});
const
result
=
globExprsToRgGlobs
(
totalExcludePattern
,
absoluteGlobs
&&
folderQuery
.
folder
,
excludesToSkip
);
globArgs
.
push
(...
result
.
globArgs
);
if
(
result
.
siblingClauses
)
{
siblingClauses
=
objects
.
assign
(
siblingClauses
,
result
.
siblingClauses
);
}
});
return
{
globArgs
,
siblingClauses
};
}
export
function
foldersToIncludeGlobs
(
folderQueries
:
IFolderSearch
[],
globalInclude
:
glob
.
IExpression
,
absoluteGlobs
=
true
):
string
[]
{
const
globArgs
:
string
[]
=
[];
folderQueries
.
forEach
(
folderQuery
=>
{
const
totalIncludePattern
=
objects
.
assign
({},
globalInclude
||
{},
folderQuery
.
includePattern
||
{});
const
result
=
globExprsToRgGlobs
(
totalIncludePattern
,
absoluteGlobs
&&
folderQuery
.
folder
);
globArgs
.
push
(...
result
.
globArgs
);
});
return
globArgs
;
}
function
globExprsToRgGlobs
(
patterns
:
glob
.
IExpression
,
folder
?:
string
,
excludesToSkip
?:
Set
<
string
>
):
IRgGlobResult
{
const
globArgs
:
string
[]
=
[];
let
siblingClauses
:
glob
.
IExpression
=
null
;
Object
.
keys
(
patterns
)
.
forEach
(
key
=>
{
if
(
excludesToSkip
&&
excludesToSkip
.
has
(
key
))
{
return
;
}
if
(
!
key
)
{
return
;
}
const
value
=
patterns
[
key
];
key
=
trimTrailingSlash
(
folder
?
getAbsoluteGlob
(
folder
,
key
)
:
key
);
// glob.ts requires forward slashes, but a UNC path still must start with \\
// #38165 and #38151
if
(
strings
.
startsWith
(
key
,
'
\\\\
'
))
{
key
=
'
\\\\
'
+
key
.
substr
(
2
).
replace
(
/
\\
/g
,
'
/
'
);
}
else
{
key
=
key
.
replace
(
/
\\
/g
,
'
/
'
);
}
if
(
typeof
value
===
'
boolean
'
&&
value
)
{
if
(
strings
.
startsWith
(
key
,
'
\\\\
'
))
{
// Absolute globs UNC paths don't work properly, see #58758
key
+=
'
**
'
;
}
globArgs
.
push
(
fixDriveC
(
key
));
}
else
if
(
value
&&
value
.
when
)
{
if
(
!
siblingClauses
)
{
siblingClauses
=
{};
}
siblingClauses
[
key
]
=
value
;
}
});
return
{
globArgs
,
siblingClauses
};
}
/**
* Resolves a glob like "node_modules/**" in "/foo/bar" to "/foo/bar/node_modules/**".
* Special cases C:/foo paths to write the glob like /foo instead - see https://github.com/BurntSushi/ripgrep/issues/530.
*
* Exported for testing
*/
export
function
getAbsoluteGlob
(
folder
:
string
,
key
:
string
):
string
{
return
paths
.
isAbsolute
(
key
)
?
key
:
path
.
join
(
folder
,
key
);
}
function
trimTrailingSlash
(
str
:
string
):
string
{
str
=
strings
.
rtrim
(
str
,
'
\\
'
);
return
strings
.
rtrim
(
str
,
'
/
'
);
}
export
function
fixDriveC
(
path
:
string
):
string
{
const
root
=
paths
.
getRoot
(
path
);
return
root
.
toLowerCase
()
===
'
c:/
'
?
path
.
replace
(
/^c:
[/\\]
/i
,
'
/
'
)
:
path
;
}
src/vs/workbench/services/search/node/ripgrepSearchUtils.ts
浏览文件 @
c26d6e6c
...
...
@@ -3,20 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import
*
as
path
from
'
path
'
;
import
{
startsWith
}
from
'
vs/base/common/strings
'
;
import
{
ILogService
}
from
'
vs/platform/log/common/log
'
;
import
*
as
vscode
from
'
vscode
'
;
export
type
Maybe
<
T
>
=
T
|
null
|
undefined
;
export
function
fixDriveC
(
_path
:
string
):
string
{
const
root
=
path
.
parse
(
_path
).
root
;
return
root
.
toLowerCase
()
===
'
c:/
'
?
_path
.
replace
(
/^c:
[/\\]
/i
,
'
/
'
)
:
_path
;
}
export
function
anchorGlob
(
glob
:
string
):
string
{
return
startsWith
(
glob
,
'
**
'
)
||
startsWith
(
glob
,
'
/
'
)
?
glob
:
`/
${
glob
}
`
;
}
...
...
@@ -85,7 +77,11 @@ export class Range {
with
(
_
:
any
):
Range
{
return
null
;
}
}
export
class
OutputChannel
{
export
interface
IOutputChannel
{
appendLine
(
msg
:
string
):
void
;
}
export
class
OutputChannel
implements
IOutputChannel
{
constructor
(@
ILogService
private
logService
:
ILogService
)
{
}
appendLine
(
msg
:
string
):
void
{
...
...
src/vs/workbench/services/search/node/ripgrepTextSearch.ts
已删除
100644 → 0
浏览文件 @
ee40fa3e
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import
*
as
cp
from
'
child_process
'
;
import
{
EventEmitter
}
from
'
events
'
;
import
*
as
path
from
'
path
'
;
import
{
NodeStringDecoder
,
StringDecoder
}
from
'
string_decoder
'
;
import
*
as
glob
from
'
vs/base/common/glob
'
;
import
*
as
objects
from
'
vs/base/common/objects
'
;
import
*
as
paths
from
'
vs/base/common/paths
'
;
import
*
as
platform
from
'
vs/base/common/platform
'
;
import
*
as
strings
from
'
vs/base/common/strings
'
;
import
{
URI
}
from
'
vs/base/common/uri
'
;
import
{
TPromise
}
from
'
vs/base/common/winjs.base
'
;
import
*
as
encoding
from
'
vs/base/node/encoding
'
;
import
*
as
extfs
from
'
vs/base/node/extfs
'
;
import
{
Range
}
from
'
vs/editor/common/core/range
'
;
import
{
IProgress
,
ITextSearchPreviewOptions
,
ITextSearchStats
,
TextSearchResult
}
from
'
vs/platform/search/common/search
'
;
import
{
rgPath
}
from
'
vscode-ripgrep
'
;
import
{
FileMatch
,
IFolderSearch
,
IRawSearch
,
ISerializedFileMatch
,
ISerializedSearchSuccess
}
from
'
./search
'
;
// If vscode-ripgrep is in an .asar file, then the binary is unpacked.
const
rgDiskPath
=
rgPath
.
replace
(
/
\b
node_modules
\.
asar
\b
/
,
'
node_modules.asar.unpacked
'
);
export
class
RipgrepEngine
{
private
isDone
=
false
;
private
rgProc
:
cp
.
ChildProcess
;
private
killRgProcFn
:
(
code
?:
number
)
=>
void
;
private
postProcessExclusions
:
glob
.
ParsedExpression
;
private
ripgrepParser
:
RipgrepParser
;
private
resultsHandledP
:
TPromise
<
any
>
=
TPromise
.
wrap
(
null
);
constructor
(
private
config
:
IRawSearch
)
{
this
.
killRgProcFn
=
()
=>
this
.
rgProc
&&
this
.
rgProc
.
kill
();
}
cancel
():
void
{
this
.
isDone
=
true
;
this
.
ripgrepParser
.
cancel
();
this
.
rgProc
.
kill
();
}
// TODO@Rob - make promise-based once the old search is gone, and I don't need them to have matching interfaces anymore
search
(
onResult
:
(
match
:
ISerializedFileMatch
)
=>
void
,
onMessage
:
(
message
:
IProgress
)
=>
void
,
done
:
(
error
:
Error
,
complete
:
ISerializedSearchSuccess
)
=>
void
):
void
{
if
(
!
this
.
config
.
folderQueries
.
length
&&
!
this
.
config
.
extraFiles
.
length
)
{
process
.
removeListener
(
'
exit
'
,
this
.
killRgProcFn
);
done
(
null
,
{
type
:
'
success
'
,
limitHit
:
false
,
stats
:
<
ITextSearchStats
>
{
type
:
'
searchProcess
'
}
});
return
;
}
const
rgArgs
=
getRgArgs
(
this
.
config
);
if
(
rgArgs
.
siblingClauses
)
{
this
.
postProcessExclusions
=
glob
.
parseToAsync
(
rgArgs
.
siblingClauses
,
{
trimForExclusions
:
true
});
}
const
cwd
=
platform
.
isWindows
?
'
c:/
'
:
'
/
'
;
const
escapedArgs
=
rgArgs
.
args
.
map
(
arg
=>
arg
.
match
(
/^-/
)
?
arg
:
`'
${
arg
}
'`
)
.
join
(
'
'
);
let
rgCmd
=
`rg
${
escapedArgs
}
\n - cwd:
${
cwd
}
`
;
if
(
rgArgs
.
siblingClauses
)
{
rgCmd
+=
`\n - Sibling clauses:
${
JSON
.
stringify
(
rgArgs
.
siblingClauses
)}
`
;
}
onMessage
({
message
:
rgCmd
});
this
.
rgProc
=
cp
.
spawn
(
rgDiskPath
,
rgArgs
.
args
,
{
cwd
});
process
.
once
(
'
exit
'
,
this
.
killRgProcFn
);
this
.
ripgrepParser
=
new
RipgrepParser
(
this
.
config
.
maxResults
,
cwd
,
this
.
config
.
previewOptions
);
this
.
ripgrepParser
.
on
(
'
result
'
,
(
match
:
ISerializedFileMatch
)
=>
{
if
(
this
.
postProcessExclusions
)
{
const
handleResultP
=
(
<
TPromise
<
string
>>
this
.
postProcessExclusions
(
match
.
path
,
undefined
,
glob
.
hasSiblingPromiseFn
(()
=>
getSiblings
(
match
.
path
))))
.
then
(
globMatch
=>
{
if
(
!
globMatch
)
{
onResult
(
match
);
}
});
this
.
resultsHandledP
=
TPromise
.
join
([
this
.
resultsHandledP
,
handleResultP
]);
}
else
{
onResult
(
match
);
}
});
this
.
ripgrepParser
.
on
(
'
hitLimit
'
,
()
=>
{
this
.
cancel
();
process
.
removeListener
(
'
exit
'
,
this
.
killRgProcFn
);
done
(
null
,
{
type
:
'
success
'
,
limitHit
:
true
,
stats
:
{
type
:
'
searchProcess
'
}
});
});
this
.
rgProc
.
stdout
.
on
(
'
data
'
,
data
=>
{
this
.
ripgrepParser
.
handleData
(
data
);
});
let
gotData
=
false
;
this
.
rgProc
.
stdout
.
once
(
'
data
'
,
()
=>
gotData
=
true
);
let
stderr
=
''
;
this
.
rgProc
.
stderr
.
on
(
'
data
'
,
data
=>
{
const
message
=
data
.
toString
();
onMessage
({
message
});
stderr
+=
message
;
});
this
.
rgProc
.
on
(
'
close
'
,
code
=>
{
// Trigger last result, then wait on async result handling
this
.
ripgrepParser
.
flush
();
this
.
resultsHandledP
.
then
(()
=>
{
this
.
rgProc
=
null
;
if
(
!
this
.
isDone
)
{
this
.
isDone
=
true
;
let
displayMsg
:
string
;
process
.
removeListener
(
'
exit
'
,
this
.
killRgProcFn
);
if
(
stderr
&&
!
gotData
&&
(
displayMsg
=
rgErrorMsgForDisplay
(
stderr
)))
{
done
(
new
Error
(
displayMsg
),
{
type
:
'
success
'
,
limitHit
:
false
,
stats
:
null
});
}
else
{
done
(
null
,
{
type
:
'
success
'
,
limitHit
:
false
,
stats
:
null
});
}
}
});
});
}
}
/**
* Read the first line of stderr and return an error for display or undefined, based on a whitelist.
* Ripgrep produces stderr output which is not from a fatal error, and we only want the search to be
* "failed" when a fatal error was produced.
*/
export
function
rgErrorMsgForDisplay
(
msg
:
string
):
string
|
undefined
{
const
lines
=
msg
.
trim
().
split
(
'
\n
'
);
const
firstLine
=
lines
[
0
].
trim
();
if
(
strings
.
startsWith
(
firstLine
,
'
Error parsing regex
'
))
{
return
firstLine
;
}
if
(
strings
.
startsWith
(
firstLine
,
'
regex parse error
'
))
{
return
strings
.
uppercaseFirstLetter
(
lines
[
lines
.
length
-
1
].
trim
());
}
if
(
strings
.
startsWith
(
firstLine
,
'
error parsing glob
'
)
||
strings
.
startsWith
(
firstLine
,
'
unsupported encoding
'
))
{
// Uppercase first letter
return
firstLine
.
charAt
(
0
).
toUpperCase
()
+
firstLine
.
substr
(
1
);
}
if
(
firstLine
===
`Literal '\\n' not allowed.`
)
{
// I won't localize this because none of the Ripgrep error messages are localized
return
`Literal '\\n' currently not supported`
;
}
if
(
strings
.
startsWith
(
firstLine
,
'
Literal
'
))
{
// Other unsupported chars
return
firstLine
;
}
return
undefined
;
}
export
class
RipgrepParser
extends
EventEmitter
{
private
fileMatch
:
FileMatch
;
private
remainder
=
''
;
private
isDone
:
boolean
;
private
stringDecoder
:
NodeStringDecoder
;
private
numResults
=
0
;
constructor
(
private
maxResults
:
number
,
private
rootFolder
:
string
,
private
previewOptions
?:
ITextSearchPreviewOptions
)
{
super
();
this
.
stringDecoder
=
new
StringDecoder
();
}
public
cancel
():
void
{
this
.
isDone
=
true
;
}
public
flush
():
void
{
this
.
handleDecodedData
(
this
.
stringDecoder
.
end
());
if
(
this
.
fileMatch
)
{
this
.
onResult
();
}
}
public
handleData
(
data
:
Buffer
|
string
):
void
{
if
(
this
.
isDone
)
{
return
;
}
const
dataStr
=
typeof
data
===
'
string
'
?
data
:
this
.
stringDecoder
.
write
(
data
);
this
.
handleDecodedData
(
dataStr
);
}
private
handleDecodedData
(
decodedData
:
string
):
void
{
// check for newline before appending to remainder
let
newlineIdx
=
decodedData
.
indexOf
(
'
\n
'
);
// If the previous data chunk didn't end in a newline, prepend it to this chunk
const
dataStr
=
this
.
remainder
+
decodedData
;
if
(
newlineIdx
>=
0
)
{
newlineIdx
+=
this
.
remainder
.
length
;
}
else
{
// Shortcut
this
.
remainder
=
dataStr
;
return
;
}
let
prevIdx
=
0
;
while
(
newlineIdx
>=
0
)
{
this
.
handleLine
(
dataStr
.
substring
(
prevIdx
,
newlineIdx
).
trim
());
prevIdx
=
newlineIdx
+
1
;
newlineIdx
=
dataStr
.
indexOf
(
'
\n
'
,
prevIdx
);
}
this
.
remainder
=
dataStr
.
substring
(
prevIdx
).
trim
();
}
private
handleLine
(
outputLine
:
string
):
void
{
if
(
this
.
isDone
||
!
outputLine
)
{
return
;
}
let
parsedLine
:
any
;
try
{
parsedLine
=
JSON
.
parse
(
outputLine
);
}
catch
(
e
)
{
throw
new
Error
(
`malformed line from rg:
${
outputLine
}
`
);
}
if
(
parsedLine
.
type
===
'
begin
'
)
{
const
path
=
bytesOrTextToString
(
parsedLine
.
data
.
path
);
this
.
fileMatch
=
this
.
getFileMatch
(
path
);
}
else
if
(
parsedLine
.
type
===
'
match
'
)
{
this
.
handleMatchLine
(
parsedLine
);
}
else
if
(
parsedLine
.
type
===
'
end
'
)
{
if
(
this
.
fileMatch
)
{
this
.
onResult
();
}
}
}
private
handleMatchLine
(
parsedLine
:
any
):
void
{
let
hitLimit
=
false
;
const
uri
=
URI
.
file
(
path
.
join
(
this
.
rootFolder
,
parsedLine
.
data
.
path
.
text
));
parsedLine
.
data
.
submatches
.
map
((
match
:
any
)
=>
{
if
(
hitLimit
)
{
return
null
;
}
this
.
numResults
++
;
if
(
this
.
numResults
>=
this
.
maxResults
)
{
// Finish the line, then report the result below
hitLimit
=
true
;
}
return
this
.
submatchToResult
(
parsedLine
,
match
,
uri
);
}).
forEach
((
result
:
any
)
=>
{
if
(
result
)
{
this
.
fileMatch
.
addMatch
(
result
);
}
});
if
(
hitLimit
)
{
this
.
cancel
();
this
.
onResult
();
this
.
emit
(
'
hitLimit
'
);
}
}
private
submatchToResult
(
parsedLine
:
any
,
match
:
any
,
uri
:
URI
):
TextSearchResult
{
const
lineNumber
=
parsedLine
.
data
.
line_number
-
1
;
let
lineText
=
bytesOrTextToString
(
parsedLine
.
data
.
lines
);
let
matchText
=
bytesOrTextToString
(
match
.
match
);
const
newlineMatches
=
matchText
.
match
(
/
\n
/g
);
const
newlines
=
newlineMatches
?
newlineMatches
.
length
:
0
;
let
startCol
=
match
.
start
;
const
endLineNumber
=
lineNumber
+
newlines
;
let
endCol
=
match
.
end
-
(
lineText
.
lastIndexOf
(
'
\n
'
,
lineText
.
length
-
2
)
+
1
);
if
(
lineNumber
===
0
)
{
if
(
strings
.
startsWithUTF8BOM
(
lineText
))
{
lineText
=
strings
.
stripUTF8BOM
(
lineText
);
startCol
-=
3
;
endCol
-=
3
;
}
}
const
range
=
new
Range
(
lineNumber
,
startCol
,
endLineNumber
,
endCol
);
return
new
TextSearchResult
(
lineText
,
range
,
this
.
previewOptions
);
}
private
getFileMatch
(
relativeOrAbsolutePath
:
string
):
FileMatch
{
const
absPath
=
path
.
isAbsolute
(
relativeOrAbsolutePath
)
?
relativeOrAbsolutePath
:
path
.
join
(
this
.
rootFolder
,
relativeOrAbsolutePath
);
return
new
FileMatch
(
absPath
);
}
private
onResult
():
void
{
this
.
emit
(
'
result
'
,
this
.
fileMatch
.
serialize
());
this
.
fileMatch
=
null
;
}
}
function
bytesOrTextToString
(
obj
:
any
):
string
{
return
obj
.
bytes
?
new
Buffer
(
obj
.
bytes
,
'
base64
'
).
toString
()
:
obj
.
text
;
}
export
interface
IRgGlobResult
{
globArgs
:
string
[];
siblingClauses
:
glob
.
IExpression
;
}
export
function
foldersToRgExcludeGlobs
(
folderQueries
:
IFolderSearch
[],
globalExclude
:
glob
.
IExpression
,
excludesToSkip
?:
Set
<
string
>
,
absoluteGlobs
=
true
):
IRgGlobResult
{
const
globArgs
:
string
[]
=
[];
let
siblingClauses
:
glob
.
IExpression
=
{};
folderQueries
.
forEach
(
folderQuery
=>
{
const
totalExcludePattern
=
objects
.
assign
({},
folderQuery
.
excludePattern
||
{},
globalExclude
||
{});
const
result
=
globExprsToRgGlobs
(
totalExcludePattern
,
absoluteGlobs
&&
folderQuery
.
folder
,
excludesToSkip
);
globArgs
.
push
(...
result
.
globArgs
);
if
(
result
.
siblingClauses
)
{
siblingClauses
=
objects
.
assign
(
siblingClauses
,
result
.
siblingClauses
);
}
});
return
{
globArgs
,
siblingClauses
};
}
export
function
foldersToIncludeGlobs
(
folderQueries
:
IFolderSearch
[],
globalInclude
:
glob
.
IExpression
,
absoluteGlobs
=
true
):
string
[]
{
const
globArgs
:
string
[]
=
[];
folderQueries
.
forEach
(
folderQuery
=>
{
const
totalIncludePattern
=
objects
.
assign
({},
globalInclude
||
{},
folderQuery
.
includePattern
||
{});
const
result
=
globExprsToRgGlobs
(
totalIncludePattern
,
absoluteGlobs
&&
folderQuery
.
folder
);
globArgs
.
push
(...
result
.
globArgs
);
});
return
globArgs
;
}
function
globExprsToRgGlobs
(
patterns
:
glob
.
IExpression
,
folder
?:
string
,
excludesToSkip
?:
Set
<
string
>
):
IRgGlobResult
{
const
globArgs
:
string
[]
=
[];
let
siblingClauses
:
glob
.
IExpression
=
null
;
Object
.
keys
(
patterns
)
.
forEach
(
key
=>
{
if
(
excludesToSkip
&&
excludesToSkip
.
has
(
key
))
{
return
;
}
if
(
!
key
)
{
return
;
}
const
value
=
patterns
[
key
];
key
=
trimTrailingSlash
(
folder
?
getAbsoluteGlob
(
folder
,
key
)
:
key
);
// glob.ts requires forward slashes, but a UNC path still must start with \\
// #38165 and #38151
if
(
strings
.
startsWith
(
key
,
'
\\\\
'
))
{
key
=
'
\\\\
'
+
key
.
substr
(
2
).
replace
(
/
\\
/g
,
'
/
'
);
}
else
{
key
=
key
.
replace
(
/
\\
/g
,
'
/
'
);
}
if
(
typeof
value
===
'
boolean
'
&&
value
)
{
if
(
strings
.
startsWith
(
key
,
'
\\\\
'
))
{
// Absolute globs UNC paths don't work properly, see #58758
key
+=
'
**
'
;
}
globArgs
.
push
(
fixDriveC
(
key
));
}
else
if
(
value
&&
value
.
when
)
{
if
(
!
siblingClauses
)
{
siblingClauses
=
{};
}
siblingClauses
[
key
]
=
value
;
}
});
return
{
globArgs
,
siblingClauses
};
}
/**
* Resolves a glob like "node_modules/**" in "/foo/bar" to "/foo/bar/node_modules/**".
* Special cases C:/foo paths to write the glob like /foo instead - see https://github.com/BurntSushi/ripgrep/issues/530.
*
* Exported for testing
*/
export
function
getAbsoluteGlob
(
folder
:
string
,
key
:
string
):
string
{
return
paths
.
isAbsolute
(
key
)
?
key
:
path
.
join
(
folder
,
key
);
}
function
trimTrailingSlash
(
str
:
string
):
string
{
str
=
strings
.
rtrim
(
str
,
'
\\
'
);
return
strings
.
rtrim
(
str
,
'
/
'
);
}
export
function
fixDriveC
(
path
:
string
):
string
{
const
root
=
paths
.
getRoot
(
path
);
return
root
.
toLowerCase
()
===
'
c:/
'
?
path
.
replace
(
/^c:
[/\\]
/i
,
'
/
'
)
:
path
;
}
function
getRgArgs
(
config
:
IRawSearch
)
{
const
args
=
[
'
--hidden
'
,
'
--heading
'
,
'
--line-number
'
,
'
--color
'
,
'
ansi
'
,
'
--colors
'
,
'
path:none
'
,
'
--colors
'
,
'
line:none
'
,
'
--colors
'
,
'
match:fg:red
'
,
'
--colors
'
,
'
match:style:nobold
'
];
args
.
push
(
config
.
contentPattern
.
isCaseSensitive
?
'
--case-sensitive
'
:
'
--ignore-case
'
);
// includePattern can't have siblingClauses
foldersToIncludeGlobs
(
config
.
folderQueries
,
config
.
includePattern
).
forEach
(
globArg
=>
{
args
.
push
(
'
-g
'
,
globArg
);
});
let
siblingClauses
:
glob
.
IExpression
;
// Find excludes that are exactly the same in all folderQueries - e.g. from user settings, and that start with `**`.
// To make the command shorter, don't resolve these against every folderQuery path - see #33189.
const
universalExcludes
=
findUniversalExcludes
(
config
.
folderQueries
);
const
rgGlobs
=
foldersToRgExcludeGlobs
(
config
.
folderQueries
,
config
.
excludePattern
,
universalExcludes
);
rgGlobs
.
globArgs
.
forEach
(
rgGlob
=>
args
.
push
(
'
-g
'
,
`!
${
rgGlob
}
`
));
if
(
universalExcludes
)
{
universalExcludes
.
forEach
(
exclude
=>
args
.
push
(
'
-g
'
,
`!
${
trimTrailingSlash
(
exclude
)}
`
));
}
siblingClauses
=
rgGlobs
.
siblingClauses
;
if
(
config
.
maxFilesize
)
{
args
.
push
(
'
--max-filesize
'
,
config
.
maxFilesize
+
''
);
}
if
(
config
.
disregardIgnoreFiles
)
{
// Don't use .gitignore or .ignore
args
.
push
(
'
--no-ignore
'
);
}
else
{
args
.
push
(
'
--no-ignore-parent
'
);
}
// Follow symlinks
if
(
!
config
.
ignoreSymlinks
)
{
args
.
push
(
'
--follow
'
);
}
if
(
config
.
folderQueries
[
0
])
{
const
folder0Encoding
=
config
.
folderQueries
[
0
].
fileEncoding
;
if
(
folder0Encoding
&&
folder0Encoding
!==
'
utf8
'
&&
config
.
folderQueries
.
every
(
fq
=>
fq
.
fileEncoding
===
folder0Encoding
))
{
args
.
push
(
'
--encoding
'
,
encoding
.
toCanonicalName
(
folder0Encoding
));
}
}
// Ripgrep handles -- as a -- arg separator. Only --.
// - is ok, --- is ok, --some-flag is handled as query text. Need to special case.
if
(
config
.
contentPattern
.
pattern
===
'
--
'
)
{
config
.
contentPattern
.
isRegExp
=
true
;
config
.
contentPattern
.
pattern
=
'
\\
-
\\
-
'
;
}
let
searchPatternAfterDoubleDashes
:
string
;
if
(
config
.
contentPattern
.
isWordMatch
)
{
const
regexp
=
strings
.
createRegExp
(
config
.
contentPattern
.
pattern
,
config
.
contentPattern
.
isRegExp
,
{
wholeWord
:
config
.
contentPattern
.
isWordMatch
});
const
regexpStr
=
regexp
.
source
.
replace
(
/
\\\/
/g
,
'
/
'
);
// RegExp.source arbitrarily returns escaped slashes. Search and destroy.
args
.
push
(
'
--regexp
'
,
regexpStr
);
}
else
if
(
config
.
contentPattern
.
isRegExp
)
{
args
.
push
(
'
--regexp
'
,
fixRegexEndingPattern
(
config
.
contentPattern
.
pattern
));
}
else
{
searchPatternAfterDoubleDashes
=
config
.
contentPattern
.
pattern
;
args
.
push
(
'
--fixed-strings
'
);
}
args
.
push
(
'
--no-config
'
);
if
(
config
.
disregardGlobalIgnoreFiles
)
{
args
.
push
(
'
--no-ignore-global
'
);
}
args
.
push
(
'
--json
'
);
if
(
config
.
contentPattern
.
isMultiline
)
{
args
.
push
(
'
--multiline
'
);
}
// Folder to search
args
.
push
(
'
--
'
);
if
(
searchPatternAfterDoubleDashes
)
{
// Put the query after --, in case the query starts with a dash
args
.
push
(
searchPatternAfterDoubleDashes
);
}
args
.
push
(...
config
.
folderQueries
.
map
(
q
=>
q
.
folder
));
args
.
push
(...
config
.
extraFiles
);
return
{
args
,
siblingClauses
};
}
function
getSiblings
(
file
:
string
):
TPromise
<
string
[]
>
{
return
new
TPromise
<
string
[]
>
((
resolve
,
reject
)
=>
{
extfs
.
readdir
(
path
.
dirname
(
file
),
(
error
:
Error
,
files
:
string
[])
=>
{
if
(
error
)
{
reject
(
error
);
}
resolve
(
files
);
});
});
}
function
findUniversalExcludes
(
folderQueries
:
IFolderSearch
[]):
Set
<
string
>
{
if
(
folderQueries
.
length
<
2
)
{
// Nothing to simplify
return
null
;
}
const
firstFolder
=
folderQueries
[
0
];
if
(
!
firstFolder
.
excludePattern
)
{
return
null
;
}
const
universalExcludes
=
new
Set
<
string
>
();
Object
.
keys
(
firstFolder
.
excludePattern
).
forEach
(
key
=>
{
if
(
strings
.
startsWith
(
key
,
'
**
'
)
&&
folderQueries
.
every
(
q
=>
q
.
excludePattern
&&
q
.
excludePattern
[
key
]
===
true
))
{
universalExcludes
.
add
(
key
);
}
});
return
universalExcludes
;
}
// Exported for testing
export
function
fixRegexEndingPattern
(
pattern
:
string
):
string
{
// Replace an unescaped $ at the end of the pattern with \r?$
// Match $ preceeded by none or even number of literal \
return
pattern
.
match
(
/
([^\\]
|^
)(\\\\)
*
\$
$/
)
?
pattern
.
replace
(
/
\$
$/
,
'
\\
r?$
'
)
:
pattern
;
}
src/vs/workbench/services/search/node/ripgrepTextSearchEH.ts
浏览文件 @
c26d6e6c
...
...
@@ -7,18 +7,18 @@ import * as cp from 'child_process';
import
{
EventEmitter
}
from
'
events
'
;
import
*
as
path
from
'
path
'
;
import
{
NodeStringDecoder
,
StringDecoder
}
from
'
string_decoder
'
;
import
{
startsWith
}
from
'
vs/base/common/strings
'
;
import
{
URI
}
from
'
vs/base/common/uri
'
;
import
*
as
vscode
from
'
vscode
'
;
import
{
rgPath
}
from
'
vscode-ripgrep
'
;
import
{
anchorGlob
,
createTextSearchResult
,
Maybe
,
Range
,
OutputChannel
}
from
'
./ripgrepSearchUtils
'
;
import
{
URI
}
from
'
vs/base/common/uri
'
;
import
{
startsWith
}
from
'
vs/base/common/strings
'
;
import
{
anchorGlob
,
createTextSearchResult
,
IOutputChannel
,
Maybe
,
Range
}
from
'
./ripgrepSearchUtils
'
;
// If vscode-ripgrep is in an .asar file, then the binary is unpacked.
const
rgDiskPath
=
rgPath
.
replace
(
/
\b
node_modules
\.
asar
\b
/
,
'
node_modules.asar.unpacked
'
);
export
class
RipgrepTextSearchEngine
{
constructor
(
private
outputChannel
:
OutputChannel
)
{
}
constructor
(
private
outputChannel
:
I
OutputChannel
)
{
}
provideTextSearchResults
(
query
:
vscode
.
TextSearchQuery
,
options
:
vscode
.
TextSearchOptions
,
progress
:
vscode
.
Progress
<
vscode
.
TextSearchResult
>
,
token
:
vscode
.
CancellationToken
):
Thenable
<
vscode
.
TextSearchComplete
>
{
this
.
outputChannel
.
appendLine
(
`provideTextSearchResults
${
query
.
pattern
}
,
${
JSON
.
stringify
({
...
...
@@ -302,6 +302,10 @@ function getRgArgs(query: vscode.TextSearchQuery, options: vscode.TextSearchOpti
args
.
push
(
'
--json
'
);
if
(
query
.
isMultiline
)
{
args
.
push
(
'
--multiline
'
);
}
// Folder to search
args
.
push
(
'
--
'
);
...
...
@@ -370,7 +374,7 @@ function stripUTF8BOM(str: string): string {
return
startsWithUTF8BOM
(
str
)
?
str
.
substr
(
1
)
:
str
;
}
function
fixRegexEndingPattern
(
pattern
:
string
):
string
{
export
function
fixRegexEndingPattern
(
pattern
:
string
):
string
{
// Replace an unescaped $ at the end of the pattern with \r?$
// Match $ preceeded by none or even number of literal \
return
pattern
.
match
(
/
([^\\]
|^
)(\\\\)
*
\$
$/
)
?
...
...
src/vs/workbench/services/search/node/search.ts
浏览文件 @
c26d6e6c
...
...
@@ -4,15 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import
{
Event
}
from
'
vs/base/common/event
'
;
import
{
IExpression
}
from
'
vs/base/common/glob
'
;
import
*
as
glob
from
'
vs/base/common/glob
'
;
import
{
TPromise
}
from
'
vs/base/common/winjs.base
'
;
import
{
IFileSearchStats
,
IPatternInfo
,
IProgress
,
ISearchEngineStats
,
ITextSearchPreviewOptions
,
ITextSearchResult
,
ITextSearchStats
}
from
'
vs/platform/search/common/search
'
;
import
{
IFileSearchStats
,
IPatternInfo
,
IProgress
,
ISearchEngineStats
,
ITextSearchPreviewOptions
,
ITextSearchResult
,
ITextSearchStats
,
ISearchQuery
,
IFolderQuery
}
from
'
vs/platform/search/common/search
'
;
import
{
ITelemetryData
}
from
'
vs/platform/telemetry/common/telemetry
'
;
export
interface
IFolderSearch
{
folder
:
string
;
excludePattern
?:
IExpression
;
includePattern
?:
IExpression
;
excludePattern
?:
glob
.
IExpression
;
includePattern
?:
glob
.
IExpression
;
fileEncoding
?:
string
;
disregardIgnoreFiles
?:
boolean
;
disregardGlobalIgnoreFiles
?:
boolean
;
...
...
@@ -23,8 +23,8 @@ export interface IRawSearch {
ignoreSymlinks
?:
boolean
;
extraFiles
?:
string
[];
filePattern
?:
string
;
excludePattern
?:
IExpression
;
includePattern
?:
IExpression
;
excludePattern
?:
glob
.
IExpression
;
includePattern
?:
glob
.
IExpression
;
contentPattern
?:
IPatternInfo
;
maxResults
?:
number
;
exists
?:
boolean
;
...
...
@@ -127,3 +127,102 @@ export class FileMatch implements ISerializedFileMatch {
};
}
}
/**
* Computes the patterns that the provider handles. Discards sibling clauses and 'false' patterns
*/
export
function
resolvePatternsForProvider
(
globalPattern
:
glob
.
IExpression
,
folderPattern
:
glob
.
IExpression
):
string
[]
{
const
merged
=
{
...(
globalPattern
||
{}),
...(
folderPattern
||
{})
};
return
Object
.
keys
(
merged
)
.
filter
(
key
=>
{
const
value
=
merged
[
key
];
return
typeof
value
===
'
boolean
'
&&
value
;
});
}
export
class
QueryGlobTester
{
private
_excludeExpression
:
glob
.
IExpression
;
private
_parsedExcludeExpression
:
glob
.
ParsedExpression
;
private
_parsedIncludeExpression
:
glob
.
ParsedExpression
;
constructor
(
config
:
ISearchQuery
,
folderQuery
:
IFolderQuery
)
{
this
.
_excludeExpression
=
{
...(
config
.
excludePattern
||
{}),
...(
folderQuery
.
excludePattern
||
{})
};
this
.
_parsedExcludeExpression
=
glob
.
parse
(
this
.
_excludeExpression
);
// Empty includeExpression means include nothing, so no {} shortcuts
let
includeExpression
:
glob
.
IExpression
=
config
.
includePattern
;
if
(
folderQuery
.
includePattern
)
{
if
(
includeExpression
)
{
includeExpression
=
{
...
includeExpression
,
...
folderQuery
.
includePattern
};
}
else
{
includeExpression
=
folderQuery
.
includePattern
;
}
}
if
(
includeExpression
)
{
this
.
_parsedIncludeExpression
=
glob
.
parse
(
includeExpression
);
}
}
/**
* Guaranteed sync - siblingsFn should not return a promise.
*/
public
includedInQuerySync
(
testPath
:
string
,
basename
?:
string
,
hasSibling
?:
(
name
:
string
)
=>
boolean
):
boolean
{
if
(
this
.
_parsedExcludeExpression
&&
this
.
_parsedExcludeExpression
(
testPath
,
basename
,
hasSibling
))
{
return
false
;
}
if
(
this
.
_parsedIncludeExpression
&&
!
this
.
_parsedIncludeExpression
(
testPath
,
basename
,
hasSibling
))
{
return
false
;
}
return
true
;
}
/**
* Guaranteed async.
*/
public
includedInQuery
(
testPath
:
string
,
basename
?:
string
,
hasSibling
?:
(
name
:
string
)
=>
boolean
|
TPromise
<
boolean
>
):
TPromise
<
boolean
>
{
const
excludeP
=
this
.
_parsedExcludeExpression
?
TPromise
.
as
(
this
.
_parsedExcludeExpression
(
testPath
,
basename
,
hasSibling
)).
then
(
result
=>
!!
result
)
:
TPromise
.
wrap
(
false
);
return
excludeP
.
then
(
excluded
=>
{
if
(
excluded
)
{
return
false
;
}
return
this
.
_parsedIncludeExpression
?
TPromise
.
as
(
this
.
_parsedIncludeExpression
(
testPath
,
basename
,
hasSibling
)).
then
(
result
=>
!!
result
)
:
TPromise
.
wrap
(
true
);
}).
then
(
included
=>
{
return
included
;
});
}
public
hasSiblingExcludeClauses
():
boolean
{
return
hasSiblingClauses
(
this
.
_excludeExpression
);
}
}
function
hasSiblingClauses
(
pattern
:
glob
.
IExpression
):
boolean
{
for
(
let
key
in
pattern
)
{
if
(
typeof
pattern
[
key
]
!==
'
boolean
'
)
{
return
true
;
}
}
return
false
;
}
src/vs/workbench/services/search/node/textSearchAdapter.ts
0 → 100644
浏览文件 @
c26d6e6c
/*---------------------------------------------------------------------------------------------
* 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
*
as
extfs
from
'
vs/base/node/extfs
'
;
import
{
IFolderQuery
,
IProgress
,
ISearchQuery
,
ITextSearchStats
,
QueryType
,
IFileMatch
}
from
'
vs/platform/search/common/search
'
;
import
{
RipgrepTextSearchEngine
}
from
'
vs/workbench/services/search/node/ripgrepTextSearchEH
'
;
import
{
TextSearchEngine
}
from
'
vs/workbench/services/search/node/textSearchEngine
'
;
import
{
IRawSearch
,
ISerializedFileMatch
,
ISerializedSearchSuccess
}
from
'
./search
'
;
export
class
TextSearchEngineAdapter
{
constructor
(
private
config
:
IRawSearch
)
{
}
// TODO@Rob - make promise-based once the old search is gone, and I don't need them to have matching interfaces anymore
search
(
token
:
CancellationToken
,
onResult
:
(
matches
:
ISerializedFileMatch
[])
=>
void
,
onMessage
:
(
message
:
IProgress
)
=>
void
,
done
:
(
error
:
Error
,
complete
:
ISerializedSearchSuccess
)
=>
void
):
void
{
if
(
!
this
.
config
.
folderQueries
.
length
&&
!
this
.
config
.
extraFiles
.
length
)
{
done
(
null
,
{
type
:
'
success
'
,
limitHit
:
false
,
stats
:
<
ITextSearchStats
>
{
type
:
'
searchProcess
'
}
});
return
;
}
const
query
:
ISearchQuery
=
{
type
:
QueryType
.
Text
,
cacheKey
:
this
.
config
.
cacheKey
,
contentPattern
:
this
.
config
.
contentPattern
,
extraFileResources
:
this
.
config
.
extraFiles
&&
this
.
config
.
extraFiles
.
map
(
f
=>
URI
.
file
(
f
)),
fileEncoding
:
this
.
config
.
folderQueries
[
0
].
fileEncoding
,
// ?
maxResults
:
this
.
config
.
maxResults
,
exists
:
this
.
config
.
exists
,
sortByScore
:
this
.
config
.
sortByScore
,
disregardIgnoreFiles
:
this
.
config
.
disregardIgnoreFiles
,
disregardGlobalIgnoreFiles
:
this
.
config
.
disregardGlobalIgnoreFiles
,
ignoreSymlinks
:
this
.
config
.
ignoreSymlinks
,
maxFileSize
:
this
.
config
.
maxFilesize
,
previewOptions
:
this
.
config
.
previewOptions
};
query
.
folderQueries
=
this
.
config
.
folderQueries
.
map
(
fq
=>
<
IFolderQuery
>
{
disregardGlobalIgnoreFiles
:
fq
.
disregardGlobalIgnoreFiles
,
disregardIgnoreFiles
:
fq
.
disregardIgnoreFiles
,
excludePattern
:
fq
.
excludePattern
,
fileEncoding
:
fq
.
fileEncoding
,
folder
:
URI
.
file
(
fq
.
folder
),
includePattern
:
fq
.
includePattern
});
const
pretendOutputChannel
=
{
appendLine
(
msg
)
{
onMessage
(
msg
);
}
};
const
textSearchEngine
=
new
TextSearchEngine
(
this
.
config
.
contentPattern
,
query
,
new
RipgrepTextSearchEngine
(
pretendOutputChannel
),
extfs
);
textSearchEngine
.
search
(
matches
=>
{
onResult
(
matches
.
map
(
fileMatchToSerialized
));
},
token
)
.
then
(()
=>
done
(
null
,
{
limitHit
:
false
,
stats
:
null
,
type
:
'
success
'
}));
}
}
function
fileMatchToSerialized
(
match
:
IFileMatch
):
ISerializedFileMatch
{
return
{
path
:
match
.
resource
.
fsPath
,
matches
:
match
.
matches
};
}
\ No newline at end of file
src/vs/workbench/services/search/node/textSearchEngine.ts
0 → 100644
浏览文件 @
c26d6e6c
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import
*
as
path
from
'
path
'
;
import
{
CancellationToken
,
CancellationTokenSource
}
from
'
vs/base/common/cancellation
'
;
import
{
toErrorMessage
}
from
'
vs/base/common/errorMessage
'
;
import
*
as
glob
from
'
vs/base/common/glob
'
;
import
*
as
resources
from
'
vs/base/common/resources
'
;
import
{
URI
}
from
'
vs/base/common/uri
'
;
import
{
TPromise
}
from
'
vs/base/common/winjs.base
'
;
import
*
as
extfs
from
'
vs/base/node/extfs
'
;
import
{
IFileMatch
,
IFolderQuery
,
IPatternInfo
,
ISearchCompleteStats
,
ISearchQuery
,
ITextSearchResult
}
from
'
vs/platform/search/common/search
'
;
import
*
as
vscode
from
'
vscode
'
;
import
{
QueryGlobTester
,
resolvePatternsForProvider
}
from
'
vs/workbench/services/search/node/search
'
;
export
class
TextSearchEngine
{
private
collector
:
TextSearchResultsCollector
;
private
isLimitHit
:
boolean
;
private
resultCount
=
0
;
constructor
(
private
pattern
:
IPatternInfo
,
private
config
:
ISearchQuery
,
private
provider
:
vscode
.
TextSearchProvider
,
private
_extfs
:
typeof
extfs
)
{
}
public
search
(
onProgress
:
(
matches
:
IFileMatch
[])
=>
void
,
token
:
CancellationToken
):
TPromise
<
ISearchCompleteStats
>
{
const
folderQueries
=
this
.
config
.
folderQueries
;
const
tokenSource
=
new
CancellationTokenSource
();
token
.
onCancellationRequested
(()
=>
tokenSource
.
cancel
());
return
new
TPromise
<
ISearchCompleteStats
>
((
resolve
,
reject
)
=>
{
this
.
collector
=
new
TextSearchResultsCollector
(
onProgress
);
let
isCanceled
=
false
;
const
onResult
=
(
match
:
vscode
.
TextSearchResult
,
folderIdx
:
number
)
=>
{
if
(
isCanceled
)
{
return
;
}
if
(
this
.
resultCount
>=
this
.
config
.
maxResults
)
{
this
.
isLimitHit
=
true
;
isCanceled
=
true
;
tokenSource
.
cancel
();
}
if
(
!
this
.
isLimitHit
)
{
this
.
resultCount
++
;
this
.
collector
.
add
(
match
,
folderIdx
);
}
};
// For each root folder
TPromise
.
join
(
folderQueries
.
map
((
fq
,
i
)
=>
{
return
this
.
searchInFolder
(
fq
,
r
=>
onResult
(
r
,
i
),
tokenSource
.
token
);
})).
then
(
results
=>
{
tokenSource
.
dispose
();
this
.
collector
.
flush
();
const
someFolderHitLImit
=
results
.
some
(
result
=>
result
&&
result
.
limitHit
);
resolve
({
limitHit
:
this
.
isLimitHit
||
someFolderHitLImit
,
stats
:
{
type
:
'
textSearchProvider
'
}
});
},
(
errs
:
Error
[])
=>
{
tokenSource
.
dispose
();
const
errMsg
=
errs
.
map
(
err
=>
toErrorMessage
(
err
))
.
filter
(
msg
=>
!!
msg
)[
0
];
reject
(
new
Error
(
errMsg
));
});
});
}
private
searchInFolder
(
folderQuery
:
IFolderQuery
<
URI
>
,
onResult
:
(
result
:
vscode
.
TextSearchResult
)
=>
void
,
token
:
CancellationToken
):
TPromise
<
vscode
.
TextSearchComplete
>
{
const
queryTester
=
new
QueryGlobTester
(
this
.
config
,
folderQuery
);
const
testingPs
=
[];
const
progress
=
{
report
:
(
result
:
vscode
.
TextSearchResult
)
=>
{
const
hasSibling
=
folderQuery
.
folder
.
scheme
===
'
file
'
&&
glob
.
hasSiblingPromiseFn
(()
=>
{
return
this
.
readdir
(
path
.
dirname
(
result
.
uri
.
fsPath
));
});
const
relativePath
=
path
.
relative
(
folderQuery
.
folder
.
fsPath
,
result
.
uri
.
fsPath
);
testingPs
.
push
(
queryTester
.
includedInQuery
(
relativePath
,
path
.
basename
(
relativePath
),
hasSibling
)
.
then
(
included
=>
{
if
(
included
)
{
onResult
(
result
);
}
}));
}
};
const
searchOptions
=
this
.
getSearchOptionsForFolder
(
folderQuery
);
return
new
TPromise
(
resolve
=>
process
.
nextTick
(
resolve
))
.
then
(()
=>
this
.
provider
.
provideTextSearchResults
(
patternInfoToQuery
(
this
.
pattern
),
searchOptions
,
progress
,
token
))
.
then
(
result
=>
{
return
TPromise
.
join
(
testingPs
)
.
then
(()
=>
result
);
});
}
private
readdir
(
dirname
:
string
):
TPromise
<
string
[]
>
{
return
new
TPromise
((
resolve
,
reject
)
=>
{
this
.
_extfs
.
readdir
(
dirname
,
(
err
,
files
)
=>
{
if
(
err
)
{
return
reject
(
err
);
}
resolve
(
files
);
});
});
}
private
getSearchOptionsForFolder
(
fq
:
IFolderQuery
<
URI
>
):
vscode
.
TextSearchOptions
{
const
includes
=
resolvePatternsForProvider
(
this
.
config
.
includePattern
,
fq
.
includePattern
);
const
excludes
=
resolvePatternsForProvider
(
this
.
config
.
excludePattern
,
fq
.
excludePattern
);
return
{
folder
:
URI
.
from
(
fq
.
folder
),
excludes
,
includes
,
useIgnoreFiles
:
!
this
.
config
.
disregardIgnoreFiles
,
useGlobalIgnoreFiles
:
!
this
.
config
.
disregardGlobalIgnoreFiles
,
followSymlinks
:
!
this
.
config
.
ignoreSymlinks
,
encoding
:
this
.
config
.
fileEncoding
,
maxFileSize
:
this
.
config
.
maxFileSize
,
maxResults
:
this
.
config
.
maxResults
,
previewOptions
:
this
.
config
.
previewOptions
};
}
}
function
patternInfoToQuery
(
patternInfo
:
IPatternInfo
):
vscode
.
TextSearchQuery
{
return
<
vscode
.
TextSearchQuery
>
{
isCaseSensitive
:
patternInfo
.
isCaseSensitive
||
false
,
isRegExp
:
patternInfo
.
isRegExp
||
false
,
isWordMatch
:
patternInfo
.
isWordMatch
||
false
,
isMultiline
:
patternInfo
.
isMultiline
||
false
,
pattern
:
patternInfo
.
pattern
};
}
export
class
TextSearchResultsCollector
{
private
_batchedCollector
:
BatchedCollector
<
IFileMatch
>
;
private
_currentFolderIdx
:
number
;
private
_currentUri
:
URI
;
private
_currentFileMatch
:
IFileMatch
;
constructor
(
private
_onResult
:
(
result
:
IFileMatch
[])
=>
void
)
{
this
.
_batchedCollector
=
new
BatchedCollector
<
IFileMatch
>
(
512
,
items
=>
this
.
sendItems
(
items
));
}
add
(
data
:
vscode
.
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.
if
(
this
.
_currentFileMatch
&&
(
this
.
_currentFolderIdx
!==
folderIdx
||
!
resources
.
isEqual
(
this
.
_currentUri
,
data
.
uri
)))
{
this
.
pushToCollector
();
this
.
_currentFileMatch
=
null
;
}
if
(
!
this
.
_currentFileMatch
)
{
this
.
_currentFolderIdx
=
folderIdx
;
this
.
_currentFileMatch
=
{
resource
:
data
.
uri
,
matches
:
[]
};
}
this
.
_currentFileMatch
.
matches
.
push
(
extensionResultToFrontendResult
(
data
));
}
private
pushToCollector
():
void
{
const
size
=
this
.
_currentFileMatch
?
this
.
_currentFileMatch
.
matches
.
length
:
0
;
this
.
_batchedCollector
.
addItem
(
this
.
_currentFileMatch
,
size
);
}
flush
():
void
{
this
.
pushToCollector
();
this
.
_batchedCollector
.
flush
();
}
private
sendItems
(
items
:
IFileMatch
[]):
void
{
this
.
_onResult
(
items
);
}
}
function
extensionResultToFrontendResult
(
data
:
vscode
.
TextSearchResult
):
ITextSearchResult
{
// Warning: result from RipgrepTextSearchEH has fake vscode.Range. Don't depend on any other props beyond these...
return
{
preview
:
{
match
:
{
startLineNumber
:
data
.
preview
.
match
.
start
.
line
,
startColumn
:
data
.
preview
.
match
.
start
.
character
,
endLineNumber
:
data
.
preview
.
match
.
end
.
line
,
endColumn
:
data
.
preview
.
match
.
end
.
character
},
text
:
data
.
preview
.
text
},
range
:
{
startLineNumber
:
data
.
range
.
start
.
line
,
startColumn
:
data
.
range
.
start
.
character
,
endLineNumber
:
data
.
range
.
end
.
line
,
endColumn
:
data
.
range
.
end
.
character
}
};
}
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
* But after that point, the callback is called with batches of maxBatchSize.
* If the batch isn't filled within some time, the callback is also called.
*/
export
class
BatchedCollector
<
T
>
{
private
static
readonly
TIMEOUT
=
4000
;
// After START_BATCH_AFTER_COUNT items have been collected, stop flushing on timeout
private
static
readonly
START_BATCH_AFTER_COUNT
=
50
;
private
totalNumberCompleted
=
0
;
private
batch
:
T
[]
=
[];
private
batchSize
=
0
;
private
timeoutHandle
:
any
;
constructor
(
private
maxBatchSize
:
number
,
private
cb
:
(
items
:
T
[])
=>
void
)
{
}
addItem
(
item
:
T
,
size
:
number
):
void
{
if
(
!
item
)
{
return
;
}
this
.
addItemToBatch
(
item
,
size
);
}
addItems
(
items
:
T
[],
size
:
number
):
void
{
if
(
!
items
)
{
return
;
}
this
.
addItemsToBatch
(
items
,
size
);
}
private
addItemToBatch
(
item
:
T
,
size
:
number
):
void
{
this
.
batch
.
push
(
item
);
this
.
batchSize
+=
size
;
this
.
onUpdate
();
}
private
addItemsToBatch
(
item
:
T
[],
size
:
number
):
void
{
this
.
batch
=
this
.
batch
.
concat
(
item
);
this
.
batchSize
+=
size
;
this
.
onUpdate
();
}
private
onUpdate
():
void
{
if
(
this
.
totalNumberCompleted
<
BatchedCollector
.
START_BATCH_AFTER_COUNT
)
{
// Flush because we aren't batching yet
this
.
flush
();
}
else
if
(
this
.
batchSize
>=
this
.
maxBatchSize
)
{
// Flush because the batch is full
this
.
flush
();
}
else
if
(
!
this
.
timeoutHandle
)
{
// No timeout running, start a timeout to flush
this
.
timeoutHandle
=
setTimeout
(()
=>
{
this
.
flush
();
},
BatchedCollector
.
TIMEOUT
);
}
}
flush
():
void
{
if
(
this
.
batchSize
)
{
this
.
totalNumberCompleted
+=
this
.
batchSize
;
this
.
cb
(
this
.
batch
);
this
.
batch
=
[];
this
.
batchSize
=
0
;
if
(
this
.
timeoutHandle
)
{
clearTimeout
(
this
.
timeoutHandle
);
this
.
timeoutHandle
=
0
;
}
}
}
}
src/vs/workbench/services/search/test/node/ripgrepFileSearch.test.ts
0 → 100644
浏览文件 @
c26d6e6c
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import
*
as
assert
from
'
assert
'
;
import
*
as
platform
from
'
vs/base/common/platform
'
;
import
{
fixDriveC
,
getAbsoluteGlob
}
from
'
vs/workbench/services/search/node/ripgrepFileSearch
'
;
suite
(
'
RipgrepFileSearch - etc
'
,
()
=>
{
function
testGetAbsGlob
(
params
:
string
[]):
void
{
const
[
folder
,
glob
,
expectedResult
]
=
params
;
assert
.
equal
(
fixDriveC
(
getAbsoluteGlob
(
folder
,
glob
)),
expectedResult
,
JSON
.
stringify
(
params
));
}
test
(
'
getAbsoluteGlob_win
'
,
()
=>
{
if
(
!
platform
.
isWindows
)
{
return
;
}
[
[
'
C:/foo/bar
'
,
'
glob/**
'
,
'
/foo
\\
bar
\\
glob
\\
**
'
],
[
'
c:/
'
,
'
glob/**
'
,
'
/glob
\\
**
'
],
[
'
C:
\\
foo
\\
bar
'
,
'
glob
\\
**
'
,
'
/foo
\\
bar
\\
glob
\\
**
'
],
[
'
c:
\\
foo
\\
bar
'
,
'
glob
\\
**
'
,
'
/foo
\\
bar
\\
glob
\\
**
'
],
[
'
c:
\\
'
,
'
glob
\\
**
'
,
'
/glob
\\
**
'
],
[
'
\\\\
localhost
\\
c$
\\
foo
\\
bar
'
,
'
glob/**
'
,
'
\\\\
localhost
\\
c$
\\
foo
\\
bar
\\
glob
\\
**
'
],
// absolute paths are not resolved further
[
'
c:/foo/bar
'
,
'
/path/something
'
,
'
/path/something
'
],
[
'
c:/foo/bar
'
,
'
c:
\\
project
\\
folder
'
,
'
/project
\\
folder
'
]
].
forEach
(
testGetAbsGlob
);
});
test
(
'
getAbsoluteGlob_posix
'
,
()
=>
{
if
(
platform
.
isWindows
)
{
return
;
}
[
[
'
/foo/bar
'
,
'
glob/**
'
,
'
/foo/bar/glob/**
'
],
[
'
/
'
,
'
glob/**
'
,
'
/glob/**
'
],
// absolute paths are not resolved further
[
'
/
'
,
'
/project/folder
'
,
'
/project/folder
'
],
].
forEach
(
testGetAbsGlob
);
});
});
src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts
浏览文件 @
c26d6e6c
...
...
@@ -4,46 +4,23 @@
*--------------------------------------------------------------------------------------------*/
import
*
as
assert
from
'
assert
'
;
import
*
as
platform
from
'
vs/base/common/platform
'
;
import
{
fixDriveC
,
fixRegexEndingPattern
,
getAbsoluteGlob
}
from
'
vs/workbench/services/search/node/ripgrepTextSearch
'
;
import
{
fixRegexEndingPattern
}
from
'
vs/workbench/services/search/node/ripgrepTextSearchEH
'
;
suite
(
'
RipgrepTextSearch - etc
'
,
()
=>
{
function
testGetAbsGlob
(
params
:
string
[]):
void
{
const
[
folder
,
glob
,
expectedResult
]
=
params
;
assert
.
equal
(
fixDriveC
(
getAbsoluteGlob
(
folder
,
glob
)),
expectedResult
,
JSON
.
stringify
(
params
));
}
test
(
'
getAbsoluteGlob_win
'
,
()
=>
{
if
(
!
platform
.
isWindows
)
{
return
;
}
[
[
'
C:/foo/bar
'
,
'
glob/**
'
,
'
/foo
\\
bar
\\
glob
\\
**
'
],
[
'
c:/
'
,
'
glob/**
'
,
'
/glob
\\
**
'
],
[
'
C:
\\
foo
\\
bar
'
,
'
glob
\\
**
'
,
'
/foo
\\
bar
\\
glob
\\
**
'
],
[
'
c:
\\
foo
\\
bar
'
,
'
glob
\\
**
'
,
'
/foo
\\
bar
\\
glob
\\
**
'
],
[
'
c:
\\
'
,
'
glob
\\
**
'
,
'
/glob
\\
**
'
],
[
'
\\\\
localhost
\\
c$
\\
foo
\\
bar
'
,
'
glob/**
'
,
'
\\\\
localhost
\\
c$
\\
foo
\\
bar
\\
glob
\\
**
'
],
// absolute paths are not resolved further
[
'
c:/foo/bar
'
,
'
/path/something
'
,
'
/path/something
'
],
[
'
c:/foo/bar
'
,
'
c:
\\
project
\\
folder
'
,
'
/project
\\
folder
'
]
].
forEach
(
testGetAbsGlob
);
});
test
(
'
getAbsoluteGlob_posix
'
,
()
=>
{
if
(
platform
.
isWindows
)
{
return
;
test
(
'
fixRegexEndingPattern
'
,
()
=>
{
function
testFixRegexEndingPattern
([
input
,
expectedResult
]:
string
[]):
void
{
assert
.
equal
(
fixRegexEndingPattern
(
input
),
expectedResult
);
}
[
[
'
/foo/bar
'
,
'
glob/**
'
,
'
/foo/bar/glob/**
'
],
[
'
/
'
,
'
glob/**
'
,
'
/glob/**
'
],
// absolute paths are not resolved further
[
'
/
'
,
'
/project/folder
'
,
'
/project/folder
'
],
].
forEach
(
testGetAbsGlob
);
[
'
foo
'
,
'
foo
'
],
[
''
,
''
],
[
'
^foo.*bar
\\
s+
'
,
'
^foo.*bar
\\
s+
'
],
[
'
foo$
'
,
'
foo
\\
r?$
'
],
[
'
$
'
,
'
\\
r?$
'
],
[
'
foo
\\
$
'
,
'
foo
\\
$
'
],
[
'
foo
\\\\
$
'
,
'
foo
\\\\\\
r?$
'
],
].
forEach
(
testFixRegexEndingPattern
);
});
test
(
'
fixRegexEndingPattern
'
,
()
=>
{
...
...
src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts
浏览文件 @
c26d6e6c
...
...
@@ -11,9 +11,10 @@ import { TPromise } from 'vs/base/common/winjs.base';
import
{
FileWalker
}
from
'
vs/workbench/services/search/node/fileSearch
'
;
import
{
ISerializedFileMatch
,
IRawSearch
,
IFolderSearch
}
from
'
vs/workbench/services/search/node/search
'
;
import
{
Engine
as
TextSearchEngine
}
from
'
vs/workbench/services/search/node/textSearch
'
;
import
{
RipgrepEngine
}
from
'
vs/workbench/services/search/node/ripgrepTextSearch
'
;
import
{
TextSearchEngineAdapter
}
from
'
vs/workbench/services/search/node/textSearchAdapter
'
;
import
{
TextSearchWorkerProvider
}
from
'
vs/workbench/services/search/node/textSearchWorkerProvider
'
;
import
{
getPathFromAmdModule
}
from
'
vs/base/common/amd
'
;
import
{
CancellationTokenSource
}
from
'
vs/base/common/cancellation
'
;
function
countAll
(
matches
:
ISerializedFileMatch
[]):
number
{
return
matches
.
reduce
((
acc
,
m
)
=>
acc
+
m
.
numMatches
,
0
);
...
...
@@ -62,12 +63,12 @@ function doLegacySearchTest(config: IRawSearch, expectedResultCount: number | Fu
function
doRipgrepSearchTest
(
config
:
IRawSearch
,
expectedResultCount
:
number
|
Function
):
TPromise
<
void
>
{
return
new
TPromise
<
void
>
((
resolve
,
reject
)
=>
{
let
engine
=
new
RipgrepEngine
(
config
);
let
engine
=
new
TextSearchEngineAdapter
(
config
);
let
c
=
0
;
engine
.
search
(
(
result
)
=>
{
if
(
result
)
{
c
+=
result
.
numMatches
;
engine
.
search
(
new
CancellationTokenSource
().
token
,
(
results
)
=>
{
if
(
result
s
)
{
c
+=
result
s
.
reduce
((
acc
,
cur
)
=>
acc
+
cur
.
numMatches
,
0
)
;
}
},
()
=>
{
},
(
error
)
=>
{
try
{
...
...
@@ -75,7 +76,7 @@ function doRipgrepSearchTest(config: IRawSearch, expectedResultCount: number | F
if
(
typeof
expectedResultCount
===
'
function
'
)
{
assert
(
expectedResultCount
(
c
));
}
else
{
assert
.
equal
(
c
,
expectedResultCount
,
'
rg
'
);
assert
.
equal
(
c
,
expectedResultCount
,
`rg
${
c
}
!==
${
expectedResultCount
}
`
);
}
}
catch
(
e
)
{
reject
(
e
);
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录