From 5caf0991bff58d4b9de6d1320b68c4bae771291f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 01:38:45 +0000 Subject: [PATCH] plugin: anchor generated top entry point on the design class The `@top` (`TopAnnotPhase`) generated `top_` entry-point object was created without a real source span on its definition tree. As a result `ExtractSemanticDB` recorded it only in the SemanticDB `Symbols` section with no `Occurrences` entry (the symbol name `top_Foo` is not textually present in the source), so Metals had no position to anchor the `run`/`debug` code lenses on and fell back to placing them at the very top of the file. Position the generated module symbol (`coord`) and its definition tree on the design class's name span. Combined with the matching SemanticDB change, this makes the generated entry point appear as a definition occurrence anchored on the design class, so the `run`/`debug` lenses show up directly above the class that introduces the entry point. Using the class name (rather than the `@top` annotation) also keeps this working if the design no longer needs an explicit `@top` annotation. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Uze3nGgdwa8W2wkfoaDoHR --- .../src/main/scala/plugin/TopAnnotPhase.scala | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/plugin/src/main/scala/plugin/TopAnnotPhase.scala b/plugin/src/main/scala/plugin/TopAnnotPhase.scala index 0fdccb6ee..2ac39b665 100644 --- a/plugin/src/main/scala/plugin/TopAnnotPhase.scala +++ b/plugin/src/main/scala/plugin/TopAnnotPhase.scala @@ -209,6 +209,14 @@ class TopAnnotPhase(setting: Setting) extends CommonPhase: None else topName = s"top_${topName}" + // Anchor the synthetic entry point on the design class's + // name. This is what tooling (e.g. Metals `run`/`debug` + // code lenses) uses to position itself on the class that + // introduces the entry point rather than at the top of the + // file. Using the class name (instead of the `@top` + // annotation) also keeps this working if/when the design no + // longer needs an explicit `@top` annotation. + val designNameSpan = td.nameSpan // the top entry point module symbol val dfApp = newCompleteModuleSymbol( packageOwner, @@ -217,7 +225,7 @@ class TopAnnotPhase(setting: Setting) extends CommonPhase: Touched | NoInits, List(defn.ObjectType, appTpe), Scopes.newScope, - coord = topAnnotTree.span, + coord = designNameSpan, compUnitInfo = clsSym.compUnitInfo ) val moduleCls = dfApp.moduleClass.asClass @@ -281,7 +289,18 @@ class TopAnnotPhase(setting: Setting) extends CommonPhase: ) val dsnInst = New(clsSym.typeRef, dsnInstArgs) val setDsn = This(moduleCls).select("setDsn".toTermName).appliedTo(dsnInst) - Some(td, ModuleDef(dfApp, List(setInitials, setDsn)), rest) + // Give the generated module definition a real source span + // (the design class's name) so that `ExtractSemanticDB` + // records a definition *occurrence* for it. Without a + // positioned tree the synthetic entry point only appears in + // the SemanticDB `Symbols` section (no `Occurrences`), which + // forces Metals to fall back to placing the run/debug code + // lenses at the very top of the file instead of above the + // design class. + val moduleDef = ModuleDef(dfApp, List(setInitials, setDsn)) + val positionedModuleDef = + Thicket(moduleDef.trees.map(_.withSpan(designNameSpan))) + Some(td, positionedModuleDef, rest) end if end if end if