Skip to content

Add generic go-to-definition for file path strings in BRS/BS/XML files#1648

Draft
Copilot wants to merge 13 commits intomasterfrom
copilot/add-go-to-definition-for-script-uri
Draft

Add generic go-to-definition for file path strings in BRS/BS/XML files#1648
Copilot wants to merge 13 commits intomasterfrom
copilot/add-go-to-definition-for-script-uri

Conversation

Copy link
Contributor

Copilot AI commented Mar 19, 2026

Ctrl+clicking a uri attribute value in an XML <script> tag did not navigate to the referenced file, unlike import statements in .bs files which already supported this. Additionally, VS Code was splitting the URI path on / separators on Ctrl+hover, making each path segment a separate underlined chunk instead of the whole URI being a single link.

This has been extended into a fully generic file-path go-to-definition feature: any string that resolves to a known file — in any AST node across .brs, .bs, and .xml files — is now clickable, including image and asset files not loaded into the BrighterScript program.

Changes

  • DefinitionProvider.ts:
    • Added tryGetFilePathLocationLink helper with a multi-step file resolution strategy:
      1. In-program check: resolves any non-empty string via util.getPkgPathFromTarget and checks program.getFile() — covers .brs/.bs/.xml code files. No path prefix (pkg:/, ./, etc.) is required.
      2. Disk fallback with proper reverse-mapping: for files not loaded in the program (e.g. image assets), iterates the project's normalized files deploy entries and uses reverseLookupSrcPath to translate each entry's dest path back to a candidate src path (handling globstar patterns and dest remaps such as { src: 'assets/**', dest: 'images' }). Each candidate is verified with rokuDeploy.getDestPath (pure path computation, no disk I/O) before fsExtra.existsSync is called — ensuring disk access only happens for paths that would actually match the deploy pattern. If no verified candidate exists on disk, null is returned so VS Code gets no LocationLink and does not show a segmented hover.
    • Added findSrcPathForPkgPath helper that drives the reverse-mapping loop.
    • Added reverseLookupSrcPath helper that per-entry translates a pkg dest path to a candidate src path: string entries map to rootDir + pkgPath; { src, dest } entries with a globstar strip the dest prefix and prepend the glob base directory.
    • Wrapped process() in a try-catch so errors from mangled or partially-parsed ASTs are swallowed rather than surfacing to the LSP client.
    • Extended brsFileGetDefinition(): after handling import statements, any StringLiteral token is tried as a file path — if it resolves to a known file (in-program or on disk), go-to-definition navigates there.
    • Replaced the XML script-tag-specific handler with a generic XML attribute walker (xmlGetFilePathDefinitionFromAttributes + xmlWalkNodeForFilePath) that walks the entire component tree. xmlGetFilePathDefinitionFromAttributes delegates to tryGetFilePathLocationLink so both BRS/BS and XML paths share the same resolution logic.
    • All returned LocationLink objects include originSelectionRange set to the full path (excluding quotes), so VS Code underlines the entire URI as a single unit on Ctrl+hover rather than splitting on / separators.
  • interfaces.ts: Widened ProvideDefinitionEvent.definitions from Location[] to Array<Location | LocationLink> so plugins can contribute LocationLink objects with full control over highlight ranges.
  • Scope.ts / XmlScope.ts / BrsFile.ts: Updated deprecated getDefinition return types to Array<Location | LocationLink>.
  • DefinitionProvider.spec.ts: Added tests for BRS pkg:/, ./, ../, and bare-name path detection; negative case for non-existent file strings; XML child-node uri, backgroundURI, and relative ./ attributes; mangled BRS and XML AST safety; disk-based image asset lookup for both BRS string literals and XML attributes (hit and miss cases); dest-remapped asset lookup ({ src: 'assets/**', dest: 'images' } correctly resolves pkg:/images/hero.png to the physical assets/hero.png).

Examples

' BRS/BS — all of these are now ctrl+clickable
poster.uri = "pkg:/images/hero.jpg"   ' image asset — resolved via deploy patterns + disk
poster.uri = "./assets/hero.jpg"
poster.uri = "../shared/hero.jpg"
poster.uri = "hero.jpg"               ' bare name, resolved relative to current file
<!-- XML — any attribute value that resolves to a known file is ctrl+clickable -->
<component name="MainScene" extends="Scene">
  <script type="text/brightscript" uri="pkg:/components/scene/MainScene.brs" />
  <children>
    <Poster uri="pkg:/images/hero.jpg" />   <!-- image resolved via deploy patterns + disk -->
    <Poster uri="hero.jpg" />
    <Scene backgroundURI="pkg:/images/background.jpg" />
    <Poster uri="./localImage.jpg" />
  </children>
</component>
Original prompt

This section details on the original issue you should resolve

<issue_title>Clicking a script uri should trigger go-to-definition</issue_title>
<issue_description>ctrl+click (in vscode) should go to the definition of the MainScene.brs file. (like what we do with import statements).

<?xml version="1.0" encoding="utf-8" ?>
<component name="MainScene" extends="Scene">
  <script type="text/brightscript" uri="pkg:/components/scene/MainScene.brs" />

  <interface>
    <field id="appExit" type="bool" alwaysnotify="true" value="false"/>
  </interface>

  <children>
    <MarkupList id="list" itemComponentName="ListItem" itemSize="[1900, 250]" itemSpacing="[0, 20]" drawFocusFeedback="false" translation="[20, 20]" wrapDividerBitmapUri="" />
    <Video id="video" visible="false" />
  </children>
</component>

Implement this in the correct bsc plugin region, and add unit tests.</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Copilot AI changed the title [WIP] Add go-to-definition functionality for script URIs Add go-to-definition for XML script tag URI attributes Mar 19, 2026
Copilot AI requested a review from TwitchBronBron March 19, 2026 19:03
@TwitchBronBron TwitchBronBron added the create-package create a temporary npm package on every commit label Mar 19, 2026
@rokucommunity-bot
Copy link
Contributor

Hey there! I just built a new temporary npm package based on 072beff. You can download it here or install it by running the following command:

npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.70.3-copilot-add-go-to-definition-for-script-uri.20260319190736.tgz

Copy link
Member

@TwitchBronBron TwitchBronBron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, for some reason, when I ctrl+hover over the paths, it breaks components, scene, mainscene.brs into separate chunks. any way to mak ethe whole thing a single clickable? Why's it doing that in vscode? Is that under our control? Please fix

@rokucommunity-bot
Copy link
Contributor

Hey there! I just built a new temporary npm package based on f6d9243. You can download it here or install it by running the following command:

npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.70.3-copilot-add-go-to-definition-for-script-uri.20260319194507.tgz

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 19, 2026

So, for some reason, when I ctrl+hover over the paths, it breaks components, scene, mainscene.brs into separate chunks. any way to mak ethe whole thing a single clickable? Why's it doing that in vscod...

The path-segment splitting was caused by VS Code's word boundary detection for XML files — when showing the underline for a ctrl+click target, VS Code splits on / characters rather than treating the whole attribute value as one unit.

Fixed in commit 97b0f09 by implementing the LSP textDocument/documentLink protocol for XML script tag URIs. Document links explicitly tell VS Code the exact range of a link (the full URI value, e.g. pkg:/components/scene/MainScene.brs) so it renders as a single clickable region instead of per-segment. This required changes across the full LSP stack: Program, LspProject, Project, WorkerThreadProject, ProjectManager, and LanguageServer.

Copilot AI changed the title Add go-to-definition for XML script tag URI attributes Add go-to-definition and document links for XML script tag URI attributes Mar 19, 2026
Copilot AI requested a review from TwitchBronBron March 19, 2026 19:54
@rokucommunity-bot
Copy link
Contributor

Hey there! I just built a new temporary npm package based on 97b0f09. You can download it here or install it by running the following command:

npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.70.3-copilot-add-go-to-definition-for-script-uri.20260319195550.tgz

Copy link
Member

@TwitchBronBron TwitchBronBron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking better. A few thoughts. I'd like to make this a contributable bsc plugin hook. With the before, provide, after hooks. Similar to other events.

Also, in my typescript projects their clickable import path links are only underlined when I hold ctrl. But these links you're contributing are underlined all the time. Is there a different way of contributing them?

…y underline

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 19, 2026

Looking better. A few thoughts. I'd like to make this a contributable bsc plugin hook. With the before, provide, after hooks. Similar to other events.

Also, in my typescript projects their clickable...

Both addressed in commit 2a9130a:

Plugin hooks: Added beforeProvideDocumentLinks, provideDocumentLinks, and afterProvideDocumentLinks hooks to the Plugin interface following the same pattern as other events. BscPlugin now delegates to a new DocumentLinksProvider class. ProvideDefinitionEvent.definitions was also widened from Location[] to Array<Location | LocationLink> so plugins can contribute LocationLink objects with full control over highlight ranges.

Ctrl-only underline: Removed the LSP documentLinkProvider capability and onDocumentLinks handler (which caused the always-on underline). Instead, getDefinition() now checks document links for the file — when the cursor is within a document link's range, the definition is returned as a LocationLink with originSelectionRange set to the full URI path. VS Code uses this to highlight the entire path as a single unit, but only while holding Ctrl — identical to how TypeScript highlights import paths.

Copilot AI changed the title Add go-to-definition and document links for XML script tag URI attributes Add go-to-definition for XML script tag URI attributes with plugin hooks Mar 19, 2026
Copilot AI requested a review from TwitchBronBron March 19, 2026 23:10
@rokucommunity-bot
Copy link
Contributor

Hey there! I just built a new temporary npm package based on 2a9130a. You can download it here or install it by running the following command:

npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.70.3-copilot-add-go-to-definition-for-script-uri.20260320125752.tgz

Copy link
Member

@TwitchBronBron TwitchBronBron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the reasons for keeping the DocumentLinksProvider if the DefinitionProvider is already handling this? I'd rather let definition provider contribute the links if we can? Can we fully accomplish the ctrl hover feature without actually needing document link provider? (but still keeping the full hover instead of the segmented chunks like i complained about earlier)

Copy link
Member

@TwitchBronBron TwitchBronBron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. So, now let's make this more flexible. I want it to do the following. Since the DefinitionProvider reacts to a specific file AND position, you should able to handle ANY file path from any ast element. xml, attribute, brs ast, etc.

So when you get the request, look at the item under the cursor. Build a file path for it (i.e. if it's relative, base that on relative to current file), if it's absolute, base that on the pkgPath (or destPath) of the project, handling pkg:/ and libpkg and all the other stuff we already talked about.

This way, we are very generic, as long as it looks like a path, it's clickable. then it doesn't matter what ast it lives in. support this for brs bs and xml files. ensure you've added tests for these use cases.

some good examples are:

poster = createObject('roSGNode", "PosteR")
poster.uri = "./relative/path.jpg"
poster.uri = "pkg:/images/image.jpg"

then xml nodes like Poster who have a uri field, and the Scene node backgroundURI, etc.

Copilot AI and others added 2 commits March 20, 2026 16:45
… XML attribute values

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/d4d2b757-f74c-4e91-9fc0-3b8bcfa0128b
…ix attribute array type

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/d4d2b757-f74c-4e91-9fc0-3b8bcfa0128b
Copy link
Contributor Author

Copilot AI commented Mar 20, 2026

Okay. So, now let's make this more flexible. I want it to do the following. Since the DefinitionProvider reacts to a specific file AND position, you should able to handle ANY file path from any ast ...

Done in commit d5eeb89.

The DefinitionProvider is now fully generic for file paths:

BRS/BS files — any StringLiteral token that starts with pkg:/, libpkg:/, ./, or ../ is resolved via util.getPkgPathFromTarget and navigated to (returned as a LocationLink with originSelectionRange covering the full path, no quotes). Arbitrary strings like "Poster" in createObject calls are unaffected.

XML files — every attribute value across the entire component tree (component-level, <script> tags, all <children> nodes, <Customization> nodes) is tried against the file index. Bare relative paths like uri="MainScene.brs" continue to work in addition to pkg:/ prefixed and explicit relative (./) paths.

Tests cover: pkg:/ BRS assignment, ./ relative BRS, ../ relative BRS, no-false-positive for "hello world", XML child node uri, XML backgroundURI, XML relative ./ attribute.

Copilot AI changed the title Add go-to-definition for XML script tag URI attributes Add generic go-to-definition for file path strings in BRS/BS/XML files Mar 20, 2026
Copilot AI requested a review from TwitchBronBron March 20, 2026 16:49
Copy link
Member

@TwitchBronBron TwitchBronBron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure you have good handling for when the AST is mangled. we don't want the request failing. So be safe about it.

private tryGetFilePathLocationLink(pathStr: string, containingFilePkgPath: string, originRange: Range): LocationLink | null {
// Require a recognised path prefix so we don't accidentally match arbitrary strings
// (e.g. component names in createObject calls).
if (!/^(?:pkg:|libpkg:|\.\/|\.\.\/)/i.test(pathStr)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like uri="MyImage.jpg"? Not sure we can just assume it'll start with one of these. It's probably safe to just build the pkgPath no matter what and verify there's a file with that exact name.

You should also consider files that don't have extensions. For example uri="MyImageWIthoutExt" would work too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 693de41. Removed the prefix restriction from tryGetFilePathLocationLink — any non-empty string is now resolved via util.getPkgPathFromTarget and checked against the program's file index. This means uri="MyImage.jpg" (bare name, with or without extension) works. Non-matching strings like "Poster" safely return no result since no file is found. Tests for bare-filename resolution are included.

… add bare-filename tests

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/9b2d1051-87c4-45b6-9154-a7e689bd463b
Copy link
Contributor Author

Copilot AI commented Mar 20, 2026

Make sure you have good handling for when the AST is mangled. we don't want the request failing. So be safe about it.

Done in 693de41. Added a top-level try-catch in process() so any error from a mangled or partially-parsed AST is swallowed rather than surfacing to the LSP client. The definitions array is always returned (possibly empty). Tests for mangled BRS and XML ASTs are included.

@rokucommunity-bot
Copy link
Contributor

Hey there! I just built a new temporary npm package based on 7327f93. You can download it here or install it by running the following command:

npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.70.3-copilot-add-go-to-definition-for-script-uri.20260320190031.tgz

Copy link
Member

@TwitchBronBron TwitchBronBron left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have a few more issues to resolve.

  • for files that don't exist, it's still building individual segments. And then opening a file? But i would expect the entire link to be clickable, or not clickable at all. Image
  • this doesn't work for images right now, because images aren't actually loaded into brighterscript (by design). So, i'd like you to do the following: 1 if file is loaded in the program (brs/xml/bs...but don't check for extension), then we have the file. if file not loaded into the program, then grab the Program.options.files array, and build a full path of our file based on the dest patterns, and walk through them using rokudeploy to figure out whether that file WOULD match the files array. if yes, then look on disk for the file. but if not match the files array, then do NOT turn it into a link.

…efactor XML attr handler

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/378b0bc4-7f8c-443a-9ec5-67c8362244ff
Copy link
Contributor Author

Copilot AI commented Mar 20, 2026

have a few more issues to resolve.

  • for files that don't exist, it's still building individual segments. And then opening a file? But i would expect the entire link to be clickable, or not clickable...

Done in 3f7d09b.

Segmented-hover for non-existent files: tryGetFilePathLocationLink now has a two-step fallback. If a file is not loaded in the program (e.g. an image), it checks the project's deploy files patterns via rokuDeploy.getDestPath and whether the file actually exists on disk (fsExtra.existsSync). Only if both are true is a LocationLink returned. When neither condition holds (file doesn't exist anywhere), null is returned and VS Code gets no LocationLink to display — so no segmented hover.

Images and other assets: if the file matches the project's files array patterns and exists on disk, a LocationLink is returned pointing to the physical file. If it doesn't match the patterns or isn't on disk, no link is contributed.

Also refactored xmlGetFilePathDefinitionFromAttributes to delegate to tryGetFilePathLocationLink so both BRS/BS and XML paths share the same disk-fallback logic. Tests cover: image exists on disk + matches files array → link returned (BRS + XML); image missing from disk → empty result (BRS + XML).

if (rootDir && files?.length) {
const srcPath = path.join(rootDir, pkgPath);
if (
rokuDeploy.getDestPath(srcPath, files, rootDir) !== undefined &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this might not actually be accurate. the srcPath you've created here is actually the destPath of the theoretical image. We need to realistically convert this theoretical destPath of the item back into its srcPath and THEN determine if that file exists. rokuDeploy may not have this functionality, but would be good to look through it.

Also, we can approximate as well. you could look through the {src, dest} objects and do a rough reverse translation to see what src path the file MIGHT be in. I just want to make sure we're not hitting the disk for files that wouldn't initially match. That's expensive for "get defintion" requests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dbb9f60. The disk lookup is now a proper reverse-translation:

  1. reverseLookupSrcPath translates a pkg dest path back to a candidate src path per entry — for string entries it's rootDir + pkgPath, for { src, dest } entries with globstar it strips the dest prefix and prepends the src glob base (e.g., { src: 'assets/**', dest: 'images' } + pkgPath='images/hero.png' → candidate = rootDir/assets/hero.png).

  2. Verify before hitting disk: each candidate is passed to rokuDeploy.getDestPath first (pure path computation, no disk I/O). Only if the returned dest matches our expected pkgPath exactly do we call fsExtra.existsSync. This ensures we never do a disk read for a candidate that wouldn't match.

Added test: remapped { src: 'assets/**', dest: 'images' } with pkg:/images/hero.png correctly resolves to rootDir/assets/hero.png on disk.

…fy before hitting disk

Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
Agent-Logs-Url: https://github.com/rokucommunity/brighterscript/sessions/23c7d943-1957-4a69-b8c1-ed6e72dcb640
@rokucommunity-bot
Copy link
Contributor

Hey there! I just built a new temporary npm package based on dbb9f60. You can download it here or install it by running the following command:

npm install https://github.com/rokucommunity/brighterscript/releases/download/v0.0.0-packages/brighterscript-0.70.3-copilot-add-go-to-definition-for-script-uri.20260320201719.tgz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

create-package create a temporary npm package on every commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clicking a script uri should trigger go-to-definition

2 participants