Skip to content

Implement goto-definition with Oak#1153

Open
lionel- wants to merge 11 commits intomainfrom
oak/jump-to-def
Open

Implement goto-definition with Oak#1153
lionel- wants to merge 11 commits intomainfrom
oak/jump-to-def

Conversation

@lionel-
Copy link
Copy Markdown
Contributor

@lionel- lionel- commented Apr 15, 2026

Branched from #1152
Part of #1151 and #1148

Progress towards posit-dev/positron#11112
Progress towards posit-dev/positron#8631

  • Introduces the oak_ide crate. Like in rust-analyser and ty, the _ide crate implements IDE functionality decoupled from LSP considerations (LSP positions, WorldState, etc).
  • Wire up symbol resolution to our LSP handler: oak_index (I'd like to rename it to oak_semantic) → oak_ideark (we'll move the worldstate and LSP handlers to a new crate later on).
  • Finalise cross-file resolution at WorldState level, switching between package scope and search path scope depending on project type.

For packages, I realised that the collation approach laid out in #1148 needed a significant refinement: only top-level symbols are sensitive to the collation order. Symbols in lazy scopes (e.g. inside function definitions) see the whole finalised namespace. Because of this, FileScope::at() takes a position in the file so it can select the right scope. You can also request the full lazy scope without a position.

Currently, scripts are fully isolated from other files. I'm working on adding support for source() directives in the semantic index. Then goto-definition will work more like in RStudio and only see symbols you've explicitly sourced (posit-dev/positron#11112).

With this PR, we're ready to integrate with #1146 to jump at the source of package exports.

Base automatically changed from oak/collation to main April 17, 2026 16:32
Copy link
Copy Markdown
Contributor

@DavisVaughan DavisVaughan left a comment

Choose a reason for hiding this comment

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

Two main things

  • #1159 to try and simplify some lookups
  • file_scope() feels like it could use another pass, found quite a few things there that smell off

Comment on lines +149 to +167
/// Find the definition site at `offset`, if any.
pub fn definition_at_offset(&self, offset: TextSize) -> Option<(ScopeId, DefinitionId)> {
let scope = self.scope_at(offset);
let def_id = self
.definitions(scope)
.iter()
.find_map(|(id, d)| d.range().contains(offset).then_some(id));
Some((scope, def_id?))
}

/// Find the use site at `offset`, if any.
pub fn use_at_offset(&self, offset: TextSize) -> Option<(ScopeId, UseId)> {
let scope = self.scope_at(offset);
let use_id = self
.uses(scope)
.iter()
.find_map(|(id, u)| u.range().contains(offset).then_some(id));
Some((scope, use_id?))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These felt a bit wrong to me because at the call site in goto_definition:

  • You look up the scope twice (once for defn, once for use)
  • You have the Definition and Use right here, but don't return them, then need to look them up again in goto_definition by their Id

Check out #1159 for an alternate approach that tries to simplify this

Comment thread crates/oak_ide/src/lib.rs

/// The external scope chain for a file, determined by its project context.
#[derive(Debug)]
pub enum FileScope {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feels worth of a file_scope.rs file

Comment thread crates/oak_ide/src/lib.rs
Comment on lines +25 to +28
/// Script or file outside a package. The scope chain is the R
/// search path: `library()` attachments from the file itself,
/// default packages (stats, graphics, etc.), and base.
SearchPath(Vec<BindingSource>),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

...and any source() calls?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Update: See i was confused by the name! I didn't realize this was only for external things

Comment thread crates/oak_ide/src/lib.rs
Comment on lines +53 to +54
let scope = index.scope_at(offset);
match index.scope(scope).kind() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh, like in #1159 what if you make scope_at() return (ScopeId, Scope)? You've got it right there in scope_at(), so looking it up twice feels like a crime!

Comment thread crates/oak_ide/src/lib.rs
}

/// The full scope for lazy contexts. Useful for features that don't
/// have a cursor position (e.g. completions, workspace symbols).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Completions definitely have a cursor position right?

let filenames: Vec<String> = filenames.into_iter().collect();
let ordered = collation_order(&pkg.description, &filenames);

let filename = file.path().rsplit('/').next().unwrap_or_default();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

again, probably want file_name() via a Path

Comment on lines +148 to +151
let path = r_dir.join(name);
let Some(uri) = Url::from_file_path(&path).log_err() else {
continue;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think something is kinda wasteful here.

When we iterate over r_dir with list_r_files() we get a vector of PathBufs already, then we are converting them to strings in filenames/ordered, and now back to PathBufs.

It seems likely we can just keep them as PathBufs the whole time, and teach collation_order() how to sort a vector of PathBuf instead.

let doc = if let Some(open) = self.documents.get(&uri) {
open
} else {
let Ok(contents) = std::fs::read_to_string(&path) else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.log_err()?

Comment on lines +165 to +167
lazy.extend(layers.clone());
if past_current {
top_level.extend(layers);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tricky little use of past_current here!

I guess this also relies on the fact that collation_order() sorts in alphabetical order even without a Collate field, which is also the order that R sources them?

Comment on lines +172 to +174
top_level.push(BindingSource::PackageExports("base".to_string()));
lazy.extend(root_layers);
lazy.push(BindingSource::PackageExports("base".to_string()));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not utils/tools and friends because in a package they require an Import, right?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants