4.2.0 fixes #51

This commit is contained in:
Aaronepower 2016-08-21 20:24:00 +01:00
parent e1a4529e2d
commit 68c62f8dc2
9 changed files with 335 additions and 368 deletions

View File

@ -1,18 +1,19 @@
os:
- linux
- osx
- linux
- osx
env:
- RUST_TEST_THREADS=1
language: rust
rust:
- stable
- beta
- nightly
- stable
- beta
- nightly
script:
- cargo build --verbose --release
- cargo build --verbose --release --features all
- cargo test --verbose --release
- cargo test --verbose --release --features all
matrix:
allow_failure:
- rust: nightly
allow_failure:
- rust: nightly
sudo: false

View File

@ -8,7 +8,7 @@ license = "MIT/Apache-2.0"
name = "tokei"
readme = "README.md"
repository = "https://github.com/Aaronepower/tokei.git"
version = "4.1.0"
version = "4.2.0"
[[bin]]
doc = false

View File

@ -5,7 +5,7 @@ about: Count Code, Quickly.
author: Aaron P. <theaaronepower@gmail.com>
bin_name: Tokei
name: Tokei
version: 4.1.0
version: 4.2.0
args:
- exclude:
help: Ignore all files & directories containing the word.

View File

@ -31,6 +31,12 @@ pub struct Language {
/// Whether the language supports nested multi line comments or not.
#[serde(skip_deserializing, skip_serializing)]
pub nested: bool,
/// A list of specific nested comments if this is empty all `multi_line` comments count.
#[serde(skip_deserializing, skip_serializing)]
pub nested_comments: Vec<(&'static str, &'static str)>,
/// A list of quotes by default it is `""`.
#[serde(skip_deserializing, skip_serializing)]
pub quotes: Vec<(&'static str, &'static str)>,
}
#[cfg(not(feature = "io"))]
@ -54,6 +60,10 @@ pub struct Language {
pub multi_line: Vec<(&'static str, &'static str)>,
/// Whether the language supports nested multi line comments or not.
pub nested: bool,
/// A list of specific nested comments if this is empty all `multi_line` comments count.
pub nested_comments: Vec<(&'static str, &'static str)>,
/// A list of quotes by default it is `""`.
pub quotes: Vec<(&'static str, &'static str)>,
}
@ -101,6 +111,7 @@ impl Language {
Language {
line_comment: vec!["//"],
multi_line: vec![("/*", "*/")],
quotes: vec![("\"", "\"")],
..Self::default()
}
}
@ -116,7 +127,11 @@ impl Language {
/// assert_eq!(ocaml.multi_line, coq.multi_line);
/// ```
pub fn new_func() -> Self {
Language { multi_line: vec![("(*", "*)")], ..Self::default() }
Language {
multi_line: vec![("(*", "*)")],
quotes: vec![("\"", "\"")],
..Self::default()
}
}
/// Convience constructor for creating a language that has the same commenting syntax as HTML like languages.
@ -130,7 +145,11 @@ impl Language {
/// assert_eq!(xml.multi_line, html.multi_line);
/// ```
pub fn new_html() -> Self {
Language { multi_line: vec![("<!--", "-->")], ..Self::default() }
Language {
multi_line: vec![("<!--", "-->")],
quotes: vec![("\"", "\"")],
..Self::default()
}
}
/// Convience constructor for creating a language that has the same commenting syntax as Bash.
@ -154,7 +173,11 @@ impl Language {
/// let mustache = Language::new_multi(vec![("{{!", "}}")]);
/// ```
pub fn new_multi(multi_line: Vec<(&'static str, &'static str)>) -> Self {
Language { multi_line: multi_line, ..Self::default() }
Language {
multi_line: multi_line,
quotes: vec![("\"", "\"")],
..Self::default()
}
}
/// Convience constructor for creating a language that has the same commenting syntax as Prolog.
@ -171,6 +194,7 @@ impl Language {
Language {
line_comment: vec!["%"],
multi_line: vec![("/*", "*/")],
quotes: vec![("\"", "\"")],
..Self::default()
}
}
@ -182,7 +206,11 @@ impl Language {
/// let haskell = Language::new_single(vec!["--"]);
/// ```
pub fn new_single(line_comment: Vec<&'static str>) -> Self {
Language { line_comment: line_comment, ..Self::default() }
Language {
line_comment: line_comment,
quotes: vec![("\"", "\"")],
..Self::default()
}
}
/// Checks if the language is empty. Empty meaning it doesn't have any statistics.
@ -221,7 +249,40 @@ impl Language {
self
}
/// Sorts each of the `Stats` structs contained in the language based on what category is provided
/// Specify if the the language supports nested multi line comments.
/// And which are nested. If this is specified there is no need to
/// call the `nested` function.
///
/// ```
/// # use tokei::*;
/// let mut d = Language::new(vec!["//"], vec![("/*", "*/")])
/// .nested_comments(vec![("/+", "+/")]);
/// assert!(d.nested);
/// assert_eq!(d.nested_comments, vec![("/+", "+/")]);
/// ```
pub fn nested_comments(mut self, nested_comments: Vec<(&'static str, &'static str)>) -> Self {
self.nested = true;
self.nested_comments = nested_comments;
self
}
/// Specifies if the language has a quotes to define a string where
/// the commenting syntax would be ignored. By default it is only
/// `""` quotes that are ignored.
///
/// ```
/// # use tokei::*;
/// let mut javascript = Language::new(vec!["//"], vec![("/*", "*/")])
/// .set_quotes(vec![("\"", "\""), ("'", "'")]);
/// assert!(!javascript.quotes.is_empty());
/// ```
pub fn set_quotes(mut self, quotes: Vec<(&'static str, &'static str)>) -> Self {
self.quotes = quotes;
self
}
/// Sorts each of the `Stats` structs contained in the language based
/// on what category is provided
/// panic!'s if given the wrong category.
///
/// ```

View File

@ -7,7 +7,7 @@ use std::borrow::Cow;
use std::fmt;
use std::path::Path;
use utils::*;
use utils::fs;
use self::LanguageType::*;
#[cfg_attr(feature = "io", derive(Deserialize, Serialize))]
@ -269,7 +269,7 @@ impl LanguageType {
/// assert_eq!(rust, Some(LanguageType::Rust));
/// ```
pub fn from_extension<P: AsRef<Path>>(entry: P) -> Option<Self> {
if let Some(extension) = get_extension(entry) {
if let Some(extension) = fs::get_extension(entry) {
match &*extension {
"as" => Some(ActionScript),
"ada" | "adb" | "ads" => Some(Ada),

View File

@ -19,7 +19,7 @@ use serde_yaml;
use toml;
use rayon::prelude::*;
use utils::*;
use utils::{fs, multi_line};
use super::{Language, LanguageType};
use super::LanguageType::*;
use stats::Stats;
@ -31,6 +31,87 @@ const TOML_ERROR: &'static str = "Tokei was not compiled with the `toml-io` flag
#[cfg(not(feature = "yaml"))]
const YAML_ERROR: &'static str = "Tokei was not compiled with the `yaml` flag.";
fn count_files(language_tuple: &mut (&LanguageType, &mut Language)) {
let &mut (name, ref mut language) = language_tuple;
if language.files.is_empty() {
return;
}
let is_fortran = name == &FortranModern || name == &FortranLegacy;
let files: Vec<_> = language.files.drain(..).collect();
let mut contents = String::new();
let mut stack = vec![];
let mut quote = None;
for file in files {
let mut stats = Stats::new(opt_or_cont!(file.to_str()));
stack.clear();
contents.clear();
rs_or_cont!(rs_or_cont!(File::open(file)).read_to_string(&mut contents));
let lines = contents.lines();
if language.is_blank() {
stats.code += lines.count();
continue;
}
'line: for line in lines {
stats.lines += 1;
// FORTRAN has a rule where it only counts as a comment if it's the first
// character in the column, so removing starting whitespace could cause a
// miscount.
let line = if is_fortran {
line
} else {
line.trim()
};
if line.trim().is_empty() {
stats.blanks += 1;
continue;
}
for single in &language.line_comment {
if line.starts_with(single) {
stats.comments += 1;
continue 'line;
}
}
let mut started_with = false;
if quote.is_none() {
let chain = language.multi_line.iter().chain(language.nested_comments.iter());
for &(start, _) in chain {
if line.starts_with(start) {
started_with = true;
}
}
}
multi_line::handle_multi_line(line, &language, &mut stack, &mut quote);
if !stack.is_empty() {
if started_with {
stats.code += 1;
} else {
stats.comments += 1;
}
} else {
stats.code += 1;
}
}
**language += stats;
}
}
/// A collection of existing languages([_List of Languages_](https://github.com/Aaronepower/tokei#supported-languages))
#[derive(Debug, Clone)]
pub struct Languages {
@ -168,97 +249,11 @@ impl Languages {
where I: Into<Cow<'a, [&'a str]>>
{
get_all_files(paths.into(), ignored.into(), &mut self.inner);
fs::get_all_files(paths.into(), ignored.into(), &mut self.inner);
let mut language_iter: Vec<_> = self.inner.iter_mut().collect();
language_iter.par_iter_mut().for_each(|&mut (name, ref mut language)| {
if language.files.is_empty() {
return;
}
let is_fortran = name == &FortranModern || name == &FortranLegacy;
let files: Vec<_> = language.files.drain(..).collect();
let mut contents = String::new();
for file in files {
let mut is_in_comments = false;
let mut previous_comment_start = "";
let mut comment_depth: usize = 0;
let mut stats = Stats::new(opt_or_cont!(file.to_str()));
let contents = {
contents.clear();
let _ = rs_or_cont!(rs_or_cont!(File::open(file))
.read_to_string(&mut contents));
contents
};
let lines = contents.lines();
if language.is_blank() {
stats.code += lines.count();
continue;
}
'line: for line in lines {
stats.lines += 1;
// FORTRAN has a rule where it only counts as a comment if it's the first
// character in the column, so removing starting whitespace could cause a
// miscount.
let line = if is_fortran {
line
} else {
line.trim()
};
if line.trim().is_empty() {
stats.blanks += 1;
continue;
}
if line.starts_with(multi_line) {
if let Some(multi_line) = has_trailing_comments(line, &language) {
previous_comment_start = multi_line;
is_in_comments = true;
if language.nested {
comment_depth += 1;
}
}
}
if is_in_comments {
for &(multi_line, multi_line_end) in &language.multi_line {
if multi_line == previous_comment_start {
if let Some(pos) = line.find(multi_line_end) {
if language.nested {
comment_depth -= 1;
if comment_depth == 0 {
is_in_comments = false;
}
} else {
is_in_comments = false;
}
}
}
}
stats.comments += 1;
continue;
}
for single in &language.line_comment {
if line.starts_with(single) {
stats.comments += 1;
continue 'line;
}
}
stats.code += 1;
}
**language += stats;
}
});
language_iter.par_iter_mut().for_each(count_files);
}
/// Constructs a new, blank `Languages`.
@ -279,7 +274,8 @@ impl Languages {
C => Language::new_c(),
CHeader => Language::new_c(),
Clojure => Language::new_single(vec![";","#"]),
CoffeeScript => Language::new(vec!["#"], vec![("###", "###")]),
CoffeeScript => Language::new(vec!["#"], vec![("###", "###")])
.set_quotes(vec![("\"", "\""), ("'", "'")]),
ColdFusion => Language::new_multi(vec![("<!---", "--->")]),
ColdFusionScript => Language::new_c(),
Coq => Language::new_func(),
@ -287,8 +283,9 @@ impl Languages {
CppHeader => Language::new_c(),
CSharp => Language::new_c(),
CShell => Language::new_hash(),
Css => Language::new_c(),
D => Language::new(vec!["//"], vec![("/*", "*/"), ("/+", "+/")]).nested(),
Css => Language::new_c()
.set_quotes(vec![("\"", "\""), ("'", "'")]),
D => Language::new(vec!["//"], vec![("/*", "*/")]).nested_comments(vec![("/+", "+/")]),
Dart => Language::new_c(),
DeviceTree => Language::new_c(),
Erlang => Language::new_single(vec!["%"]),
@ -296,9 +293,11 @@ impl Languages {
FortranLegacy => Language::new_single(vec!["c","C","!","*"]),
FortranModern => Language::new_single(vec!["!"]),
Go => Language::new_c(),
Handlebars => Language::new_multi(vec![("<!--", "-->"), ("{{!", "}}")]),
Handlebars => Language::new_multi(vec![("<!--", "-->"), ("{{!", "}}")])
.set_quotes(vec![("\"", "\""), ("'", "'")]),
Haskell => Language::new_single(vec!["--"]),
Html => Language::new_html(),
Html => Language::new_html()
.set_quotes(vec![("\"", "\""), ("'", "'")]),
Idris => Language::new(vec!["--"], vec![("{-", "-}")]),
Isabelle => Language::new(
vec!["--"],
@ -310,9 +309,11 @@ impl Languages {
),
Jai => Language::new_c(),
Java => Language::new_c(),
JavaScript => Language::new_c(),
JavaScript => Language::new_c()
.set_quotes(vec![("\"", "\""), ("'", "'")]),
Json => Language::new_blank(),
Jsx => Language::new_c(),
Jsx => Language::new_c()
.set_quotes(vec![("\"", "\""), ("'", "'")]),
Julia => Language::new(vec!["#"], vec![("#=", "=#")]),
Kotlin => Language::new_c(),
Less => Language::new_c(),
@ -580,12 +581,14 @@ impl DerefMut for Languages {
mod accuracy_tests {
use super::*;
use std::io;
use language::LanguageType;
fn write(contents: &'static str, extension: &'static str) -> io::Result<()> {
fn write(contents: &'static str, file_name: &str) -> io::Result<()> {
use std::io::prelude::*;
use std::fs::File;
use std::fs::{File, create_dir};
let mut f = try!(File::create(format!("./_temp/_temp_file.{}", extension)));
let _ = create_dir("./_temp/");
let mut f = try!(File::create(file_name));
try!(f.write_all(&contents.as_bytes()));
Ok(())
@ -596,93 +599,84 @@ mod accuracy_tests {
Ok(())
}
fn test_accuracy(ext: &'static str, num: u32, contents: &'static str) {
write(contents, ext).unwrap();
fn test_accuracy(file_name: &'static str, expected: u32, contents: &'static str) {
let file_name = format!("./_temp/{}", file_name);
write(contents, &*file_name).unwrap();
let mut l = Languages::new();
let l_type = LanguageType::from_extension(file_name).expect("Can't find language type");
l.get_statistics(vec!["./temp/"], vec![]);
l.get_statistics(vec!["./_temp/"], vec![]);
assert_eq!(num as usize,
l.get_mut(&::language::LanguageType::from(ext)).unwrap().code);
let _ = cleanup();
let language = l.get_mut(&l_type).expect("Couldn't find language");
assert_eq!(expected as usize, language.code);
}
#[test]
fn inside_quotes() {
test_accuracy("rs",
test_accuracy("inside_quotes.rs",
8,
"fn main() {
let start = \"/*\";
r#"fn main() {
let start = "/*";
loop {
\
if x.len() >= 2 && x[0] == '*' && x[1] == '/' { // found the */
\
break;
if x.len() >= 2 && x[0] == '*' && x[1] == '/' { // found the */
break;
}
}
}
")
}"#)
}
#[test]
fn shouldnt_panic() {
test_accuracy("rs",
test_accuracy("shouldnt_panic.rs",
9,
"fn foo() {
let this_ends = \"a \\\"test/*.\";
\
call1();
r#"fn foo() {
let this_ends = "a \"test/*.";
call1();
call2();
let this_does_not = /* a /* \
nested */ comment \" */
\"*/another /*test
\
call3();
*/\";
}")
let this_does_not = /* a /* nested */ comment " */
"*/another /*test
call3();
*/";
}"#)
}
#[test]
fn all_quotes_no_comment() {
test_accuracy("rs",
test_accuracy("all_quotes_no_comment.rs",
10,
"fn foobar() {
let does_not_start = // \"
\"until here,
\
test/*
test\"; // a quote: \"
let also_doesnt_start = \
/* \" */
\"until here,
test,*/
test\"; \
// another quote: \"
}")
r#"fn foobar() {
let does_not_start = // "
"until here,
test/*
test"; // a quote: "
let also_doesnt_start = /* " */
"until here,
test,*/
test"; // another quote: "
}"#)
}
#[test]
fn commenting_on_comments() {
test_accuracy("rs",
test_accuracy("commenting_on_comments.rs",
5,
"fn foo() {
let a = 4; // /*
let b = 5;
\
let c = 6; // */
}")
r#"fn foo() {
let a = 4; // /*
let b = 5;
let c = 6; // */
}"#)
}
#[test]
fn deez_nesting_comments() {
test_accuracy("d",
fn nesting_with_nesting_comments() {
test_accuracy("nesting_with_nesting_comments.d",
5,
"void main() {
auto x = 5; /+ a /+ nested +/ comment /* +/
\
writefln(\"hello\");
auto y = 4; // */
}")
r#"void main() {
auto x = 5; /+ a /+ nested +/ comment /* +/
writefln("hello");
auto y = 4; // */
}"#)
}
}

View File

@ -14,78 +14,6 @@ use walkdir::{WalkDir, WalkDirIterator};
use language::{Language, LanguageType};
use language::LanguageType::*;
pub fn handle_multi_line() -> usize {
unreachable!()
}
/// This is used to catch lines like "let x = 5; /* Comment */"
pub fn has_trailing_comments(line: &str, language: &Language) -> bool {
let mut is_in_comments = 0u64;
line = if !language.single.is_empty() {
let found = None;
for single in &language.line_comment {
if let Some(pos) = line.find(single) {
found = Some(pos);
break;
}
}
if let Some(pos) = found {
&line[0..pos]
} else {
line
}
} else {
line
};
let mut unfinished_comments = vec![];
for &(comment, comment_end) in &language {
let start = match line.find(comment) {
Some(start) => start,
None => continue,
};
// This should short circuit 99% of languages.
if !language.nested && language.multi_line.len() == 1 {
if let Some(end) = line.rfind(comment_end) {
if let Some(end_check) = line.rfind(comment) {
if end_check > end {
return true;
} else {
return false;
}
}
} else {
return true;
}
}
let mut chars = line[start..end + comment_end.len()].chars();
loop {
let window = chars.as_str();
if window.starts_with(comment) {
if nested {
is_in_comments += 1;
} else {
is_in_comments = 1;
}
} else if window.starts_with(comment_end) {
is_in_comments = is_in_comments.saturating_sub(1);
}
if chars.next().is_none() {
break;
}
}
}
is_in_comments != 0
}
pub fn get_all_files<'a>(paths: Cow<'a, [&'a str]>,
ignored_directories: Cow<'a, [&'a str]>,
languages: &mut BTreeMap<LanguageType, Language>) {
@ -186,42 +114,3 @@ pub fn get_filetype_from_shebang<P: AsRef<Path>>(file: P) -> Option<&'static str
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn both_comments_in_line() {
assert!(!has_trailing_comments("Hello /* /* */ World", "//", ("/*", "*/"), false));
}
#[test]
fn comment_hidden_in_single() {
assert!(has_trailing_comments("Hello /* World // */", "//", ("/*", "*/"), true))
}
#[test]
fn comment_start_in_line() {
assert!(has_trailing_comments("Hello /* World", "//", ("/*", "*/"), false));
}
#[test]
fn both_comments_in_line_nested() {
assert!(has_trailing_comments("Hello (* (* *) World", "--", ("(*", "*)"), true));
}
#[test]
fn comments_of_uneven_length() {
assert!(has_trailing_comments("Hello \\<open> \\<open> \\<close> World",
"",
("\\<open>", "\\<close>"),
true));
}
#[test]
fn comment_start_in_line_nested() {
assert!(has_trailing_comments("Hello (* World", "", ("(*", "*)"), true));
}
}

View File

@ -2,6 +2,3 @@
mod macros;
pub mod fs;
pub mod multi_line;
pub use self::fs::*;
pub use self::multi_line::*;

View File

@ -1,40 +1,20 @@
use std::cmp;
use language::Language;
/// This is used to catch lines like "let x = 5; /* Comment */"
pub fn has_trailing_comments(line: &str, language: &Language) -> Vec<&'static str> {
let line = slice_to_single(line, language);
let mut is_in_comments = 0u64;
let mut start = None;
let mut stack = vec![];
for &(comment, comment_end) in &language.multi_line {
start = line.find(comment).and_then(|x| cmp::min(x, start.unwrap_or(x)));
// This should short circuit 99% of languages.
if start.is_none() && !language.nested && language.multi_line.len() == 1 {
if let Some(end) = line.rfind(comment_end) {
if let Some(end_check) = line.rfind(comment) {
if end_check > end {
return true;
} else {
return false;
}
}
} else {
return true;
}
}
}
let start = match start {
Some(pos) => pos,
None => return stack,
};
let mut chars = line[start..].chars();
pub fn handle_multi_line(line: &str,
language: &Language,
stack: &mut Vec<&'static str>,
quote: &mut Option<&'static str>) {
let mut chars = line.chars();
let mut cont = false;
loop {
let nested_is_empty = language.nested_comments.is_empty();
'window: loop {
let window = chars.as_str();
if window.is_empty() {
break;
}
chars.next();
// Prevents counting overlaps like /*/*
if cont {
@ -42,90 +22,135 @@ pub fn has_trailing_comments(line: &str, language: &Language) -> Vec<&'static st
continue;
}
let mut end = false;
if let &mut Some(quote_str) = quote {
if window.starts_with("\\") {
cont = true;
continue;
} else if window.starts_with(quote_str) {
end = true;
}
}
if end {
if let &mut Some(quote_str) = quote {
*quote = None;
if quote_str.chars().count() == 1 {
cont = true
}
continue;
}
}
if quote.is_some() {
continue;
}
let mut pop = false;
if let Some(last) = stack.last() {
if window.starts_with(last) {
stack.pop();
cont = true;
continue;
pop = true;
}
}
for &(comment, comment_end) in &language.multi_line {
if pop {
stack.pop();
cont = true;
continue;
}
if stack.is_empty() {
for &(start, end) in &language.quotes {
if window.starts_with(start) {
*quote = Some(end);
cont = true;
continue 'window;
}
}
}
for comment in &language.line_comment {
if window.starts_with(comment) {
if nested {
stack.push(comment_end);
break 'window;
}
}
for &(start, end) in &language.nested_comments {
if window.starts_with(start) {
stack.push(end);
cont = true;
continue 'window;
}
}
for &(start, end) in &language.multi_line {
if window.starts_with(start) {
if language.nested && nested_is_empty {
stack.push(end);
} else if stack.len() == 0 {
stack.push(comment_end);
stack.push(end);
}
cont = true;
continue;
continue 'window;
}
}
if chars.next().is_none() {
break;
}
}
stack
}
#[inline]
fn slice_to_single(line: &str, language: &language) -> &str {
if !language.single.is_empty() {
let found = None;
for single in &language.line_comment {
if let Some(pos) = line.find(single) {
found = Some(pos);
break;
}
}
if let Some(pos) = found {
&line[0..pos]
} else {
line
}
} else {
line
}
}
#[cfg(test)]
mod tests {
use super::*;
use language::Language;
#[test]
fn both_comments_in_line() {
assert!(!has_trailing_comments("Hello /* /* */ World", "//", ("/*", "*/"), false));
let mut stack = vec![];
let mut quote = None;
let language = Language::new_c();
handle_multi_line("Hello /* /* */ World", &language, &mut stack, &mut quote);
assert_eq!(stack.len(), 0);
}
#[test]
fn comment_hidden_in_single() {
assert!(has_trailing_comments("Hello /* World // */", "//", ("/*", "*/"), true))
let mut stack = vec![];
let mut quote = None;
let language = Language::new_c();
handle_multi_line("Hello World // /*", &language, &mut stack, &mut quote);
assert_eq!(stack.len(), 0);
}
#[test]
fn comment_start_in_line() {
assert!(has_trailing_comments("Hello /* World", "//", ("/*", "*/"), false));
fn comment_start() {
let mut stack = vec![];
let mut quote = None;
let language = Language::new_c();
handle_multi_line("/*Hello World", &language, &mut stack, &mut quote);
assert_eq!(stack.len(), 1);
}
#[test]
fn both_comments_in_line_nested() {
assert!(has_trailing_comments("Hello (* (* *) World", "--", ("(*", "*)"), true));
let mut stack = vec![];
let mut quote = None;
let language = Language::new_func().nested();
handle_multi_line("Hello (* (* *) World", &language, &mut stack, &mut quote);
assert_eq!(stack.len(), 1);
}
#[test]
fn comments_of_uneven_length() {
assert!(has_trailing_comments("Hello \\<open> \\<open> \\<close> World",
"",
("\\<open>", "\\<close>"),
true));
}
#[test]
fn comment_start_in_line_nested() {
assert!(has_trailing_comments("Hello (* World", "", ("(*", "*)"), true));
let mut stack = vec![];
let mut quote = None;
let language = Language::new(vec![], vec![("\\<open>", "\\<close>")]).nested();
handle_multi_line("Hello \\<open> \\<open> \\<close> World",
&language,
&mut stack,
&mut quote);
assert_eq!(stack.len(), 1);
}
}