| 1 | import os |
| 2 | import term |
| 3 | import v.util.diff |
| 4 | import json |
| 5 | |
| 6 | const vroot = os.real_path(@VMODROOT) |
| 7 | const tmp_dir = os.real_path(os.temp_dir()) |
| 8 | const text_file = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'sample_text.vv') |
| 9 | // note: windows path separator will cause json decode fail |
| 10 | const json_errors_text_file = os.to_slash(text_file) |
| 11 | const mod1_text_file = os.join_path(vroot, 'vlib', 'v', 'tests', 'vls', 'sample_mod1', 'sample.v') |
| 12 | |
| 13 | const autocomplete_info_for_mod_sample_mod1 = '{"details": [ |
| 14 | {"kind":3,"label":"public_fn1","detail":"string","declaration":"fn public_fn1(val int) string","documentation":""}, |
| 15 | {"kind":22,"label":"PublicStruct1","detail":"","declaration":"","documentation":""}, |
| 16 | {"kind":7,"label":"PublicAlias1_1","detail":"","declaration":"","documentation":""}, |
| 17 | {"kind":7,"label":"PublicAlias1_2","detail":"","declaration":"","documentation":""}, |
| 18 | {"kind":13,"label":"PublicEnum1","detail":"","declaration":"","documentation":""}, |
| 19 | {"kind":8,"label":"PublicInterface1","detail":"","declaration":"","documentation":""}, |
| 20 | {"kind":21,"label":"public_const1","detail":"","declaration":"","documentation":""} |
| 21 | ]}' |
| 22 | |
| 23 | const autocomplete_info_for_mod_sample_mod2 = '{"details": [ |
| 24 | {"kind":3,"label":"public_fn2","detail":"string","declaration":"fn public_fn2(val int) string","documentation":""}, |
| 25 | {"kind":22,"label":"PublicStruct2","detail":"","declaration":"","documentation":""}, |
| 26 | {"kind":13,"label":"PublicEnum2","detail":"","declaration":"","documentation":""}, |
| 27 | {"kind":8,"label":"PublicInterface2","detail":"","declaration":"","documentation":""}, |
| 28 | {"kind":7,"label":"PublicAlias2","detail":"","declaration":"","documentation":""}, |
| 29 | {"kind":21,"label":"public_const2","detail":"","declaration":"","documentation":""} |
| 30 | ]}' |
| 31 | |
| 32 | const autocomplete_info_for_mod_struct = '{"details": [ |
| 33 | {"kind":5,"label":"a","detail":"int","declaration":"","documentation":""}, |
| 34 | {"kind":5,"label":"b","detail":"string","declaration":"","documentation":""}, |
| 35 | {"kind":2,"label":"add","detail":"void","declaration":"","documentation":""} |
| 36 | ]}' |
| 37 | |
| 38 | const hover_info_for_public_fn1 = '{"contents":{"kind":"markdown","value":"```v\\nfn public_fn1(val int) string\\n```"}}' |
| 39 | |
| 40 | const hover_info_for_public_struct1 = '{"contents":{"kind":"markdown","value":"```v\\nstruct PublicStruct1\\n```"}}' |
| 41 | |
| 42 | const fn_signature_info_for_all_before_last = '{ |
| 43 | "signatures":[{ |
| 44 | "label":"all_before_last(sub string) string", |
| 45 | "parameters":[{ |
| 46 | "label":"sub string" |
| 47 | }] |
| 48 | }], |
| 49 | "activeSignature":0, |
| 50 | "activeParameter":0 |
| 51 | } |
| 52 | ' |
| 53 | |
| 54 | enum Method { |
| 55 | unknown @['unknown'] |
| 56 | initialize @['initialize'] |
| 57 | initialized @['initialized'] |
| 58 | did_open @['textDocument/didOpen'] |
| 59 | did_change @['textDocument/didChange'] |
| 60 | definition @['textDocument/definition'] |
| 61 | completion @['textDocument/completion'] |
| 62 | signature_help @['textDocument/signatureHelp'] |
| 63 | hover @['textDocument/hover'] |
| 64 | set_trace @['$/setTrace'] |
| 65 | cancel_request @['$/cancelRequest'] |
| 66 | shutdown @['shutdown'] |
| 67 | exit @['exit'] |
| 68 | } |
| 69 | |
| 70 | struct TestData { |
| 71 | method Method |
| 72 | cmd string |
| 73 | output string |
| 74 | } |
| 75 | |
| 76 | const test_data = [ |
| 77 | TestData{ |
| 78 | method: .completion |
| 79 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:19:3" ${os.quoted_path(text_file)}' |
| 80 | output: autocomplete_info_for_mod_sample_mod1 |
| 81 | }, |
| 82 | TestData{ |
| 83 | method: .completion |
| 84 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:20:13" ${os.quoted_path(text_file)}' |
| 85 | output: autocomplete_info_for_mod_sample_mod2 |
| 86 | }, |
| 87 | TestData{ |
| 88 | method: .completion |
| 89 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:22:3" ${os.quoted_path(text_file)}' |
| 90 | output: autocomplete_info_for_mod_struct |
| 91 | }, |
| 92 | TestData{ |
| 93 | method: .completion |
| 94 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:23:3" ${os.quoted_path(text_file)}' |
| 95 | output: '' |
| 96 | }, |
| 97 | TestData{ |
| 98 | method: .completion |
| 99 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:26:28" ${os.quoted_path(text_file)}' |
| 100 | output: autocomplete_info_for_mod_sample_mod1 |
| 101 | }, |
| 102 | TestData{ |
| 103 | method: .signature_help |
| 104 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:25:fn^26" ${os.quoted_path(text_file)}' |
| 105 | output: fn_signature_info_for_all_before_last |
| 106 | }, |
| 107 | TestData{ |
| 108 | method: .completion |
| 109 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:27:9" ${os.quoted_path(text_file)}' |
| 110 | output: '' |
| 111 | }, |
| 112 | TestData{ |
| 113 | method: .completion |
| 114 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:28:9" ${os.quoted_path(text_file)}' |
| 115 | output: '' |
| 116 | }, |
| 117 | TestData{ |
| 118 | method: .hover |
| 119 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:30:hv^10" ${os.quoted_path(text_file)}' |
| 120 | output: hover_info_for_public_fn1 |
| 121 | }, |
| 122 | TestData{ |
| 123 | method: .hover |
| 124 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:31:hv^12" ${os.quoted_path(text_file)}' |
| 125 | output: hover_info_for_public_struct1 |
| 126 | }, |
| 127 | TestData{ |
| 128 | method: .definition |
| 129 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:30:gd^10" ${os.quoted_path(text_file)}' |
| 130 | output: '${mod1_text_file}:50:7' |
| 131 | }, |
| 132 | TestData{ |
| 133 | method: .definition |
| 134 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:31:gd^12" ${os.quoted_path(text_file)}' |
| 135 | output: '${mod1_text_file}:8:11' |
| 136 | }, |
| 137 | TestData{ |
| 138 | method: .definition |
| 139 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:32:gd^11" ${os.quoted_path(text_file)}' |
| 140 | output: '${mod1_text_file}:41:9' |
| 141 | }, |
| 142 | TestData{ |
| 143 | method: .definition |
| 144 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:33:gd^15" ${os.quoted_path(text_file)}' |
| 145 | output: '${mod1_text_file}:44:9' |
| 146 | }, |
| 147 | TestData{ |
| 148 | method: .definition |
| 149 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:34:gd^13" ${os.quoted_path(text_file)}' |
| 150 | output: '${mod1_text_file}:19:10' |
| 151 | }, |
| 152 | TestData{ |
| 153 | method: .definition |
| 154 | cmd: 'v -w -check -json-errors -nocolor -vls-mode -line-info "${text_file}:39:gd^13" ${os.quoted_path(text_file)}' |
| 155 | output: '${mod1_text_file}:50:7' |
| 156 | }, |
| 157 | TestData{ |
| 158 | method: .did_change |
| 159 | cmd: 'v -w -vls-mode -check -json-errors ${os.quoted_path(text_file)}' |
| 160 | output: '[ |
| 161 | { |
| 162 | "path":"${json_errors_text_file}", |
| 163 | "message":"unexpected token `:=`, expecting `)`", |
| 164 | "line_nr":26, |
| 165 | "col":4, |
| 166 | "len":0 |
| 167 | } |
| 168 | , |
| 169 | { |
| 170 | "path":"${json_errors_text_file}", |
| 171 | "message":"unexpected token `:=`, expecting `)`", |
| 172 | "line_nr":29, |
| 173 | "col":4, |
| 174 | "len":0 |
| 175 | } |
| 176 | , |
| 177 | { |
| 178 | "path":"${json_errors_text_file}", |
| 179 | "message":"undefined ident: ``", |
| 180 | "line_nr":19, |
| 181 | "col":3, |
| 182 | "len":0 |
| 183 | } |
| 184 | , |
| 185 | { |
| 186 | "path":"${json_errors_text_file}", |
| 187 | "message":"undefined ident: ``", |
| 188 | "line_nr":20, |
| 189 | "col":13, |
| 190 | "len":0 |
| 191 | } |
| 192 | , |
| 193 | { |
| 194 | "path":"${json_errors_text_file}", |
| 195 | "message":"type `main.MyS` has no field named `s`.\\n2 possibilities: `a`, `b`.", |
| 196 | "line_nr":23, |
| 197 | "col":2, |
| 198 | "len":0 |
| 199 | } |
| 200 | , |
| 201 | { |
| 202 | "path":"${json_errors_text_file}", |
| 203 | "message":"undefined ident: `j`", |
| 204 | "line_nr":26, |
| 205 | "col":2, |
| 206 | "len":0 |
| 207 | } |
| 208 | , |
| 209 | { |
| 210 | "path":"${json_errors_text_file}", |
| 211 | "message":"`j` (no value) used as value in argument 1 to `string.all_before_last`", |
| 212 | "line_nr":26, |
| 213 | "col":2, |
| 214 | "len":0 |
| 215 | } |
| 216 | , |
| 217 | { |
| 218 | "path":"${json_errors_text_file}", |
| 219 | "message":"undefined ident: ``", |
| 220 | "line_nr":26, |
| 221 | "col":28, |
| 222 | "len":0 |
| 223 | } |
| 224 | , |
| 225 | { |
| 226 | "path":"${json_errors_text_file}", |
| 227 | "message":"undefined ident: `strings`", |
| 228 | "line_nr":27, |
| 229 | "col":2, |
| 230 | "len":0 |
| 231 | } |
| 232 | , |
| 233 | { |
| 234 | "path":"${json_errors_text_file}", |
| 235 | "message":"`strings` does not return a value", |
| 236 | "line_nr":28, |
| 237 | "col":2, |
| 238 | "len":0 |
| 239 | } |
| 240 | , |
| 241 | { |
| 242 | "path":"${json_errors_text_file}", |
| 243 | "message":"`strings.builtin` does not return a value", |
| 244 | "line_nr":29, |
| 245 | "col":2, |
| 246 | "len":0 |
| 247 | } |
| 248 | , |
| 249 | { |
| 250 | "path":"${json_errors_text_file}", |
| 251 | "message":"expected 1 argument, but got 2", |
| 252 | "line_nr":27, |
| 253 | "col":2, |
| 254 | "len":0 |
| 255 | } |
| 256 | , |
| 257 | { |
| 258 | "path":"${json_errors_text_file}", |
| 259 | "message":"unknown type `main.NotExistStruct`", |
| 260 | "line_nr":38, |
| 261 | "col":11, |
| 262 | "len":0 |
| 263 | } |
| 264 | ] |
| 265 | ' |
| 266 | }, |
| 267 | ] |
| 268 | |
| 269 | // copy from `vls` |
| 270 | struct JsonError { |
| 271 | path string |
| 272 | message string |
| 273 | line_nr int |
| 274 | col int |
| 275 | len int |
| 276 | } |
| 277 | |
| 278 | struct Detail { |
| 279 | kind int // The type of item (e.g., Method, Function, Field) |
| 280 | label string // The name of the completion item |
| 281 | detail string // Additional info like the function signature or return type |
| 282 | documentation string // The documentation for the item |
| 283 | insert_text ?string @[json: 'insertText'] |
| 284 | insert_text_format ?int @[json: 'insertTextFormat'] // 1 for PlainText, 2 for Snippet |
| 285 | } |
| 286 | |
| 287 | struct JsonVarAC { |
| 288 | details []Detail |
| 289 | } |
| 290 | |
| 291 | struct SignatureHelp { |
| 292 | signatures []SignatureInformation |
| 293 | active_signature int @[json: 'activeSignature'] |
| 294 | active_parameter int @[json: 'activeParameter'] |
| 295 | } |
| 296 | |
| 297 | struct SignatureInformation { |
| 298 | label string |
| 299 | parameters []ParameterInformation |
| 300 | } |
| 301 | |
| 302 | struct ParameterInformation { |
| 303 | label string |
| 304 | } |
| 305 | |
| 306 | fn test_main() { |
| 307 | mut total_errors := 0 |
| 308 | |
| 309 | for t in test_data { |
| 310 | res := os.execute(t.cmd) |
| 311 | if res.exit_code < 0 { |
| 312 | println('fail execute ${t.cmd}') |
| 313 | panic(res.output) |
| 314 | } |
| 315 | res_output := $if windows { |
| 316 | res.output.replace('\r\n', '\n') |
| 317 | } $else { |
| 318 | res.output |
| 319 | } |
| 320 | if t.output.trim_space() != res_output.trim_space() { |
| 321 | println('${term.red('FAIL')} ${t.cmd}') |
| 322 | if diff_ := diff.compare_text(t.output, res_output) { |
| 323 | println(term.header('difference:', '-')) |
| 324 | println(diff_) |
| 325 | } else { |
| 326 | println(term.header('expected:', '-')) |
| 327 | println(t.output) |
| 328 | println(term.header('found:', '-')) |
| 329 | println(res_output) |
| 330 | } |
| 331 | println(term.h_divider('-')) |
| 332 | total_errors++ |
| 333 | } else { |
| 334 | println('${term.green('OK ')} ${t.cmd}') |
| 335 | } |
| 336 | |
| 337 | // Try to decode the response message and verify |
| 338 | if t.output.trim_space().len > 0 { |
| 339 | dump(t.output) |
| 340 | match t.method { |
| 341 | .definition { |
| 342 | check_valid_goto_definition(t.output)! |
| 343 | } |
| 344 | .completion { |
| 345 | check_valid_auto_completion(t.output)! |
| 346 | } |
| 347 | .did_change { |
| 348 | check_valid_json_errors(t.output)! |
| 349 | } |
| 350 | .signature_help { |
| 351 | check_valid_fn_signature(t.output)! |
| 352 | } |
| 353 | .hover { |
| 354 | check_valid_hover(t.output)! |
| 355 | } |
| 356 | else {} |
| 357 | } |
| 358 | } |
| 359 | } |
| 360 | assert total_errors == 0 |
| 361 | } |
| 362 | |
| 363 | fn check_valid_goto_definition(message string) ! { |
| 364 | // `/home/path/aaa.v:19:10` |
| 365 | fields := message.split(':') |
| 366 | if fields.len >= 3 { |
| 367 | path := fields[..fields.len - 2].join(':') |
| 368 | line_nr := fields[fields.len - 2].int() |
| 369 | col := fields[fields.len - 1].int() |
| 370 | if line_nr <= 0 { |
| 371 | return error('goto_definition: line_nr should > 0: ${line_nr}') |
| 372 | } |
| 373 | if col <= 0 { |
| 374 | return error('goto_definition: col should > 0: ${col}') |
| 375 | } |
| 376 | if path.len == 0 { |
| 377 | return error('goto_definition: file.len should > 0: ${path}') |
| 378 | } |
| 379 | } else { |
| 380 | return error('goto_definition: goto_definition format error') |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | fn check_valid_auto_completion(message string) ! { |
| 385 | // {"kind":5,"label":"a","detail":"int","documentation":""}, |
| 386 | result := json.decode(JsonVarAC, message) or { return error('completion: fail to json decode') } |
| 387 | for detail in result.details { |
| 388 | if detail.kind <= 0 || detail.kind > 25 { |
| 389 | return error('completion: kind should in 1-25 : ${detail.kind}') |
| 390 | } |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | fn check_valid_json_errors(message string) ! { |
| 395 | results := json.decode([]JsonError, message) or { |
| 396 | return error('json_errors: fail to json decode') |
| 397 | } |
| 398 | for result in results { |
| 399 | if result.path.len == 0 { |
| 400 | return error('json_errors: path.len should > 0') |
| 401 | } |
| 402 | if result.message.len == 0 { |
| 403 | return error('json_errors: message.len should > 0') |
| 404 | } |
| 405 | if result.line_nr <= 0 { |
| 406 | return error('json_errors: line_nr should > 0') |
| 407 | } |
| 408 | if result.col <= 0 { |
| 409 | return error('json_errors: col should > 0') |
| 410 | } |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | struct HoverContents { |
| 415 | kind string |
| 416 | value string |
| 417 | } |
| 418 | |
| 419 | struct HoverResult { |
| 420 | contents HoverContents |
| 421 | } |
| 422 | |
| 423 | fn check_valid_hover(message string) ! { |
| 424 | result := json.decode(HoverResult, message) or { |
| 425 | return error('hover: fail to json decode: ${err}') |
| 426 | } |
| 427 | if result.contents.kind != 'markdown' { |
| 428 | return error('hover: contents.kind should be "markdown": ${result.contents.kind}') |
| 429 | } |
| 430 | if result.contents.value.len == 0 { |
| 431 | return error('hover: contents.value should not be empty') |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | fn check_valid_fn_signature(message string) ! { |
| 436 | result := json.decode(SignatureHelp, message) or { |
| 437 | return error('fn_signature: fail to json decode') |
| 438 | } |
| 439 | if result.signatures.len != 1 { |
| 440 | return error('fn_signature: signatures.len != 1') |
| 441 | } |
| 442 | if result.signatures[0].label.len == 0 { |
| 443 | return error('fn_signature: label.len == 0') |
| 444 | } |
| 445 | } |
| 446 | |