@@ -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,133 +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- public setContent ( line : string ) {
107- if ( line . length == 0 ) {
108- this . content = [ ] ;
109- this . length = 0 ;
110- return ;
111- }
112- this . content = line . split ( " " ) ;
113- this . startColumns = this . content . map ( ( ) => 0 ) ;
114- for ( let i = 1 ; i < this . content . length ; i ++ ) {
115- this . startColumns [ i ] = this . startColumns [ i - 1 ] + this . content [ i - 1 ] . length + 1 ;
116- }
117- this . length = line . length ;
101+ private tokenize ( text : string [ ] ) : Token [ ] {
102+ if ( ! text || text . length == 0 ) {
103+ return [ ] ;
104+ }
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+ }
118+ }
119+
120+ return tokens ;
118121 }
119122
120123 /**
121124 * Checks the set content for errors
122125 * @returns An array of errors. An empty array means that the content is valid
123126 */
124- public verify ( ) : ValidationError [ ] {
125- 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 ) ;
126130 }
127131
128- private verifyNode ( nodes : AutoCompleteNode [ ] , index : number , comesFromFinal : boolean ) : ValidationError [ ] {
129- 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 ) {
130140 if ( nodes . length == 0 || comesFromFinal ) {
131141 return [ ] ;
132142 } else {
133- 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 ) ;
134157 }
135158 }
136159
137160 const foundErrors : ValidationError [ ] = [ ] ;
138161 let childErrors : ValidationError [ ] = [ ] ;
139162 for ( const n of nodes ) {
140- const v = n . word . verifyWord ( this . content [ index ] ) ;
163+ const v = n . word . verifyWord ( tokens [ index ] . text ) ;
141164 if ( v . length > 0 ) {
142165 foundErrors . push ( {
143166 message : v [ 0 ] ,
144- startColumn : this . startColumns [ index ] ,
145- 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 ,
146170 } ) ;
147171 continue ;
148172 }
149173
150- const childResult = this . verifyNode ( n . children , index + 1 , n . canBeFinal || false ) ;
174+ const childResult = this . verifyNode ( n . children , tokens , index + 1 , n . canBeFinal || false ) ;
151175 if ( childResult . length == 0 ) {
152176 return [ ] ;
153177 } else {
154178 childErrors = childErrors . concat ( childResult ) ;
155179 }
156180 }
157181 if ( childErrors . length > 0 ) {
158- return childErrors ;
182+ return deduplicateErrors ( childErrors ) ;
159183 }
160- return foundErrors ;
184+ return deduplicateErrors ( foundErrors ) ;
161185 }
162186
163187 /**
164188 * Calculates the completion options for the current content
165189 */
166- public getCompletion ( ) : monaco . languages . CompletionItem [ ] {
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+ }
167202 let result : WordCompletion [ ] = [ ] ;
168- if ( this . content . length == 0 ) {
203+ if ( tokens . length == 0 ) {
169204 for ( const r of this . roots ) {
170205 result = result . concat ( r . word . completionOptions ( "" ) ) ;
171206 }
172207 } else {
173- result = this . completeNode ( this . roots , 0 ) ;
208+ result = this . completeNode ( this . roots , tokens , 0 ) ;
174209 }
175- return this . transformResults ( result ) ;
210+ return this . transformResults ( result , tokens ) ;
176211 }
177212
178- 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+
179228 let result : WordCompletion [ ] = [ ] ;
180- if ( index == this . content . length - 1 ) {
229+ if ( index == tokens . length - 1 ) {
181230 for ( const node of nodes ) {
182- result = result . concat ( node . word . completionOptions ( this . content [ index ] ) ) ;
231+ result = result . concat ( node . word . completionOptions ( tokens [ index ] . text ) ) ;
183232 }
184233 return result ;
185234 }
186235 for ( const n of nodes ) {
187- if ( ! n . word . verifyWord ( this . content [ index ] ) ) {
236+ if ( ! n . word . verifyWord ( tokens [ index ] . text ) ) {
188237 continue ;
189238 }
190- result = result . concat ( this . completeNode ( n . children , index + 1 ) ) ;
239+ result = result . concat ( this . completeNode ( n . children , tokens , index + 1 ) ) ;
191240 }
192241 return result ;
193242 }
194243
195- private transformResults ( comp : WordCompletion [ ] ) : monaco . languages . CompletionItem [ ] {
244+ private transformResults ( comp : WordCompletion [ ] , tokens : Token [ ] ) : monaco . languages . CompletionItem [ ] {
196245 const result : monaco . languages . CompletionItem [ ] = [ ] ;
197246 const filtered = comp . filter (
198247 ( c , idx ) => comp . findIndex ( ( c2 ) => c2 . insertText === c . insertText && c2 . kind === c . kind ) === idx ,
199248 ) ;
200249 for ( const c of filtered ) {
201- const r = this . transformResult ( c ) ;
250+ const r = this . transformResult ( c , tokens ) ;
202251 result . push ( r ) ;
203252 }
204253 return result ;
205254 }
206255
207- private transformResult ( comp : WordCompletion ) : monaco . languages . CompletionItem {
208- 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 ;
209259 return {
210260 insertText : comp . insertText ,
211261 kind : comp . kind ,
212262 label : comp . label ?? comp . insertText ,
213263 insertTextRules : comp . insertTextRules ,
214- range : new monaco . Range ( 1 , wordStart + ( comp . startOffset ?? 0 ) , 1 , 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+ ) ,
215270 } ;
216271 }
217272}
218273
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+
219286export interface AutoCompleteNode {
220287 word : AbstractWord ;
221288 children : AutoCompleteNode [ ] ;
0 commit comments