use crate::kinds::SyntaxKind::*; use rowan::{ast::AstNode as OtherAstNode, NodeOrToken}; use crate::ast; use super::{support::children_tokens_u, AstToken, InterpolPart, StrContent}; impl ast::Str { pub fn parts(&self) -> impl Iterator> { self.syntax().children_with_tokens().filter_map(|child| match child { NodeOrToken::Token(token) if token.kind() == TOKEN_STRING_CONTENT => { Some(InterpolPart::Literal(StrContent::cast(token).unwrap())) } NodeOrToken::Token(token) => { assert!(token.kind() == TOKEN_STRING_START || token.kind() == TOKEN_STRING_END); None } NodeOrToken::Node(node) => { assert_eq!(node.kind(), NODE_INTERPOL); Some(InterpolPart::Interpolation(ast::Interpol::cast(node).unwrap())) } }) } pub fn normalized_parts(&self) -> Vec> { let multiline = children_tokens_u(self).next().is_some_and(|t| t.text() == "''"); let mut is_first_literal = true; let mut at_start_of_line = true; let mut min_indent = 1000000; let mut cur_indent = 0; let mut n = 0; let mut first_is_literal = false; let parts: Vec> = self.parts().collect(); if multiline { for part in &parts { match part { InterpolPart::Interpolation(_) => { if at_start_of_line { at_start_of_line = false; min_indent = min_indent.min(cur_indent); } n += 1; } InterpolPart::Literal(literal) => { let mut token_text = literal.syntax().text(); if n == 0 { first_is_literal = true; } if is_first_literal && first_is_literal { is_first_literal = false; if let Some(p) = token_text.find('\n') { if token_text[0..p].chars().all(|c| c == ' ') { token_text = &token_text[p + 1..] } } } for c in token_text.chars() { if at_start_of_line { if c == ' ' { cur_indent += 1; } else if c == '\n' { cur_indent = 0; } else { at_start_of_line = false; min_indent = min_indent.min(cur_indent); } } else if c == '\n' { at_start_of_line = true; cur_indent = 0; } } n += 1; } } } } let mut normalized_parts = Vec::new(); let mut cur_dropped = 0; let mut i = 0; is_first_literal = true; at_start_of_line = true; for part in parts { match part { InterpolPart::Interpolation(interpol) => { at_start_of_line = false; cur_dropped = 0; normalized_parts.push(InterpolPart::Interpolation(interpol)); i += 1; } InterpolPart::Literal(literal) => { let mut token_text = literal.syntax().text(); if multiline { if is_first_literal && first_is_literal { is_first_literal = false; if let Some(p) = token_text.find('\n') { if token_text[0..p].chars().all(|c| c == ' ') { token_text = &token_text[p + 1..]; if token_text.is_empty() { i += 1; continue; } } } } let mut str = String::new(); for c in token_text.chars() { if at_start_of_line { if c == ' ' { if cur_dropped >= min_indent { str.push(c); } cur_dropped += 1; } else if c == '\n' { cur_dropped = 0; str.push(c); } else { at_start_of_line = false; cur_dropped = 0; str.push(c); } } else { str.push(c); if c == '\n' { at_start_of_line = true; } } } if i == n - 1 { if let Some(p) = str.rfind('\n') { if str[p + 1..].chars().all(|c| c == ' ') { str.truncate(p + 1); } } } normalized_parts.push(InterpolPart::Literal(unescape(&str, multiline))); i += 1; } else { normalized_parts .push(InterpolPart::Literal(unescape(token_text, multiline))); } } } } normalized_parts } } /// Interpret escape sequences in the nix string and return the converted value pub fn unescape(input: &str, multiline: bool) -> String { let mut output = String::new(); let mut input = input.chars().peekable(); loop { match input.next() { None => break, Some('"') if !multiline => break, Some('\\') if !multiline => match input.next() { None => break, Some('n') => output.push('\n'), Some('r') => output.push('\r'), Some('t') => output.push('\t'), Some(c) => output.push(c), }, Some('\'') if multiline => match input.next() { None => { output.push('\''); } Some('\'') => match input.peek() { Some('\'') => { input.next().unwrap(); output.push_str("''"); } Some('$') => { input.next().unwrap(); output.push('$'); } Some('\\') => { input.next().unwrap(); match input.next() { None => break, Some('n') => output.push('\n'), Some('r') => output.push('\r'), Some('t') => output.push('\t'), Some(c) => output.push(c), } } _ => break, }, Some(c) => { output.push('\''); output.push(c); } }, Some(c) => output.push(c), } } output } #[cfg(test)] mod tests { use crate::Root; use super::*; #[test] fn string_unescapes() { assert_eq!(unescape(r#"Hello\n\"World\" :D"#, false), "Hello\n\"World\" :D"); assert_eq!(unescape(r#"\"Hello\""#, false), "\"Hello\""); assert_eq!(unescape(r#"Hello''\n'''World''' :D"#, true), "Hello\n''World'' :D"); assert_eq!(unescape(r#""Hello""#, true), "\"Hello\""); } #[test] fn parts_leading_ws() { let inp = "''\n hello\n world''"; let expr = Root::parse(inp).ok().unwrap().expr().unwrap(); match expr { ast::Expr::Str(str) => { assert_eq!( str.normalized_parts(), vec![InterpolPart::Literal("hello\nworld".to_string())] ) } _ => unreachable!(), } } #[test] fn parts_trailing_ws_single_line() { let inp = "''hello ''"; let expr = Root::parse(inp).ok().unwrap().expr().unwrap(); match expr { ast::Expr::Str(str) => { assert_eq!( str.normalized_parts(), vec![InterpolPart::Literal("hello ".to_string())] ) } _ => unreachable!(), } } #[test] fn parts_trailing_ws_multiline() { let inp = "''hello\n ''"; let expr = Root::parse(inp).ok().unwrap().expr().unwrap(); match expr { ast::Expr::Str(str) => { assert_eq!( str.normalized_parts(), vec![InterpolPart::Literal("hello\n".to_string())] ) } _ => unreachable!(), } } #[test] fn parts() { use crate::{NixLanguage, SyntaxNode}; use rowan::{GreenNodeBuilder, Language}; fn string_node(content: &str) -> ast::Str { let mut builder = GreenNodeBuilder::new(); builder.start_node(NixLanguage::kind_to_raw(NODE_STRING)); builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_START), "''"); builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_CONTENT), content); builder.token(NixLanguage::kind_to_raw(TOKEN_STRING_END), "''"); builder.finish_node(); ast::Str::cast(SyntaxNode::new_root(builder.finish())).unwrap() } let txtin = r#" |trailing-whitespace |trailing-whitespace This is a multiline string :D indented by two \'\'\'\'\ ''${ interpolation was escaped } two single quotes: ''' three single quotes: '''' "# .replace("|trailing-whitespace", ""); if let [InterpolPart::Literal(lit)] = &ast::Str::normalized_parts(&string_node(txtin.as_str()))[..] { assert_eq!(lit, // Get the below with nix repl " \n \nThis is a multiline string :D\n indented by two\n\\'\\'\\'\\'\\\n${ interpolation was escaped }\ntwo single quotes: ''\nthree single quotes: '''\n" ); } else { unreachable!(); } } #[test] fn parts_ast() { fn assert_eq_ast_ctn(it: &mut dyn Iterator>, x: &str) { let tmp = it.next().expect("unexpected EOF"); if let InterpolPart::Interpolation(astn) = tmp { assert_eq!(astn.expr().unwrap().syntax().to_string(), x); } else { unreachable!("unexpected literal {:?}", tmp); } } let inp = r#"'' This version of Nixpkgs requires Nix >= ${requiredVersion}, please upgrade: - If you are running NixOS, `nixos-rebuild' can be used to upgrade your system. - Alternatively, with Nix > 2.0 `nix upgrade-nix' can be used to imperatively upgrade Nix. You may use `nix-env --version' to check which version you have. - If you installed Nix using the install script (https://nixos.org/nix/install), it is safe to upgrade by running it again: curl -L https://nixos.org/nix/install | sh For more information, please see the NixOS release notes at https://nixos.org/nixos/manual or locally at ${toString ./nixos/doc/manual/release-notes}. If you need further help, see https://nixos.org/nixos/support.html ''"#; let expr = Root::parse(inp).ok().unwrap().expr().unwrap(); match expr { ast::Expr::Str(s) => { let mut it = s.normalized_parts().into_iter(); assert_eq!( it.next().unwrap(), InterpolPart::Literal("\nThis version of Nixpkgs requires Nix >= ".to_string()) ); assert_eq_ast_ctn(&mut it, "requiredVersion"); assert_eq!(it.next().unwrap(), InterpolPart::Literal( ", please upgrade:\n\n- If you are running NixOS, `nixos-rebuild' can be used to upgrade your system.\n\n- Alternatively, with Nix > 2.0 `nix upgrade-nix' can be used to imperatively\n upgrade Nix. You may use `nix-env --version' to check which version you have.\n\n- If you installed Nix using the install script (https://nixos.org/nix/install),\n it is safe to upgrade by running it again:\n\n curl -L https://nixos.org/nix/install | sh\n\nFor more information, please see the NixOS release notes at\nhttps://nixos.org/nixos/manual or locally at\n".to_string() )); assert_eq_ast_ctn(&mut it, "toString ./nixos/doc/manual/release-notes"); assert_eq!( it.next().unwrap(), InterpolPart::Literal( ".\n\nIf you need further help, see https://nixos.org/nixos/support.html\n" .to_string() ) ); } _ => unreachable!(), } } }