Skip to content

Commit

Permalink
Move things around
Browse files Browse the repository at this point in the history
  • Loading branch information
giacomocavalieri committed Jul 19, 2024
1 parent b743c87 commit b42ff26
Showing 1 changed file with 146 additions and 144 deletions.
290 changes: 146 additions & 144 deletions compiler-core/src/language_server/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use itertools::Itertools;
use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Url};

use crate::{
ast::{self, visit::Visit as _, AssignName, AssignmentKind, Import, SrcSpan, TypedPattern},
ast::{
self, visit::Visit as _, AssignName, AssignmentKind, Import, Pattern, SrcSpan, TypedPattern,
},
build::Module,
line_numbers::LineNumbers,
parse::extra::ModuleExtra,
Expand Down Expand Up @@ -60,141 +62,6 @@ impl CodeActionBuilder {
}
}

// Builder for code action to convert `let assert` into a case expression
pub struct LetAssertToCase<'a> {
module: &'a Module,
params: &'a CodeActionParams,
actions: Vec<CodeAction>,
line_numbers: LineNumbers,
pattern_variables: Vec<EcoString>,
}

impl<'ast> ast::visit::Visit<'ast> for LetAssertToCase<'_> {
fn visit_typed_assignment(&mut self, assignment: &'ast ast::TypedAssignment) {
// To prevent weird behaviour when `let assert` statements are nested,
// we only check for the code action between the `let` and `=`.
let code_action_location =
SrcSpan::new(assignment.location.start, assignment.value.location().start);
let code_action_range = src_span_to_lsp_range(code_action_location, &self.line_numbers);

self.visit_typed_expr(&assignment.value);

// Only offer the code action if the cursor is over the statement
if !overlaps(code_action_range, self.params.range) {
return;
}

// This pattern only applies to `let assert`
if !matches!(assignment.kind, AssignmentKind::Assert { .. }) {
return;
};

// Get the source code for the tested expression
let location = assignment.value.location();
let expr = self
.module
.code
.get(location.start as usize..location.end as usize)
.expect("Location must be valid");

// Get the source code for the pattern
let pattern_location = assignment.pattern.location();
let pattern = self
.module
.code
.get(pattern_location.start as usize..pattern_location.end as usize)
.expect("Location must be valid");

let range = src_span_to_lsp_range(assignment.location, &self.line_numbers);
let indent = " ".repeat(range.start.character as usize);

// Figure out which variables are assigned in the pattern
self.pattern_variables.clear();
self.visit_typed_pattern(&assignment.pattern);
let variables = std::mem::take(&mut self.pattern_variables);

let assigned = match variables.len() {
0 => "_",
1 => variables.first().expect("Variables is length one"),
_ => &format!("#({})", variables.join(", ")),
};

let edit = TextEdit {
range,
new_text: format!(
"let {assigned} = case {expr} {{
{indent} {pattern} -> {value}
{indent} _ -> panic
{indent}}}",
// "_" isn't a valid expression, so we just return Nil from the case expression
value = if assigned == "_" { "Nil" } else { assigned }
),
};

let uri = &self.params.text_document.uri;

CodeActionBuilder::new("Convert to case")
.kind(CodeActionKind::REFACTOR)
.changes(uri.clone(), vec![edit])
.preferred(true)
.push_to(&mut self.actions);
}

fn visit_typed_pattern_variable(
&mut self,
_location: &'ast SrcSpan,
name: &'ast EcoString,
_type: &'ast Arc<Type>,
) {
self.pattern_variables.push(name.clone());
}

fn visit_typed_pattern_assign(
&mut self,
location: &'ast SrcSpan,
name: &'ast EcoString,
pattern: &'ast TypedPattern,
) {
self.pattern_variables.push(name.clone());
ast::visit::visit_typed_pattern_assign(self, location, name, pattern);
}

fn visit_typed_pattern_string_prefix(
&mut self,
_location: &'ast SrcSpan,
_left_location: &'ast SrcSpan,
left_side_assignment: &'ast Option<(EcoString, SrcSpan)>,
_right_location: &'ast SrcSpan,
_left_side_string: &'ast EcoString,
right_side_assignment: &'ast AssignName,
) {
if let Some((name, _)) = left_side_assignment {
self.pattern_variables.push(name.clone());
}
if let AssignName::Variable(name) = right_side_assignment {
self.pattern_variables.push(name.clone());
}
}
}

impl<'a> LetAssertToCase<'a> {
pub fn new(module: &'a Module, params: &'a CodeActionParams) -> Self {
let line_numbers = LineNumbers::new(&module.code);
Self {
module,
params,
actions: Vec::new(),
line_numbers,
pattern_variables: Vec::new(),
}
}

pub fn code_actions(mut self) -> Vec<CodeAction> {
self.visit_typed_module(&self.module.ast);
self.actions
}
}

/// Code action to move all the imports to the top of the module.
#[derive(Debug)]
pub struct MoveImportsToTop<'a> {
Expand Down Expand Up @@ -271,13 +138,13 @@ impl<'a> MoveImportsToTop<'a> {
if edits.is_empty() {
vec![]
} else {
let mut action = vec![];
let mut actions = vec![];
CodeActionBuilder::new("Move all imports to the top of the module")
.kind(CodeActionKind::REFACTOR_REWRITE)
.changes(self.params.text_document.uri.clone(), edits)
.preferred(false)
.push_to(&mut action);
action
.push_to(&mut actions);
actions
}
}

Expand Down Expand Up @@ -385,14 +252,14 @@ impl<'ast> ast::visit::Visit<'ast> for RedundantTupleInCaseSubject<'_> {
let mut clause_edits = vec![];
for clause in clauses {
match clause.pattern.get(subject_idx) {
Some(ast::Pattern::Tuple { location, elems }) => {
Some(Pattern::Tuple { location, elems }) => {
clause_edits.extend(self.delete_tuple_tokens(
*location,
elems.last().map(|elem| elem.location()),
))
}

Some(ast::Pattern::Discard { location, .. }) => {
Some(Pattern::Discard { location, .. }) => {
clause_edits.push(self.discard_tuple_items(*location, elems.len()))
}

Expand Down Expand Up @@ -435,13 +302,13 @@ impl<'a> RedundantTupleInCaseSubject<'a> {

self.edits.sort_by_key(|edit| edit.range.start);

let mut action = vec![];
let mut actions = vec![];
CodeActionBuilder::new("Remove redundant tuples")
.kind(CodeActionKind::REFACTOR_REWRITE)
.changes(self.params.text_document.uri.clone(), self.edits)
.preferred(true)
.push_to(&mut action);
action
.push_to(&mut actions);
actions
}

fn delete_tuple_tokens(
Expand Down Expand Up @@ -534,3 +401,138 @@ impl<'a> RedundantTupleInCaseSubject<'a> {
}
}
}

// Builder for code action to convert `let assert` into a case expression
pub struct LetAssertToCase<'a> {
module: &'a Module,
params: &'a CodeActionParams,
actions: Vec<CodeAction>,
line_numbers: LineNumbers,
pattern_variables: Vec<EcoString>,
}

impl<'ast> ast::visit::Visit<'ast> for LetAssertToCase<'_> {
fn visit_typed_assignment(&mut self, assignment: &'ast ast::TypedAssignment) {
// To prevent weird behaviour when `let assert` statements are nested,
// we only check for the code action between the `let` and `=`.
let code_action_location =
SrcSpan::new(assignment.location.start, assignment.value.location().start);
let code_action_range = src_span_to_lsp_range(code_action_location, &self.line_numbers);

self.visit_typed_expr(&assignment.value);

// Only offer the code action if the cursor is over the statement
if !overlaps(code_action_range, self.params.range) {
return;
}

// This pattern only applies to `let assert`
if !matches!(assignment.kind, AssignmentKind::Assert { .. }) {
return;
};

// Get the source code for the tested expression
let location = assignment.value.location();
let expr = self
.module
.code
.get(location.start as usize..location.end as usize)
.expect("Location must be valid");

// Get the source code for the pattern
let pattern_location = assignment.pattern.location();
let pattern = self
.module
.code
.get(pattern_location.start as usize..pattern_location.end as usize)
.expect("Location must be valid");

let range = src_span_to_lsp_range(assignment.location, &self.line_numbers);
let indent = " ".repeat(range.start.character as usize);

// Figure out which variables are assigned in the pattern
self.pattern_variables.clear();
self.visit_typed_pattern(&assignment.pattern);
let variables = std::mem::take(&mut self.pattern_variables);

let assigned = match variables.len() {
0 => "_",
1 => variables.first().expect("Variables is length one"),
_ => &format!("#({})", variables.join(", ")),
};

let edit = TextEdit {
range,
new_text: format!(
"let {assigned} = case {expr} {{
{indent} {pattern} -> {value}
{indent} _ -> panic
{indent}}}",
// "_" isn't a valid expression, so we just return Nil from the case expression
value = if assigned == "_" { "Nil" } else { assigned }
),
};

let uri = &self.params.text_document.uri;

CodeActionBuilder::new("Convert to case")
.kind(CodeActionKind::REFACTOR)
.changes(uri.clone(), vec![edit])
.preferred(true)
.push_to(&mut self.actions);
}

fn visit_typed_pattern_variable(
&mut self,
_location: &'ast SrcSpan,
name: &'ast EcoString,
_type: &'ast Arc<Type>,
) {
self.pattern_variables.push(name.clone());
}

fn visit_typed_pattern_assign(
&mut self,
location: &'ast SrcSpan,
name: &'ast EcoString,
pattern: &'ast TypedPattern,
) {
self.pattern_variables.push(name.clone());
ast::visit::visit_typed_pattern_assign(self, location, name, pattern);
}

fn visit_typed_pattern_string_prefix(
&mut self,
_location: &'ast SrcSpan,
_left_location: &'ast SrcSpan,
left_side_assignment: &'ast Option<(EcoString, SrcSpan)>,
_right_location: &'ast SrcSpan,
_left_side_string: &'ast EcoString,
right_side_assignment: &'ast AssignName,
) {
if let Some((name, _)) = left_side_assignment {
self.pattern_variables.push(name.clone());
}
if let AssignName::Variable(name) = right_side_assignment {
self.pattern_variables.push(name.clone());
}
}
}

impl<'a> LetAssertToCase<'a> {
pub fn new(module: &'a Module, params: &'a CodeActionParams) -> Self {
let line_numbers = LineNumbers::new(&module.code);
Self {
module,
params,
actions: Vec::new(),
line_numbers,
pattern_variables: Vec::new(),
}
}

pub fn code_actions(mut self) -> Vec<CodeAction> {
self.visit_typed_module(&self.module.ast);
self.actions
}
}

0 comments on commit b42ff26

Please sign in to comment.