@@ -2,7 +2,23 @@ import * as vscode from "vscode";
22import { SearchResult , SearchMatch } from "../../api/atelier" ;
33import { AtelierAPI } from "../../api" ;
44import { 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+ }
622
723export class TextSearchProvider implements vscode . TextSearchProvider {
824 /**
@@ -21,67 +37,196 @@ export class TextSearchProvider implements vscode.TextSearchProvider {
2137 const api = new AtelierAPI ( options . folder ) ;
2238 let counter = 0 ;
2339 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 ;
2549 }
2650 return api
2751 . actionSearch ( {
2852 query : query . pattern ,
2953 regex : query . isRegExp ,
3054 word : query . isWordMatch ,
3155 case : query . isCaseSensitive ,
56+ files : fileSpecFromURI ( options . folder ) ,
3257 // 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
3459 max : options . maxResults ?? 100000 ,
3560 } )
3661 . 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 ( ) ;
6771 }
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 ) ) ;
73209 } ) ;
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+ } ;
81221 } )
82222 . 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+ } ;
85230 } ) ;
86231 }
87232}
0 commit comments