Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add action to move imports to top of module #3413

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 13 additions & 227 deletions compiler-core/src/language_server/code_action.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::{iter, sync::Arc};
use std::sync::Arc;

use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, TextEdit, Url};

use crate::{
ast::{self, visit::Visit as _, AssignName, AssignmentKind, Pattern, SrcSpan, TypedPattern},
ast::{self, visit::Visit as _, AssignName, AssignmentKind, SrcSpan, TypedPattern},
build::Module,
line_numbers::LineNumbers,
parse::extra::ModuleExtra,
type_::Type,
};
use ecow::EcoString;
use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, TextEdit, Url};

use super::{engine::overlaps, src_span_to_lsp_range};

pub mod move_imports_to_top;
pub mod redundant_tuple_in_case_subject;

#[derive(Debug)]
pub struct CodeActionBuilder {
action: CodeAction,
Expand Down Expand Up @@ -53,227 +56,8 @@ impl CodeActionBuilder {
self
}

pub fn push_to(self, actions: &mut Vec<CodeAction>) {
actions.push(self.action);
}
}

/// Code action to remove literal tuples in case subjects, essentially making
/// the elements of the tuples into the case's subjects.
///
/// The code action is only available for the i'th subject if:
/// - it is a non-empty tuple, and
/// - the i'th pattern (including alternative patterns) is a literal tuple for all clauses.
///
/// # Basic example:
///
/// The following case expression:
///
/// ```gleam
/// case #(1, 2) {
/// #(a, b) -> 0
/// }
/// ```
///
/// Becomes:
///
/// ```gleam
/// case 1, 2 {
/// a, b -> 0
/// }
/// ```
///
/// # Another example:
///
/// The following case expression does not produce any code action
///
/// ```gleam
/// case #(1, 2) {
/// a -> 0 // <- the pattern is not a tuple
/// }
/// ```
pub struct RedundantTupleInCaseSubject<'a> {
line_numbers: LineNumbers,
code: &'a EcoString,
extra: &'a ModuleExtra,
params: &'a CodeActionParams,
module: &'a ast::TypedModule,
edits: Vec<TextEdit>,
hovered: bool,
}

impl<'ast> ast::visit::Visit<'ast> for RedundantTupleInCaseSubject<'_> {
fn visit_typed_expr_case(
&mut self,
location: &'ast SrcSpan,
typ: &'ast Arc<Type>,
subjects: &'ast [ast::TypedExpr],
clauses: &'ast [ast::TypedClause],
) {
'subj: for (subject_idx, subject) in subjects.iter().enumerate() {
let ast::TypedExpr::Tuple {
location, elems, ..
} = subject
else {
continue;
};

// Ignore empty tuple
if elems.is_empty() {
continue;
}

let mut clause_edits = vec![];
for clause in clauses {
match clause.pattern.get(subject_idx) {
Some(Pattern::Tuple { location, elems }) => {
clause_edits.extend(self.delete_tuple_tokens(
*location,
elems.last().map(|elem| elem.location()),
))
}

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

// Do not edit for this subject at all and go to the next subject
_ => continue 'subj,
}
}

let range = src_span_to_lsp_range(*location, &self.line_numbers);
self.hovered = self.hovered || overlaps(self.params.range, range);

self.edits.extend(
self.delete_tuple_tokens(*location, elems.last().map(|elem| elem.location())),
);
self.edits.extend(clause_edits);
}

ast::visit::visit_typed_expr_case(self, location, typ, subjects, clauses)
}
}

impl<'a> RedundantTupleInCaseSubject<'a> {
pub fn new(module: &'a Module, params: &'a CodeActionParams) -> Self {
Self {
line_numbers: LineNumbers::new(&module.code),
code: &module.code,
extra: &module.extra,
params,
module: &module.ast,
edits: vec![],
hovered: false,
}
}

pub fn code_actions(mut self) -> Vec<CodeAction> {
self.visit_typed_module(self.module);
if !self.hovered {
return vec![];
}

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

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 actions);

actions
}

fn delete_tuple_tokens(
&self,
location: SrcSpan,
last_elem_location: Option<SrcSpan>,
) -> Vec<TextEdit> {
let tuple_code = self
.code
.get(location.start as usize..location.end as usize)
.expect("valid span");

let mut edits = vec![];

// Delete `#`
edits.push(TextEdit {
range: src_span_to_lsp_range(
SrcSpan::new(location.start, location.start + 1),
&self.line_numbers,
),
new_text: "".to_string(),
});

// Delete `(`
let (lparen_offset, _) = tuple_code
.match_indices('(')
// Ignore in comments
.find(|(i, _)| !self.extra.is_within_comment(location.start + *i as u32))
.expect("`(` not found in tuple");

edits.push(TextEdit {
range: src_span_to_lsp_range(
SrcSpan::new(
location.start + lparen_offset as u32,
location.start + lparen_offset as u32 + 1,
),
&self.line_numbers,
),
new_text: "".to_string(),
});

// Delete trailing `,` (if applicable)
if let Some(last_elem_location) = last_elem_location {
// Get the code after the last element until the tuple's `)`
let code_after_last_elem = self
.code
.get(last_elem_location.end as usize..location.end as usize)
.expect("valid span");

if let Some((trailing_comma_offset, _)) = code_after_last_elem
.rmatch_indices(',')
// Ignore in comments
.find(|(i, _)| {
!self
.extra
.is_within_comment(last_elem_location.end + *i as u32)
})
{
edits.push(TextEdit {
range: src_span_to_lsp_range(
SrcSpan::new(
last_elem_location.end + trailing_comma_offset as u32,
last_elem_location.end + trailing_comma_offset as u32 + 1,
),
&self.line_numbers,
),
new_text: "".to_string(),
});
}
}

// Delete )
edits.push(TextEdit {
range: src_span_to_lsp_range(
SrcSpan::new(location.end - 1, location.end),
&self.line_numbers,
),
new_text: "".to_string(),
});

edits
}

fn discard_tuple_items(&self, discard_location: SrcSpan, tuple_items: usize) -> TextEdit {
// Replace the old discard with multiple discard, one for each of the
// tuple items.
TextEdit {
range: src_span_to_lsp_range(discard_location, &self.line_numbers),
new_text: itertools::intersperse(iter::repeat("_").take(tuple_items), ", ").collect(),
}
pub fn build(self) -> CodeAction {
self.action
}
}

Expand Down Expand Up @@ -350,11 +134,13 @@ impl<'ast> ast::visit::Visit<'ast> for LetAssertToCase<'_> {

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

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

self.actions.push(action);
}

fn visit_typed_pattern_variable(
Expand Down
Loading
Loading