Skip to content

Commit c319a7b

Browse files
GearsDatapackslpil
authored andcommitted
Fix bug with indentation detection and multibyte characters
1 parent 5cba45d commit c319a7b

File tree

4 files changed

+81
-73
lines changed

4 files changed

+81
-73
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,3 +399,7 @@
399399
- Fixed a bug where using the pipe operator inside a record update would cause
400400
the compiler to generate invalid code on the JavaScript target.
401401
([Surya Rose](https://github.com/GearsDatapacks))
402+
403+
- Fixed a bug where some code actions would produce invalid code when a file
404+
contained characters that were represented with more than one byte in UTF-8.
405+
([Surya Rose](https://github.com/GearsDatapacks))

compiler-core/src/language_server/code_action.rs

Lines changed: 41 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ impl CodeActionBuilder {
8484
}
8585
}
8686

87+
/// A small helper function to get the indentation at a given position.
88+
fn count_indentation(code: &str, line_numbers: &LineNumbers, line: u32) -> usize {
89+
let mut indent_size = 0;
90+
let line_start = *line_numbers
91+
.line_starts
92+
.get(line as usize)
93+
.expect("Line number should be valid");
94+
95+
let mut chars = code[line_start as usize..].chars();
96+
while chars.next() == Some(' ') {
97+
indent_size += 1;
98+
}
99+
100+
indent_size
101+
}
102+
87103
/// Code action to remove literal tuples in case subjects, essentially making
88104
/// the elements of the tuples into the case's subjects.
89105
///
@@ -1036,28 +1052,7 @@ pub fn code_action_add_missing_patterns(
10361052
continue;
10371053
};
10381054

1039-
// Find the start of the line. We can't just use the start of the case
1040-
// expression for cases like:
1041-
//
1042-
//```gleam
1043-
// let value = case a {}
1044-
//```
1045-
//
1046-
// Here, the start of the expression is part-way through the line, meaning
1047-
// we think we are more indented than we actually are
1048-
//
1049-
let mut indent_size = 0;
1050-
let line_start = *edits
1051-
.line_numbers
1052-
.line_starts
1053-
.get(range.start.line as usize)
1054-
.expect("Line number should be valid");
1055-
let chars = module.code.chars();
1056-
let mut chars = chars.skip(line_start as usize);
1057-
// Count indentation
1058-
while chars.next() == Some(' ') {
1059-
indent_size += 1;
1060-
}
1055+
let indent_size = count_indentation(&module.code, &edits.line_numbers, range.start.line);
10611056

10621057
let indent = " ".repeat(indent_size);
10631058

@@ -1102,13 +1097,7 @@ pub fn code_action_add_missing_patterns(
11021097
.end;
11031098

11041099
// Find the opening brace of the case expression
1105-
1106-
// Calculate the number of characters from the start of the line to the end of the
1107-
// last subject, to skip, so we can find the opening brace.
1108-
// That is: the location we want to get to, minus the start of the line which we skipped to begin with,
1109-
// minus the number we skipped for the indent, minus one more because we go one past the end of indentation
1110-
let num_to_skip = last_subject_location - line_start - indent_size as u32 - 1;
1111-
let chars = chars.skip(num_to_skip as usize);
1100+
let chars = module.code[last_subject_location as usize..].chars();
11121101
let mut start_brace_location = last_subject_location;
11131102
for char in chars {
11141103
start_brace_location += 1;
@@ -2850,27 +2839,22 @@ impl<'a> ExtractVariable<'a> {
28502839

28512840
let range = self.edits.src_span_to_lsp_range(insert_location);
28522841

2853-
let line_starts = self.edits.line_numbers.line_starts.to_owned();
2854-
let line_start = line_starts
2855-
.get(range.start.line as usize)
2856-
.expect("Line number should be valid");
2857-
2858-
let chars = self.module.code.chars();
2859-
let mut chars = chars.skip(*line_start as usize);
2860-
2861-
// Count indentation
2862-
let mut indent_size = 0;
2863-
while chars.next() == Some(' ') {
2864-
indent_size += 1;
2865-
}
2842+
let indent_size = count_indentation(
2843+
&self.module.code,
2844+
&self.edits.line_numbers,
2845+
range.start.line,
2846+
);
28662847

28672848
let mut indent = " ".repeat(indent_size);
28682849

28692850
// We insert the variable declaration
28702851
// Wrap in a block if needed
28712852
let mut insertion = format!("let {variable_name} = {content}");
28722853
if self.to_be_wrapped {
2873-
let line_end = line_starts
2854+
let line_end = self
2855+
.edits
2856+
.line_numbers
2857+
.line_starts
28742858
.get((range.end.line + 1) as usize)
28752859
.expect("Line number should be valid");
28762860

@@ -3443,23 +3427,16 @@ impl<'a> ExtractConstant<'a> {
34433427
let range = self
34443428
.edits
34453429
.src_span_to_lsp_range(self.selected_expression.expect("Real range value"));
3446-
let mut indent_size = 0;
3447-
let line_start = *self
3448-
.edits
3449-
.line_numbers
3450-
.line_starts
3451-
.get(range.start.line as usize)
3452-
.expect("Line number should be valid");
3453-
let chars = self.module.code.chars();
3454-
let mut chars = chars.skip(line_start as usize);
3455-
// Count indentation
3456-
while chars.next() == Some(' ') {
3457-
indent_size += 1;
3458-
}
3430+
3431+
let indent_size = count_indentation(
3432+
&self.module.code,
3433+
&self.edits.line_numbers,
3434+
range.start.line,
3435+
);
34593436

34603437
let expr_span_with_new_line = SrcSpan {
34613438
// We remove leading indentation + 1 to remove the newline with it
3462-
start: expr_span.start - (indent_size + 1),
3439+
start: expr_span.start - (indent_size as u32 + 1),
34633440
end: expr_span.end,
34643441
};
34653442
self.edits.delete(expr_span_with_new_line);
@@ -5550,8 +5527,7 @@ impl<'a> InlineVariable<'a> {
55505527

55515528
let mut location = assignment.location;
55525529

5553-
let chars = self.module.code.chars();
5554-
let mut chars = chars.skip(assignment.location.end as usize);
5530+
let mut chars = self.module.code[location.end as usize..].chars();
55555531
// Delete any whitespace after the removed statement
55565532
while chars.next().is_some_and(char::is_whitespace) {
55575533
location.end += 1;
@@ -6415,20 +6391,12 @@ impl<'a> WrapInBlock<'a> {
64156391
.edits
64166392
.src_span_to_lsp_range(self.selected_expression.expect("Real range value"));
64176393

6418-
let line_start = *self
6419-
.edits
6420-
.line_numbers
6421-
.line_starts
6422-
.get(range.start.line as usize)
6423-
.expect("Line number should be valid");
6424-
let chars = self.module.code.chars();
6425-
let mut chars = chars.skip(line_start as usize);
6426-
6427-
// Count indentation
6428-
let mut indent_size = 0;
6429-
while chars.next() == Some(' ') {
6430-
indent_size += 1;
6431-
}
6394+
let indent_size = count_indentation(
6395+
&self.module.code,
6396+
&self.edits.line_numbers,
6397+
range.start.line,
6398+
);
6399+
64326400
let expr_indent_size = indent_size + 2;
64336401

64346402
let indent = " ".repeat(indent_size);

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8089,3 +8089,18 @@ pub fn main(w: Wibble) {
80898089
find_position_of("case w").select_until(find_position_of("{}")),
80908090
);
80918091
}
8092+
8093+
// https://github.com/gleam-lang/gleam/issues/3628#issuecomment-2543342212
8094+
#[test]
8095+
fn add_missing_patterns_multibyte_grapheme() {
8096+
assert_code_action!(
8097+
ADD_MISSING_PATTERNS,
8098+
r#"
8099+
// ä
8100+
fn wibble() {
8101+
case True {}
8102+
}
8103+
"#,
8104+
find_position_of("case").select_until(find_position_of("True {"))
8105+
);
8106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: compiler-core/src/language_server/tests/action.rs
3+
expression: "\n// ä\nfn wibble() {\n case True {}\n}\n"
4+
---
5+
----- BEFORE ACTION
6+
7+
// ä
8+
fn wibble() {
9+
case True {}
10+
▔▔▔▔▔↑
11+
}
12+
13+
14+
----- AFTER ACTION
15+
16+
// ä
17+
fn wibble() {
18+
case True {
19+
True -> todo
20+
}
21+
}

0 commit comments

Comments
 (0)