@@ -8,10 +8,17 @@ export interface RequiredCompletionParts {
88
99export interface ValidationError {
1010 message : string ;
11+ line : number ;
1112 startColumn : number ;
1213 endColumn : number ;
1314}
1415
16+ interface Token {
17+ text : string ;
18+ line : number ;
19+ column : number ;
20+ }
21+
1522export type WordCompletion = RequiredCompletionParts & Partial < monaco . languages . CompletionItem > ;
1623
1724export interface AbstractWord {
@@ -89,138 +96,193 @@ export class NegatableWord implements AbstractWord {
8996}
9097
9198export class AutoCompleteTree {
92- private content : string [ ] ;
93- /** value matches the start column of the value at the same index in content */
94- private startColumns : number [ ] ;
95- private length : number ;
96-
97- constructor ( private roots : AutoCompleteNode [ ] ) {
98- this . content = [ ] ;
99- this . startColumns = [ ] ;
100- this . length = 0 ;
101- }
99+ constructor ( private roots : AutoCompleteNode [ ] ) { }
102100
103- /**
104- * Sets the content of the tree for the next analyzing cycle
105- */
106- private setContent ( line : string ) {
107- if ( ! line ) {
108- line = "" ;
109- }
110- if ( line . length == 0 ) {
111- this . content = [ ] ;
112- this . length = 0 ;
113- return ;
101+ private tokenize ( text : string [ ] ) : Token [ ] {
102+ if ( ! text || text . length == 0 ) {
103+ return [ ] ;
114104 }
115- this . content = line . split ( " " ) ;
116- this . startColumns = this . content . map ( ( ) => 0 ) ;
117- for ( let i = 1 ; i < this . content . length ; i ++ ) {
118- this . startColumns [ i ] = this . startColumns [ i - 1 ] + this . content [ i - 1 ] . length + 1 ;
105+
106+ const tokens : Token [ ] = [ ] ;
107+ for ( const [ lineNumber , line ] of text . entries ( ) ) {
108+ const lineTokens = line . split ( / \s + / ) . filter ( ( t ) => t . length > 0 ) ;
109+ let column = 0 ;
110+ for ( const token of lineTokens ) {
111+ column = line . indexOf ( token , column ) ;
112+ tokens . push ( {
113+ text : token ,
114+ line : lineNumber + 1 ,
115+ column : column + 1 ,
116+ } ) ;
117+ }
119118 }
120- this . length = line . length ;
119+
120+ return tokens ;
121121 }
122122
123123 /**
124124 * Checks the set content for errors
125125 * @returns An array of errors. An empty array means that the content is valid
126126 */
127- public verify ( line : string ) : ValidationError [ ] {
128- this . setContent ( line ) ;
129- return this . verifyNode ( this . roots , 0 , false ) ;
127+ public verify ( lines : string [ ] ) : ValidationError [ ] {
128+ const tokens = this . tokenize ( lines ) ;
129+ return this . verifyNode ( this . roots , tokens , 0 , false , true ) ;
130130 }
131131
132- private verifyNode ( nodes : AutoCompleteNode [ ] , index : number , comesFromFinal : boolean ) : ValidationError [ ] {
133- if ( index >= this . content . length ) {
132+ private verifyNode (
133+ nodes : AutoCompleteNode [ ] ,
134+ tokens : Token [ ] ,
135+ index : number ,
136+ comesFromFinal : boolean ,
137+ skipStartCheck = false ,
138+ ) : ValidationError [ ] {
139+ if ( index >= tokens . length ) {
134140 if ( nodes . length == 0 || comesFromFinal ) {
135141 return [ ] ;
136142 } else {
137- return [ { message : "Unexpected end of line" , startColumn : this . length - 1 , endColumn : this . length } ] ;
143+ return [
144+ {
145+ message : "Unexpected end of line" ,
146+ line : tokens [ index - 1 ] . line ,
147+ startColumn : tokens [ index - 1 ] . column + tokens [ index - 1 ] . text . length - 1 ,
148+ endColumn : tokens [ index - 1 ] . column + tokens [ index - 1 ] . text . length ,
149+ } ,
150+ ] ;
151+ }
152+ }
153+ if ( ! skipStartCheck && tokens [ index ] . column == 1 ) {
154+ const matchesAnyRoot = this . roots . some ( ( r ) => r . word . verifyWord ( tokens [ index ] . text ) . length === 0 ) ;
155+ if ( matchesAnyRoot ) {
156+ return this . verifyNode ( this . roots , tokens , index , false , true ) ;
138157 }
139158 }
140159
141160 const foundErrors : ValidationError [ ] = [ ] ;
142161 let childErrors : ValidationError [ ] = [ ] ;
143162 for ( const n of nodes ) {
144- const v = n . word . verifyWord ( this . content [ index ] ) ;
163+ const v = n . word . verifyWord ( tokens [ index ] . text ) ;
145164 if ( v . length > 0 ) {
146165 foundErrors . push ( {
147166 message : v [ 0 ] ,
148- startColumn : this . startColumns [ index ] ,
149- endColumn : this . startColumns [ index ] + this . content [ index ] . length ,
167+ startColumn : tokens [ index ] . column ,
168+ endColumn : tokens [ index ] . column + tokens [ index ] . text . length ,
169+ line : tokens [ index ] . line ,
150170 } ) ;
151171 continue ;
152172 }
153173
154- const childResult = this . verifyNode ( n . children , index + 1 , n . canBeFinal || false ) ;
174+ const childResult = this . verifyNode ( n . children , tokens , index + 1 , n . canBeFinal || false ) ;
155175 if ( childResult . length == 0 ) {
156176 return [ ] ;
157177 } else {
158178 childErrors = childErrors . concat ( childResult ) ;
159179 }
160180 }
161181 if ( childErrors . length > 0 ) {
162- return childErrors ;
182+ return deduplicateErrors ( childErrors ) ;
163183 }
164- return foundErrors ;
184+ return deduplicateErrors ( foundErrors ) ;
165185 }
166186
167187 /**
168188 * Calculates the completion options for the current content
169189 */
170- public getCompletion ( line : string , lineNumber = 1 ) : monaco . languages . CompletionItem [ ] {
171- this . setContent ( line ) ;
190+ public getCompletion ( lines : string [ ] ) : monaco . languages . CompletionItem [ ] {
191+ const tokens = this . tokenize ( lines ) ;
192+ const endsWithWhitespace =
193+ ( lines . length > 0 && lines [ lines . length - 1 ] . charAt ( lines [ lines . length - 1 ] . length - 1 ) . match ( / \s / ) ) ||
194+ lines [ lines . length - 1 ] . length == 0 ;
195+ if ( endsWithWhitespace ) {
196+ tokens . push ( {
197+ text : "" ,
198+ line : lines . length ,
199+ column : lines [ lines . length - 1 ] . length + 1 ,
200+ } ) ;
201+ }
172202 let result : WordCompletion [ ] = [ ] ;
173- if ( this . content . length == 0 ) {
203+ if ( tokens . length == 0 ) {
174204 for ( const r of this . roots ) {
175205 result = result . concat ( r . word . completionOptions ( "" ) ) ;
176206 }
177207 } else {
178- result = this . completeNode ( this . roots , 0 ) ;
208+ result = this . completeNode ( this . roots , tokens , 0 ) ;
179209 }
180- return this . transformResults ( result , lineNumber ) ;
210+ return this . transformResults ( result , tokens ) ;
181211 }
182212
183- private completeNode ( nodes : AutoCompleteNode [ ] , index : number ) : WordCompletion [ ] {
213+ private completeNode (
214+ nodes : AutoCompleteNode [ ] ,
215+ tokens : Token [ ] ,
216+ index : number ,
217+ skipStartCheck = false ,
218+ ) : WordCompletion [ ] {
219+ // check for new start
220+
221+ if ( ! skipStartCheck && tokens [ index ] . column == 1 ) {
222+ const matchesAnyRoot = this . roots . some ( ( n ) => n . word . verifyWord ( tokens [ index ] . text ) . length === 0 ) ;
223+ if ( matchesAnyRoot ) {
224+ return this . completeNode ( this . roots , tokens , index , true ) ;
225+ }
226+ }
227+
184228 let result : WordCompletion [ ] = [ ] ;
185- if ( index == this . content . length - 1 ) {
229+ if ( index == tokens . length - 1 ) {
186230 for ( const node of nodes ) {
187- result = result . concat ( node . word . completionOptions ( this . content [ index ] ) ) ;
231+ result = result . concat ( node . word . completionOptions ( tokens [ index ] . text ) ) ;
188232 }
189233 return result ;
190234 }
191235 for ( const n of nodes ) {
192- if ( ! n . word . verifyWord ( this . content [ index ] ) ) {
236+ if ( ! n . word . verifyWord ( tokens [ index ] . text ) ) {
193237 continue ;
194238 }
195- result = result . concat ( this . completeNode ( n . children , index + 1 ) ) ;
239+ result = result . concat ( this . completeNode ( n . children , tokens , index + 1 ) ) ;
196240 }
197241 return result ;
198242 }
199243
200- private transformResults ( comp : WordCompletion [ ] , lineNumber = 1 ) : monaco . languages . CompletionItem [ ] {
244+ private transformResults ( comp : WordCompletion [ ] , tokens : Token [ ] ) : monaco . languages . CompletionItem [ ] {
201245 const result : monaco . languages . CompletionItem [ ] = [ ] ;
202246 const filtered = comp . filter (
203247 ( c , idx ) => comp . findIndex ( ( c2 ) => c2 . insertText === c . insertText && c2 . kind === c . kind ) === idx ,
204248 ) ;
205249 for ( const c of filtered ) {
206- const r = this . transformResult ( c , lineNumber ) ;
250+ const r = this . transformResult ( c , tokens ) ;
207251 result . push ( r ) ;
208252 }
209253 return result ;
210254 }
211255
212- private transformResult ( comp : WordCompletion , lineNumber = 1 ) : monaco . languages . CompletionItem {
213- const wordStart = this . content . length == 0 ? 1 : this . length - this . content [ this . content . length - 1 ] . length + 1 ;
256+ private transformResult ( comp : WordCompletion , tokens : Token [ ] ) : monaco . languages . CompletionItem {
257+ const wordStart = tokens . length == 0 ? 1 : tokens [ tokens . length - 1 ] . column ;
258+ const lineNumber = tokens . length == 0 ? 1 : tokens [ tokens . length - 1 ] . line ;
214259 return {
215260 insertText : comp . insertText ,
216261 kind : comp . kind ,
217262 label : comp . label ?? comp . insertText ,
218263 insertTextRules : comp . insertTextRules ,
219- range : new monaco . Range ( lineNumber , wordStart + ( comp . startOffset ?? 0 ) , lineNumber , this . length + 1 ) ,
264+ range : new monaco . Range (
265+ lineNumber ,
266+ wordStart + ( comp . startOffset ?? 0 ) ,
267+ lineNumber ,
268+ wordStart + ( comp . startOffset ?? 0 ) + comp . insertText . length ,
269+ ) ,
220270 } ;
221271 }
222272}
223273
274+ function deduplicateErrors ( errors : ValidationError [ ] ) : ValidationError [ ] {
275+ const seen = new Set < string > ( ) ;
276+ return errors . filter ( ( error ) => {
277+ const key = `${ error . line } -${ error . startColumn } -${ error . endColumn } -${ error . message } ` ;
278+ if ( seen . has ( key ) ) {
279+ return false ;
280+ }
281+ seen . add ( key ) ;
282+ return true ;
283+ } ) ;
284+ }
285+
224286export interface AutoCompleteNode {
225287 word : AbstractWord ;
226288 children : AutoCompleteNode [ ] ;
0 commit comments