diff options
author | Lukas Wirth <[email protected]> | 2021-03-30 16:20:43 +0100 |
---|---|---|
committer | Lukas Wirth <[email protected]> | 2021-03-30 16:20:43 +0100 |
commit | 9a327311e4a9b9102528751e052c63266c00c6bd (patch) | |
tree | 6cddc362ef8237570c0ce1f8fef73d9dcc9eec8a /crates/hir_def | |
parent | 9df78ec4a4e41ca94b25f292aba90e266f104f02 (diff) |
Implement basic Documentation source to syntax range mapping
Diffstat (limited to 'crates/hir_def')
-rw-r--r-- | crates/hir_def/src/attr.rs | 109 |
1 files changed, 107 insertions, 2 deletions
diff --git a/crates/hir_def/src/attr.rs b/crates/hir_def/src/attr.rs index 52a2bce9b..7791402c9 100644 --- a/crates/hir_def/src/attr.rs +++ b/crates/hir_def/src/attr.rs | |||
@@ -1,6 +1,10 @@ | |||
1 | //! A higher level attributes based on TokenTree, with also some shortcuts. | 1 | //! A higher level attributes based on TokenTree, with also some shortcuts. |
2 | 2 | ||
3 | use std::{ops, sync::Arc}; | 3 | use std::{ |
4 | cmp::Ordering, | ||
5 | ops::{self, Range}, | ||
6 | sync::Arc, | ||
7 | }; | ||
4 | 8 | ||
5 | use base_db::CrateId; | 9 | use base_db::CrateId; |
6 | use cfg::{CfgExpr, CfgOptions}; | 10 | use cfg::{CfgExpr, CfgOptions}; |
@@ -12,7 +16,7 @@ use mbe::ast_to_token_tree; | |||
12 | use smallvec::{smallvec, SmallVec}; | 16 | use smallvec::{smallvec, SmallVec}; |
13 | use syntax::{ | 17 | use syntax::{ |
14 | ast::{self, AstNode, AttrsOwner}, | 18 | ast::{self, AstNode, AttrsOwner}, |
15 | match_ast, AstToken, SmolStr, SyntaxNode, | 19 | match_ast, AstToken, SmolStr, SyntaxNode, TextRange, TextSize, |
16 | }; | 20 | }; |
17 | use tt::Subtree; | 21 | use tt::Subtree; |
18 | 22 | ||
@@ -451,6 +455,54 @@ impl AttrsWithOwner { | |||
451 | .collect(), | 455 | .collect(), |
452 | } | 456 | } |
453 | } | 457 | } |
458 | |||
459 | pub fn docs_with_rangemap( | ||
460 | &self, | ||
461 | db: &dyn DefDatabase, | ||
462 | ) -> Option<(Documentation, DocsRangeMap)> { | ||
463 | // FIXME: code duplication in `docs` above | ||
464 | let docs = self.by_key("doc").attrs().flat_map(|attr| match attr.input.as_ref()? { | ||
465 | AttrInput::Literal(s) => Some((s, attr.index)), | ||
466 | AttrInput::TokenTree(_) => None, | ||
467 | }); | ||
468 | let indent = docs | ||
469 | .clone() | ||
470 | .flat_map(|(s, _)| s.lines()) | ||
471 | .filter(|line| !line.chars().all(|c| c.is_whitespace())) | ||
472 | .map(|line| line.chars().take_while(|c| c.is_whitespace()).count()) | ||
473 | .min() | ||
474 | .unwrap_or(0); | ||
475 | let mut buf = String::new(); | ||
476 | let mut mapping = Vec::new(); | ||
477 | for (doc, idx) in docs { | ||
478 | // str::lines doesn't yield anything for the empty string | ||
479 | if !doc.is_empty() { | ||
480 | for line in doc.split('\n') { | ||
481 | let line = line.trim_end(); | ||
482 | let (offset, line) = match line.char_indices().nth(indent) { | ||
483 | Some((offset, _)) => (offset, &line[offset..]), | ||
484 | None => (0, line), | ||
485 | }; | ||
486 | let buf_offset = buf.len(); | ||
487 | buf.push_str(line); | ||
488 | mapping.push(( | ||
489 | Range { start: buf_offset, end: buf.len() }, | ||
490 | idx, | ||
491 | Range { start: offset, end: line.len() }, | ||
492 | )); | ||
493 | buf.push('\n'); | ||
494 | } | ||
495 | } else { | ||
496 | buf.push('\n'); | ||
497 | } | ||
498 | } | ||
499 | buf.pop(); | ||
500 | if buf.is_empty() { | ||
501 | None | ||
502 | } else { | ||
503 | Some((Documentation(buf), DocsRangeMap { mapping, source: self.source_map(db).attrs })) | ||
504 | } | ||
505 | } | ||
454 | } | 506 | } |
455 | 507 | ||
456 | fn inner_attributes( | 508 | fn inner_attributes( |
@@ -507,6 +559,59 @@ impl AttrSourceMap { | |||
507 | } | 559 | } |
508 | } | 560 | } |
509 | 561 | ||
562 | /// A struct to map text ranges from [`Documentation`] back to TextRanges in the syntax tree. | ||
563 | pub struct DocsRangeMap { | ||
564 | source: Vec<InFile<Either<ast::Attr, ast::Comment>>>, | ||
565 | // (docstring-line-range, attr_index, attr-string-range) | ||
566 | // a mapping from the text range of a line of the [`Documentation`] to the attribute index and | ||
567 | // the original (untrimmed) syntax doc line | ||
568 | mapping: Vec<(Range<usize>, u32, Range<usize>)>, | ||
569 | } | ||
570 | |||
571 | impl DocsRangeMap { | ||
572 | pub fn map(&self, range: Range<usize>) -> Option<InFile<TextRange>> { | ||
573 | let found = self | ||
574 | .mapping | ||
575 | .binary_search_by(|(probe, ..)| { | ||
576 | if probe.contains(&range.start) { | ||
577 | Ordering::Equal | ||
578 | } else { | ||
579 | probe.start.cmp(&range.end) | ||
580 | } | ||
581 | }) | ||
582 | .ok()?; | ||
583 | let (line_docs_range, idx, original_line_src_range) = self.mapping[found].clone(); | ||
584 | if range.end > line_docs_range.end { | ||
585 | return None; | ||
586 | } | ||
587 | |||
588 | let relative_range = Range { | ||
589 | start: range.start - line_docs_range.start, | ||
590 | end: range.end - line_docs_range.start, | ||
591 | }; | ||
592 | let range_len = TextSize::from((range.end - range.start) as u32); | ||
593 | |||
594 | let &InFile { file_id, value: ref source } = &self.source[idx as usize]; | ||
595 | match source { | ||
596 | Either::Left(_) => None, // FIXME, figure out a nice way to handle doc attributes here | ||
597 | // as well as for whats done in syntax highlight doc injection | ||
598 | Either::Right(comment) => { | ||
599 | let text_range = comment.syntax().text_range(); | ||
600 | let range = TextRange::at( | ||
601 | text_range.start() | ||
602 | + TextSize::from( | ||
603 | (comment.prefix().len() | ||
604 | + original_line_src_range.start | ||
605 | + relative_range.start) as u32, | ||
606 | ), | ||
607 | text_range.len().min(range_len), | ||
608 | ); | ||
609 | Some(InFile { file_id, value: range }) | ||
610 | } | ||
611 | } | ||
612 | } | ||
613 | } | ||
614 | |||
510 | #[derive(Debug, Clone, PartialEq, Eq)] | 615 | #[derive(Debug, Clone, PartialEq, Eq)] |
511 | pub struct Attr { | 616 | pub struct Attr { |
512 | index: u32, | 617 | index: u32, |