this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

lsp/eval: process field attributes and specialise queries for embeds

Add specific support to the lsp evaluator for processing embed field
attributes; querying the import graph for embeddings; and providing
specialised behaviour for doc-comments, and completions when working in
embedded files.

The goal is that when editing an embedded file, you get doc comments /
hover docs, and completions, based on where the file is embedded. This
is, in some ways, a bit odd: an embedded file is upstream, and if you
think about upstream imports, when editing a normal .cue file, you don't
get hover docs / completions based on where the current package is
imported downstream. Nevertheless, it's thought that this feature is
going to be very well received and useful.

To do this, we need a slightly different way of calculating those hover
docs and completions; we need for the evaluator to be able to query the
import graph for embeddings and discover where the current evaluator is
embedded (and vice versa), and when evaluating the AST and in particular
a field, we need to process any embed attributes it has.

Signed-off-by: Matthew Sackman <matthew@cue.works>
Change-Id: I85b0d58c1fbd831f2c64d7ad69254b631eac7ad8
Reviewed-on: https://cue.gerrithub.io/c/cue-lang/cue/+/1228810
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>

+578 -12
+277
cmd/cue/cmd/integration/workspace/embedded_test.go
··· 1 1 package workspace 2 2 3 3 import ( 4 + "fmt" 4 5 "testing" 5 6 6 7 "cuelang.org/go/internal/golangorgx/gopls/protocol" 7 8 . "cuelang.org/go/internal/golangorgx/gopls/test/integration" 9 + "cuelang.org/go/internal/lsp/rangeset" 8 10 "github.com/go-quicktest/qt" 11 + "golang.org/x/tools/txtar" 9 12 ) 10 13 11 14 func TestEmbedSimple(t *testing.T) { ··· 263 266 ) 264 267 }) 265 268 } 269 + 270 + func TestEmbedHover(t *testing.T) { 271 + const files = ` 272 + -- cue.mod/module.cue -- 273 + module: "mod.example/x" 274 + language: version: "v0.16.0" 275 + 276 + -- a.cue -- 277 + @extern(embed) 278 + package a 279 + 280 + out: s @embed(file=data/file.json) 281 + 282 + out: field: { 283 + // does the field contain cows? 284 + cows: bool 285 + } 286 + 287 + glob: {"data/d1.json": s} @embed(glob=data/d*.json) 288 + -- b.cue -- 289 + package a 290 + 291 + s: { 292 + // how many fields do we have? 293 + fieldCount: int 294 + } 295 + 296 + s: field: { 297 + // does the field contain sheep? 298 + sheep: bool 299 + } 300 + -- data/file.json -- 301 + { 302 + "field": { 303 + "sheep": true, 304 + "cows": false 305 + }, 306 + "fieldCount": "wrong" 307 + } 308 + -- data/d1.json -- 309 + { 310 + "field": { 311 + "sheep": false, 312 + "cows": true 313 + }, 314 + "fieldCount": 6 315 + } 316 + -- data/d2.json -- 317 + { 318 + "field": { 319 + "cows": false 320 + }, 321 + "fieldCount": -1 322 + } 323 + ` 324 + 325 + WithOptions(RootURIAsDefaultFolder()).Run(t, files, func(t *testing.T, env *Env) { 326 + rootURI := env.Sandbox.Workdir.RootURI() 327 + 328 + env.OpenFile("data/file.json") 329 + env.OpenFile("data/d1.json") 330 + env.OpenFile("data/d2.json") 331 + env.Await( 332 + env.DoneWithOpen(), 333 + LogMatching(protocol.Debug, 3, false, `Package dirs=\[%v/data\] importPath=mod\.example/x/data@v0:_.+ Created`, rootURI), 334 + LogMatching(protocol.Debug, 3, false, `Package dirs=\[%v/data\] importPath=mod\.example/x/data@v0:_.+ Reloaded`, rootURI), 335 + ) 336 + 337 + mappers := make(map[string]*protocol.Mapper) 338 + for _, file := range txtar.Parse([]byte(files)).Files { 339 + mapper := protocol.NewMapper(rootURI+"/"+protocol.DocumentURI(file.Name), file.Data) 340 + mappers[file.Name] = mapper 341 + } 342 + 343 + testCases := map[position]string{ 344 + fln("data/file.json", 3, 1, `sheep`): fmt.Sprintf(` 345 + does the field contain sheep? 346 + ([b.cue line 10](%s/b.cue#L10))`[1:], 347 + rootURI), 348 + fln("data/file.json", 4, 1, `cows`): fmt.Sprintf(` 349 + does the field contain cows? 350 + ([a.cue line 8](%s/a.cue#L8))`[1:], 351 + rootURI), 352 + fln("data/file.json", 6, 1, `fieldCount`): fmt.Sprintf(` 353 + how many fields do we have? 354 + ([b.cue line 5](%s/b.cue#L5))`[1:], 355 + rootURI), 356 + 357 + fln("data/d1.json", 3, 1, `sheep`): fmt.Sprintf(` 358 + does the field contain sheep? 359 + ([b.cue line 10](%s/b.cue#L10))`[1:], 360 + rootURI), 361 + fln("data/d1.json", 6, 1, `fieldCount`): fmt.Sprintf(` 362 + how many fields do we have? 363 + ([b.cue line 5](%s/b.cue#L5))`[1:], 364 + rootURI), 365 + } 366 + 367 + ranges := rangeset.NewFilenameRangeSet() 368 + 369 + for p, expectation := range testCases { 370 + p.determinePos(mappers) 371 + // it's len(p.str)+1 because we want to go from the cursor 372 + // before the start of the str up to cursor after the end of 373 + // str. So |str to str| 374 + strLen := len(p.str) + 1 375 + ranges.Add(p.filename, p.offset, p.offset+strLen) 376 + for i := range strLen { 377 + pos := p.pos 378 + pos.Character += uint32(i) 379 + got, _ := env.Hover(protocol.Location{ 380 + URI: p.mapper.URI, 381 + Range: protocol.Range{Start: pos}, 382 + }) 383 + qt.Assert(t, qt.Equals(got.Value, expectation), qt.Commentf("%v(+%d)", p, i)) 384 + } 385 + } 386 + 387 + // Test that all offsets not explicitly mentioned in 388 + // expectations, have no hovers (for the open files only). 389 + for filename, mapper := range mappers { 390 + if !env.Editor.HasBuffer(filename) { 391 + continue 392 + } 393 + for i := range len(mapper.Content) { 394 + if ranges.Contains(filename, i) { 395 + continue 396 + } 397 + pos, err := mapper.OffsetPosition(i) 398 + if err != nil { 399 + t.Fatal(err) 400 + } 401 + got, _ := env.Hover(protocol.Location{ 402 + URI: mapper.URI, 403 + Range: protocol.Range{ 404 + Start: pos, 405 + }, 406 + }) 407 + qt.Assert(t, qt.IsNil(got), qt.Commentf("%v:%v (0-based)", filename, pos)) 408 + } 409 + } 410 + 411 + }) 412 + } 413 + 414 + func TestEmbedDefinitions(t *testing.T) { 415 + const files = ` 416 + -- cue.mod/module.cue -- 417 + module: "mod.example/x" 418 + language: version: "v0.16.0" 419 + 420 + -- a.cue -- 421 + @extern(embed) 422 + package a 423 + 424 + out: s @embed(file=data/file.json) 425 + 426 + out: field: { 427 + // does the field contain cows? 428 + cows: bool 429 + } 430 + 431 + glob: {"data/d1.json": s} @embed(glob=data/d*.json) 432 + 433 + x1: out.field 434 + x2: glob."data/d2.json".field 435 + -- b.cue -- 436 + package a 437 + 438 + s: { 439 + // how many fields do we have? 440 + fieldCount: int 441 + } 442 + 443 + s: field: { 444 + // does the field contain sheep? 445 + sheep: bool 446 + } 447 + -- data/file.json -- 448 + { 449 + "field": { 450 + "sheep": true, 451 + "cows": false 452 + }, 453 + "fieldCount": "wrong" 454 + } 455 + -- data/d1.json -- 456 + { 457 + "field": { 458 + "sheep": false, 459 + "cows": true 460 + }, 461 + "fieldCount": 6 462 + } 463 + -- data/d2.json -- 464 + { 465 + "field": { 466 + "cows": false 467 + }, 468 + "fieldCount": -1 469 + } 470 + ` 471 + 472 + WithOptions(RootURIAsDefaultFolder()).Run(t, files, func(t *testing.T, env *Env) { 473 + rootURI := env.Sandbox.Workdir.RootURI() 474 + 475 + env.OpenFile("a.cue") 476 + env.Await( 477 + env.DoneWithOpen(), 478 + LogExactf(protocol.Debug, 1, false, "Package dirs=[%v] importPath=mod.example/x@v0:a Reloaded", rootURI), 479 + ) 480 + 481 + mappers := make(map[string]*protocol.Mapper) 482 + for _, file := range txtar.Parse([]byte(files)).Files { 483 + mapper := protocol.NewMapper(rootURI+"/"+protocol.DocumentURI(file.Name), file.Data) 484 + mappers[file.Name] = mapper 485 + } 486 + 487 + testCases := map[position][]position{ 488 + fln("a.cue", 13, 1, "field"): { 489 + fln("a.cue", 6, 1, "field"), 490 + fln("b.cue", 8, 1, "field"), 491 + fln("data/file.json", 2, 1, "field"), 492 + }, 493 + 494 + fln("a.cue", 14, 1, "field"): { 495 + fln("data/d2.json", 2, 1, "field"), 496 + }, 497 + 498 + fln("a.cue", 4, 1, "@embed(file=data/file.json)"): { 499 + fln("data/file.json", 1, 1, ""), 500 + }, 501 + 502 + fln("a.cue", 11, 1, "@embed(glob=data/d*.json)"): { 503 + fln("data/d1.json", 1, 1, ""), 504 + fln("data/d2.json", 1, 1, ""), 505 + }, 506 + } 507 + 508 + for posFrom, posWants := range testCases { 509 + posFrom.determinePos(mappers) 510 + for i := range posWants { 511 + posWant := &posWants[i] 512 + posWant.determinePos(mappers) 513 + } 514 + 515 + strLen := len(posFrom.str) + 1 516 + for i := range strLen { 517 + pos := posFrom.pos 518 + pos.Character += uint32(i) 519 + 520 + posGots := env.Definition(protocol.Location{ 521 + URI: posFrom.mapper.URI, 522 + Range: protocol.Range{Start: pos}, 523 + }) 524 + qt.Assert(t, qt.Equals(len(posGots), len(posWants))) 525 + 526 + for i, posWant := range posWants { 527 + posGot := posGots[i] 528 + posWantLoc := protocol.Location{ 529 + URI: posWant.mapper.URI, 530 + Range: protocol.Range{ 531 + Start: posWant.pos, 532 + End: posWant.pos, 533 + }, 534 + } 535 + posWantLoc.Range.End.Character += uint32(len(posWant.str)) 536 + qt.Assert(t, qt.Equals(posGot, posWantLoc)) 537 + } 538 + } 539 + 540 + } 541 + }) 542 + }
+23 -3
internal/lsp/cache/eval.go
··· 24 24 "strings" 25 25 26 26 "cuelang.org/go/cue/ast" 27 + "cuelang.org/go/cue/build" 27 28 "cuelang.org/go/internal/golangorgx/gopls/protocol" 28 29 "cuelang.org/go/internal/lsp/eval" 29 30 ) ··· 213 214 214 215 completions := fe.CompletionsForOffset(offset) 215 216 217 + isJsonSrc := file.buildFile != nil && file.buildFile.Encoding == build.JSON 218 + isYamlSrc := file.buildFile != nil && file.buildFile.Encoding == build.YAML 219 + 216 220 var completionItems []protocol.CompletionItem 217 221 218 222 for completion, names := range completions { 219 223 if len(names) == 0 { 220 224 continue 221 225 } 226 + if completion.Kind == protocol.VariableCompletion && (isJsonSrc || isYamlSrc) { 227 + // Don't suggest values if we're in a json or yaml file, 228 + // because they can't have values which are references. 229 + continue 230 + } 222 231 223 232 completionRange, rangeErr := srcMapper.OffsetRange(completion.Start, completion.End) 224 233 if rangeErr != nil { ··· 252 261 completionRange.End = pos 253 262 254 263 for name := range names { 255 - if !ast.IsValidIdent(name) { 264 + if isJsonSrc || !ast.IsValidIdent(name) { 256 265 name = strconv.Quote(name) 257 266 } 258 267 item := protocol.CompletionItem{ ··· 281 290 } 282 291 } 283 292 284 - func (w *Workspace) FileEvaluatorForURI(fileUri protocol.DocumentURI, loadAllPkgsInMod bool) (*File, *eval.FileEvaluator, *protocol.Mapper, error) { 293 + type loadAllPkgs uint8 294 + 295 + const ( 296 + LoadNothing loadAllPkgs = iota 297 + LoadAll 298 + LoadAllIfNonCue 299 + ) 300 + 301 + func (w *Workspace) FileEvaluatorForURI(fileUri protocol.DocumentURI, loadExtra loadAllPkgs) (*File, *eval.FileEvaluator, *protocol.Mapper, error) { 285 302 mod, err := w.FindModuleForFile(fileUri) 286 303 if err != nil && err != errModuleNotFound { 287 304 return nil, nil, nil, err ··· 290 307 var e *eval.Evaluator 291 308 292 309 if mod != nil { 293 - if loadAllPkgsInMod { 310 + if loadExtra == LoadAll { 294 311 mod.loadAllCuePackages() 295 312 } 296 313 ip, _, _ := mod.FindImportPathForFile(fileUri) 297 314 if ip != nil { 298 315 pkg := mod.Package(*ip) 299 316 if pkg != nil { 317 + if !pkg.isCue && loadExtra == LoadAllIfNonCue { 318 + mod.loadAllCuePackages() 319 + } 300 320 e = pkg.eval 301 321 } 302 322 }
+12
internal/lsp/cache/file.go
··· 19 19 "slices" 20 20 21 21 "cuelang.org/go/cue/ast" 22 + "cuelang.org/go/cue/build" 22 23 cueerrors "cuelang.org/go/cue/errors" 23 24 "cuelang.org/go/cue/token" 25 + "cuelang.org/go/internal/filetypes" 24 26 "cuelang.org/go/internal/golangorgx/gopls/protocol" 25 27 ) 26 28 ··· 33 35 workspace: w, 34 36 uri: uri, 35 37 } 38 + // Unfortunately this is repeating work done in fscache, but due 39 + // to import cycles, there's no way we can attach the build.File 40 + // to the token.File (or ast.File). TODO - solve cycle somehow? 41 + bf, _ := filetypes.ParseFileAndType(uri.Path(), "", filetypes.Input) 42 + f.buildFile = bf 43 + 36 44 w.files[uri] = f 37 45 } 38 46 return f ··· 78 86 type File struct { 79 87 workspace *Workspace 80 88 uri protocol.DocumentURI 89 + buildFile *build.File 81 90 // isOpen records if this File is open within the client / editor. 82 91 isOpen bool 83 92 ··· 181 190 if tokFile == nil { 182 191 f.mapper = nil 183 192 } else { 193 + if f.buildFile != nil { 194 + f.buildFile.Source = tokFile.Content() 195 + } 184 196 f.mapper = protocol.NewMapper(f.uri, tokFile.Content()) 185 197 w.mappers[tokFile] = f.mapper 186 198 }
+57
internal/lsp/cache/package.go
··· 413 413 ImportCanonicalisation: importCanonicalisation, 414 414 ForPackage: pkg.forPackage, 415 415 PkgImporters: pkg.pkgImporters, 416 + ForEmbedAttribute: pkg.forEmbedAttribute, 417 + PkgEmbedders: pkg.pkgEmbedders, 418 + PackageIsEmbedded: !isCue, 416 419 } 417 420 418 421 // eval.New does almost no work - calculation of resolutions is ··· 471 474 for i, pkg := range pkg.importedBy { 472 475 evals[i] = pkg.eval 473 476 } 477 + return evals 478 + } 479 + 480 + // forEmbedAttribute is a callback for the evaluator. See 481 + // [eval.Config.ForEmbedAttribute] 482 + func (pkg *Package) forEmbedAttribute(attrPos token.Pos) (*embed.Embed, []*eval.Evaluator) { 483 + if !pkg.isCue { 484 + return nil, nil 485 + } 486 + 487 + // Scenario: we are a normal cue package, and we're trying to 488 + // resolve an embed attribute into a set of evaluators of those 489 + // remote embedded packages/files. 490 + 491 + embedding, found := pkg.embeddings[attrPos] 492 + if !found { 493 + return nil, nil 494 + } 495 + evals := make([]*eval.Evaluator, 0, len(embedding.results)) 496 + for _, embedded := range embedding.results { 497 + if embeddedPkg := embedded.pkg; embeddedPkg != nil { 498 + evals = append(evals, embeddedPkg.eval) 499 + } 500 + } 501 + 502 + return embedding.Embed, evals 503 + } 504 + 505 + // pkgEmbedders is a callback for the evaluator. See 506 + // [eval.Config.PkgEmbedders] 507 + func (pkg *Package) pkgEmbedders() map[*eval.Evaluator][]*embed.Embed { 508 + if pkg.isCue { 509 + return nil 510 + } 511 + // Scenario: we are an embedded package, and we're trying to find 512 + // out which cue packages embed us. 513 + 514 + evals := make(map[*eval.Evaluator][]*embed.Embed) 515 + // For each package which embeds us, 516 + for _, embedderPkg := range pkg.embeddedBy { 517 + // look through all their embed attributes. 518 + for _, embedding := range embedderPkg.embeddings { 519 + // For each embed attribute, look at every pkg it expanded 520 + // to: 521 + for _, embedded := range embedding.results { 522 + if embedded.pkg != pkg { 523 + continue 524 + } 525 + evals[embedderPkg.eval] = append(evals[embedderPkg.eval], embedding.Embed) 526 + break 527 + } 528 + } 529 + } 530 + 474 531 return evals 475 532 } 476 533
+202 -3
internal/lsp/eval/eval.go
··· 332 332 "fmt" 333 333 "iter" 334 334 "maps" 335 + stdpath "path" 336 + "path/filepath" 335 337 "slices" 336 338 "strconv" 337 339 "strings" 338 340 339 341 "cuelang.org/go/cue/ast" 342 + "cuelang.org/go/cue/interpreter/embed" 340 343 "cuelang.org/go/cue/token" 341 344 "cuelang.org/go/internal/golangorgx/gopls/protocol" 345 + "cuelang.org/go/internal/lsp/fscache" 342 346 "cuelang.org/go/internal/lsp/rangeset" 343 347 ) 344 348 ··· 347 351 // given import path, or nil if the package cannot be resolved. 348 352 type EvaluatorForPackageFunc = func(importPath ast.ImportPath) *Evaluator 349 353 354 + // EvaluatorsForEmbedAttrFunc is a callback function used to resolve 355 + // embed attributes into [embed.Embed] values, and a list of 356 + // [Evaluator]s for the embedded packages. 357 + type EvaluatorsForEmbedAttrFunc = func(attrPos token.Pos) (*embed.Embed, []*Evaluator) 358 + 350 359 // ImportersFunc is a callback function to fetch the package 351 360 // evaluator for packages that directly import the current package. 352 361 type ImportersFunc = func() []*Evaluator 362 + 363 + // EmbeddersFunc is a callback function to fetch the evaluators for 364 + // all the packages that embed us. Each evaluator is associated with 365 + // the [embed.Embed] attributes that expanded to this package. 366 + type EmbeddersFunc = func() map[*Evaluator][]*embed.Embed 353 367 354 368 type Evaluator struct { 355 369 config Config ··· 384 398 // PkgImporters is a callback function to fetch the evaluator for 385 399 // all packages that directly import this package. 386 400 PkgImporters ImportersFunc 401 + // ForEmbedAttribute is callback function to resolve embed 402 + // attributes. 403 + ForEmbedAttribute EvaluatorsForEmbedAttrFunc 404 + // PkgEmbedders is a callback function to fetch the evaluators for 405 + // the packages that embed us. 406 + PkgEmbedders EmbeddersFunc 407 + // PackageIsEmbedded indicates that this evaluator represents a 408 + // package which is embedded (e.g. made from JSON files), as 409 + // opposed to being constructed of normal ".cue" files. 410 + PackageIsEmbedded bool 387 411 } 388 412 389 413 func (c *Config) init() { ··· 392 416 } 393 417 if c.PkgImporters == nil { 394 418 c.PkgImporters = func() []*Evaluator { return nil } 419 + } 420 + 421 + if c.ForEmbedAttribute == nil { 422 + c.ForEmbedAttribute = func(attrPos token.Pos) (*embed.Embed, []*Evaluator) { return nil, nil } 423 + } 424 + if c.PkgEmbedders == nil { 425 + c.PkgEmbedders = func() map[*Evaluator][]*embed.Embed { return nil } 395 426 } 396 427 } 397 428 ··· 580 611 return name 581 612 } 582 613 614 + // embedders constructs the set of navigables in remote evaluators 615 + // that correspond to where this package appears in those remote ASTs 616 + // via embed attributes. 617 + func (e *Evaluator) embedders() map[*navigable]struct{} { 618 + var result []*navigable 619 + for remotePkg, embeds := range e.config.PkgEmbedders() { 620 + remotePkg.bootFiles() 621 + for _, embed := range embeds { 622 + remotePos := embed.Field.Value.Pos() 623 + remoteFilename := remotePos.Filename() 624 + remoteFe := remotePkg.byFilename[remoteFilename] 625 + if remoteFe == nil { 626 + continue 627 + } 628 + remoteFrames := remoteFe.evalForOffset(remotePos.Offset()) 629 + var unexpanded []*navigable 630 + for _, fr := range remoteFrames { 631 + unexpanded = append(unexpanded, fr.navigable) 632 + } 633 + 634 + if embed.IsGlob() { 635 + // The evalForOffset call above will have also discovered 636 + // the embed attribute is a glob, and will have 637 + // constructed a navigable with bindings for each file 638 + // name of the embedded files. Here we're doing the "other 639 + // side" of that: navigating by the names of the files 640 + // that make up this Evaluator. 641 + remoteDir := stdpath.Dir(filepath.ToSlash(remoteFilename)) + "/" 642 + for filename := range e.byFilename { 643 + filename = filepath.ToSlash(filename) 644 + fieldName := strings.TrimPrefix(filename, remoteDir) 645 + result = append(result, navigateByName(expandNavigables(unexpanded), fieldName)...) 646 + } 647 + 648 + } else { 649 + result = append(result, unexpanded...) 650 + } 651 + } 652 + } 653 + return expandNavigables(result) 654 + } 655 + 583 656 type FileEvaluator struct { 584 657 evaluator *Evaluator 585 658 // File is the original [ast.File] that was passed to [New]. ··· 619 692 func (fe *FileEvaluator) DocCommentsForOffset(offset int) map[ast.Node][]*ast.CommentGroup { 620 693 commentsMap := make(map[ast.Node][]*ast.CommentGroup) 621 694 622 - for nav := range fe.definitionsForOffset(offset) { 695 + navs := fe.definitionsForOffset(offset) 696 + if fe.evaluator.config.PackageIsEmbedded { 697 + navs = maps.Keys(expandNavigablesViaPath(slices.Collect(navs))) 698 + } 699 + 700 + for nav := range navs { 623 701 for _, fr := range nav.frames { 624 702 if fr.key != nil { 625 703 if comments := fr.docComments(); len(comments) > 0 { ··· 839 917 addCompletions(embedCompletions, nameSet) 840 918 } 841 919 920 + expander := expandNavigables 921 + if fe.evaluator.config.PackageIsEmbedded { 922 + expander = expandNavigablesViaPath 923 + } 924 + 842 925 for nav, fieldCompletions := range suggestFieldsFrom { 843 926 nameSet := make(map[string]struct{}) 844 - for nav := range expandNavigables([]*navigable{nav}) { 927 + for nav := range expander([]*navigable{nav}) { 845 928 for name := range nav.bindings { 846 929 if !strings.HasPrefix(name, "__") { 847 930 nameSet[name] = struct{}{} ··· 998 1081 } 999 1082 evaluatedNavs[nav] = struct{}{} 1000 1083 1084 + // If this nav's evaluator is embedded, we assume it does not 1085 + // support references, (e.g. json) and so there's no point 1086 + // evaluating it, because doing so cannot possibly find any 1087 + // uses of the targetNavs. This massively reduces the amount 1088 + // of evaluation done for enormous json files, for example 1089 + // (without this, we'd evaluate the entire json file for no 1090 + // reason). 1091 + if nav.evaluator.config.PackageIsEmbedded { 1092 + continue 1093 + } 1001 1094 nav.eval() 1002 1095 for _, fr := range nav.frames { 1003 1096 for _, childFr := range fr.childFrames { ··· 1049 1142 for _, remotePkg := range nav.evaluator.config.PkgImporters() { 1050 1143 navsWorklist = append(navsWorklist, remotePkg.initialNavsForImport(ip)...) 1051 1144 } 1145 + // Embeds however are different: they are embedded into a 1146 + // specific part of a CUE AST, and it absolutely makes sense 1147 + // to start by evaluating that section of the AST. 1148 + navsWorklist = slices.AppendSeq(navsWorklist, maps.Keys(nav.evaluator.embedders())) 1052 1149 } 1053 1150 } 1054 1151 } ··· 1449 1546 return navsSet 1450 1547 } 1451 1548 1549 + // expandNavigablesViaPath is used by embedded packages only. From the 1550 + // given navs it walks up the tree, constructing a path as it goes. If 1551 + // it reaches the root of the navigable tree it looks for where this 1552 + // evaluator/package is embedded into remote evaluators, and from 1553 + // those locations it walks forward down the (reversed) path. 1554 + func expandNavigablesViaPath(navs []*navigable) map[*navigable]struct{} { 1555 + result := expandNavigables(navs) 1556 + for nav := range result { 1557 + var names []string 1558 + for ; nav != nil && nav.name != ""; nav = nav.parent { 1559 + names = append(names, nav.name) 1560 + } 1561 + 1562 + var walkFrom []*navigable 1563 + 1564 + if nav.parent == nav.evaluator.pkgFrame.navigable { 1565 + walkFrom = slices.Collect(maps.Keys(nav.evaluator.embedders())) 1566 + } else { 1567 + walkFrom = []*navigable{nav} 1568 + } 1569 + 1570 + for _, name := range slices.Backward(names) { 1571 + walkFrom = navigateByName(expandNavigables(walkFrom), name) 1572 + } 1573 + maps.Copy(result, expandNavigables(walkFrom)) 1574 + } 1575 + return result 1576 + } 1577 + 1452 1578 // frame corresponds to a node from the AST. A frame can be created at 1453 1579 // any time, and creates the opportunity for evaluation to be paused 1454 1580 // (and later resumed). Any binding reachable via ··· 1624 1750 // imports of this package, in some other package. 1625 1751 1626 1752 childFr := f.newFrame(nil, f.fileEvaluator.evaluator.pkgDecls) 1627 - childFr.key = node.Name 1753 + if fscache.IsPhantomPackage(node) { 1754 + // For packages with invented names, be careful to avoid 1755 + // returning file coordinates that use the length of the 1756 + // invented name. 1757 + childFr.key = &ast.Ident{ 1758 + Name: "", 1759 + NamePos: f.fileEvaluator.File.Pos().File().Pos(0, token.NoRelPos), 1760 + } 1761 + } else { 1762 + childFr.key = node.Name 1763 + } 1628 1764 childFr.addRange(node) 1629 1765 childFr.docsNode = node 1630 1766 p := &path{ ··· 1970 2106 // expressions in the field label. Therefore, 1971 2107 // len(fieldDecl.exprs) > 0 implies !strings.HasPrefix(keyName, "__") 1972 2108 childFr.parent.newFrame(fieldDecl, nil) 2109 + } 2110 + 2111 + for _, attr := range node.Attrs { 2112 + childFr.addRange(attr) 2113 + embed, remoteEvals := f.fileEvaluator.evaluator.config.ForEmbedAttribute(attr.Pos()) 2114 + if len(remoteEvals) == 0 { 2115 + continue 2116 + } 2117 + isGlob := embed.IsGlob() 2118 + for _, remotePkgEvaluator := range remoteEvals { 2119 + // The attribute is an embed of 1 or more files, which 2120 + // we have converted to pkgs with evaluators. Booting 2121 + // them means their pkgDecls have frames. 2122 + // 2123 + // The behaviour here is very similar to processing an 2124 + // ImportSpec. 2125 + remotePkgEvaluator.bootFiles() 2126 + 2127 + childFrs := []*frame{childFr} 2128 + if isGlob { 2129 + childFrs = childFrs[:0] 2130 + dir := stdpath.Dir(filepath.ToSlash(f.fileEvaluator.File.Filename)) + "/" 2131 + for remoteFilename := range remotePkgEvaluator.byFilename { 2132 + remoteFilename = filepath.ToSlash(remoteFilename) 2133 + fieldName := strings.TrimPrefix(remoteFilename, dir) 2134 + fieldNameIdent := ast.NewIdent(fieldName) 2135 + grandChildFr := childFr.newFrame(fieldNameIdent, childFr.navigable.ensureNavigable(fieldName)) 2136 + grandChildFr.addRange(attr) 2137 + childFr.appendBinding(fieldName, grandChildFr) 2138 + childFrs = append(childFrs, grandChildFr) 2139 + } 2140 + } 2141 + 2142 + for _, childFr := range childFrs { 2143 + // We add a path that records that this field resolves 2144 + // to the remote package. DefinitionsForOffset always 2145 + // passes through a path, so this path ensures 2146 + // DefinitionsForOffset on the attr itself will take 2147 + // you to the embedded files. 2148 + p := &path{ 2149 + frame: childFr, 2150 + components: []pathComponent{{ 2151 + unexpanded: []*navigable{remotePkgEvaluator.pkgDecls}, 2152 + node: attr, 2153 + }}, 2154 + } 2155 + childFr.childPaths = append(childFr.childPaths, p) 2156 + 2157 + // Ensure that the child frame resolves to the file 2158 + // navs from the remote package. This allows paths that 2159 + // travels through the embed and into the remote pkg to 2160 + // resolve correctly. 2161 + remotePkgFileFrames := remotePkgEvaluator.pkgFrame.childFrames 2162 + remotePkgNavs := make([]*navigable, len(remotePkgFileFrames)) 2163 + for i, remoteFileFr := range remotePkgFileFrames { 2164 + remotePkgNavs[i] = remoteFileFr.navigable 2165 + } 2166 + childFr.navigable.ensureResolvesTo(remotePkgNavs) 2167 + 2168 + // We also record that the attr uses the remote package. 2169 + remotePkgEvaluator.pkgDecls.recordUsage(attr, childFr) 2170 + } 2171 + } 1973 2172 } 1974 2173 1975 2174 default:
+7 -6
internal/lsp/server/eval.go
··· 18 18 "context" 19 19 20 20 "cuelang.org/go/internal/golangorgx/gopls/protocol" 21 + "cuelang.org/go/internal/lsp/cache" 21 22 ) 22 23 23 24 func (s *server) Definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { 24 25 uri := params.TextDocument.URI 25 26 w := s.workspace 26 - file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, false) 27 + file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, cache.LoadNothing) 27 28 if file == nil || err != nil { 28 29 return nil, err 29 30 } ··· 33 34 func (s *server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { 34 35 uri := params.TextDocument.URI 35 36 w := s.workspace 36 - file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, false) 37 + file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, cache.LoadNothing) 37 38 if file == nil || err != nil { 38 39 return nil, err 39 40 } ··· 43 44 func (s *server) Hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { 44 45 uri := params.TextDocument.URI 45 46 w := s.workspace 46 - file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, false) 47 + file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, cache.LoadAllIfNonCue) 47 48 if file == nil || err != nil { 48 49 return nil, err 49 50 } ··· 53 54 func (s *server) References(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) { 54 55 uri := params.TextDocument.URI 55 56 w := s.workspace 56 - file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, true) 57 + file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, cache.LoadAll) 57 58 if file == nil || err != nil { 58 59 return nil, err 59 60 } ··· 63 64 func (s *server) Rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { 64 65 uri := params.TextDocument.URI 65 66 w := s.workspace 66 - file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, true) 67 + file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, cache.LoadAll) 67 68 if file == nil || err != nil { 68 69 return nil, err 69 70 } ··· 73 74 func (s *server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.PrepareRenamePlaceholder, error) { 74 75 uri := params.TextDocument.URI 75 76 w := s.workspace 76 - file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, true) 77 + file, fe, srcMapper, err := w.FileEvaluatorForURI(uri, cache.LoadAll) 77 78 if file == nil || err != nil { 78 79 return nil, err 79 80 }