Skip to content

Commit a8a08f1

Browse files
GearsDatapackslpil
authored andcommitted
Support "Generate function" code action in other modules
1 parent dca6588 commit a8a08f1

8 files changed

+365
-22
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,35 @@
271271

272272
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
273273

274+
- The "Generate function" code action now allows generating function in other
275+
modules. For example, given the following code:
276+
277+
```gleam
278+
// maths.gleam
279+
pub fn add(a: Int, b: Int) -> Int { a + b }
280+
281+
// main.gleam
282+
import maths
283+
284+
pub fn main() -> Int {
285+
echo maths.add(1, 2)
286+
echo maths.subtract(from: 2, subtract: 1)
287+
// ^ Trigger the "Generate function" code action here
288+
}
289+
```
290+
291+
The language sever will edit the `maths.gleam` file:
292+
293+
```gleam
294+
pub fn add(a: Int, b: Int) -> Int { a + b }
295+
296+
pub fn subtract(from from: Int, subtract subtract: Int) -> Int {
297+
todo
298+
}
299+
```
300+
301+
([Surya Rose](https://github.com/GearsDatapacks))
302+
274303
- The "Add type annotations" and "Generate function" code actions now ignore type
275304
variables defined in other functions, improving the generated code. For example:
276305

compiler-core/src/language_server/code_action.rs

Lines changed: 130 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use crate::{
55
ast::{
66
self, ArgNames, AssignName, AssignmentKind, BitArraySegmentTruncation, BoundVariable,
77
CallArg, CustomType, FunctionLiteralKind, ImplicitCallArgOrigin, Import, PIPE_PRECEDENCE,
8-
Pattern, PatternUnusedArguments, PipelineAssignmentKind, RecordConstructor, SrcSpan,
9-
TodoKind, TypedArg, TypedAssignment, TypedClauseGuard, TypedExpr, TypedModuleConstant,
10-
TypedPattern, TypedPipelineAssignment, TypedRecordConstructor, TypedStatement, TypedUse,
11-
visit::Visit as _,
8+
Pattern, PatternUnusedArguments, PipelineAssignmentKind, Publicity, RecordConstructor,
9+
SrcSpan, TodoKind, TypedArg, TypedAssignment, TypedClauseGuard, TypedExpr,
10+
TypedModuleConstant, TypedPattern, TypedPipelineAssignment, TypedRecordConstructor,
11+
TypedStatement, TypedUse, visit::Visit as _,
1212
},
1313
build::{Located, Module},
1414
config::PackageConfig,
@@ -3690,7 +3690,7 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> {
36903690
};
36913691

36923692
let decoder_type = self.printer.print_type(&Type::Named {
3693-
publicity: ast::Publicity::Public,
3693+
publicity: Publicity::Public,
36943694
package: STDLIB_PACKAGE_NAME.into(),
36953695
module: DECODE_MODULE.into(),
36963696
name: "Decoder".into(),
@@ -4089,7 +4089,7 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateJsonEncoder<'ast> {
40894089
};
40904090

40914091
let json_type = self.printer.print_type(&Type::Named {
4092-
publicity: ast::Publicity::Public,
4092+
publicity: Publicity::Public,
40934093
package: JSON_PACKAGE_NAME.into(),
40944094
module: JSON_MODULE.into(),
40954095
name: "Json".into(),
@@ -4934,13 +4934,15 @@ fn pretty_constructor_name(
49344934
///
49354935
pub struct GenerateFunction<'a> {
49364936
module: &'a Module,
4937+
modules: &'a std::collections::HashMap<EcoString, Module>,
49374938
params: &'a CodeActionParams,
49384939
edits: TextEdits<'a>,
49394940
last_visited_function_end: Option<u32>,
49404941
function_to_generate: Option<FunctionToGenerate<'a>>,
49414942
}
49424943

49434944
struct FunctionToGenerate<'a> {
4945+
module: Option<&'a str>,
49444946
name: &'a str,
49454947
arguments_types: Vec<Arc<Type>>,
49464948

@@ -4956,11 +4958,13 @@ struct FunctionToGenerate<'a> {
49564958
impl<'a> GenerateFunction<'a> {
49574959
pub fn new(
49584960
module: &'a Module,
4961+
modules: &'a std::collections::HashMap<EcoString, Module>,
49594962
line_numbers: &'a LineNumbers,
49604963
params: &'a CodeActionParams,
49614964
) -> Self {
49624965
Self {
49634966
module,
4967+
modules,
49644968
params,
49654969
edits: TextEdits::new(line_numbers),
49664970
last_visited_function_end: None,
@@ -4971,16 +4975,53 @@ impl<'a> GenerateFunction<'a> {
49714975
pub fn code_actions(mut self) -> Vec<CodeAction> {
49724976
self.visit_typed_module(&self.module.ast);
49734977

4974-
let Some(FunctionToGenerate {
4978+
let Some(
4979+
function_to_generate @ FunctionToGenerate {
4980+
module,
4981+
previous_function_end: Some(insert_at),
4982+
..
4983+
},
4984+
) = self.function_to_generate.take()
4985+
else {
4986+
return vec![];
4987+
};
4988+
4989+
if let Some(module) = module {
4990+
if let Some(module) = self.modules.get(module) {
4991+
let insert_at = if module.code.is_empty() {
4992+
0
4993+
} else {
4994+
(module.code.len() - 1) as u32
4995+
};
4996+
self.code_action_for_module(
4997+
module,
4998+
Publicity::Public,
4999+
function_to_generate,
5000+
insert_at,
5001+
)
5002+
} else {
5003+
Vec::new()
5004+
}
5005+
} else {
5006+
let module = self.module;
5007+
self.code_action_for_module(module, Publicity::Private, function_to_generate, insert_at)
5008+
}
5009+
}
5010+
5011+
fn code_action_for_module(
5012+
mut self,
5013+
module: &Module,
5014+
publicity: Publicity,
5015+
function_to_generate: FunctionToGenerate<'a>,
5016+
insert_at: u32,
5017+
) -> Vec<CodeAction> {
5018+
let FunctionToGenerate {
49755019
name,
49765020
arguments_types,
49775021
given_arguments,
4978-
previous_function_end: Some(insert_at),
49795022
return_type,
4980-
}) = self.function_to_generate
4981-
else {
4982-
return vec![];
4983-
};
5023+
..
5024+
} = function_to_generate;
49845025

49855026
// Labels do not share the same namespace as argument so we use two separate
49865027
// generators to avoid renaming a label in case it shares a name with an argument.
@@ -4989,7 +5030,7 @@ impl<'a> GenerateFunction<'a> {
49895030

49905031
// Since we are generating a new function, type variables from other
49915032
// functions and constants are irrelevant to the types we print.
4992-
let mut printer = Printer::new_without_type_variables(&self.module.ast.names);
5033+
let mut printer = Printer::new_without_type_variables(&module.ast.names);
49935034
let arguments = arguments_types
49945035
.iter()
49955036
.enumerate()
@@ -5009,15 +5050,20 @@ impl<'a> GenerateFunction<'a> {
50095050

50105051
let return_type = printer.print_type(&return_type);
50115052

5053+
let publicity = if publicity.is_public() { "pub " } else { "" };
5054+
50125055
self.edits.insert(
50135056
insert_at,
5014-
format!("\n\nfn {name}({arguments}) -> {return_type} {{\n todo\n}}"),
5057+
format!("\n\n{publicity}fn {name}({arguments}) -> {return_type} {{\n todo\n}}"),
50155058
);
50165059

5060+
let Some(uri) = url_from_path(module.input_path.as_str()) else {
5061+
return Vec::new();
5062+
};
50175063
let mut action = Vec::with_capacity(1);
50185064
CodeActionBuilder::new("Generate function")
50195065
.kind(CodeActionKind::QUICKFIX)
5020-
.changes(self.params.text_document.uri.clone(), self.edits.edits)
5066+
.changes(uri, self.edits.edits)
50215067
.preferred(true)
50225068
.push_to(&mut action);
50235069
action
@@ -5041,10 +5087,32 @@ impl<'a> GenerateFunction<'a> {
50415087
given_arguments,
50425088
return_type,
50435089
previous_function_end: self.last_visited_function_end,
5090+
module: None,
50445091
})
50455092
}
50465093
}
50475094
}
5095+
5096+
fn try_save_function_from_other_module(
5097+
&mut self,
5098+
module: &'a str,
5099+
name: &'a str,
5100+
function_type: &Arc<Type>,
5101+
given_arguments: Option<&'a [TypedCallArg]>,
5102+
) {
5103+
if let Some((arguments_types, return_type)) = function_type.fn_types()
5104+
&& is_valid_lowercase_name(name)
5105+
{
5106+
self.function_to_generate = Some(FunctionToGenerate {
5107+
name,
5108+
arguments_types,
5109+
given_arguments,
5110+
return_type,
5111+
previous_function_end: self.last_visited_function_end,
5112+
module: Some(module),
5113+
})
5114+
}
5115+
}
50485116
}
50495117

50505118
impl<'ast> ast::visit::Visit<'ast> for GenerateFunction<'ast> {
@@ -5062,6 +5130,27 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateFunction<'ast> {
50625130
ast::visit::visit_typed_expr_invalid(self, location, type_);
50635131
}
50645132

5133+
fn visit_typed_expr_module_select(
5134+
&mut self,
5135+
_location: &'ast SrcSpan,
5136+
_field_start: &'ast u32,
5137+
type_: &'ast Arc<Type>,
5138+
label: &'ast EcoString,
5139+
module_name: &'ast EcoString,
5140+
_module_alias: &'ast EcoString,
5141+
constructor: &'ast ModuleValueConstructor,
5142+
) {
5143+
match constructor {
5144+
// Invalid module selects leave the `name` field blank
5145+
ModuleValueConstructor::Fn { name, .. } if name == "" => {
5146+
self.try_save_function_from_other_module(module_name, label, type_, None);
5147+
}
5148+
ModuleValueConstructor::Fn { .. }
5149+
| ModuleValueConstructor::Record { .. }
5150+
| ModuleValueConstructor::Constant { .. } => {}
5151+
}
5152+
}
5153+
50655154
fn visit_typed_expr_call(
50665155
&mut self,
50675156
location: &'ast SrcSpan,
@@ -5073,13 +5162,34 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateFunction<'ast> {
50735162
// function that has the proper labels.
50745163
let fun_range = self.edits.src_span_to_lsp_range(fun.location());
50755164

5076-
if within(self.params.range, fun_range) && fun.is_invalid() {
5077-
if labels_are_correct(arguments) {
5078-
self.try_save_function_to_generate(fun.location(), &fun.type_(), Some(arguments));
5165+
if within(self.params.range, fun_range) {
5166+
if !labels_are_correct(arguments) {
5167+
return;
5168+
}
5169+
5170+
match fun {
5171+
TypedExpr::Invalid { type_, location } => {
5172+
return self.try_save_function_to_generate(*location, type_, Some(arguments));
5173+
}
5174+
TypedExpr::ModuleSelect {
5175+
module_name,
5176+
label,
5177+
type_,
5178+
constructor: ModuleValueConstructor::Fn { name, .. },
5179+
..
5180+
} if name == "" => {
5181+
return self.try_save_function_from_other_module(
5182+
module_name,
5183+
label,
5184+
type_,
5185+
Some(arguments),
5186+
);
5187+
}
5188+
_ => {}
50795189
}
5080-
} else {
5081-
ast::visit::visit_typed_expr_call(self, location, type_, fun, arguments);
50825190
}
5191+
5192+
ast::visit::visit_typed_expr_call(self, location, type_, fun, arguments);
50835193
}
50845194
}
50855195

compiler-core/src/language_server/engine.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,10 @@ where
428428
actions.extend(InterpolateString::new(module, &lines, &params).code_actions());
429429
actions.extend(ExtractVariable::new(module, &lines, &params).code_actions());
430430
actions.extend(ExtractConstant::new(module, &lines, &params).code_actions());
431-
actions.extend(GenerateFunction::new(module, &lines, &params).code_actions());
431+
actions.extend(
432+
GenerateFunction::new(module, &this.compiler.modules, &lines, &params)
433+
.code_actions(),
434+
);
432435
actions.extend(
433436
GenerateVariant::new(module, &this.compiler, &lines, &params).code_actions(),
434437
);

compiler-core/src/language_server/tests/action.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9544,3 +9544,93 @@ pub fn main() {
95449544
find_position_of("something").to_selection()
95459545
);
95469546
}
9547+
9548+
#[test]
9549+
fn generate_function_in_other_module() {
9550+
let src = "
9551+
import wibble
9552+
9553+
pub fn main() {
9554+
wibble.wibble()
9555+
wibble.wobble()
9556+
}
9557+
";
9558+
9559+
assert_code_action!(
9560+
GENERATE_FUNCTION,
9561+
TestProject::for_source(src).add_module("wibble", "pub fn wibble() {}"),
9562+
find_position_of("wobble").to_selection()
9563+
);
9564+
}
9565+
9566+
#[test]
9567+
fn generating_function_in_other_module_uses_local_names() {
9568+
let src = r#"
9569+
import wibble
9570+
9571+
pub fn main() -> List(Nil) {
9572+
wibble.wibble(1, #(True, "Hello"))
9573+
}
9574+
"#;
9575+
9576+
assert_code_action!(
9577+
GENERATE_FUNCTION,
9578+
TestProject::for_source(src).add_module(
9579+
"wibble",
9580+
"import gleam.{type Int as Number, type Bool as Boolean, type String as Text, type Nil as Nothing}"
9581+
),
9582+
find_position_of("wibble(").to_selection()
9583+
);
9584+
}
9585+
9586+
#[test]
9587+
fn generating_function_in_other_module_uses_labels() {
9588+
let src = r#"
9589+
import wibble
9590+
9591+
pub fn main() {
9592+
wibble.wibble("Unlabelled", int: 1, bool: True)
9593+
}
9594+
"#;
9595+
9596+
assert_code_action!(
9597+
GENERATE_FUNCTION,
9598+
TestProject::for_source(src).add_module("wibble", ""),
9599+
find_position_of("wibble(").to_selection()
9600+
);
9601+
}
9602+
9603+
#[test]
9604+
fn no_code_action_to_generate_existing_function_in_other_module() {
9605+
let src = r#"
9606+
import wibble
9607+
9608+
pub fn main() {
9609+
wibble.wibble(1, 2, 3)
9610+
}
9611+
"#;
9612+
9613+
assert_no_code_actions!(
9614+
GENERATE_FUNCTION,
9615+
TestProject::for_source(src).add_module("wibble", "pub fn wibble(a, b, c) { a + b + c }"),
9616+
find_position_of("wibble(").to_selection()
9617+
);
9618+
}
9619+
9620+
#[test]
9621+
fn do_not_generate_function_in_other_package() {
9622+
let src = r#"
9623+
import maths
9624+
9625+
pub fn main() {
9626+
maths.add(1, 2)
9627+
maths.subtract(1, 2)
9628+
}
9629+
"#;
9630+
9631+
assert_no_code_actions!(
9632+
GENERATE_FUNCTION,
9633+
TestProject::for_source(src).add_dep_module("maths", "pub fn add(a, b) { a + b }"),
9634+
find_position_of("subtract").to_selection()
9635+
);
9636+
}

0 commit comments

Comments
 (0)