#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Style {
Rust,
CLike,
Python,
Sql,
Toml,
Json,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct LexState {
block_comment: bool,
backtick: bool,
triple: Option<char>,
dollar: Option<String>,
}
impl LexState {
pub(crate) fn is_open(&self) -> bool {
self.block_comment || self.backtick || self.triple.is_some() || self.dollar.is_some()
}
}
pub(crate) fn code_chars(line: &str, st: &mut LexState, style: Style, out: &mut Vec<u8>) {
let b = line.as_bytes();
let n = b.len();
let mut i = 0;
while i < n {
let c = b[i];
if st.block_comment {
match find_from(b, i, b"*/") {
Some(j) => i = j + 2,
None => return,
}
st.block_comment = false;
continue;
}
if st.backtick {
match memchr::memchr(b'`', &b[i..]) {
Some(j) => i += j + 1,
None => return,
}
st.backtick = false;
continue;
}
if let Some(q) = st.triple {
let closer = [q as u8, q as u8, q as u8];
match find_from(b, i, &closer) {
Some(j) => i = j + 3,
None => return,
}
st.triple = None;
continue;
}
if let Some(tag) = &st.dollar {
let closer = format!("${tag}$");
match find_from(b, i, closer.as_bytes()) {
Some(j) => i = j + closer.len(),
None => return,
}
st.dollar = None;
continue;
}
match style {
Style::Rust | Style::CLike => {
if c == b'/' && i + 1 < n && b[i + 1] == b'/' {
return;
}
if c == b'/' && i + 1 < n && b[i + 1] == b'*' {
st.block_comment = true;
i += 2;
continue;
}
}
Style::Python | Style::Toml => {
if c == b'#' {
return;
}
}
Style::Sql => {
if c == b'-' && i + 1 < n && b[i + 1] == b'-' {
return;
}
if c == b'/' && i + 1 < n && b[i + 1] == b'*' {
st.block_comment = true;
i += 2;
continue;
}
}
Style::Json => {}
}
if matches!(style, Style::Python | Style::Toml)
&& (c == b'"' || c == b'\'')
&& i + 2 < n
&& b[i + 1] == c
&& b[i + 2] == c
{
st.triple = Some(c as char);
i += 3;
continue;
}
if style == Style::CLike && c == b'`' {
st.backtick = true;
i += 1;
continue;
}
if style == Style::Sql && c == b'$' {
let mut j = i + 1;
while j < n && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
j += 1;
}
if j < n && b[j] == b'$' && (j == i + 1 || !b[i + 1].is_ascii_digit()) {
st.dollar = Some(String::from_utf8_lossy(&b[i + 1..j]).into_owned());
i = j + 1;
continue;
}
}
if c == b'"' || (c == b'\'' && style != Style::Rust) {
if let Some(j) = close_quote(b, i + 1, c, style != Style::Sql) {
i = j + 1;
continue;
}
out.push(c);
i += 1;
continue;
}
if c == b'\'' && style == Style::Rust {
if let Some(j) = rust_char_literal_end(b, i) {
i = j + 1;
continue;
}
out.push(c);
i += 1;
continue;
}
out.push(if c.is_ascii() { c } else { b'?' });
i += 1;
}
}
fn find_from(b: &[u8], from: usize, needle: &[u8]) -> Option<usize> {
if from >= b.len() {
return None;
}
memchr::memmem::find(&b[from..], needle).map(|j| from + j)
}
fn close_quote(b: &[u8], from: usize, q: u8, escapes: bool) -> Option<usize> {
let mut i = from;
while i < b.len() {
if escapes && b[i] == b'\\' {
i += 2;
continue;
}
if b[i] == q {
return Some(i);
}
i += 1;
}
None
}
fn rust_char_literal_end(b: &[u8], at: usize) -> Option<usize> {
let n = b.len();
if at + 2 >= n {
return None;
}
if b[at + 1] == b'\\' {
let end = memchr::memchr(b'\'', &b[at + 2..]).map(|j| at + 2 + j)?;
return (end - at <= 12).then_some(end);
}
let ch_len = utf8_len(b[at + 1]);
let end = at + 1 + ch_len;
(end < n && b[end] == b'\'').then_some(end)
}
fn utf8_len(first: u8) -> usize {
if first < 0x80 {
1
} else if first >> 5 == 0b110 {
2
} else if first >> 4 == 0b1110 {
3
} else {
4
}
}
pub(crate) struct Lexed {
buf: Vec<u8>,
spans: Vec<(usize, usize)>,
open_after: Vec<bool>,
}
impl Lexed {
pub(crate) fn new(lines: &[&str], style: Style) -> Lexed {
let mut st = LexState::default();
let mut buf = Vec::with_capacity(lines.iter().map(|l| l.len()).sum());
let mut spans = Vec::with_capacity(lines.len());
let mut open_after = Vec::with_capacity(lines.len());
for line in lines {
let s = buf.len();
code_chars(line, &mut st, style, &mut buf);
spans.push((s, buf.len()));
open_after.push(st.is_open());
}
Lexed {
buf,
spans,
open_after,
}
}
pub(crate) fn code(&self, i: usize) -> &[u8] {
let (s, e) = self.spans[i];
&self.buf[s..e]
}
pub(crate) fn open_after(&self, i: usize) -> bool {
self.open_after[i]
}
pub(crate) fn open_before(&self, i: usize) -> bool {
i > 0 && self.open_after[i - 1]
}
fn last_code(&self, i: usize) -> Option<u8> {
self.code(i)
.iter()
.rev()
.find(|c| !c.is_ascii_whitespace())
.copied()
}
fn first_code(&self, i: usize) -> Option<u8> {
self.code(i)
.iter()
.find(|c| !c.is_ascii_whitespace())
.copied()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Terminator {
Semicolon,
SemicolonOrNewline,
}
const MAX_EXTENT_LINES: usize = 5_000;
pub(crate) fn item_extent(
lines: &[&str],
lx: &Lexed,
start: usize,
term: Terminator,
item_start: &dyn Fn(&str) -> bool,
) -> usize {
let mut depth: usize = 0;
let mut body = false;
let last = lines.len().saturating_sub(1);
let stop = (start + MAX_EXTENT_LINES).min(last);
let mut i = start;
while i <= stop {
if i > start && !body && depth == 0 && !lx.open_before(i) && item_start(lines[i]) {
return start;
}
for &c in lx.code(i) {
match c {
b'(' | b'[' => depth += 1,
b')' | b']' => depth = depth.saturating_sub(1),
b'{' => {
if depth == 0 {
body = true;
}
depth += 1;
}
b'}' => {
depth = depth.saturating_sub(1);
if body && depth == 0 {
return i;
}
}
b';' if depth == 0 && !body => return i,
_ => {}
}
}
if !body && depth == 0 && term == Terminator::SemicolonOrNewline && !lx.open_after(i) {
let next_first = (i < last).then(|| lx.first_code(i + 1)).flatten();
let ends_here = match lx.last_code(i) {
None => i > start, Some(c) => !continues_after(c) && !continues_before(next_first),
};
if ends_here {
return i;
}
}
i += 1;
}
if body { stop } else { start }
}
fn continues_after(c: u8) -> bool {
matches!(
c,
b'=' | b','
| b'('
| b'['
| b'{'
| b'+'
| b'-'
| b'*'
| b'/'
| b'|'
| b'&'
| b'?'
| b':'
| b'.'
| b'<'
| b'>'
| b'\\'
| b'!'
| b'%'
| b'^'
)
}
fn continues_before(c: Option<u8>) -> bool {
matches!(
c,
Some(b'|' | b'&' | b'.' | b'?' | b':' | b')' | b']' | b'}' | b'+' | b'-' | b'=')
)
}
pub(crate) fn value_extent(lx: &Lexed, start: usize) -> usize {
let mut depth: usize = 0;
let last = lx.spans.len().saturating_sub(1);
let stop = (start + MAX_EXTENT_LINES).min(last);
let mut i = start;
while i <= stop {
for &c in lx.code(i) {
match c {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth = depth.saturating_sub(1),
_ => {}
}
}
if depth == 0 && !lx.open_after(i) {
return i;
}
i += 1;
}
start
}
pub(crate) fn indent_of(line: &str) -> usize {
let mut w = 0;
for c in line.chars() {
match c {
' ' => w += 1,
'\t' => w += 4,
_ => break,
}
}
w
}
pub(crate) fn is_blank(line: &str) -> bool {
line.trim().is_empty()
}
pub(crate) fn indent_extent(lines: &[&str], lx: &Lexed, start: usize) -> usize {
let base = indent_of(lines[start]);
let last = lines.len().saturating_sub(1);
let stop = (start + MAX_EXTENT_LINES).min(last);
let mut depth: usize = 0;
let mut header_end = start;
let mut i = start;
while i <= stop {
for &c in lx.code(i) {
match c {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth = depth.saturating_sub(1),
_ => {}
}
}
header_end = i;
if depth == 0 && !lx.open_after(i) {
break;
}
i += 1;
}
indent_block_extent(lines, lx, header_end, base)
}
pub(crate) fn indent_block_extent(lines: &[&str], lx: &Lexed, from: usize, base: usize) -> usize {
let last = lines.len().saturating_sub(1);
let stop = (from + MAX_EXTENT_LINES).min(last);
let mut end = from;
let mut j = from + 1;
while j <= stop {
let line = lines[j];
if lx.open_before(j) {
end = j;
j += 1;
continue;
}
if is_blank(line) {
j += 1;
continue;
}
if indent_of(line) > base {
end = j;
j += 1;
continue;
}
break;
}
end
}
pub(crate) fn extend_up(lines: &[&str], line: usize, pred: impl Fn(&str) -> bool) -> usize {
let mut s = line;
while s > 0 {
let above = lines[s - 1];
if is_blank(above) || !pred(above) {
break;
}
s -= 1;
}
s
}
#[cfg(test)]
mod tests {
use super::*;
fn code(line: &str, style: Style) -> String {
let mut st = LexState::default();
let mut out = Vec::new();
code_chars(line, &mut st, style, &mut out);
String::from_utf8(out).unwrap()
}
#[test]
fn strings_and_comments_are_not_code() {
assert_eq!(code(r#"let a = "}"; // }"#, Style::Rust), "let a = ; ");
assert_eq!(code("x = '}' # }", Style::Python), "x = ");
assert_eq!(code("SELECT '}' -- }", Style::Sql), "SELECT ");
assert_eq!(
code("fn f<'a>(x: &'a str) {", Style::Rust),
"fn f<'a>(x: &'a str) {"
);
assert_eq!(code("let c = '{';", Style::Rust), "let c = ;");
assert_eq!(code("let s = `a{b}`;", Style::CLike), "let s = ;");
assert_eq!(code("<p>don't</p> }", Style::CLike), "<p>don't</p> }");
}
#[test]
fn multi_line_constructs_carry_state() {
let mut st = LexState::default();
let mut out = Vec::new();
code_chars("/* a {", &mut st, Style::Rust, &mut out);
assert!(st.block_comment);
code_chars("} */ ok", &mut st, Style::Rust, &mut out);
assert!(!st.block_comment);
assert_eq!(String::from_utf8(out).unwrap(), " ok");
let mut st = LexState::default();
let mut out = Vec::new();
code_chars("s = \"\"\"{", &mut st, Style::Python, &mut out);
assert!(st.triple.is_some());
code_chars("}\"\"\" + x", &mut st, Style::Python, &mut out);
assert_eq!(String::from_utf8(out).unwrap(), "s = + x");
let mut st = LexState::default();
let mut out = Vec::new();
code_chars("AS $body$ BEGIN;", &mut st, Style::Sql, &mut out);
assert_eq!(st.dollar.as_deref(), Some("body"));
code_chars("END; $body$ LANGUAGE x;", &mut st, Style::Sql, &mut out);
assert_eq!(String::from_utf8(out).unwrap(), "AS LANGUAGE x;");
}
fn never(_: &str) -> bool {
false
}
fn lx(lines: &[&str], style: Style) -> Lexed {
Lexed::new(lines, style)
}
#[test]
fn item_extent_bodies_and_statements() {
let src =
"fn a() {\n if x { y }\n}\nconst B: u8 = 1;\nstruct C;\nfn d() ->\n u8\n{\n 1\n}\n";
let lines: Vec<&str> = src.lines().collect();
assert_eq!(
item_extent(
&lines,
&lx(&lines, Style::Rust),
0,
Terminator::Semicolon,
&never
),
2
);
assert_eq!(
item_extent(
&lines,
&lx(&lines, Style::Rust),
3,
Terminator::Semicolon,
&never
),
3
);
assert_eq!(
item_extent(
&lines,
&lx(&lines, Style::Rust),
4,
Terminator::Semicolon,
&never
),
4
);
assert_eq!(
item_extent(
&lines,
&lx(&lines, Style::Rust),
5,
Terminator::Semicolon,
&never
),
9
);
}
#[test]
fn item_extent_without_semicolons() {
let src = "const a = 5\nconst b = foo(\n 1,\n)\ntype U =\n | A\n | B\nconst c = x +\n y\nlet d\n";
let lines: Vec<&str> = src.lines().collect();
let t = Terminator::SemicolonOrNewline;
let l = lx(&lines, Style::CLike);
assert_eq!(item_extent(&lines, &l, 0, t, &never), 0);
assert_eq!(item_extent(&lines, &l, 1, t, &never), 3);
assert_eq!(item_extent(&lines, &l, 4, t, &never), 6);
assert_eq!(item_extent(&lines, &l, 7, t, &never), 8);
assert_eq!(item_extent(&lines, &l, 9, t, &never), 9);
}
#[test]
fn unterminated_statement_falls_back_to_its_own_line() {
let src = "const A: &str = \"open\nfn later() {}\n";
let lines: Vec<&str> = src.lines().collect();
let starts_item = |l: &str| l.starts_with("fn ");
assert_eq!(
item_extent(
&lines,
&lx(&lines, Style::Rust),
0,
Terminator::Semicolon,
&starts_item
),
0
);
assert_eq!(
item_extent(
&lines,
&lx(&lines, Style::Rust),
0,
Terminator::Semicolon,
&never
),
1
);
}
#[test]
fn indent_extent_python() {
let src = "def f(\n a,\n):\n x = 1\n\n return x\n\ndef g():\n pass\n";
let lines: Vec<&str> = src.lines().collect();
let l = lx(&lines, Style::Python);
assert_eq!(indent_extent(&lines, &l, 0), 5);
assert_eq!(indent_extent(&lines, &l, 7), 8);
}
#[test]
fn indent_extent_keeps_docstrings_whole() {
let src =
"def f():\n \"\"\"Doc\nnot indented but inside\n \"\"\"\n return 1\nx = 2\n";
let lines: Vec<&str> = src.lines().collect();
assert_eq!(indent_extent(&lines, &lx(&lines, Style::Python), 0), 4);
}
#[test]
fn value_extent_brackets() {
let src = "a = [\n 1,\n]\nb = 2\nc = \"\"\"\nx\n\"\"\"\n";
let lines: Vec<&str> = src.lines().collect();
let l = lx(&lines, Style::Toml);
assert_eq!(value_extent(&l, 0), 2);
assert_eq!(value_extent(&l, 3), 3);
assert_eq!(value_extent(&l, 4), 6);
}
#[test]
fn extend_up_is_contiguous() {
let lines = ["/// doc", "", "#[test]", "/// more", "fn f() {}"];
assert_eq!(
extend_up(&lines, 4, |l| l.starts_with("///") || l.starts_with("#[")),
2
);
assert_eq!(extend_up(&lines, 0, |_| true), 0);
}
}