Skip to content

Commit

Permalink
feat: LSP code action "Fill struct fields" (#5885)
Browse files Browse the repository at this point in the history
# Description

## Problem

Part of #1579

## Summary

My second mostly-used code action in Rust is "Fill struct fields" (and
"Fill match arms", but we don't have match in Noir yet). This PR
implements that.


![lsp-fill-struct-fields](https://github.com/user-attachments/assets/cd8bc4bd-c06e-4270-bfb3-7e703ee3899c)

## Additional Context

We don't have `todo!()` in Noir, so I used `()` instead. I think the
most helpful thing about this code action is filling out the field
names, so using `()` or `todo!()` is almost the same as you'll have to
replace either with something else.

## Documentation

Check one:
- [x] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.
  • Loading branch information
asterite authored Sep 3, 2024
1 parent d1d93c7 commit 1e6e4f4
Show file tree
Hide file tree
Showing 4 changed files with 571 additions and 262 deletions.
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

0 comments on commit 1e6e4f4

Please sign in to comment.