Skip to content

Commit f5aeecf

Browse files
committed
fix(language_server): add whitespace for // oxlint-disable-next-line fix
1 parent 6e52bbd commit f5aeecf

File tree

1 file changed

+144
-3
lines changed

1 file changed

+144
-3
lines changed

crates/oxc_linter/src/lsp.rs

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,31 @@ fn disable_for_this_line<'a>(
207207
rope: &Rope,
208208
source_text: &str,
209209
) -> FixWithPosition<'a> {
210-
let mut start_position = offset_to_position(rope, error_offset, source_text);
211-
start_position.character = 0; // TODO: character should be set to match the first non-whitespace character in the source text to match the existing indentation.
210+
// 1. search for the first line break character before the error_offset in source_text (if any)
211+
// 2. count all whitespace characters after the line break character
212+
// 3. set start_position.character to the line break character
213+
// 4. add the same whitespace characters to the content of the fix
214+
let bytes = source_text.as_bytes()[..error_offset as usize].iter().rev();
215+
let mut line_break_offset = error_offset;
216+
for byte in bytes {
217+
if *byte == b'\n' || *byte == b'\r' {
218+
break;
219+
}
220+
line_break_offset -= 1;
221+
}
222+
let whitespace_string = source_text
223+
.as_bytes()
224+
.get(line_break_offset as usize..error_offset as usize)
225+
.and_then(|slice| std::str::from_utf8(slice).ok())
226+
.map(|s| s.chars().filter(|c| c.is_whitespace()).collect::<String>())
227+
.unwrap_or_default();
228+
229+
let start_position = offset_to_position(rope, line_break_offset, source_text);
212230
let end_position = start_position.clone();
213231
FixWithPosition {
214-
content: Cow::Owned(format!("// oxlint-disable-next-line {rule_name}\n")),
232+
content: Cow::Owned(format!(
233+
"{whitespace_string}// oxlint-disable-next-line {rule_name}\n"
234+
)),
215235
span: SpanPositionMessage::new(start_position, end_position)
216236
.with_message(Some(Cow::Owned(format!("Disable {rule_name} for this line")))),
217237
}
@@ -344,6 +364,127 @@ mod test {
344364
assert_eq!(fix.span.start.character, 6);
345365
}
346366

367+
#[test]
368+
fn disable_for_this_line_single_line() {
369+
let source = "console.log('hello');";
370+
let rope = Rope::from_str(source);
371+
let fix = super::disable_for_this_line("no-console", 8, &rope, source);
372+
373+
assert_eq!(fix.content, "// oxlint-disable-next-line no-console\n");
374+
assert_eq!(fix.span.start.line, 0);
375+
assert_eq!(fix.span.start.character, 0);
376+
}
377+
378+
#[test]
379+
fn disable_for_this_line_with_spaces() {
380+
let source = " console.log('hello');";
381+
let rope = Rope::from_str(source);
382+
let fix = super::disable_for_this_line("no-console", 10, &rope, source);
383+
384+
assert_eq!(fix.content, " // oxlint-disable-next-line no-console\n");
385+
assert_eq!(fix.span.start.line, 0);
386+
assert_eq!(fix.span.start.character, 0);
387+
}
388+
389+
#[test]
390+
fn disable_for_this_line_with_tabs() {
391+
let source = "\t\tconsole.log('hello');";
392+
let rope = Rope::from_str(source);
393+
let fix = super::disable_for_this_line("no-console", 10, &rope, source);
394+
395+
assert_eq!(fix.content, "\t\t// oxlint-disable-next-line no-console\n");
396+
assert_eq!(fix.span.start.line, 0);
397+
assert_eq!(fix.span.start.character, 0);
398+
}
399+
400+
#[test]
401+
fn disable_for_this_line_mixed_tabs_spaces() {
402+
let source = "\t \tconsole.log('hello');";
403+
let rope = Rope::from_str(source);
404+
let fix = super::disable_for_this_line("no-console", 12, &rope, source);
405+
406+
assert_eq!(fix.content, "\t \t// oxlint-disable-next-line no-console\n");
407+
assert_eq!(fix.span.start.line, 0);
408+
assert_eq!(fix.span.start.character, 0);
409+
}
410+
411+
#[test]
412+
fn disable_for_this_line_multiline_with_tabs() {
413+
let source = "function test() {\n\tconsole.log('hello');\n}";
414+
let rope = Rope::from_str(source);
415+
let fix = super::disable_for_this_line("no-console", 27, &rope, source);
416+
417+
assert_eq!(fix.content, "\t// oxlint-disable-next-line no-console\n");
418+
assert_eq!(fix.span.start.line, 1);
419+
assert_eq!(fix.span.start.character, 0);
420+
}
421+
422+
#[test]
423+
fn disable_for_this_line_multiline_with_spaces() {
424+
let source = "function test() {\n console.log('hello');\n}";
425+
let rope = Rope::from_str(source);
426+
let fix = super::disable_for_this_line("no-console", 30, &rope, source);
427+
428+
assert_eq!(fix.content, " // oxlint-disable-next-line no-console\n");
429+
assert_eq!(fix.span.start.line, 1);
430+
assert_eq!(fix.span.start.character, 0);
431+
}
432+
433+
#[test]
434+
fn disable_for_this_line_complex_indentation() {
435+
let source = "function test() {\n\t \t console.log('hello');\n}";
436+
let rope = Rope::from_str(source);
437+
let fix = super::disable_for_this_line("no-console", 33, &rope, source);
438+
439+
assert_eq!(fix.content, "\t \t // oxlint-disable-next-line no-console\n");
440+
assert_eq!(fix.span.start.line, 1);
441+
assert_eq!(fix.span.start.character, 0);
442+
}
443+
444+
#[test]
445+
fn disable_for_this_line_no_indentation() {
446+
let source = "function test() {\nconsole.log('hello');\n}";
447+
let rope = Rope::from_str(source);
448+
let fix = super::disable_for_this_line("no-console", 26, &rope, source);
449+
450+
assert_eq!(fix.content, "// oxlint-disable-next-line no-console\n");
451+
assert_eq!(fix.span.start.line, 1);
452+
assert_eq!(fix.span.start.character, 0);
453+
}
454+
455+
#[test]
456+
fn disable_for_this_line_crlf_with_tabs() {
457+
let source = "function test() {\r\n\tconsole.log('hello');\r\n}";
458+
let rope = Rope::from_str(source);
459+
let fix = super::disable_for_this_line("no-console", 28, &rope, source);
460+
461+
assert_eq!(fix.content, "\t// oxlint-disable-next-line no-console\n");
462+
assert_eq!(fix.span.start.line, 1);
463+
assert_eq!(fix.span.start.character, 0);
464+
}
465+
466+
#[test]
467+
fn disable_for_this_line_deeply_nested() {
468+
let source = "if (true) {\n\t\tif (nested) {\n\t\t\tconsole.log('deep');\n\t\t}\n}";
469+
let rope = Rope::from_str(source);
470+
let fix = super::disable_for_this_line("no-console", 40, &rope, source);
471+
472+
assert_eq!(fix.content, "\t\t\t// oxlint-disable-next-line no-console\n");
473+
assert_eq!(fix.span.start.line, 2);
474+
assert_eq!(fix.span.start.character, 0);
475+
}
476+
477+
#[test]
478+
fn disable_for_this_line_at_start_of_file() {
479+
let source = "console.log('hello');";
480+
let rope = Rope::from_str(source);
481+
let fix = super::disable_for_this_line("no-console", 0, &rope, source);
482+
483+
assert_eq!(fix.content, "// oxlint-disable-next-line no-console\n");
484+
assert_eq!(fix.span.start.line, 0);
485+
assert_eq!(fix.span.start.character, 0);
486+
}
487+
347488
fn assert_position(source: &str, offset: u32, expected: (u32, u32)) {
348489
let position = offset_to_position(&Rope::from_str(source), offset, source);
349490
assert_eq!(position.line, expected.0);

0 commit comments

Comments
 (0)