@@ -2,7 +2,23 @@ import * as vscode from "vscode";
2
2
import { SearchResult , SearchMatch } from "../../api/atelier" ;
3
3
import { AtelierAPI } from "../../api" ;
4
4
import { DocumentContentProvider } from "../DocumentContentProvider" ;
5
- import { outputChannel } from "../../utils" ;
5
+ import { notNull , outputChannel , throttleRequests } from "../../utils" ;
6
+ import { config } from "../../extension" ;
7
+ import { fileSpecFromURI } from "../../utils/FileProviderUtil" ;
8
+
9
+ /**
10
+ * Convert an `attrline` in a description to a line number in `document`.
11
+ */
12
+ function descLineToDocLine ( content : string [ ] , attrline : number , line : number ) : number {
13
+ let result = 0 ;
14
+ for ( let i = line - 1 ; i >= 0 ; i -- ) {
15
+ if ( ! content [ i ] . startsWith ( "///" ) ) {
16
+ result = i ;
17
+ break ;
18
+ }
19
+ }
20
+ return result + attrline ;
21
+ }
6
22
7
23
export class TextSearchProvider implements vscode . TextSearchProvider {
8
24
/**
@@ -21,67 +37,196 @@ export class TextSearchProvider implements vscode.TextSearchProvider {
21
37
const api = new AtelierAPI ( options . folder ) ;
22
38
let counter = 0 ;
23
39
if ( ! api . enabled ) {
24
- return null ;
40
+ return {
41
+ message : {
42
+ text : "An active server connection is required for searching `isfs` folders." ,
43
+ type : vscode . TextSearchCompleteMessageType . Warning ,
44
+ } ,
45
+ } ;
46
+ }
47
+ if ( token . isCancellationRequested ) {
48
+ return ;
25
49
}
26
50
return api
27
51
. actionSearch ( {
28
52
query : query . pattern ,
29
53
regex : query . isRegExp ,
30
54
word : query . isWordMatch ,
31
55
case : query . isCaseSensitive ,
56
+ files : fileSpecFromURI ( options . folder ) ,
32
57
// If options.maxResults is null the search is supposed to return an unlimited number of results
33
- // Since there's no way for us to pass "unlimited" to the server, I choose a very large number
58
+ // Since there's no way for us to pass "unlimited" to the server, I chose a very large number
34
59
max : options . maxResults ?? 100000 ,
35
60
} )
36
61
. then ( ( data ) => data . result )
37
- . then ( ( files : SearchResult [ ] ) =>
38
- files . map ( async ( file ) => {
39
- const fileName = file . doc ;
40
- const uri = DocumentContentProvider . getUri ( fileName , "" , "" , true , options . folder ) ;
41
- try {
42
- const document = await vscode . workspace . openTextDocument ( uri ) ;
43
- return {
44
- ...file ,
45
- uri,
46
- document,
47
- } ;
48
- } catch ( _ex ) {
49
- return null ;
50
- }
51
- } )
52
- )
53
- . then ( ( files ) => Promise . all ( files ) )
54
- . then ( ( files ) => {
55
- files . forEach ( ( file ) => {
56
- const { uri, document, matches } = file ;
57
- matches . forEach ( ( match : SearchMatch ) => {
58
- const { text, member } = match ;
59
- let { line } = match ;
60
- if ( member ) {
61
- const memberMatchPattern = new RegExp ( `((?:Class)?Method|Property|XData|Query|Trigger) ${ member } ` , "i" ) ;
62
- for ( let i = 0 ; i < document . lineCount ; i ++ ) {
63
- const text = document . lineAt ( i ) . text ;
64
- if ( text . match ( memberMatchPattern ) ) {
65
- line = line ? line + i + 1 : i ;
66
- }
62
+ . then ( async ( files : SearchResult [ ] ) => {
63
+ if ( token . isCancellationRequested ) {
64
+ return ;
65
+ }
66
+ const result = await Promise . allSettled (
67
+ files . map (
68
+ throttleRequests ( async ( file : SearchResult ) => {
69
+ if ( token . isCancellationRequested ) {
70
+ throw new vscode . CancellationError ( ) ;
67
71
}
68
- }
69
- progress . report ( {
70
- uri,
71
- lineNumber : line || 1 ,
72
- text,
72
+ const uri = DocumentContentProvider . getUri ( file . doc , "" , "" , true , options . folder ) ;
73
+ const content = await api . getDoc ( file . doc ) . then ( ( data ) => < string [ ] > data . result . content ) ;
74
+ // Find all lines that we have matches on
75
+ const lines = file . matches
76
+ . map ( ( match : SearchMatch ) => {
77
+ let line = Number ( match . line ) ;
78
+ if ( match . member !== undefined ) {
79
+ // This is an attribute of a class member
80
+ const memberMatchPattern = new RegExp (
81
+ `^((?:Class|Client)?Method|Property|XData|Query|Trigger|Parameter|Relationship|Index|ForeignKey|Storage|Projection) ${ match . member } `
82
+ ) ;
83
+ for ( let i = 0 ; i < content . length ; i ++ ) {
84
+ if ( content [ i ] . match ( memberMatchPattern ) ) {
85
+ let memend = i + 1 ;
86
+ if (
87
+ config ( "multilineMethodArgs" , api . configName ) &&
88
+ content [ i ] . match ( / ^ (?: C l a s s | C l i e n t ) ? M e t h o d | Q u e r y / )
89
+ ) {
90
+ // The class member definition is on multiple lines so update the end
91
+ for ( let j = i + 1 ; j < content . length ; j ++ ) {
92
+ if ( content [ j ] . trim ( ) === "{" ) {
93
+ memend = j ;
94
+ break ;
95
+ }
96
+ }
97
+ }
98
+ if ( match . attr === undefined ) {
99
+ if ( match . line === undefined ) {
100
+ // This is in the class member definition
101
+ line = i ;
102
+ } else {
103
+ // This is in the implementation
104
+ line = memend + Number ( match . line ) ;
105
+ }
106
+ } else {
107
+ if ( match . attrline === undefined ) {
108
+ // This is in the class member definition
109
+ line = 1 ;
110
+ } else {
111
+ if ( match . attr === "Description" ) {
112
+ // This is in the description
113
+ line = descLineToDocLine ( content , match . attrline , i ) ;
114
+ } else {
115
+ // This is in the implementation
116
+ line = memend + match . attrline ;
117
+ }
118
+ }
119
+ }
120
+ break ;
121
+ }
122
+ }
123
+ } else if ( match . attr !== undefined ) {
124
+ if ( match . attr === "IncludeCode" ) {
125
+ // This is in the Include line
126
+ for ( let i = 0 ; i < content . length ; i ++ ) {
127
+ if ( content [ i ] . match ( / ^ I n c l u d e / ) ) {
128
+ line = i ;
129
+ break ;
130
+ }
131
+ }
132
+ } else if ( match . attr === "IncludeGenerator" ) {
133
+ // This is in the IncludeGenerator line
134
+ for ( let i = 0 ; i < content . length ; i ++ ) {
135
+ if ( content [ i ] . match ( / ^ I n c l u d e G e n e r a t o r / ) ) {
136
+ line = i ;
137
+ break ;
138
+ }
139
+ }
140
+ } else if ( match . attr === "Import" ) {
141
+ // This is in the Import line
142
+ for ( let i = 0 ; i < content . length ; i ++ ) {
143
+ if ( content [ i ] . match ( / ^ I m p o r t / ) ) {
144
+ line = i ;
145
+ break ;
146
+ }
147
+ }
148
+ } else {
149
+ // This is in the class definition
150
+ const classMatchPattern = new RegExp ( `^Class ${ file . doc . slice ( 0 , file . doc . lastIndexOf ( "." ) ) } ` ) ;
151
+ for ( let i = 0 ; i < content . length ; i ++ ) {
152
+ if ( content [ i ] . match ( classMatchPattern ) ) {
153
+ if ( match . attrline ) {
154
+ // This is in the class description
155
+ line = descLineToDocLine ( content , match . attrline , i ) ;
156
+ } else {
157
+ line = i ;
158
+ }
159
+ break ;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ return typeof line === "number" ? line : null ;
165
+ } )
166
+ . filter ( notNull ) ;
167
+ // Filter out duplicates and compute all matches for each one
168
+ [ ...new Set ( lines ) ] . forEach ( ( line ) => {
169
+ const text = content [ line ] ;
170
+ const regex = new RegExp (
171
+ query . isRegExp ? query . pattern : query . pattern . replace ( / [ - [ \] { } ( ) * + ? . , \\ ^ $ | # \s ] / g, "\\$&" ) ,
172
+ query . isCaseSensitive ? "g" : "gi"
173
+ ) ;
174
+ let regexMatch : RegExpExecArray ;
175
+ const matchRanges : vscode . Range [ ] = [ ] ;
176
+ const previewRanges : vscode . Range [ ] = [ ] ;
177
+ while ( ( regexMatch = regex . exec ( text ) ) !== null && counter < options . maxResults ) {
178
+ const start = regexMatch . index ;
179
+ const end = start + regexMatch [ 0 ] . length ;
180
+ matchRanges . push ( new vscode . Range ( line , start , line , end ) ) ;
181
+ previewRanges . push ( new vscode . Range ( 0 , start , 0 , end ) ) ;
182
+ counter ++ ;
183
+ }
184
+ if ( matchRanges . length && previewRanges . length ) {
185
+ progress . report ( {
186
+ uri,
187
+ ranges : matchRanges ,
188
+ preview : {
189
+ text,
190
+ matches : previewRanges ,
191
+ } ,
192
+ } ) ;
193
+ }
194
+ } ) ;
195
+ } )
196
+ )
197
+ ) ;
198
+ if ( token . isCancellationRequested ) {
199
+ return ;
200
+ }
201
+ let message : vscode . TextSearchCompleteMessage ;
202
+ const rejected = result . filter ( ( r ) => r . status == "rejected" ) . length ;
203
+ if ( rejected > 0 ) {
204
+ outputChannel . appendLine ( "Search errors:" ) ;
205
+ result
206
+ . filter ( ( r ) => r . status == "rejected" )
207
+ . forEach ( ( r : PromiseRejectedResult ) => {
208
+ outputChannel . appendLine ( typeof r . reason == "object" ? r . reason . toString ( ) : String ( r . reason ) ) ;
73
209
} ) ;
74
- counter ++ ;
75
- if ( counter >= options . maxResults ) {
76
- return ;
77
- }
78
- } ) ;
79
- } ) ;
80
- return { limitHit : counter >= options . maxResults } ;
210
+ message = {
211
+ text : `Failed to display results from ${ rejected } file${
212
+ rejected > 1 ? "s" : ""
213
+ } . Check \`ObjectScript\` Output channel for details.`,
214
+ type : vscode . TextSearchCompleteMessageType . Warning ,
215
+ } ;
216
+ }
217
+ return {
218
+ limitHit : counter >= options . maxResults ,
219
+ message,
220
+ } ;
81
221
} )
82
222
. catch ( ( error ) => {
83
- outputChannel . appendLine ( error ) ;
84
- return null ;
223
+ outputChannel . appendLine ( typeof error == "object" ? error . toString ( ) : String ( error ) ) ;
224
+ return {
225
+ message : {
226
+ text : "An error occurred during the search. Check `ObjectScript` Output channel for details." ,
227
+ type : vscode . TextSearchCompleteMessageType . Warning ,
228
+ } ,
229
+ } ;
85
230
} ) ;
86
231
}
87
232
}
0 commit comments