Merge pull request #21405 from tilladam/master

Implement Span::line() and Span::column() for proc-macro server
This commit is contained in:
Lukas Wirth 2026-01-05 08:04:25 +00:00 committed by GitHub
commit fd1457d742
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 161 additions and 18 deletions

View file

@ -1864,6 +1864,7 @@ dependencies = [
"intern",
"libc",
"libloading",
"line-index 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"memmap2",
"object",
"paths",

View file

@ -554,14 +554,12 @@ impl ProcMacroExpander for Expander {
Ok(SubResponse::LocalFilePathResult { name })
}
SubRequest::SourceText { file_id, ast_id, start, end } => {
let ast_id = span::ErasedFileAstId::from_raw(ast_id);
let editioned_file_id = span::EditionedFileId::from_raw(file_id);
let span = Span {
range: TextRange::new(TextSize::from(start), TextSize::from(end)),
anchor: SpanAnchor { file_id: editioned_file_id, ast_id },
ctx: SyntaxContext::root(editioned_file_id.edition()),
};
let range = db.resolve_span(span);
let range = resolve_sub_span(
db,
file_id,
ast_id,
TextRange::new(TextSize::from(start), TextSize::from(end)),
);
let source = db.file_text(range.file_id.file_id(db)).text(db);
let text = source
.get(usize::from(range.range.start())..usize::from(range.range.end()))
@ -569,6 +567,18 @@ impl ProcMacroExpander for Expander {
Ok(SubResponse::SourceTextResult { text })
}
SubRequest::LineColumn { file_id, ast_id, offset } => {
let range =
resolve_sub_span(db, file_id, ast_id, TextRange::empty(TextSize::from(offset)));
let source = db.file_text(range.file_id.file_id(db)).text(db);
let line_index = ide_db::line_index::LineIndex::new(source);
let (line, column) = line_index
.try_line_col(range.range.start())
.map(|lc| (lc.line + 1, lc.col + 1))
.unwrap_or((1, 1));
// proc_macro::Span line/column are 1-based
Ok(SubResponse::LineColumnResult { line, column })
}
SubRequest::FilePath { file_id } => {
let file_id = FileId::from_raw(file_id);
let source_root_id = db.file_source_root(file_id).source_root_id(db);
@ -603,6 +613,22 @@ impl ProcMacroExpander for Expander {
}
}
fn resolve_sub_span(
db: &dyn ExpandDatabase,
file_id: u32,
ast_id: u32,
range: TextRange,
) -> hir_expand::FileRange {
let ast_id = span::ErasedFileAstId::from_raw(ast_id);
let editioned_file_id = span::EditionedFileId::from_raw(file_id);
let span = Span {
range,
anchor: SpanAnchor { file_id: editioned_file_id, ast_id },
ctx: SyntaxContext::root(editioned_file_id.edition()),
};
db.resolve_span(span)
}
#[cfg(test)]
mod tests {
use ide_db::base_db::RootQueryDb;

View file

@ -13,13 +13,25 @@ pub enum SubRequest {
FilePath { file_id: u32 },
SourceText { file_id: u32, ast_id: u32, start: u32, end: u32 },
LocalFilePath { file_id: u32 },
LineColumn { file_id: u32, ast_id: u32, offset: u32 },
}
#[derive(Debug, Serialize, Deserialize)]
pub enum SubResponse {
FilePathResult { name: String },
SourceTextResult { text: Option<String> },
LocalFilePathResult { name: Option<String> },
FilePathResult {
name: String,
},
SourceTextResult {
text: Option<String>,
},
LocalFilePathResult {
name: Option<String>,
},
/// Line and column are 1-based.
LineColumnResult {
line: u32,
column: u32,
},
}
#[derive(Debug, Serialize, Deserialize)]

View file

@ -220,6 +220,20 @@ impl<C: Codec> proc_macro_srv::ProcMacroClientInterface for ProcMacroClientHandl
_ => None,
}
}
fn line_column(&mut self, span: proc_macro_srv::span::Span) -> Option<(u32, u32)> {
let proc_macro_srv::span::Span { range, anchor, ctx: _ } = span;
match self.roundtrip(bidirectional::SubRequest::LineColumn {
file_id: anchor.file_id.as_u32(),
ast_id: anchor.ast_id.into_raw(),
offset: range.start().into(),
}) {
Some(bidirectional::BidirectionalMessage::SubResponse(
bidirectional::SubResponse::LineColumnResult { line, column },
)) => Some((line, column)),
_ => None,
}
}
}
fn handle_expand_ra<C: Codec>(

View file

@ -31,6 +31,7 @@ libc.workspace = true
[dev-dependencies]
expect-test.workspace = true
line-index.workspace = true
# used as proc macro test targets
proc-macro-test.path = "./proc-macro-test"

View file

@ -79,6 +79,16 @@ pub fn fn_like_span_ops(args: TokenStream) -> TokenStream {
TokenStream::from_iter(vec![first, second, third])
}
/// Returns the line and column of the first token's span as two integer literals.
#[proc_macro]
pub fn fn_like_span_line_column(args: TokenStream) -> TokenStream {
let first = args.into_iter().next().unwrap();
let span = first.span();
let line = Literal::usize_unsuffixed(span.line());
let column = Literal::usize_unsuffixed(span.column());
TokenStream::from_iter(vec![TokenTree::Literal(line), TokenTree::Literal(column)])
}
#[proc_macro_attribute]
pub fn attr_noop(_args: TokenStream, item: TokenStream) -> TokenStream {
item

View file

@ -98,6 +98,8 @@ pub trait ProcMacroClientInterface {
fn file(&mut self, file_id: span::FileId) -> String;
fn source_text(&mut self, span: Span) -> Option<String>;
fn local_file(&mut self, file_id: span::FileId) -> Option<String>;
/// Line and column are 1-based.
fn line_column(&mut self, span: Span) -> Option<(u32, u32)>;
}
const EXPANDER_STACK_SIZE: usize = 8 * 1024 * 1024;

View file

@ -257,14 +257,12 @@ impl server::Span for RaSpanServer<'_> {
Span { range: TextRange::empty(span.range.start()), ..span }
}
fn line(&mut self, _span: Self::Span) -> usize {
// FIXME requires db to resolve line index, THIS IS NOT INCREMENTAL
1
fn line(&mut self, span: Self::Span) -> usize {
self.callback.as_mut().and_then(|cb| cb.line_column(span)).map_or(1, |(l, _)| l as usize)
}
fn column(&mut self, _span: Self::Span) -> usize {
// FIXME requires db to resolve line index, THIS IS NOT INCREMENTAL
1
fn column(&mut self, span: Self::Span) -> usize {
self.callback.as_mut().and_then(|cb| cb.line_column(span)).map_or(1, |(_, c)| c as usize)
}
}

View file

@ -703,6 +703,7 @@ fn list_test_macros() {
fn_like_mk_idents [Bang]
fn_like_span_join [Bang]
fn_like_span_ops [Bang]
fn_like_span_line_column [Bang]
attr_noop [Attr]
attr_panic [Attr]
attr_error [Attr]
@ -712,3 +713,17 @@ fn list_test_macros() {
DeriveError [CustomDerive]"#]]
.assert_eq(&res);
}
#[test]
fn test_fn_like_span_line_column() {
assert_expand_with_callback(
"fn_like_span_line_column",
// Input text with known position: "hello" starts at offset 1 (line 2, column 1 in 1-based)
"
hello",
expect![[r#"
LITER 42:Root[0000, 0]@0..100#ROOT2024 Integer 2
LITER 42:Root[0000, 0]@0..100#ROOT2024 Integer 1
"#]],
);
}

View file

@ -6,7 +6,8 @@ use span::{
};
use crate::{
EnvSnapshot, ProcMacroSrv, SpanId, dylib, proc_macro_test_dylib_path, token_stream::TokenStream,
EnvSnapshot, ProcMacroClientInterface, ProcMacroSrv, SpanId, dylib, proc_macro_test_dylib_path,
token_stream::TokenStream,
};
fn parse_string(call_site: SpanId, src: &str) -> TokenStream<SpanId> {
@ -109,3 +110,66 @@ pub(crate) fn list() -> Vec<String> {
let res = srv.list_macros(&dylib_path).unwrap();
res.into_iter().map(|(name, kind)| format!("{name} [{kind:?}]")).collect()
}
/// A mock callback for testing that computes line/column from the input text.
struct MockCallback<'a> {
text: &'a str,
}
impl ProcMacroClientInterface for MockCallback<'_> {
fn source_text(&mut self, span: Span) -> Option<String> {
self.text
.get(usize::from(span.range.start())..usize::from(span.range.end()))
.map(ToOwned::to_owned)
}
fn file(&mut self, _file_id: FileId) -> String {
String::new()
}
fn local_file(&mut self, _file_id: FileId) -> Option<String> {
None
}
fn line_column(&mut self, span: Span) -> Option<(u32, u32)> {
let line_index = line_index::LineIndex::new(self.text);
let line_col = line_index.try_line_col(span.range.start())?;
// proc_macro uses 1-based line/column
Some((line_col.line as u32 + 1, line_col.col as u32 + 1))
}
}
pub fn assert_expand_with_callback(
macro_name: &str,
#[rust_analyzer::rust_fixture] ra_fixture: &str,
expect_spanned: Expect,
) {
let path = proc_macro_test_dylib_path();
let expander = dylib::Expander::new(&temp_dir::TempDir::new().unwrap(), &path).unwrap();
let def_site = Span {
range: TextRange::new(0.into(), 150.into()),
anchor: SpanAnchor {
file_id: EditionedFileId::current_edition(FileId::from_raw(41)),
ast_id: ROOT_ERASED_FILE_AST_ID,
},
ctx: SyntaxContext::root(span::Edition::CURRENT),
};
let call_site = Span {
range: TextRange::new(0.into(), 100.into()),
anchor: SpanAnchor {
file_id: EditionedFileId::current_edition(FileId::from_raw(42)),
ast_id: ROOT_ERASED_FILE_AST_ID,
},
ctx: SyntaxContext::root(span::Edition::CURRENT),
};
let mixed_site = call_site;
let fixture = parse_string_spanned(call_site.anchor, call_site.ctx, ra_fixture);
let mut callback = MockCallback { text: ra_fixture };
let res = expander
.expand(macro_name, fixture, None, def_site, call_site, mixed_site, Some(&mut callback))
.unwrap();
expect_spanned.assert_eq(&format!("{res:?}"));
}