Skip to content

Commit b822da0

Browse files
committed
feat(service): code action for removing mut
1 parent 368ed86 commit b822da0

File tree

8 files changed

+472
-0
lines changed

8 files changed

+472
-0
lines changed

crates/service/src/features/code_action.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ impl LanguageService {
115115
}
116116
}
117117
}
118+
SyntaxKind::GLOBAL_TYPE => {
119+
if quickfix {
120+
if let Some(action) =
121+
remove_mut::act(self, uri, &line_index, &it, &params.context)
122+
{
123+
actions.push(CodeActionOrCommand::CodeAction(action));
124+
}
125+
}
126+
}
118127
_ => {}
119128
}
120129
node = it;

crates/service/src/refactorings/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod fix_invalid_mem_arg;
33
pub mod func_header_join;
44
pub mod func_header_split;
55
pub mod if_br_to_br_if;
6+
pub mod remove_mut;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use crate::{
2+
helpers,
3+
uri::{InternUri, UrisCtx},
4+
LanguageService,
5+
};
6+
use line_index::LineIndex;
7+
use lsp_types::{
8+
CodeAction, CodeActionContext, CodeActionKind, NumberOrString, TextEdit, WorkspaceEdit,
9+
};
10+
use rowan::ast::support;
11+
use std::collections::HashMap;
12+
use wat_syntax::{SyntaxKind, SyntaxNode};
13+
14+
pub fn act(
15+
service: &LanguageService,
16+
uri: InternUri,
17+
line_index: &LineIndex,
18+
node: &SyntaxNode,
19+
context: &CodeActionContext,
20+
) -> Option<CodeAction> {
21+
let mut_token =
22+
support::token(node, SyntaxKind::KEYWORD).filter(|keyword| keyword.text() == "mut")?;
23+
let token_lsp_range = helpers::rowan_range_to_lsp_range(line_index, mut_token.text_range());
24+
let diagnostic = context
25+
.diagnostics
26+
.iter()
27+
.find(|diagnostic| match &diagnostic.code {
28+
Some(NumberOrString::String(code)) => {
29+
code == "needless-mut" && diagnostic.range == token_lsp_range
30+
}
31+
_ => false,
32+
})?;
33+
34+
let mut text_edits = Vec::with_capacity(4);
35+
if let Some(l_paren) = support::token(node, SyntaxKind::L_PAREN) {
36+
text_edits.push(TextEdit {
37+
range: helpers::rowan_range_to_lsp_range(line_index, l_paren.text_range()),
38+
new_text: "".into(),
39+
});
40+
}
41+
text_edits.push(TextEdit {
42+
range: token_lsp_range,
43+
new_text: "".into(),
44+
});
45+
if let Some(whitespace) = mut_token
46+
.next_token()
47+
.filter(|token| token.kind() == SyntaxKind::WHITESPACE)
48+
{
49+
text_edits.push(TextEdit {
50+
range: helpers::rowan_range_to_lsp_range(line_index, whitespace.text_range()),
51+
new_text: "".into(),
52+
});
53+
}
54+
if let Some(r_paren) = support::token(node, SyntaxKind::R_PAREN) {
55+
text_edits.push(TextEdit {
56+
range: helpers::rowan_range_to_lsp_range(line_index, r_paren.text_range()),
57+
new_text: "".into(),
58+
});
59+
}
60+
61+
#[expect(clippy::mutable_key_type)]
62+
if text_edits.is_empty() {
63+
None
64+
} else {
65+
let mut changes = HashMap::with_capacity(1);
66+
changes.insert(service.lookup_uri(uri), text_edits);
67+
Some(CodeAction {
68+
title: "Remove `mut`".into(),
69+
kind: Some(CodeActionKind::QUICKFIX),
70+
edit: Some(WorkspaceEdit {
71+
changes: Some(changes),
72+
..Default::default()
73+
}),
74+
is_preferred: Some(true),
75+
diagnostics: Some(vec![diagnostic.clone()]),
76+
..Default::default()
77+
})
78+
}
79+
}

crates/service/tests/code_action/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod fix_invalid_mem_arg;
55
mod func_header_join;
66
mod func_header_split;
77
mod if_br_to_br_if;
8+
mod remove_mut;
89

910
fn create_params(uri: Uri, range: Range) -> CodeActionParams {
1011
CodeActionParams {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use super::*;
2+
use insta::assert_json_snapshot;
3+
use lsp_types::{Diagnostic, NumberOrString, Position, Range, Uri};
4+
use wat_service::LanguageService;
5+
6+
fn create_params(uri: Uri, range: Range, token_range: Range) -> CodeActionParams {
7+
CodeActionParams {
8+
text_document: TextDocumentIdentifier { uri },
9+
range,
10+
context: CodeActionContext {
11+
diagnostics: vec![Diagnostic {
12+
range: token_range,
13+
code: Some(NumberOrString::String("needless-mut".into())),
14+
message: "".into(),
15+
..Default::default()
16+
}],
17+
only: None,
18+
trigger_kind: None,
19+
},
20+
work_done_progress_params: Default::default(),
21+
partial_result_params: Default::default(),
22+
}
23+
}
24+
25+
#[test]
26+
fn no_mut() {
27+
let uri = "untitled:test".parse::<Uri>().unwrap();
28+
let source = "
29+
(module
30+
(global $a i32))
31+
";
32+
let mut service = LanguageService::default();
33+
service.commit(uri.clone(), source.into());
34+
let response = service.code_action(super::create_params(
35+
uri,
36+
Range::new(Position::new(2, 15), Position::new(2, 15)),
37+
));
38+
assert!(response.is_none());
39+
}
40+
41+
#[test]
42+
fn no_diagnostics() {
43+
let uri = "untitled:test".parse::<Uri>().unwrap();
44+
let source = "
45+
(module
46+
(global $a (mut i32)))
47+
";
48+
let mut service = LanguageService::default();
49+
service.commit(uri.clone(), source.into());
50+
let response = service.code_action(super::create_params(
51+
uri,
52+
Range::new(Position::new(2, 15), Position::new(2, 15)),
53+
));
54+
assert!(response.is_none());
55+
}
56+
57+
#[test]
58+
fn unrelated_range() {
59+
let uri = "untitled:test".parse::<Uri>().unwrap();
60+
let source = "
61+
(module
62+
(global $a (mut i32))
63+
(global $b (mut i32)))
64+
";
65+
let mut service = LanguageService::default();
66+
service.commit(uri.clone(), source.into());
67+
let response = service.code_action(create_params(
68+
uri,
69+
Range::new(Position::new(2, 15), Position::new(2, 15)),
70+
Range::new(Position::new(3, 14), Position::new(3, 17)),
71+
));
72+
assert!(response.is_none());
73+
}
74+
75+
#[test]
76+
fn unrelated_diagnostic() {
77+
let uri = "untitled:test".parse::<Uri>().unwrap();
78+
let source = "
79+
(module
80+
(global $a (mut i32))
81+
(global $b (mut i32)))
82+
";
83+
let mut service = LanguageService::default();
84+
service.commit(uri.clone(), source.into());
85+
let response = service.code_action(CodeActionParams {
86+
text_document: TextDocumentIdentifier { uri },
87+
range: Range::new(Position::new(2, 15), Position::new(2, 15)),
88+
context: CodeActionContext {
89+
diagnostics: vec![Diagnostic {
90+
range: Range::new(Position::new(2, 15), Position::new(2, 15)),
91+
code: Some(NumberOrString::String("global-mut".into())),
92+
message: "".into(),
93+
..Default::default()
94+
}],
95+
only: None,
96+
trigger_kind: None,
97+
},
98+
work_done_progress_params: Default::default(),
99+
partial_result_params: Default::default(),
100+
});
101+
assert!(response.is_none());
102+
}
103+
104+
#[test]
105+
fn simple() {
106+
let uri = "untitled:test".parse::<Uri>().unwrap();
107+
let source = "
108+
(module
109+
(global $a (mut i32)))
110+
";
111+
let mut service = LanguageService::default();
112+
service.commit(uri.clone(), source.into());
113+
let response = service.code_action(create_params(
114+
uri,
115+
Range::new(Position::new(2, 15), Position::new(2, 15)),
116+
Range::new(Position::new(2, 14), Position::new(2, 17)),
117+
));
118+
assert_json_snapshot!(response);
119+
}
120+
121+
#[test]
122+
fn missing_r_paren() {
123+
let uri = "untitled:test".parse::<Uri>().unwrap();
124+
let source = "
125+
(module
126+
(global $a (mut i32
127+
";
128+
let mut service = LanguageService::default();
129+
service.commit(uri.clone(), source.into());
130+
let response = service.code_action(create_params(
131+
uri,
132+
Range::new(Position::new(2, 15), Position::new(2, 15)),
133+
Range::new(Position::new(2, 14), Position::new(2, 17)),
134+
));
135+
assert_json_snapshot!(response);
136+
}
137+
138+
#[test]
139+
fn with_comments() {
140+
let uri = "untitled:test".parse::<Uri>().unwrap();
141+
let source = "
142+
(module
143+
(global $a ((;a;) mut(;b;) i32)))
144+
";
145+
let mut service = LanguageService::default();
146+
service.commit(uri.clone(), source.into());
147+
let response = service.code_action(create_params(
148+
uri,
149+
Range::new(Position::new(2, 20), Position::new(2, 20)),
150+
Range::new(Position::new(2, 20), Position::new(2, 23)),
151+
));
152+
assert_json_snapshot!(response);
153+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
source: crates/service/tests/code_action/remove_mut.rs
3+
expression: response
4+
---
5+
[
6+
{
7+
"title": "Remove `mut`",
8+
"kind": "quickfix",
9+
"diagnostics": [
10+
{
11+
"range": {
12+
"start": {
13+
"line": 2,
14+
"character": 14
15+
},
16+
"end": {
17+
"line": 2,
18+
"character": 17
19+
}
20+
},
21+
"code": "needless-mut",
22+
"message": ""
23+
}
24+
],
25+
"edit": {
26+
"changes": {
27+
"untitled:test": [
28+
{
29+
"range": {
30+
"start": {
31+
"line": 2,
32+
"character": 13
33+
},
34+
"end": {
35+
"line": 2,
36+
"character": 14
37+
}
38+
},
39+
"newText": ""
40+
},
41+
{
42+
"range": {
43+
"start": {
44+
"line": 2,
45+
"character": 14
46+
},
47+
"end": {
48+
"line": 2,
49+
"character": 17
50+
}
51+
},
52+
"newText": ""
53+
},
54+
{
55+
"range": {
56+
"start": {
57+
"line": 2,
58+
"character": 17
59+
},
60+
"end": {
61+
"line": 2,
62+
"character": 18
63+
}
64+
},
65+
"newText": ""
66+
}
67+
]
68+
}
69+
},
70+
"isPreferred": true
71+
}
72+
]

0 commit comments

Comments
 (0)