Skip to content

Commit b926525

Browse files
committed
Implement Span::line() and Span::column() for proc-macro server
Add proper line/column resolution for proc-macro spans via a callback mechanism. Previously these methods returned hardcoded 1 values. The implementation adds: - SubRequest::LineColumn and SubResponse::LineColumnResult to the bidirectional protocol - ProcMacroClientInterface::line_column() method - Callback handling in load-cargo using LineIndex - Server implementation in RaSpanServer that uses the callback - a test for Span::line() and Span::column() in proc-macro server Add fn_like_span_line_column test proc-macro that exercises the new line/column API, and a corresponding test with a mock callback.
1 parent 3493b4c commit b926525

File tree

10 files changed

+161
-18
lines changed

10 files changed

+161
-18
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/load-cargo/src/lib.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -554,21 +554,31 @@ impl ProcMacroExpander for Expander {
554554
Ok(SubResponse::LocalFilePathResult { name })
555555
}
556556
SubRequest::SourceText { file_id, ast_id, start, end } => {
557-
let ast_id = span::ErasedFileAstId::from_raw(ast_id);
558-
let editioned_file_id = span::EditionedFileId::from_raw(file_id);
559-
let span = Span {
560-
range: TextRange::new(TextSize::from(start), TextSize::from(end)),
561-
anchor: SpanAnchor { file_id: editioned_file_id, ast_id },
562-
ctx: SyntaxContext::root(editioned_file_id.edition()),
563-
};
564-
let range = db.resolve_span(span);
557+
let range = resolve_sub_span(
558+
db,
559+
file_id,
560+
ast_id,
561+
TextRange::new(TextSize::from(start), TextSize::from(end)),
562+
);
565563
let source = db.file_text(range.file_id.file_id(db)).text(db);
566564
let text = source
567565
.get(usize::from(range.range.start())..usize::from(range.range.end()))
568566
.map(ToOwned::to_owned);
569567

570568
Ok(SubResponse::SourceTextResult { text })
571569
}
570+
SubRequest::LineColumn { file_id, ast_id, offset } => {
571+
let range =
572+
resolve_sub_span(db, file_id, ast_id, TextRange::empty(TextSize::from(offset)));
573+
let source = db.file_text(range.file_id.file_id(db)).text(db);
574+
let line_index = ide_db::line_index::LineIndex::new(source);
575+
let (line, column) = line_index
576+
.try_line_col(range.range.start())
577+
.map(|lc| (lc.line as u32 + 1, lc.col as u32 + 1))
578+
.unwrap_or((1, 1));
579+
// proc_macro::Span line/column are 1-based
580+
Ok(SubResponse::LineColumnResult { line, column })
581+
}
572582
SubRequest::FilePath { file_id } => {
573583
let file_id = FileId::from_raw(file_id);
574584
let source_root_id = db.file_source_root(file_id).source_root_id(db);
@@ -603,6 +613,22 @@ impl ProcMacroExpander for Expander {
603613
}
604614
}
605615

616+
fn resolve_sub_span(
617+
db: &dyn ExpandDatabase,
618+
file_id: u32,
619+
ast_id: u32,
620+
range: TextRange,
621+
) -> hir_expand::FileRange {
622+
let ast_id = span::ErasedFileAstId::from_raw(ast_id);
623+
let editioned_file_id = span::EditionedFileId::from_raw(file_id);
624+
let span = Span {
625+
range,
626+
anchor: SpanAnchor { file_id: editioned_file_id, ast_id },
627+
ctx: SyntaxContext::root(editioned_file_id.edition()),
628+
};
629+
db.resolve_span(span)
630+
}
631+
606632
#[cfg(test)]
607633
mod tests {
608634
use ide_db::base_db::RootQueryDb;

crates/proc-macro-api/src/bidirectional_protocol/msg.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,25 @@ pub enum SubRequest {
1313
FilePath { file_id: u32 },
1414
SourceText { file_id: u32, ast_id: u32, start: u32, end: u32 },
1515
LocalFilePath { file_id: u32 },
16+
LineColumn { file_id: u32, ast_id: u32, offset: u32 },
1617
}
1718

1819
#[derive(Debug, Serialize, Deserialize)]
1920
pub enum SubResponse {
20-
FilePathResult { name: String },
21-
SourceTextResult { text: Option<String> },
22-
LocalFilePathResult { name: Option<String> },
21+
FilePathResult {
22+
name: String,
23+
},
24+
SourceTextResult {
25+
text: Option<String>,
26+
},
27+
LocalFilePathResult {
28+
name: Option<String>,
29+
},
30+
/// Line and column are 1-based.
31+
LineColumnResult {
32+
line: u32,
33+
column: u32,
34+
},
2335
}
2436

2537
#[derive(Debug, Serialize, Deserialize)]

crates/proc-macro-srv-cli/src/main_loop.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,20 @@ impl<C: Codec> proc_macro_srv::ProcMacroClientInterface for ProcMacroClientHandl
220220
_ => None,
221221
}
222222
}
223+
224+
fn line_column(&mut self, span: proc_macro_srv::span::Span) -> Option<(u32, u32)> {
225+
let proc_macro_srv::span::Span { range, anchor, ctx: _ } = span;
226+
match self.roundtrip(bidirectional::SubRequest::LineColumn {
227+
file_id: anchor.file_id.as_u32(),
228+
ast_id: anchor.ast_id.into_raw(),
229+
offset: range.start().into(),
230+
}) {
231+
Some(bidirectional::BidirectionalMessage::SubResponse(
232+
bidirectional::SubResponse::LineColumnResult { line, column },
233+
)) => Some((line, column)),
234+
_ => None,
235+
}
236+
}
223237
}
224238

225239
fn handle_expand_ra<C: Codec>(

crates/proc-macro-srv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ libc.workspace = true
3131

3232
[dev-dependencies]
3333
expect-test.workspace = true
34+
line-index.workspace = true
3435

3536
# used as proc macro test targets
3637
proc-macro-test.path = "./proc-macro-test"

crates/proc-macro-srv/proc-macro-test/imp/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ pub fn fn_like_span_ops(args: TokenStream) -> TokenStream {
7979
TokenStream::from_iter(vec![first, second, third])
8080
}
8181

82+
/// Returns the line and column of the first token's span as two integer literals.
83+
#[proc_macro]
84+
pub fn fn_like_span_line_column(args: TokenStream) -> TokenStream {
85+
let first = args.into_iter().next().unwrap();
86+
let span = first.span();
87+
let line = Literal::usize_unsuffixed(span.line());
88+
let column = Literal::usize_unsuffixed(span.column());
89+
TokenStream::from_iter(vec![TokenTree::Literal(line), TokenTree::Literal(column)])
90+
}
91+
8292
#[proc_macro_attribute]
8393
pub fn attr_noop(_args: TokenStream, item: TokenStream) -> TokenStream {
8494
item

crates/proc-macro-srv/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ pub trait ProcMacroClientInterface {
9898
fn file(&mut self, file_id: span::FileId) -> String;
9999
fn source_text(&mut self, span: Span) -> Option<String>;
100100
fn local_file(&mut self, file_id: span::FileId) -> Option<String>;
101+
/// Line and column are 1-based.
102+
fn line_column(&mut self, span: Span) -> Option<(u32, u32)>;
101103
}
102104

103105
const EXPANDER_STACK_SIZE: usize = 8 * 1024 * 1024;

crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,12 @@ impl server::Span for RaSpanServer<'_> {
257257
Span { range: TextRange::empty(span.range.start()), ..span }
258258
}
259259

260-
fn line(&mut self, _span: Self::Span) -> usize {
261-
// FIXME requires db to resolve line index, THIS IS NOT INCREMENTAL
262-
1
260+
fn line(&mut self, span: Self::Span) -> usize {
261+
self.callback.as_mut().and_then(|cb| cb.line_column(span)).map_or(1, |(l, _)| l as usize)
263262
}
264263

265-
fn column(&mut self, _span: Self::Span) -> usize {
266-
// FIXME requires db to resolve line index, THIS IS NOT INCREMENTAL
267-
1
264+
fn column(&mut self, span: Self::Span) -> usize {
265+
self.callback.as_mut().and_then(|cb| cb.line_column(span)).map_or(1, |(_, c)| c as usize)
268266
}
269267
}
270268

crates/proc-macro-srv/src/tests/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,7 @@ fn list_test_macros() {
703703
fn_like_mk_idents [Bang]
704704
fn_like_span_join [Bang]
705705
fn_like_span_ops [Bang]
706+
fn_like_span_line_column [Bang]
706707
attr_noop [Attr]
707708
attr_panic [Attr]
708709
attr_error [Attr]
@@ -712,3 +713,17 @@ fn list_test_macros() {
712713
DeriveError [CustomDerive]"#]]
713714
.assert_eq(&res);
714715
}
716+
717+
#[test]
718+
fn test_fn_like_span_line_column() {
719+
assert_expand_with_callback(
720+
"fn_like_span_line_column",
721+
// Input text with known position: "hello" starts at offset 1 (line 2, column 1 in 1-based)
722+
"
723+
hello",
724+
expect![[r#"
725+
LITER 42:Root[0000, 0]@0..100#ROOT2024 Integer 2
726+
LITER 42:Root[0000, 0]@0..100#ROOT2024 Integer 1
727+
"#]],
728+
);
729+
}

crates/proc-macro-srv/src/tests/utils.rs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use span::{
66
};
77

88
use crate::{
9-
EnvSnapshot, ProcMacroSrv, SpanId, dylib, proc_macro_test_dylib_path, token_stream::TokenStream,
9+
EnvSnapshot, ProcMacroClientInterface, ProcMacroSrv, SpanId, dylib, proc_macro_test_dylib_path,
10+
token_stream::TokenStream,
1011
};
1112

1213
fn parse_string(call_site: SpanId, src: &str) -> TokenStream<SpanId> {
@@ -109,3 +110,66 @@ pub(crate) fn list() -> Vec<String> {
109110
let res = srv.list_macros(&dylib_path).unwrap();
110111
res.into_iter().map(|(name, kind)| format!("{name} [{kind:?}]")).collect()
111112
}
113+
114+
/// A mock callback for testing that computes line/column from the input text.
115+
struct MockCallback<'a> {
116+
text: &'a str,
117+
}
118+
119+
impl ProcMacroClientInterface for MockCallback<'_> {
120+
fn source_text(&mut self, span: Span) -> Option<String> {
121+
self.text
122+
.get(usize::from(span.range.start())..usize::from(span.range.end()))
123+
.map(ToOwned::to_owned)
124+
}
125+
126+
fn file(&mut self, _file_id: FileId) -> String {
127+
String::new()
128+
}
129+
130+
fn local_file(&mut self, _file_id: FileId) -> Option<String> {
131+
None
132+
}
133+
134+
fn line_column(&mut self, span: Span) -> Option<(u32, u32)> {
135+
let line_index = line_index::LineIndex::new(self.text);
136+
let line_col = line_index.try_line_col(span.range.start())?;
137+
// proc_macro uses 1-based line/column
138+
Some((line_col.line as u32 + 1, line_col.col as u32 + 1))
139+
}
140+
}
141+
142+
pub fn assert_expand_with_callback(
143+
macro_name: &str,
144+
#[rust_analyzer::rust_fixture] ra_fixture: &str,
145+
expect_spanned: Expect,
146+
) {
147+
let path = proc_macro_test_dylib_path();
148+
let expander = dylib::Expander::new(&temp_dir::TempDir::new().unwrap(), &path).unwrap();
149+
150+
let def_site = Span {
151+
range: TextRange::new(0.into(), 150.into()),
152+
anchor: SpanAnchor {
153+
file_id: EditionedFileId::current_edition(FileId::from_raw(41)),
154+
ast_id: ROOT_ERASED_FILE_AST_ID,
155+
},
156+
ctx: SyntaxContext::root(span::Edition::CURRENT),
157+
};
158+
let call_site = Span {
159+
range: TextRange::new(0.into(), 100.into()),
160+
anchor: SpanAnchor {
161+
file_id: EditionedFileId::current_edition(FileId::from_raw(42)),
162+
ast_id: ROOT_ERASED_FILE_AST_ID,
163+
},
164+
ctx: SyntaxContext::root(span::Edition::CURRENT),
165+
};
166+
let mixed_site = call_site;
167+
168+
let fixture = parse_string_spanned(call_site.anchor, call_site.ctx, ra_fixture);
169+
170+
let mut callback = MockCallback { text: ra_fixture };
171+
let res = expander
172+
.expand(macro_name, fixture, None, def_site, call_site, mixed_site, Some(&mut callback))
173+
.unwrap();
174+
expect_spanned.assert_eq(&format!("{res:?}"));
175+
}

0 commit comments

Comments
 (0)