diff --git a/Cargo.lock b/Cargo.lock index 42eaeb01f1f2..8188fbf96064 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/load-cargo/src/lib.rs b/crates/load-cargo/src/lib.rs index e8d98b1ce661..33468a5003c3 100644 --- a/crates/load-cargo/src/lib.rs +++ b/crates/load-cargo/src/lib.rs @@ -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; diff --git a/crates/proc-macro-api/src/bidirectional_protocol/msg.rs b/crates/proc-macro-api/src/bidirectional_protocol/msg.rs index e41f8a5d7da7..0e3b700dcc5a 100644 --- a/crates/proc-macro-api/src/bidirectional_protocol/msg.rs +++ b/crates/proc-macro-api/src/bidirectional_protocol/msg.rs @@ -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 }, - LocalFilePathResult { name: Option }, + FilePathResult { + name: String, + }, + SourceTextResult { + text: Option, + }, + LocalFilePathResult { + name: Option, + }, + /// Line and column are 1-based. + LineColumnResult { + line: u32, + column: u32, + }, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/proc-macro-srv-cli/src/main_loop.rs b/crates/proc-macro-srv-cli/src/main_loop.rs index b2f4b96bd255..22536a4e52b1 100644 --- a/crates/proc-macro-srv-cli/src/main_loop.rs +++ b/crates/proc-macro-srv-cli/src/main_loop.rs @@ -220,6 +220,20 @@ impl 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( diff --git a/crates/proc-macro-srv/Cargo.toml b/crates/proc-macro-srv/Cargo.toml index 361017178409..8e5617f8a20e 100644 --- a/crates/proc-macro-srv/Cargo.toml +++ b/crates/proc-macro-srv/Cargo.toml @@ -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" diff --git a/crates/proc-macro-srv/proc-macro-test/imp/src/lib.rs b/crates/proc-macro-srv/proc-macro-test/imp/src/lib.rs index b4fac26d6e72..06c76b6d0381 100644 --- a/crates/proc-macro-srv/proc-macro-test/imp/src/lib.rs +++ b/crates/proc-macro-srv/proc-macro-test/imp/src/lib.rs @@ -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 diff --git a/crates/proc-macro-srv/src/lib.rs b/crates/proc-macro-srv/src/lib.rs index f2d1dfbba4cc..c1ef49a7176b 100644 --- a/crates/proc-macro-srv/src/lib.rs +++ b/crates/proc-macro-srv/src/lib.rs @@ -98,6 +98,8 @@ pub trait ProcMacroClientInterface { fn file(&mut self, file_id: span::FileId) -> String; fn source_text(&mut self, span: Span) -> Option; fn local_file(&mut self, file_id: span::FileId) -> Option; + /// Line and column are 1-based. + fn line_column(&mut self, span: Span) -> Option<(u32, u32)>; } const EXPANDER_STACK_SIZE: usize = 8 * 1024 * 1024; diff --git a/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs b/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs index 32725afc5527..3a25391b573b 100644 --- a/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs +++ b/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs @@ -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) } } diff --git a/crates/proc-macro-srv/src/tests/mod.rs b/crates/proc-macro-srv/src/tests/mod.rs index 20507a6def54..ebef9a9a519a 100644 --- a/crates/proc-macro-srv/src/tests/mod.rs +++ b/crates/proc-macro-srv/src/tests/mod.rs @@ -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 + "#]], + ); +} diff --git a/crates/proc-macro-srv/src/tests/utils.rs b/crates/proc-macro-srv/src/tests/utils.rs index 61fcd810b1d9..81ff1965d68b 100644 --- a/crates/proc-macro-srv/src/tests/utils.rs +++ b/crates/proc-macro-srv/src/tests/utils.rs @@ -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 { @@ -109,3 +110,66 @@ pub(crate) fn list() -> Vec { 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 { + 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 { + 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:?}")); +}