Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
45 changes: 40 additions & 5 deletions crates/ide-completion/src/completions/postfix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,20 @@ fn get_receiver_text(
range.range = TextRange::at(range.range.start(), range.range.len() - TextSize::of('.'))
}
let file_text = sema.db.file_text(range.file_id.file_id(sema.db));
let mut text = file_text.text(sema.db)[range.range].to_owned();
let text = file_text.text(sema.db);
let indent_spaces = indent_of_tail_line(&text[TextRange::up_to(range.range.start())]);
let mut text = stdx::dedent_by(indent_spaces, &text[range.range]);

// The receiver texts should be interpreted as-is, as they are expected to be
// normal Rust expressions.
escape_snippet_bits(&mut text);
text
return text;

fn indent_of_tail_line(text: &str) -> usize {
let tail_line = text.rsplit_once('\n').map_or(text, |(_, s)| s);
let trimmed = tail_line.trim_start_matches(' ');
tail_line.len() - trimmed.len()
}
}

/// Escapes `\` and `$` so that they don't get interpreted as snippet-specific constructs.
Expand Down Expand Up @@ -977,9 +985,9 @@ use core::ops::ControlFlow;

fn main() {
ControlFlow::Break(match true {
true => "\${1:placeholder}",
false => "\\\$",
})
true => "\${1:placeholder}",
false => "\\\$",
})
}
"#,
);
Expand Down Expand Up @@ -1219,4 +1227,31 @@ fn foo() {
"#,
);
}

#[test]
fn snippet_dedent() {
check_edit(
"let",
r#"
//- minicore: option
fn foo(x: Option<i32>, y: Option<i32>) {
let _f = || {
x
.and(y)
.map(|it| it+2)
.$0
};
}
"#,
r#"
fn foo(x: Option<i32>, y: Option<i32>) {
let _f = || {
let $0 = x
.and(y)
.map(|it| it+2);
};
}
"#,
);
}
}
57 changes: 51 additions & 6 deletions crates/stdx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,7 @@ pub fn trim_indent(mut text: &str) -> String {
if text.starts_with('\n') {
text = &text[1..];
}
let indent = text
.lines()
.filter(|it| !it.trim().is_empty())
.map(|it| it.len() - it.trim_start().len())
.min()
.unwrap_or(0);
let indent = indent_of(text);
text.split_inclusive('\n')
.map(
|line| {
Expand All @@ -222,6 +217,25 @@ pub fn trim_indent(mut text: &str) -> String {
.collect()
}

#[must_use]
fn indent_of(text: &str) -> usize {
text.lines()
.filter(|it| !it.trim().is_empty())
.map(|it| it.len() - it.trim_start().len())
.min()
.unwrap_or(0)
}

#[must_use]
pub fn dedent_by(spaces: usize, text: &str) -> String {
text.split_inclusive('\n')
.map(|line| {
let trimmed = line.trim_start_matches(' ');
if line.len() - trimmed.len() <= spaces { trimmed } else { &line[spaces..] }
})
.collect()
}

pub fn equal_range_by<T, F>(slice: &[T], mut key: F) -> ops::Range<usize>
where
F: FnMut(&T) -> Ordering,
Expand Down Expand Up @@ -352,6 +366,37 @@ mod tests {
);
}

#[test]
fn test_dedent() {
assert_eq!(dedent_by(0, ""), "");
assert_eq!(dedent_by(1, ""), "");
assert_eq!(dedent_by(2, ""), "");
assert_eq!(dedent_by(0, "foo"), "foo");
assert_eq!(dedent_by(2, "foo"), "foo");
assert_eq!(dedent_by(2, " foo"), "foo");
assert_eq!(dedent_by(2, " foo"), " foo");
assert_eq!(dedent_by(2, " foo\nbar"), " foo\nbar");
assert_eq!(dedent_by(2, "foo\n bar"), "foo\n bar");
assert_eq!(dedent_by(2, "foo\n\n bar"), "foo\n\n bar");
assert_eq!(dedent_by(2, "foo\n.\n bar"), "foo\n.\n bar");
assert_eq!(dedent_by(2, "foo\n .\n bar"), "foo\n.\n bar");
assert_eq!(dedent_by(2, "foo\n .\n bar"), "foo\n .\n bar");
}

#[test]
fn test_indent_of() {
assert_eq!(indent_of(""), 0);
assert_eq!(indent_of(" "), 0);
assert_eq!(indent_of(" x"), 1);
assert_eq!(indent_of(" x\n"), 1);
assert_eq!(indent_of(" x\ny"), 0);
assert_eq!(indent_of(" x\n y"), 1);
assert_eq!(indent_of(" x\n y"), 1);
assert_eq!(indent_of(" x\n y"), 2);
assert_eq!(indent_of(" x\n y\n"), 2);
assert_eq!(indent_of(" x\n\n y\n"), 2);
}

#[test]
fn test_replace() {
#[track_caller]
Expand Down