Skip to content

Commit

Permalink
feat: LSP auto-import will try to add to existing use statements (#6354)
Browse files Browse the repository at this point in the history
  • Loading branch information
asterite authored Oct 25, 2024
1 parent 91c0842 commit 647f6a4
Show file tree
Hide file tree
Showing 9 changed files with 834 additions and 155 deletions.
1 change: 1 addition & 0 deletions compiler/noirc_frontend/src/ast/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ pub enum PathKind {
pub struct UseTree {
pub prefix: Path,
pub kind: UseTreeKind,
pub span: Span,
}

impl Display for UseTree {
Expand Down
42 changes: 34 additions & 8 deletions compiler/noirc_frontend/src/parser/parser/use_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,17 @@ impl<'a> Parser<'a> {
Self::parse_use_tree_in_list,
);

UseTree { prefix, kind: UseTreeKind::List(use_trees) }
UseTree {
prefix,
kind: UseTreeKind::List(use_trees),
span: self.span_since(start_span),
}
} else {
self.expected_token(Token::LeftBrace);
self.parse_path_use_tree_end(prefix, nested)
self.parse_path_use_tree_end(prefix, nested, start_span)
}
} else {
self.parse_path_use_tree_end(prefix, nested)
self.parse_path_use_tree_end(prefix, nested, start_span)
}
}

Expand All @@ -71,6 +75,7 @@ impl<'a> Parser<'a> {
return Some(UseTree {
prefix: Path { segments: Vec::new(), kind: PathKind::Plain, span: start_span },
kind: UseTreeKind::Path(Ident::new("self".to_string(), start_span), None),
span: start_span,
});
}

Expand All @@ -89,25 +94,46 @@ impl<'a> Parser<'a> {
}
}

pub(super) fn parse_path_use_tree_end(&mut self, mut prefix: Path, nested: bool) -> UseTree {
pub(super) fn parse_path_use_tree_end(
&mut self,
mut prefix: Path,
nested: bool,
start_span: Span,
) -> UseTree {
if prefix.segments.is_empty() {
if nested {
self.expected_identifier();
} else {
self.expected_label(ParsingRuleLabel::UseSegment);
}
UseTree { prefix, kind: UseTreeKind::Path(Ident::default(), None) }
UseTree {
prefix,
kind: UseTreeKind::Path(Ident::default(), None),
span: self.span_since(start_span),
}
} else {
let ident = prefix.segments.pop().unwrap().ident;
if self.eat_keyword(Keyword::As) {
if let Some(alias) = self.eat_ident() {
UseTree { prefix, kind: UseTreeKind::Path(ident, Some(alias)) }
UseTree {
prefix,
kind: UseTreeKind::Path(ident, Some(alias)),
span: self.span_since(start_span),
}
} else {
self.expected_identifier();
UseTree { prefix, kind: UseTreeKind::Path(ident, None) }
UseTree {
prefix,
kind: UseTreeKind::Path(ident, None),
span: self.span_since(start_span),
}
}
} else {
UseTree { prefix, kind: UseTreeKind::Path(ident, None) }
UseTree {
prefix,
kind: UseTreeKind::Path(ident, None),
span: self.span_since(start_span),
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions tooling/lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ mod modules;
mod notifications;
mod requests;
mod solver;
mod tests;
mod trait_impl_method_stub_generator;
mod types;
mod utils;
Expand Down
3 changes: 2 additions & 1 deletion tooling/lsp/src/requests/code_action/remove_unused_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,12 @@ fn use_tree_without_unused_import(
let mut prefix = use_tree.prefix.clone();
prefix.segments.extend(new_use_tree.prefix.segments);

Some(UseTree { prefix, kind: new_use_tree.kind })
Some(UseTree { prefix, kind: new_use_tree.kind, span: use_tree.span })
} else {
Some(UseTree {
prefix: use_tree.prefix.clone(),
kind: UseTreeKind::List(new_use_trees),
span: use_tree.span,
})
};

Expand Down
17 changes: 2 additions & 15 deletions tooling/lsp/src/requests/code_action/tests.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#![cfg(test)]

use crate::{notifications::on_did_open_text_document, test_utils};
use crate::{notifications::on_did_open_text_document, test_utils, tests::apply_text_edit};

use lsp_types::{
CodeActionContext, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
DidOpenTextDocumentParams, PartialResultParams, Position, Range, TextDocumentIdentifier,
TextDocumentItem, TextEdit, WorkDoneProgressParams,
TextDocumentItem, WorkDoneProgressParams,
};

use super::on_code_action_request;
Expand Down Expand Up @@ -78,16 +78,3 @@ pub(crate) async fn assert_code_action(title: &str, src: &str, expected: &str) {
assert_eq!(result, expected);
}
}

fn apply_text_edit(src: &str, text_edit: &TextEdit) -> String {
let mut lines: Vec<_> = src.lines().collect();
assert_eq!(text_edit.range.start.line, text_edit.range.end.line);

let mut line = lines[text_edit.range.start.line as usize].to_string();
line.replace_range(
text_edit.range.start.character as usize..text_edit.range.end.character as usize,
&text_edit.new_text,
);
lines[text_edit.range.start.line as usize] = &line;
lines.join("\n")
}
162 changes: 161 additions & 1 deletion tooling/lsp/src/requests/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,42 @@ pub(crate) fn on_completion_request(
future::ready(result)
}

/// The position of a segment in a `use` statement.
/// We use this to determine how an auto-import should be inserted.
#[derive(Debug, Default, Copy, Clone)]
enum UseSegmentPosition {
/// The segment either doesn't exist in the source code or there are multiple segments.
/// In this case auto-import will add a new use statement.
#[default]
NoneOrMultiple,
/// The segment is the last one in the `use` statement (or nested use statement):
///
/// use foo::bar;
/// ^^^
///
/// Auto-import will transform it to this:
///
/// use foo::bar::{self, baz};
Last { span: Span },
/// The segment happens before another simple (ident) segment:
///
/// use foo::bar::qux;
/// ^^^
///
/// Auto-import will transform it to this:
///
/// use foo::bar::{qux, baz};
BeforeSegment { segment_span_until_end: Span },
/// The segment happens before a list:
///
/// use foo::bar::{qux, another};
///
/// Auto-import will transform it to this:
///
/// use foo::bar::{qux, another, baz};
BeforeList { first_entry_span: Span, list_is_empty: bool },
}

struct NodeFinder<'a> {
files: &'a FileMap,
file: FileId,
Expand Down Expand Up @@ -115,6 +151,11 @@ struct NodeFinder<'a> {
nesting: usize,
/// The line where an auto_import must be inserted
auto_import_line: usize,
/// Remember where each segment in a `use` statement is located.
/// The key is the full segment, so for `use foo::bar::baz` we'll have three
/// segments: `foo`, `foo::bar` and `foo::bar::baz`, where the span is just
/// for the last identifier (`foo`, `bar` and `baz` in the previous example).
use_segment_positions: HashMap<String, UseSegmentPosition>,
self_type: Option<Type>,
in_comptime: bool,
}
Expand Down Expand Up @@ -159,6 +200,7 @@ impl<'a> NodeFinder<'a> {
suggested_module_def_ids: HashSet::new(),
nesting: 0,
auto_import_line: 0,
use_segment_positions: HashMap::new(),
self_type: None,
in_comptime: false,
}
Expand Down Expand Up @@ -1035,17 +1077,135 @@ impl<'a> NodeFinder<'a> {
}
}

/// Determine where each segment in a `use` statement is located.
fn gather_use_tree_segments(&mut self, use_tree: &UseTree, mut prefix: String) {
let kind_string = match use_tree.prefix.kind {
PathKind::Crate => Some("crate".to_string()),
PathKind::Super => Some("super".to_string()),
PathKind::Dep | PathKind::Plain => None,
};
if let Some(kind_string) = kind_string {
if let Some(segment) = use_tree.prefix.segments.first() {
self.insert_use_segment_position(
kind_string,
UseSegmentPosition::BeforeSegment {
segment_span_until_end: Span::from(
segment.ident.span().start()..use_tree.span.end() - 1,
),
},
);
} else {
self.insert_use_segment_position_before_use_tree_kind(use_tree, kind_string);
}
}

let prefix_segments_len = use_tree.prefix.segments.len();
for (index, segment) in use_tree.prefix.segments.iter().enumerate() {
let ident = &segment.ident;
if !prefix.is_empty() {
prefix.push_str("::");
};
prefix.push_str(&ident.0.contents);

if index < prefix_segments_len - 1 {
self.insert_use_segment_position(
prefix.clone(),
UseSegmentPosition::BeforeSegment {
segment_span_until_end: Span::from(
use_tree.prefix.segments[index + 1].ident.span().start()
..use_tree.span.end() - 1,
),
},
);
} else {
self.insert_use_segment_position_before_use_tree_kind(use_tree, prefix.clone());
}
}

match &use_tree.kind {
UseTreeKind::Path(ident, alias) => {
if !prefix.is_empty() {
prefix.push_str("::");
}
prefix.push_str(&ident.0.contents);

if alias.is_none() {
self.insert_use_segment_position(
prefix,
UseSegmentPosition::Last { span: ident.span() },
);
} else {
self.insert_use_segment_position(prefix, UseSegmentPosition::NoneOrMultiple);
}
}
UseTreeKind::List(use_trees) => {
for use_tree in use_trees {
self.gather_use_tree_segments(use_tree, prefix.clone());
}
}
}
}

fn insert_use_segment_position_before_use_tree_kind(
&mut self,
use_tree: &UseTree,
prefix: String,
) {
match &use_tree.kind {
UseTreeKind::Path(ident, _alias) => {
self.insert_use_segment_position(
prefix,
UseSegmentPosition::BeforeSegment {
segment_span_until_end: Span::from(
ident.span().start()..use_tree.span.end() - 1,
),
},
);
}
UseTreeKind::List(use_trees) => {
if let Some(first_use_tree) = use_trees.first() {
self.insert_use_segment_position(
prefix,
UseSegmentPosition::BeforeList {
first_entry_span: first_use_tree.prefix.span(),
list_is_empty: false,
},
);
} else {
self.insert_use_segment_position(
prefix,
UseSegmentPosition::BeforeList {
first_entry_span: Span::from(
use_tree.span.end() - 1..use_tree.span.end() - 1,
),
list_is_empty: true,
},
);
}
}
}
}

fn insert_use_segment_position(&mut self, segment: String, position: UseSegmentPosition) {
if self.use_segment_positions.get(&segment).is_none() {
self.use_segment_positions.insert(segment, position);
} else {
self.use_segment_positions.insert(segment, UseSegmentPosition::NoneOrMultiple);
}
}

fn includes_span(&self, span: Span) -> bool {
span.start() as usize <= self.byte_index && self.byte_index <= span.end() as usize
}
}

impl<'a> Visitor for NodeFinder<'a> {
fn visit_item(&mut self, item: &Item) -> bool {
if let ItemKind::Import(..) = &item.kind {
if let ItemKind::Import(use_tree, _) = &item.kind {
if let Some(lsp_location) = to_lsp_location(self.files, self.file, item.span) {
self.auto_import_line = (lsp_location.range.end.line + 1) as usize;
}
self.gather_use_tree_segments(use_tree, String::new());
}

self.includes_span(item.span)
Expand Down
Loading

0 comments on commit 647f6a4

Please sign in to comment.