Skip to content

Commit 1e6e4f4

Browse files
authored
feat: LSP code action "Fill struct fields" (#5885)
# 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.
1 parent d1d93c7 commit 1e6e4f4

File tree

4 files changed

+571
-262
lines changed

4 files changed

+571
-262
lines changed

tooling/lsp/src/requests/code_action.rs

+23-113
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,26 @@ use async_lsp::ResponseError;
77
use fm::{FileId, FileMap, PathString};
88
use lsp_types::{
99
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
10-
Position, Range, TextDocumentPositionParams, TextEdit, Url, WorkspaceEdit,
10+
TextDocumentPositionParams, TextEdit, Url, WorkspaceEdit,
1111
};
12-
use noirc_errors::{Location, Span};
12+
use noirc_errors::Span;
1313
use noirc_frontend::{
14-
ast::{Ident, Path, Visitor},
14+
ast::{ConstructorExpression, Path, Visitor},
1515
graph::CrateId,
1616
hir::def_map::{CrateDefMap, LocalModuleId, ModuleId},
17-
macros_api::{ModuleDefId, NodeInterner},
17+
macros_api::NodeInterner,
18+
};
19+
use noirc_frontend::{
1820
parser::{Item, ItemKind, ParsedSubModule},
1921
ParsedModule,
2022
};
2123

22-
use crate::{
23-
byte_span_to_range,
24-
modules::{get_parent_module_id, module_full_path, module_id_path},
25-
utils, LspState,
26-
};
24+
use crate::{utils, LspState};
2725

2826
use super::{process_request, to_lsp_location};
2927

28+
mod fill_struct_fields;
29+
mod import_or_qualify;
3030
#[cfg(test)]
3131
mod tests;
3232

@@ -68,6 +68,7 @@ struct CodeActionFinder<'a> {
6868
uri: Url,
6969
files: &'a FileMap,
7070
file: FileId,
71+
source: &'a str,
7172
lines: Vec<&'a str>,
7273
byte_index: usize,
7374
/// The module ID in scope. This might change as we traverse the AST
@@ -108,6 +109,7 @@ impl<'a> CodeActionFinder<'a> {
108109
uri,
109110
files,
110111
file,
112+
source,
111113
lines: source.lines().collect(),
112114
byte_index,
113115
module_id,
@@ -137,46 +139,7 @@ impl<'a> CodeActionFinder<'a> {
137139
Some(code_actions)
138140
}
139141

140-
fn push_import_code_action(&mut self, full_path: &str) {
141-
let line = self.auto_import_line as u32;
142-
let character = (self.nesting * 4) as u32;
143-
let indent = " ".repeat(self.nesting * 4);
144-
let mut newlines = "\n";
145-
146-
// If the line we are inserting into is not an empty line, insert an extra line to make some room
147-
if let Some(line_text) = self.lines.get(line as usize) {
148-
if !line_text.trim().is_empty() {
149-
newlines = "\n\n";
150-
}
151-
}
152-
153-
let title = format!("Import {}", full_path);
154-
let text_edit = TextEdit {
155-
range: Range { start: Position { line, character }, end: Position { line, character } },
156-
new_text: format!("use {};{}{}", full_path, newlines, indent),
157-
};
158-
159-
let code_action = self.new_quick_fix(title, text_edit);
160-
self.code_actions.push(CodeActionOrCommand::CodeAction(code_action));
161-
}
162-
163-
fn push_qualify_code_action(&mut self, ident: &Ident, prefix: &str, full_path: &str) {
164-
let Some(range) = byte_span_to_range(
165-
self.files,
166-
self.file,
167-
ident.span().start() as usize..ident.span().start() as usize,
168-
) else {
169-
return;
170-
};
171-
172-
let title = format!("Qualify as {}", full_path);
173-
let text_edit = TextEdit { range, new_text: format!("{}::", prefix) };
174-
175-
let code_action = self.new_quick_fix(title, text_edit);
176-
self.code_actions.push(CodeActionOrCommand::CodeAction(code_action));
177-
}
178-
179-
fn new_quick_fix(&self, title: String, text_edit: TextEdit) -> CodeAction {
142+
fn new_quick_fix(&self, title: String, text_edit: TextEdit) -> CodeActionOrCommand {
180143
let mut changes = HashMap::new();
181144
changes.insert(self.uri.clone(), vec![text_edit]);
182145

@@ -186,7 +149,7 @@ impl<'a> CodeActionFinder<'a> {
186149
change_annotations: None,
187150
};
188151

189-
CodeAction {
152+
CodeActionOrCommand::CodeAction(CodeAction {
190153
title,
191154
kind: Some(CodeActionKind::QUICKFIX),
192155
diagnostics: None,
@@ -195,7 +158,7 @@ impl<'a> CodeActionFinder<'a> {
195158
is_preferred: None,
196159
disabled: None,
197160
data: None,
198-
}
161+
})
199162
}
200163

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

246209
fn visit_path(&mut self, path: &Path) {
247-
if path.segments.len() != 1 {
248-
return;
249-
}
250-
251-
let ident = &path.segments[0].ident;
252-
if !self.includes_span(ident.span()) {
253-
return;
254-
}
255-
256-
let location = Location::new(ident.span(), self.file);
257-
if self.interner.find_referenced(location).is_some() {
258-
return;
259-
}
260-
261-
let current_module_parent_id = get_parent_module_id(self.def_maps, self.module_id);
262-
263-
// The Path doesn't resolve to anything so it means it's an error and maybe we
264-
// can suggest an import or to fully-qualify the path.
265-
for (name, entries) in self.interner.get_auto_import_names() {
266-
if name != &ident.0.contents {
267-
continue;
268-
}
269-
270-
for (module_def_id, visibility, defining_module) in entries {
271-
let module_full_path = if let Some(defining_module) = defining_module {
272-
module_id_path(
273-
*defining_module,
274-
&self.module_id,
275-
current_module_parent_id,
276-
self.interner,
277-
)
278-
} else {
279-
let Some(module_full_path) = module_full_path(
280-
*module_def_id,
281-
*visibility,
282-
self.module_id,
283-
current_module_parent_id,
284-
self.interner,
285-
) else {
286-
continue;
287-
};
288-
module_full_path
289-
};
290-
291-
let full_path = if defining_module.is_some()
292-
|| !matches!(module_def_id, ModuleDefId::ModuleId(..))
293-
{
294-
format!("{}::{}", module_full_path, name)
295-
} else {
296-
module_full_path.clone()
297-
};
210+
self.import_or_qualify(path);
211+
}
298212

299-
let qualify_prefix = if let ModuleDefId::ModuleId(..) = module_def_id {
300-
let mut segments: Vec<_> = module_full_path.split("::").collect();
301-
segments.pop();
302-
segments.join("::")
303-
} else {
304-
module_full_path
305-
};
213+
fn visit_constructor_expression(
214+
&mut self,
215+
constructor: &ConstructorExpression,
216+
span: Span,
217+
) -> bool {
218+
self.fill_struct_fields(constructor, span);
306219

307-
self.push_import_code_action(&full_path);
308-
self.push_qualify_code_action(ident, &qualify_prefix, &full_path);
309-
}
310-
}
220+
true
311221
}
312222
}

0 commit comments

Comments
 (0)