@@ -33,7 +33,7 @@ func (s *mdlServer) Completion(ctx context.Context, params *protocol.CompletionP
3333
3434 // Check if typing a $ variable reference inside page/snippet context
3535 if strings .Contains (linePrefix , "$" ) {
36- varItems := s .variableCompletionItems (text , linePrefix )
36+ varItems := s .variableCompletionItems (text , linePrefix , int ( params . Position . Line ) )
3737 if len (varItems ) > 0 {
3838 return & protocol.CompletionList {
3939 IsIncomplete : false ,
@@ -373,9 +373,10 @@ func objectTypeToCompletionKind(objectType string) (protocol.CompletionItemKind,
373373}
374374
375375// variableCompletionItems returns completion items for $ variable references.
376- // It suggests $currentObject (common in data containers) and any page parameters
377- // found in the document's CREATE PAGE Params declaration.
378- func (s * mdlServer ) variableCompletionItems (docText string , linePrefix string ) []protocol.CompletionItem {
376+ // It suggests $currentObject (with entity type from enclosing data container) and
377+ // any page parameters found in the document's CREATE PAGE Params declaration.
378+ // cursorLine is the 0-based line number of the cursor position.
379+ func (s * mdlServer ) variableCompletionItems (docText string , linePrefix string , cursorLine int ) []protocol.CompletionItem {
379380 // Extract the partial after the last $ to filter suggestions
380381 lastDollar := strings .LastIndex (linePrefix , "$" )
381382 partial := ""
@@ -385,15 +386,38 @@ func (s *mdlServer) variableCompletionItems(docText string, linePrefix string) [
385386
386387 var items []protocol.CompletionItem
387388
388- // Always suggest $currentObject — it's the most common data container variable
389+ // Scan upward from cursor to find enclosing data container context
390+ entityType , widgetName := scanEnclosingDataContainer (docText , cursorLine )
391+
392+ // Suggest $currentObject with entity type if found
389393 if partial == "" || strings .HasPrefix ("CURRENTOBJECT" , partial ) {
394+ detail := "Current object from enclosing data container"
395+ if entityType != "" {
396+ detail = entityType
397+ }
390398 items = append (items , protocol.CompletionItem {
391399 Label : "$currentObject" ,
392400 Kind : protocol .CompletionItemKindVariable ,
393- Detail : "Current object from enclosing data container" ,
401+ Detail : detail ,
394402 })
395403 }
396404
405+ // Suggest $widgetName (selection) for list containers
406+ if widgetName != "" {
407+ wNameUpper := strings .ToUpper (widgetName )
408+ if partial == "" || strings .HasPrefix (wNameUpper , partial ) {
409+ detail := "Selection from list container"
410+ if entityType != "" {
411+ detail = entityType + " (selection)"
412+ }
413+ items = append (items , protocol.CompletionItem {
414+ Label : "$" + widgetName ,
415+ Kind : protocol .CompletionItemKindVariable ,
416+ Detail : detail ,
417+ })
418+ }
419+ }
420+
397421 // Extract page parameter names from CREATE PAGE ... Params: { $Name: Type, ... }
398422 paramNames := extractPageParamNames (docText )
399423 for _ , name := range paramNames {
@@ -409,21 +433,141 @@ func (s *mdlServer) variableCompletionItems(docText string, linePrefix string) [
409433 return items
410434}
411435
412- // extractPageParamNames extracts parameter names from CREATE PAGE ... Params: { $Name: Type } declarations.
436+ // scanEnclosingDataContainer scans upward from cursorLine to find the nearest
437+ // enclosing data container widget and its entity type.
438+ // Returns (entityType, widgetName) where widgetName is set for list containers.
439+ // Best-effort: uses brace matching and keyword scanning (Enclosing Scope Scanning pattern).
440+ func scanEnclosingDataContainer (text string , cursorLine int ) (string , string ) {
441+ lines := strings .Split (text , "\n " )
442+ if cursorLine >= len (lines ) {
443+ return "" , ""
444+ }
445+
446+ // Track brace nesting depth as we scan upward
447+ depth := 0
448+ for i := cursorLine ; i >= 0 ; i -- {
449+ line := lines [i ]
450+ // Count braces on this line (right to left for correct nesting)
451+ for j := len (line ) - 1 ; j >= 0 ; j -- {
452+ switch line [j ] {
453+ case '}' :
454+ depth ++
455+ case '{' :
456+ depth --
457+ }
458+ }
459+
460+ // At depth < 0, we've found an opening brace that encloses the cursor
461+ if depth < 0 {
462+ trimmed := strings .TrimSpace (line )
463+ upper := strings .ToUpper (trimmed )
464+
465+ // Check for data container keywords
466+ entityType , widgetName , isList := extractContainerInfo (upper , trimmed )
467+ if entityType != "" {
468+ if isList {
469+ return entityType , widgetName
470+ }
471+ return entityType , ""
472+ }
473+ // Reset depth — we passed through this opening brace but it wasn't a data container
474+ depth = 0
475+ }
476+ }
477+ return "" , ""
478+ }
479+
480+ // extractContainerInfo extracts entity type and widget name from a data container line.
481+ // upperLine is the uppercase version, originalLine preserves case for widget name extraction.
482+ // Returns (entityType, widgetName, isList).
483+ func extractContainerInfo (upperLine string , originalLine string ) (string , string , bool ) {
484+ // Patterns: DATAVIEW name (DataSource: ...) {
485+ // LISTVIEW name (DataSource: DATABASE FROM Module.Entity ...) {
486+ // DATAGRID name (DataSource: DATABASE FROM Module.Entity ...) {
487+ // GALLERY name (DataSource: DATABASE FROM Module.Entity ...) {
488+ type containerPattern struct {
489+ keyword string
490+ isList bool
491+ }
492+ patterns := []containerPattern {
493+ {"DATAVIEW " , false },
494+ {"LISTVIEW " , true },
495+ {"DATAGRID " , true },
496+ {"GALLERY " , true },
497+ }
498+
499+ for _ , p := range patterns {
500+ if ! strings .HasPrefix (upperLine , p .keyword ) {
501+ continue
502+ }
503+
504+ // Extract widget name (first token after keyword)
505+ rest := strings .TrimSpace (originalLine [len (p .keyword ):])
506+ widgetName := ""
507+ for j , c := range rest {
508+ if c == ' ' || c == '(' || c == '{' {
509+ widgetName = rest [:j ]
510+ break
511+ }
512+ }
513+ if widgetName == "" {
514+ widgetName = rest
515+ }
516+
517+ // Extract entity from DataSource (use original case for entity name)
518+ entityType := extractEntityFromLine (originalLine )
519+ if entityType != "" {
520+ return entityType , widgetName , p .isList
521+ }
522+ }
523+ return "" , "" , false
524+ }
525+
526+ // extractEntityFromLine extracts the entity type from a DataSource declaration in a line.
527+ // Preserves original casing of the entity name (e.g., "Sales.Order" not "SALES.ORDER").
528+ func extractEntityFromLine (line string ) string {
529+ // Case-insensitive search for "DATABASE FROM" pattern
530+ upperLine := strings .ToUpper (line )
531+ if idx := strings .Index (upperLine , "DATABASE FROM " ); idx >= 0 {
532+ rest := strings .TrimSpace (line [idx + len ("DATABASE FROM " ):])
533+ // Entity is the next qualified name (Module.Entity)
534+ end := strings .IndexAny (rest , " \t ,)}" )
535+ if end < 0 {
536+ end = len (rest )
537+ }
538+ entity := rest [:end ]
539+ if strings .Contains (entity , "." ) {
540+ return entity
541+ }
542+ }
543+ // MICROFLOW/NANOFLOW datasource — entity type not directly available
544+ // $ParamName datasource — would need to resolve param type
545+ return ""
546+ }
547+
548+ // extractPageParamNames extracts parameter names from CREATE PAGE/SNIPPET parameter declarations.
549+ // Best-effort: scans for "$Name: Type" patterns (colon after identifier distinguishes
550+ // declarations from usage like "DataSource: $Param" or context comments like "$currentObject").
413551func extractPageParamNames (text string ) []string {
414552 var names []string
415553 for _ , line := range strings .Split (text , "\n " ) {
416554 trimmed := strings .TrimSpace (line )
417- // Look for $ParamName patterns in Params declarations
418- // Format: Params: { $Name: Type } or $Name: Type on separate lines
555+ // Skip DECLARE lines (variable declarations, not page params)
556+ if strings .HasPrefix (strings .ToUpper (trimmed ), "DECLARE" ) {
557+ continue
558+ }
559+ // Skip comment lines
560+ if strings .HasPrefix (trimmed , "--" ) {
561+ continue
562+ }
419563 idx := 0
420564 for idx < len (trimmed ) {
421565 dollar := strings .Index (trimmed [idx :], "$" )
422566 if dollar < 0 {
423567 break
424568 }
425569 dollar += idx
426- // Extract the name after $
570+ // Extract the identifier after $
427571 end := dollar + 1
428572 for end < len (trimmed ) {
429573 c := trimmed [end ]
@@ -435,8 +579,10 @@ func extractPageParamNames(text string) []string {
435579 }
436580 if end > dollar + 1 {
437581 name := trimmed [dollar + 1 : end ]
438- // Skip if this looks like a variable declaration (DECLARE) rather than a param
439- if ! strings .HasPrefix (strings .ToUpper (trimmed ), "DECLARE" ) {
582+ // Only match parameter declarations: "$Name:" followed by a type.
583+ // Skip references like "DataSource: $Param" where $ is a value, not a declaration.
584+ rest := strings .TrimSpace (trimmed [end :])
585+ if strings .HasPrefix (rest , ":" ) {
440586 names = append (names , name )
441587 }
442588 }
0 commit comments