Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: LSP code action "Fill struct fields" #5885

Merged
merged 13 commits into from
Sep 3, 2024
136 changes: 23 additions & 113 deletions tooling/lsp/src/requests/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@ use async_lsp::ResponseError;
use fm::{FileId, FileMap, PathString};
use lsp_types::{
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
Position, Range, TextDocumentPositionParams, TextEdit, Url, WorkspaceEdit,
TextDocumentPositionParams, TextEdit, Url, WorkspaceEdit,
};
use noirc_errors::{Location, Span};
use noirc_errors::Span;
use noirc_frontend::{
ast::{Ident, Path, Visitor},
ast::{ConstructorExpression, Path, Visitor},
graph::CrateId,
hir::def_map::{CrateDefMap, LocalModuleId, ModuleId},
macros_api::{ModuleDefId, NodeInterner},
macros_api::NodeInterner,
};
use noirc_frontend::{
parser::{Item, ItemKind, ParsedSubModule},
ParsedModule,
};

use crate::{
byte_span_to_range,
modules::{get_parent_module_id, module_full_path, module_id_path},
utils, LspState,
};
use crate::{utils, LspState};

use super::{process_request, to_lsp_location};

mod fill_struct_fields;
mod import_or_qualify;
#[cfg(test)]
mod tests;

Expand Down Expand Up @@ -68,6 +68,7 @@ struct CodeActionFinder<'a> {
uri: Url,
files: &'a FileMap,
file: FileId,
source: &'a str,
lines: Vec<&'a str>,
byte_index: usize,
/// The module ID in scope. This might change as we traverse the AST
Expand Down Expand Up @@ -108,6 +109,7 @@ impl<'a> CodeActionFinder<'a> {
uri,
files,
file,
source,
lines: source.lines().collect(),
byte_index,
module_id,
Expand Down Expand Up @@ -137,46 +139,7 @@ impl<'a> CodeActionFinder<'a> {
Some(code_actions)
}

fn push_import_code_action(&mut self, full_path: &str) {
let line = self.auto_import_line as u32;
let character = (self.nesting * 4) as u32;
let indent = " ".repeat(self.nesting * 4);
let mut newlines = "\n";

// If the line we are inserting into is not an empty line, insert an extra line to make some room
if let Some(line_text) = self.lines.get(line as usize) {
if !line_text.trim().is_empty() {
newlines = "\n\n";
}
}

let title = format!("Import {}", full_path);
let text_edit = TextEdit {
range: Range { start: Position { line, character }, end: Position { line, character } },
new_text: format!("use {};{}{}", full_path, newlines, indent),
};

let code_action = self.new_quick_fix(title, text_edit);
self.code_actions.push(CodeActionOrCommand::CodeAction(code_action));
}

fn push_qualify_code_action(&mut self, ident: &Ident, prefix: &str, full_path: &str) {
let Some(range) = byte_span_to_range(
self.files,
self.file,
ident.span().start() as usize..ident.span().start() as usize,
) else {
return;
};

let title = format!("Qualify as {}", full_path);
let text_edit = TextEdit { range, new_text: format!("{}::", prefix) };

let code_action = self.new_quick_fix(title, text_edit);
self.code_actions.push(CodeActionOrCommand::CodeAction(code_action));
}

fn new_quick_fix(&self, title: String, text_edit: TextEdit) -> CodeAction {
fn new_quick_fix(&self, title: String, text_edit: TextEdit) -> CodeActionOrCommand {
let mut changes = HashMap::new();
changes.insert(self.uri.clone(), vec![text_edit]);

Expand All @@ -186,7 +149,7 @@ impl<'a> CodeActionFinder<'a> {
change_annotations: None,
};

CodeAction {
CodeActionOrCommand::CodeAction(CodeAction {
title,
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: None,
Expand All @@ -195,7 +158,7 @@ impl<'a> CodeActionFinder<'a> {
is_preferred: None,
disabled: None,
data: None,
}
})
}

fn includes_span(&self, span: Span) -> bool {
Expand Down Expand Up @@ -244,69 +207,16 @@ impl<'a> Visitor for CodeActionFinder<'a> {
}

fn visit_path(&mut self, path: &Path) {
if path.segments.len() != 1 {
return;
}

let ident = &path.segments[0].ident;
if !self.includes_span(ident.span()) {
return;
}

let location = Location::new(ident.span(), self.file);
if self.interner.find_referenced(location).is_some() {
return;
}

let current_module_parent_id = get_parent_module_id(self.def_maps, self.module_id);

// The Path doesn't resolve to anything so it means it's an error and maybe we
// can suggest an import or to fully-qualify the path.
for (name, entries) in self.interner.get_auto_import_names() {
if name != &ident.0.contents {
continue;
}

for (module_def_id, visibility, defining_module) in entries {
let module_full_path = if let Some(defining_module) = defining_module {
module_id_path(
*defining_module,
&self.module_id,
current_module_parent_id,
self.interner,
)
} else {
let Some(module_full_path) = module_full_path(
*module_def_id,
*visibility,
self.module_id,
current_module_parent_id,
self.interner,
) else {
continue;
};
module_full_path
};

let full_path = if defining_module.is_some()
|| !matches!(module_def_id, ModuleDefId::ModuleId(..))
{
format!("{}::{}", module_full_path, name)
} else {
module_full_path.clone()
};
self.import_or_qualify(path);
}

let qualify_prefix = if let ModuleDefId::ModuleId(..) = module_def_id {
let mut segments: Vec<_> = module_full_path.split("::").collect();
segments.pop();
segments.join("::")
} else {
module_full_path
};
fn visit_constructor_expression(
&mut self,
constructor: &ConstructorExpression,
span: Span,
) -> bool {
self.fill_struct_fields(constructor, span);

self.push_import_code_action(&full_path);
self.push_qualify_code_action(ident, &qualify_prefix, &full_path);
}
}
true
}
}
Loading
Loading